From 5a00d35ebab18e7bbc981f49d18ced843d1b1448 Mon Sep 17 00:00:00 2001 From: Saeed Vaziry Date: Sun, 18 May 2025 23:46:28 +0200 Subject: [PATCH] #591 - source-controls --- .../ServerProvider/EditServerProvider.php | 3 + .../SourceControl/ConnectSourceControl.php | 9 +- .../SourceControl/EditSourceControl.php | 30 +- .../API/ServerProviderController.php | 4 - .../API/SourceControlController.php | 4 - .../Controllers/SourceControlController.php | 73 +++ .../AbstractSourceControlProvider.php | 10 - app/SourceControlProviders/Gitlab.php | 5 +- .../SourceControlProvider.php | 19 +- composer.json | 2 +- composer.lock | 553 +++++++++--------- config/core.php | 5 + resources/js/layouts/settings/layout.tsx | 7 +- .../source-controls/components/columns.tsx | 185 ++++++ .../components/connect-source-control.tsx | 150 +++++ resources/js/pages/source-controls/index.tsx | 39 ++ resources/js/types/index.d.ts | 4 + resources/js/types/source-control.d.ts | 11 + tests/Feature/SourceControlsTest.php | 40 +- 19 files changed, 791 insertions(+), 362 deletions(-) create mode 100644 app/Http/Controllers/SourceControlController.php create mode 100644 resources/js/pages/source-controls/components/columns.tsx create mode 100644 resources/js/pages/source-controls/components/connect-source-control.tsx create mode 100644 resources/js/pages/source-controls/index.tsx create mode 100644 resources/js/types/source-control.d.ts diff --git a/app/Actions/ServerProvider/EditServerProvider.php b/app/Actions/ServerProvider/EditServerProvider.php index ebd87d1c..fee1a27a 100644 --- a/app/Actions/ServerProvider/EditServerProvider.php +++ b/app/Actions/ServerProvider/EditServerProvider.php @@ -4,6 +4,7 @@ use App\Models\Project; use App\Models\ServerProvider; +use Illuminate\Support\Facades\Validator; class EditServerProvider { @@ -12,6 +13,8 @@ class EditServerProvider */ public function edit(ServerProvider $serverProvider, Project $project, array $input): ServerProvider { + Validator::make($input, self::rules())->validate(); + $serverProvider->profile = $input['name']; $serverProvider->project_id = isset($input['global']) && $input['global'] ? null : $project->id; diff --git a/app/Actions/SourceControl/ConnectSourceControl.php b/app/Actions/SourceControl/ConnectSourceControl.php index a74337db..f711e47a 100644 --- a/app/Actions/SourceControl/ConnectSourceControl.php +++ b/app/Actions/SourceControl/ConnectSourceControl.php @@ -4,7 +4,7 @@ use App\Models\Project; use App\Models\SourceControl; -use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; use Illuminate\Validation\ValidationException; @@ -17,10 +17,12 @@ class ConnectSourceControl */ public function connect(Project $project, array $input): SourceControl { + Validator::make($input, self::rules($input))->validate(); + $sourceControl = new SourceControl([ 'provider' => $input['provider'], 'profile' => $input['name'], - 'url' => Arr::has($input, 'url') ? $input['url'] : null, + 'url' => isset($input['url']) && $input['url'] ? $input['url'] : null, 'project_id' => isset($input['global']) && $input['global'] ? null : $project->id, ]); @@ -28,8 +30,7 @@ public function connect(Project $project, array $input): SourceControl if (! $sourceControl->provider()->connect()) { throw ValidationException::withMessages([ - 'token' => __('Cannot connect to :provider or invalid token!', ['provider' => $sourceControl->provider] - ), + 'token' => __('Cannot connect to :provider or invalid token!', ['provider' => $sourceControl->provider]), ]); } diff --git a/app/Actions/SourceControl/EditSourceControl.php b/app/Actions/SourceControl/EditSourceControl.php index e6eb4f73..4419d1df 100644 --- a/app/Actions/SourceControl/EditSourceControl.php +++ b/app/Actions/SourceControl/EditSourceControl.php @@ -4,6 +4,7 @@ use App\Models\Project; use App\Models\SourceControl; +use Illuminate\Support\Facades\Validator; use Illuminate\Validation\ValidationException; class EditSourceControl @@ -15,46 +16,25 @@ class EditSourceControl */ public function edit(SourceControl $sourceControl, Project $project, array $input): SourceControl { + Validator::make($input, self::rules())->validate(); + $sourceControl->profile = $input['name']; - $sourceControl->url = $input['url'] ?? null; $sourceControl->project_id = isset($input['global']) && $input['global'] ? null : $project->id; - $sourceControl->provider_data = $sourceControl->provider()->editData($input); - - if (! $sourceControl->provider()->connect()) { - throw ValidationException::withMessages([ - 'token' => __('Cannot connect to :provider or invalid token!', ['provider' => $sourceControl->provider]), - ]); - } - $sourceControl->save(); return $sourceControl; } /** - * @param array $input * @return array> */ - public static function rules(SourceControl $sourceControl, array $input): array + public static function rules(): array { - $rules = [ + return [ 'name' => [ 'required', ], ]; - - return array_merge($rules, self::providerRules($sourceControl, $input)); - } - - /** - * @param array $input - * @return array> - * - * @throws ValidationException - */ - private static function providerRules(SourceControl $sourceControl, array $input): array - { - return $sourceControl->provider()->editRules($input); } } diff --git a/app/Http/Controllers/API/ServerProviderController.php b/app/Http/Controllers/API/ServerProviderController.php index 2ca3bfca..2556d9eb 100644 --- a/app/Http/Controllers/API/ServerProviderController.php +++ b/app/Http/Controllers/API/ServerProviderController.php @@ -53,8 +53,6 @@ public function create(Request $request, Project $project): ServerProviderResour { $this->authorize('create', ServerProvider::class); - $this->validate($request, CreateServerProvider::rules($request->all())); - /** @var User $user */ $user = auth()->user(); $serverProvider = app(CreateServerProvider::class)->create($user, $project, $request->all()); @@ -85,8 +83,6 @@ public function update(Request $request, Project $project, ServerProvider $serve $this->validateRoute($project, $serverProvider); - $this->validate($request, EditServerProvider::rules()); - $serverProvider = app(EditServerProvider::class)->edit($serverProvider, $project, $request->all()); return new ServerProviderResource($serverProvider); diff --git a/app/Http/Controllers/API/SourceControlController.php b/app/Http/Controllers/API/SourceControlController.php index 13af7080..72e435ed 100644 --- a/app/Http/Controllers/API/SourceControlController.php +++ b/app/Http/Controllers/API/SourceControlController.php @@ -53,8 +53,6 @@ public function create(Request $request, Project $project): SourceControlResourc { $this->authorize('create', SourceControl::class); - $this->validate($request, ConnectSourceControl::rules($request->all())); - $sourceControl = app(ConnectSourceControl::class)->connect($project, $request->all()); return new SourceControlResource($sourceControl); @@ -87,8 +85,6 @@ public function update(Request $request, Project $project, SourceControl $source $this->validateRoute($project, $sourceControl); - $this->validate($request, EditSourceControl::rules($sourceControl, $request->all())); - $sourceControl = app(EditSourceControl::class)->edit($sourceControl, $project, $request->all()); return new SourceControlResource($sourceControl); diff --git a/app/Http/Controllers/SourceControlController.php b/app/Http/Controllers/SourceControlController.php new file mode 100644 index 00000000..7b432a0a --- /dev/null +++ b/app/Http/Controllers/SourceControlController.php @@ -0,0 +1,73 @@ +authorize('viewAny', SourceControl::class); + + return Inertia::render('source-controls/index', [ + 'sourceControls' => SourceControlResource::collection(SourceControl::getByProjectId(user()->current_project_id)->simplePaginate(config('web.pagination_size'))), + ]); + } + + #[Get('/json', name: 'source-controls.json')] + public function json(): ResourceCollection + { + $this->authorize('viewAny', SourceControl::class); + + return SourceControlResource::collection(SourceControl::getByProjectId(user()->current_project_id)->get()); + } + + #[Post('/', name: 'source-controls.store')] + public function store(Request $request): RedirectResponse + { + $this->authorize('create', SourceControl::class); + + app(ConnectSourceControl::class)->connect(user()->currentProject, $request->all()); + + return back()->with('success', 'Source control created.'); + } + + #[Patch('/{sourceControl}', name: 'source-controls.update')] + public function update(Request $request, SourceControl $sourceControl): RedirectResponse + { + $this->authorize('update', $sourceControl); + + app(EditSourceControl::class)->edit($sourceControl, user()->currentProject, $request->all()); + + return back()->with('success', 'Source control updated.'); + } + + #[Delete('{sourceControl}', name: 'source-controls.destroy')] + public function destroy(SourceControl $sourceControl): RedirectResponse + { + $this->authorize('delete', $sourceControl); + + app(DeleteSourceControl::class)->delete($sourceControl); + + return to_route('source-controls')->with('success', 'Source control deleted.'); + } +} diff --git a/app/SourceControlProviders/AbstractSourceControlProvider.php b/app/SourceControlProviders/AbstractSourceControlProvider.php index 98dad367..055924b2 100755 --- a/app/SourceControlProviders/AbstractSourceControlProvider.php +++ b/app/SourceControlProviders/AbstractSourceControlProvider.php @@ -26,16 +26,6 @@ public function createData(array $input): array ]; } - public function editRules(array $input): array - { - return $this->createRules($input); - } - - public function editData(array $input): array - { - return $this->createData($input); - } - public function data(): array { // support for older data diff --git a/app/SourceControlProviders/Gitlab.php b/app/SourceControlProviders/Gitlab.php index b5d83c3a..3df66e58 100755 --- a/app/SourceControlProviders/Gitlab.php +++ b/app/SourceControlProviders/Gitlab.php @@ -29,12 +29,15 @@ public function createRules(array $input): array public function connect(): bool { try { + ds($this->getApiUrl()); $res = Http::withToken($this->data()['token']) - ->get($this->getApiUrl().'/projects'); + ->get($this->getApiUrl().'/version'); } catch (Exception) { return false; } + ds($res->status()); + return $res->successful(); } diff --git a/app/SourceControlProviders/SourceControlProvider.php b/app/SourceControlProviders/SourceControlProvider.php index aee88e2d..b70f14a1 100755 --- a/app/SourceControlProviders/SourceControlProvider.php +++ b/app/SourceControlProviders/SourceControlProvider.php @@ -2,6 +2,7 @@ namespace App\SourceControlProviders; +use App\Exceptions\FailedToDeployGitHook; use App\Exceptions\FailedToDeployGitKey; use App\Exceptions\FailedToDestroyGitHook; @@ -19,18 +20,6 @@ public function createRules(array $input): array; */ public function createData(array $input): array; - /** - * @param array $input - * @return array - */ - public function editRules(array $input): array; - - /** - * @param array $input - * @return array - */ - public function editData(array $input): array; - /** * @return array */ @@ -43,10 +32,10 @@ public function getRepo(string $repo): mixed; public function fullRepoUrl(string $repo, string $key): string; /** - * @param array $events + * @param array $events * @return array * - * @throws \App\Exceptions\FailedToDeployGitHook + * @throws FailedToDeployGitHook */ public function deployHook(string $repo, array $events, string $secret): array; @@ -56,7 +45,7 @@ public function deployHook(string $repo, array $events, string $secret): array; public function destroyHook(string $repo, string $hookId): void; /** - * @return array + * @return array|null */ public function getLastCommit(string $repo, string $branch): ?array; diff --git a/composer.json b/composer.json index 37e39fb9..d57b7c15 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ "ext-intl": "*", "aws/aws-sdk-php": "^3.158", "inertiajs/inertia-laravel": "^2.0", + "laradumps/laradumps": "^4.2", "laravel/fortify": "^1.17", "laravel/framework": "^12.0", "laravel/sanctum": "^4.0", @@ -25,7 +26,6 @@ "require-dev": { "fakerphp/faker": "^1.9.1", "knuckleswtf/scribe": "^5.0", - "laradumps/laradumps": "^3.0", "larastan/larastan": "^3.1", "laravel/pint": "^1.10", "laravel/sail": "^1.18", diff --git a/composer.lock b/composer.lock index 038973a4..29c8e595 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "31d55d02539efe95d4e3447b817fcb00", + "content-hash": "436252e5b1fca532bfad2c3eb094374c", "packages": [ { "name": "aws/aws-crt-php", @@ -1377,6 +1377,147 @@ }, "time": "2025-04-10T15:08:36+00:00" }, + { + "name": "laradumps/laradumps", + "version": "v4.2.0", + "source": { + "type": "git", + "url": "https://github.com/laradumps/laradumps.git", + "reference": "aca61f1088d30b2e879551343b34e2a788735fdf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laradumps/laradumps/zipball/aca61f1088d30b2e879551343b34e2a788735fdf", + "reference": "aca61f1088d30b2e879551343b34e2a788735fdf", + "shasum": "" + }, + "require": { + "illuminate/mail": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", + "laradumps/laradumps-core": "^3.2.2", + "nunomaduro/termwind": "^1.15.1|^2.0.1", + "php": "^8.1" + }, + "require-dev": { + "larastan/larastan": "^2.0|^3.0", + "laravel/framework": "^10.0|^11.0|^12.0", + "laravel/pint": "^1.17.2", + "livewire/livewire": "^3.5.6", + "mockery/mockery": "^1.6.12", + "orchestra/testbench-core": "^8.0|^9.4|^10.0", + "pestphp/pest": "^2.35.1|^3.7.0", + "symfony/var-dumper": "^6.4.0|^7.1.3" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "LaraDumps\\LaraDumps\\LaraDumpsServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "LaraDumps\\LaraDumps\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Luan Freitas", + "email": "luanfreitas10@protonmail.com", + "role": "Developer" + } + ], + "description": "LaraDumps is a friendly app designed to boost your Laravel PHP coding and debugging experience.", + "homepage": "https://github.com/laradumps/laradumps", + "support": { + "issues": "https://github.com/laradumps/laradumps/issues", + "source": "https://github.com/laradumps/laradumps/tree/v4.2.0" + }, + "funding": [ + { + "url": "https://github.com/luanfreitasdev", + "type": "github" + } + ], + "time": "2025-04-26T13:47:50+00:00" + }, + { + "name": "laradumps/laradumps-core", + "version": "v3.2.4", + "source": { + "type": "git", + "url": "https://github.com/laradumps/laradumps-core.git", + "reference": "eb0e15805ac4061a524c43ed7c1e7796ef3326d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laradumps/laradumps-core/zipball/eb0e15805ac4061a524c43ed7c1e7796ef3326d8", + "reference": "eb0e15805ac4061a524c43ed7c1e7796ef3326d8", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "nunomaduro/termwind": "^1.15|^2.0", + "php": "^8.1", + "ramsey/uuid": "^4.7.5", + "spatie/backtrace": "^1.5", + "symfony/console": "^5.4|^6.4|^7.0", + "symfony/finder": "^5.4|^6.4|^7.0", + "symfony/process": "^5.4|^6.4|^7.0", + "symfony/var-dumper": "^5.4|^6.4|^7.0", + "symfony/yaml": "^5.4|^6.4|^7.0" + }, + "require-dev": { + "illuminate/support": "^10.46", + "laravel/pint": "^1.13.7", + "pestphp/pest": "^2.0|^3.0", + "phpstan/phpstan": "^1.10.50" + }, + "bin": [ + "bin/laradumps" + ], + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "LaraDumps\\LaraDumpsCore\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Luan Freitas", + "email": "luanfreitas10@protonmail.com", + "role": "Developer" + } + ], + "description": "LaraDumps is a friendly app designed to boost your Laravel / PHP coding and debugging experience.", + "homepage": "https://github.com/laradumps/laradumps-core", + "support": { + "issues": "https://github.com/laradumps/laradumps-core/issues", + "source": "https://github.com/laradumps/laradumps-core/tree/v3.2.4" + }, + "funding": [ + { + "url": "https://github.com/luanfreitasdev", + "type": "github" + } + ], + "time": "2025-05-16T14:48:30+00:00" + }, { "name": "laravel/fortify", "version": "v1.25.4", @@ -4197,6 +4338,69 @@ ], "time": "2024-04-27T21:32:50+00:00" }, + { + "name": "spatie/backtrace", + "version": "1.7.2", + "source": { + "type": "git", + "url": "https://github.com/spatie/backtrace.git", + "reference": "9807de6b8fecfaa5b3d10650985f0348b02862b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/backtrace/zipball/9807de6b8fecfaa5b3d10650985f0348b02862b2", + "reference": "9807de6b8fecfaa5b3d10650985f0348b02862b2", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "ext-json": "*", + "laravel/serializable-closure": "^1.3 || ^2.0", + "phpunit/phpunit": "^9.3 || ^11.4.3", + "spatie/phpunit-snapshot-assertions": "^4.2 || ^5.1.6", + "symfony/var-dumper": "^5.1 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Backtrace\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van de Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "A better backtrace", + "homepage": "https://github.com/spatie/backtrace", + "keywords": [ + "Backtrace", + "spatie" + ], + "support": { + "source": "https://github.com/spatie/backtrace/tree/1.7.2" + }, + "funding": [ + { + "url": "https://github.com/sponsors/spatie", + "type": "github" + }, + { + "url": "https://spatie.be/open-source/support-us", + "type": "other" + } + ], + "time": "2025-04-28T14:55:53+00:00" + }, { "name": "spatie/laravel-route-attributes", "version": "1.25.2", @@ -6497,6 +6701,78 @@ ], "time": "2025-04-09T08:14:01+00:00" }, + { + "name": "symfony/yaml", + "version": "v7.2.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "0feafffb843860624ddfd13478f481f4c3cd8b23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/0feafffb843860624ddfd13478f481f4c3cd8b23", + "reference": "0feafffb843860624ddfd13478f481f4c3cd8b23", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.2.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-04T10:10:11+00:00" + }, { "name": "tightenco/ziggy", "version": "v2.5.2", @@ -7210,146 +7486,6 @@ ], "time": "2025-05-01T01:14:54+00:00" }, - { - "name": "laradumps/laradumps", - "version": "v3.5.3", - "source": { - "type": "git", - "url": "https://github.com/laradumps/laradumps.git", - "reference": "bbea350754afdf29dc375e1ffa798120238b1cda" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laradumps/laradumps/zipball/bbea350754afdf29dc375e1ffa798120238b1cda", - "reference": "bbea350754afdf29dc375e1ffa798120238b1cda", - "shasum": "" - }, - "require": { - "illuminate/mail": "^10.0|^11.0|^12.0", - "illuminate/support": "^10.0|^11.0|^12.0", - "laradumps/laradumps-core": ">2.4 <3.0", - "nunomaduro/termwind": "^1.15.1|^2.0.1", - "php": "^8.1" - }, - "require-dev": { - "laravel/framework": "^12.0", - "laravel/pint": "^1.17.2", - "livewire/livewire": "^3.5.6", - "mockery/mockery": "^1.6.12", - "orchestra/testbench-core": "10.0", - "pestphp/pest": "^2.35.1|^3.0", - "symfony/var-dumper": "^6.4.0|^7.1.3" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "LaraDumps\\LaraDumps\\LaraDumpsServiceProvider" - ] - } - }, - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "LaraDumps\\LaraDumps\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Luan Freitas", - "email": "luanfreitas10@protonmail.com", - "role": "Developer" - } - ], - "description": "LaraDumps is a friendly app designed to boost your Laravel PHP coding and debugging experience.", - "homepage": "https://github.com/laradumps/laradumps", - "support": { - "issues": "https://github.com/laradumps/laradumps/issues", - "source": "https://github.com/laradumps/laradumps/tree/v3.5.3" - }, - "funding": [ - { - "url": "https://github.com/luanfreitasdev", - "type": "github" - } - ], - "time": "2025-03-27T16:43:05+00:00" - }, - { - "name": "laradumps/laradumps-core", - "version": "v2.4.5", - "source": { - "type": "git", - "url": "https://github.com/laradumps/laradumps-core.git", - "reference": "b1f083e84e9275026a21ca6effa219dd16cce20a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laradumps/laradumps-core/zipball/b1f083e84e9275026a21ca6effa219dd16cce20a", - "reference": "b1f083e84e9275026a21ca6effa219dd16cce20a", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "nunomaduro/termwind": "^1.15|^2.0", - "php": "^8.1", - "ramsey/uuid": "^4.7.5", - "spatie/backtrace": "^1.5", - "symfony/console": "^5.4|^6.4|^7.0", - "symfony/finder": "^5.4|^6.4|^7.0", - "symfony/process": "^5.4|^6.4|^7.0", - "symfony/var-dumper": "^5.4|^6.4|^7.0", - "symfony/yaml": "^5.4|^6.4|^7.0" - }, - "require-dev": { - "illuminate/support": "^10.43", - "laravel/pint": "^1.13.7", - "pestphp/pest": "^2.28.1", - "phpstan/phpstan": "^1.10.50" - }, - "bin": [ - "bin/laradumps" - ], - "type": "library", - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "LaraDumps\\LaraDumpsCore\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Luan Freitas", - "email": "luanfreitas10@protonmail.com", - "role": "Developer" - } - ], - "description": "LaraDumps is a friendly app designed to boost your Laravel / PHP coding and debugging experience.", - "homepage": "https://github.com/laradumps/laradumps-core", - "support": { - "issues": "https://github.com/laradumps/laradumps-core/issues", - "source": "https://github.com/laradumps/laradumps-core/tree/v2.4.5" - }, - "funding": [ - { - "url": "https://github.com/luanfreitasdev", - "type": "github" - } - ], - "time": "2025-01-16T16:25:50+00:00" - }, { "name": "larastan/larastan", "version": "v3.4.0", @@ -9561,69 +9697,6 @@ ], "time": "2024-02-20T11:51:46+00:00" }, - { - "name": "spatie/backtrace", - "version": "1.7.2", - "source": { - "type": "git", - "url": "https://github.com/spatie/backtrace.git", - "reference": "9807de6b8fecfaa5b3d10650985f0348b02862b2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/backtrace/zipball/9807de6b8fecfaa5b3d10650985f0348b02862b2", - "reference": "9807de6b8fecfaa5b3d10650985f0348b02862b2", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8.0" - }, - "require-dev": { - "ext-json": "*", - "laravel/serializable-closure": "^1.3 || ^2.0", - "phpunit/phpunit": "^9.3 || ^11.4.3", - "spatie/phpunit-snapshot-assertions": "^4.2 || ^5.1.6", - "symfony/var-dumper": "^5.1 || ^6.0 || ^7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Spatie\\Backtrace\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Freek Van de Herten", - "email": "freek@spatie.be", - "homepage": "https://spatie.be", - "role": "Developer" - } - ], - "description": "A better backtrace", - "homepage": "https://github.com/spatie/backtrace", - "keywords": [ - "Backtrace", - "spatie" - ], - "support": { - "source": "https://github.com/spatie/backtrace/tree/1.7.2" - }, - "funding": [ - { - "url": "https://github.com/sponsors/spatie", - "type": "github" - }, - { - "url": "https://spatie.be/open-source/support-us", - "type": "other" - } - ], - "time": "2025-04-28T14:55:53+00:00" - }, { "name": "spatie/data-transfer-object", "version": "3.9.1", @@ -10133,78 +10206,6 @@ ], "time": "2025-05-02T08:36:00+00:00" }, - { - "name": "symfony/yaml", - "version": "v7.2.6", - "source": { - "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "0feafffb843860624ddfd13478f481f4c3cd8b23" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/0feafffb843860624ddfd13478f481f4c3cd8b23", - "reference": "0feafffb843860624ddfd13478f481f4c3cd8b23", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "symfony/console": "<6.4" - }, - "require-dev": { - "symfony/console": "^6.4|^7.0" - }, - "bin": [ - "Resources/bin/yaml-lint" - ], - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Loads and dumps YAML files", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/yaml/tree/v7.2.6" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-04-04T10:10:11+00:00" - }, { "name": "theseer/tokenizer", "version": "1.2.3", diff --git a/config/core.php b/config/core.php index a8145b7f..1e78bdca 100755 --- a/config/core.php +++ b/config/core.php @@ -462,6 +462,11 @@ 'gitlab' => \App\SourceControlProviders\Gitlab::class, 'bitbucket' => \App\SourceControlProviders\Bitbucket::class, ], + 'source_control_providers_custom_fields' => [ + \App\Enums\SourceControl::GITHUB => ['token'], + \App\Enums\SourceControl::GITLAB => ['token', 'url'], + \App\Enums\SourceControl::BITBUCKET => ['username', 'password'], + ], /* * available php extensions diff --git a/resources/js/layouts/settings/layout.tsx b/resources/js/layouts/settings/layout.tsx index 65d4997a..60105e42 100644 --- a/resources/js/layouts/settings/layout.tsx +++ b/resources/js/layouts/settings/layout.tsx @@ -1,5 +1,5 @@ import { type BreadcrumbItem, type NavItem } from '@/types'; -import { CloudIcon, ListIcon, UserIcon, UsersIcon } from 'lucide-react'; +import { CloudIcon, CodeIcon, ListIcon, UserIcon, UsersIcon } from 'lucide-react'; import { ReactNode } from 'react'; import Layout from '@/layouts/app/layout'; @@ -24,6 +24,11 @@ const sidebarNavItems: NavItem[] = [ href: route('server-providers'), icon: CloudIcon, }, + { + title: 'Source Controls', + href: route('source-controls'), + icon: CodeIcon, + }, ]; export default function SettingsLayout({ children, breadcrumbs }: { children: ReactNode; breadcrumbs?: BreadcrumbItem[] }) { diff --git a/resources/js/pages/source-controls/components/columns.tsx b/resources/js/pages/source-controls/components/columns.tsx new file mode 100644 index 00000000..53050203 --- /dev/null +++ b/resources/js/pages/source-controls/components/columns.tsx @@ -0,0 +1,185 @@ +import { ColumnDef } from '@tanstack/react-table'; +import DateTime from '@/components/date-time'; +import { SourceControl } from '@/types/source-control'; +import { Badge } from '@/components/ui/badge'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +import { Button } from '@/components/ui/button'; +import { useForm } from '@inertiajs/react'; +import { LoaderCircleIcon, MoreVerticalIcon } from 'lucide-react'; +import FormSuccessful from '@/components/form-successful'; +import { FormEvent, useState } from 'react'; +import InputError from '@/components/ui/input-error'; +import { Form, FormField, FormFields } from '@/components/ui/form'; +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; +import { Checkbox } from '@/components/ui/checkbox'; + +function Edit({ sourceControl }: { sourceControl: SourceControl }) { + const [open, setOpen] = useState(false); + const form = useForm({ + name: sourceControl.name, + global: sourceControl.global, + }); + + const submit = (e: FormEvent) => { + e.preventDefault(); + form.patch(route('source-controls.update', sourceControl.id)); + }; + return ( + + + e.preventDefault()}>Edit + + + + Edit {sourceControl.name} + Edit source control + +
+ + + + form.setData('name', e.target.value)} /> + + + +
+ form.setData('global', !form.data.global)} /> + +
+ +
+
+
+ + + + + + +
+
+ ); +} + +function Delete({ sourceControl }: { sourceControl: SourceControl }) { + const [open, setOpen] = useState(false); + const form = useForm(); + + const submit = () => { + form.delete(route('source-controls.destroy', sourceControl.id), { + onSuccess: () => { + setOpen(false); + }, + }); + }; + return ( + + + e.preventDefault()}> + Delete + + + + + Delete {sourceControl.name} + Delete source control + +
+

+ Are you sure you want to delete {sourceControl.name}? +

+ +
+ + + + + + +
+
+ ); +} + +export const columns: ColumnDef[] = [ + { + accessorKey: 'id', + header: 'ID', + enableColumnFilter: true, + enableSorting: true, + enableHiding: true, + }, + { + accessorKey: 'provider', + header: 'Provider', + enableColumnFilter: true, + enableSorting: true, + }, + { + accessorKey: 'name', + header: 'Name', + enableColumnFilter: true, + enableSorting: true, + }, + { + accessorKey: 'global', + header: 'Global', + enableColumnFilter: true, + enableSorting: true, + cell: ({ row }) => { + return
{row.original.global ? yes : no}
; + }, + }, + { + accessorKey: 'created_at', + header: 'Created at', + enableColumnFilter: true, + enableSorting: true, + cell: ({ row }) => { + return ; + }, + }, + { + id: 'actions', + enableColumnFilter: false, + enableSorting: false, + cell: ({ row }) => { + return ( +
+ + + + + + + + + + +
+ ); + }, + }, +]; diff --git a/resources/js/pages/source-controls/components/connect-source-control.tsx b/resources/js/pages/source-controls/components/connect-source-control.tsx new file mode 100644 index 00000000..a54bb278 --- /dev/null +++ b/resources/js/pages/source-controls/components/connect-source-control.tsx @@ -0,0 +1,150 @@ +import { LoaderCircle } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { useForm, usePage } from '@inertiajs/react'; +import { FormEventHandler, ReactNode, useEffect, useState } from 'react'; +import { Label } from '@/components/ui/label'; +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import InputError from '@/components/ui/input-error'; +import { Form, FormField, FormFields } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { SharedData } from '@/types'; +import { Checkbox } from '@/components/ui/checkbox'; + +type SourceControlForm = { + provider: string; + name: string; + global: boolean; +}; + +export default function ConnectSourceControl({ + providers, + defaultProvider, + onProviderAdded, + children, +}: { + providers: string[]; + defaultProvider?: string; + onProviderAdded?: () => void; + children: ReactNode; +}) { + const [open, setOpen] = useState(false); + + const page = usePage(); + + const form = useForm>({ + provider: 'github', + name: '', + global: false, + }); + + const submit: FormEventHandler = (e) => { + e.preventDefault(); + form.post(route('source-controls.store'), { + onSuccess: () => { + setOpen(false); + if (onProviderAdded) { + onProviderAdded(); + } + }, + }); + }; + + useEffect(() => { + form.setData('provider', defaultProvider ?? 'github'); + }, [defaultProvider]); + + return ( + + {children} + + + Connect to source control + Connect to a new source control + +
+ + + + + + + + + form.setData('name', e.target.value)} + /> + + + {page.props.configs.source_control_providers_custom_fields[form.data.provider]?.map((item: string) => ( + + + form.setData(item as keyof SourceControlForm, e.target.value)} + /> + + + ))} + +
+ form.setData('global', !form.data.global)} /> + +
+ +
+
+
+ + + + + + +
+
+ ); +} diff --git a/resources/js/pages/source-controls/index.tsx b/resources/js/pages/source-controls/index.tsx new file mode 100644 index 00000000..2113d5fd --- /dev/null +++ b/resources/js/pages/source-controls/index.tsx @@ -0,0 +1,39 @@ +import SettingsLayout from '@/layouts/settings/layout'; +import { Head, usePage } from '@inertiajs/react'; +import Container from '@/components/container'; +import Heading from '@/components/heading'; +import { Button } from '@/components/ui/button'; +import React from 'react'; +import ConnectSourceControl from '@/pages/source-controls/components/connect-source-control'; +import { DataTable } from '@/components/data-table'; +import { columns } from '@/pages/source-controls/components/columns'; +import { SourceControl } from '@/types/source-control'; +import { Configs } from '@/types'; + +type Page = { + sourceControls: { + data: SourceControl[]; + }; + configs: Configs; +}; + +export default function SourceControls() { + const page = usePage(); + + return ( + + + +
+ +
+ + + +
+
+ +
+
+ ); +} diff --git a/resources/js/types/index.d.ts b/resources/js/types/index.d.ts index 3c023974..2aec771a 100644 --- a/resources/js/types/index.d.ts +++ b/resources/js/types/index.d.ts @@ -33,6 +33,10 @@ export interface Configs { server_providers_custom_fields: { [provider: string]: string[]; }; + source_control_providers: string[]; + source_control_providers_custom_fields: { + [provider: string]: string[]; + }; operating_systems: string[]; service_versions: { [service: string]: string[]; diff --git a/resources/js/types/source-control.d.ts b/resources/js/types/source-control.d.ts new file mode 100644 index 00000000..f8bae4c8 --- /dev/null +++ b/resources/js/types/source-control.d.ts @@ -0,0 +1,11 @@ +export interface SourceControl { + id: number; + project_id?: number; + global: boolean; + name: string; + provider: string; + created_at: string; + updated_at: string; + + [key: string]: unknown; +} diff --git a/tests/Feature/SourceControlsTest.php b/tests/Feature/SourceControlsTest.php index 24d632bc..523e6e1e 100644 --- a/tests/Feature/SourceControlsTest.php +++ b/tests/Feature/SourceControlsTest.php @@ -3,11 +3,8 @@ namespace Tests\Feature; use App\Models\SourceControl; -use App\Web\Pages\Settings\SourceControls\Index; -use App\Web\Pages\Settings\SourceControls\Widgets\SourceControlsList; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Http; -use Livewire\Livewire; use Tests\TestCase; class SourceControlsTest extends TestCase @@ -15,6 +12,8 @@ class SourceControlsTest extends TestCase use RefreshDatabase; /** + * @param array $input + * * @dataProvider data */ public function test_connect_provider(string $provider, ?string $customUrl, array $input): void @@ -32,9 +31,7 @@ public function test_connect_provider(string $provider, ?string $customUrl, arra $input['url'] = $customUrl; } - Livewire::test(Index::class) - ->callAction('connect', $input) - ->assertSuccessful(); + $this->post(route('source-controls.store'), $input); $this->assertDatabaseHas('source_controls', [ 'provider' => $provider, @@ -69,9 +66,9 @@ public function test_delete_provider(string $provider): void 'profile' => 'test', ]); - Livewire::test(SourceControlsList::class) - ->callTableAction('delete', $sourceControl->id) - ->assertSuccessful(); + $this->delete(route('source-controls.destroy', $sourceControl)) + ->assertSessionDoesntHaveErrors() + ->assertRedirect(route('source-controls')); $this->assertSoftDeleted('source_controls', [ 'id' => $sourceControl->id, @@ -95,9 +92,10 @@ public function test_cannot_delete_provider(string $provider): void 'source_control_id' => $sourceControl->id, ]); - Livewire::test(SourceControlsList::class) - ->callTableAction('delete', $sourceControl->id) - ->assertNotified('This source control is being used by a site.'); + $this->delete(route('source-controls.destroy', $sourceControl)) + ->assertSessionHasErrors([ + 'source_control' => 'This source control is being used by a site.', + ]); $this->assertNotSoftDeleted('source_controls', [ 'id' => $sourceControl->id, @@ -105,6 +103,8 @@ public function test_cannot_delete_provider(string $provider): void } /** + * @param array $input + * * @dataProvider data */ public function test_edit_source_control(string $provider, ?string $url, array $input): void @@ -120,15 +120,10 @@ public function test_edit_source_control(string $provider, ?string $url, array $ 'url' => $url, ]); - Livewire::test(SourceControlsList::class) - ->callTableAction('edit', $sourceControl->id, [ - 'name' => 'new-name', - 'token' => 'test', // for GitHub and Gitlab - 'username' => 'test', // for Bitbucket - 'password' => 'test', // for Bitbucket - 'url' => $url, // for Gitlab - ]) - ->assertSuccessful(); + $input['name'] = 'new-name'; + + $this->patch(route('source-controls.update', $sourceControl), $input) + ->assertSessionDoesntHaveErrors(); $sourceControl->refresh(); @@ -136,6 +131,9 @@ public function test_edit_source_control(string $provider, ?string $url, array $ $this->assertEquals($url, $sourceControl->url); } + /** + * @return array> + */ public static function data(): array { return [