#591 - source-controls

This commit is contained in:
Saeed Vaziry
2025-05-18 23:46:28 +02:00
parent 1ed5d7362b
commit 5a00d35eba
19 changed files with 791 additions and 362 deletions

View File

@ -4,6 +4,7 @@
use App\Models\Project; use App\Models\Project;
use App\Models\ServerProvider; use App\Models\ServerProvider;
use Illuminate\Support\Facades\Validator;
class EditServerProvider class EditServerProvider
{ {
@ -12,6 +13,8 @@ class EditServerProvider
*/ */
public function edit(ServerProvider $serverProvider, Project $project, array $input): ServerProvider public function edit(ServerProvider $serverProvider, Project $project, array $input): ServerProvider
{ {
Validator::make($input, self::rules())->validate();
$serverProvider->profile = $input['name']; $serverProvider->profile = $input['name'];
$serverProvider->project_id = isset($input['global']) && $input['global'] ? null : $project->id; $serverProvider->project_id = isset($input['global']) && $input['global'] ? null : $project->id;

View File

@ -4,7 +4,7 @@
use App\Models\Project; use App\Models\Project;
use App\Models\SourceControl; use App\Models\SourceControl;
use Illuminate\Support\Arr; use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
@ -17,10 +17,12 @@ class ConnectSourceControl
*/ */
public function connect(Project $project, array $input): SourceControl public function connect(Project $project, array $input): SourceControl
{ {
Validator::make($input, self::rules($input))->validate();
$sourceControl = new SourceControl([ $sourceControl = new SourceControl([
'provider' => $input['provider'], 'provider' => $input['provider'],
'profile' => $input['name'], '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, '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()) { if (! $sourceControl->provider()->connect()) {
throw ValidationException::withMessages([ 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]),
),
]); ]);
} }

View File

@ -4,6 +4,7 @@
use App\Models\Project; use App\Models\Project;
use App\Models\SourceControl; use App\Models\SourceControl;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
class EditSourceControl class EditSourceControl
@ -15,46 +16,25 @@ class EditSourceControl
*/ */
public function edit(SourceControl $sourceControl, Project $project, array $input): SourceControl public function edit(SourceControl $sourceControl, Project $project, array $input): SourceControl
{ {
Validator::make($input, self::rules())->validate();
$sourceControl->profile = $input['name']; $sourceControl->profile = $input['name'];
$sourceControl->url = $input['url'] ?? null;
$sourceControl->project_id = isset($input['global']) && $input['global'] ? null : $project->id; $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(); $sourceControl->save();
return $sourceControl; return $sourceControl;
} }
/** /**
* @param array<string, mixed> $input
* @return array<string, array<int, mixed>> * @return array<string, array<int, mixed>>
*/ */
public static function rules(SourceControl $sourceControl, array $input): array public static function rules(): array
{ {
$rules = [ return [
'name' => [ 'name' => [
'required', 'required',
], ],
]; ];
return array_merge($rules, self::providerRules($sourceControl, $input));
}
/**
* @param array<string, mixed> $input
* @return array<string, array<int, mixed>>
*
* @throws ValidationException
*/
private static function providerRules(SourceControl $sourceControl, array $input): array
{
return $sourceControl->provider()->editRules($input);
} }
} }

View File

@ -53,8 +53,6 @@ public function create(Request $request, Project $project): ServerProviderResour
{ {
$this->authorize('create', ServerProvider::class); $this->authorize('create', ServerProvider::class);
$this->validate($request, CreateServerProvider::rules($request->all()));
/** @var User $user */ /** @var User $user */
$user = auth()->user(); $user = auth()->user();
$serverProvider = app(CreateServerProvider::class)->create($user, $project, $request->all()); $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->validateRoute($project, $serverProvider);
$this->validate($request, EditServerProvider::rules());
$serverProvider = app(EditServerProvider::class)->edit($serverProvider, $project, $request->all()); $serverProvider = app(EditServerProvider::class)->edit($serverProvider, $project, $request->all());
return new ServerProviderResource($serverProvider); return new ServerProviderResource($serverProvider);

View File

@ -53,8 +53,6 @@ public function create(Request $request, Project $project): SourceControlResourc
{ {
$this->authorize('create', SourceControl::class); $this->authorize('create', SourceControl::class);
$this->validate($request, ConnectSourceControl::rules($request->all()));
$sourceControl = app(ConnectSourceControl::class)->connect($project, $request->all()); $sourceControl = app(ConnectSourceControl::class)->connect($project, $request->all());
return new SourceControlResource($sourceControl); return new SourceControlResource($sourceControl);
@ -87,8 +85,6 @@ public function update(Request $request, Project $project, SourceControl $source
$this->validateRoute($project, $sourceControl); $this->validateRoute($project, $sourceControl);
$this->validate($request, EditSourceControl::rules($sourceControl, $request->all()));
$sourceControl = app(EditSourceControl::class)->edit($sourceControl, $project, $request->all()); $sourceControl = app(EditSourceControl::class)->edit($sourceControl, $project, $request->all());
return new SourceControlResource($sourceControl); return new SourceControlResource($sourceControl);

View File

@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers;
use App\Actions\SourceControl\ConnectSourceControl;
use App\Actions\SourceControl\DeleteSourceControl;
use App\Actions\SourceControl\EditSourceControl;
use App\Http\Resources\SourceControlResource;
use App\Models\SourceControl;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Inertia\Inertia;
use Inertia\Response;
use Spatie\RouteAttributes\Attributes\Delete;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Patch;
use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Prefix;
#[Prefix('settings/source-controls')]
#[Middleware(['auth'])]
class SourceControlController extends Controller
{
#[Get('/', name: 'source-controls')]
public function index(): Response
{
$this->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.');
}
}

View File

@ -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 public function data(): array
{ {
// support for older data // support for older data

View File

@ -29,12 +29,15 @@ public function createRules(array $input): array
public function connect(): bool public function connect(): bool
{ {
try { try {
ds($this->getApiUrl());
$res = Http::withToken($this->data()['token']) $res = Http::withToken($this->data()['token'])
->get($this->getApiUrl().'/projects'); ->get($this->getApiUrl().'/version');
} catch (Exception) { } catch (Exception) {
return false; return false;
} }
ds($res->status());
return $res->successful(); return $res->successful();
} }

View File

@ -2,6 +2,7 @@
namespace App\SourceControlProviders; namespace App\SourceControlProviders;
use App\Exceptions\FailedToDeployGitHook;
use App\Exceptions\FailedToDeployGitKey; use App\Exceptions\FailedToDeployGitKey;
use App\Exceptions\FailedToDestroyGitHook; use App\Exceptions\FailedToDestroyGitHook;
@ -19,18 +20,6 @@ public function createRules(array $input): array;
*/ */
public function createData(array $input): array; public function createData(array $input): array;
/**
* @param array<string, mixed> $input
* @return array<string, mixed>
*/
public function editRules(array $input): array;
/**
* @param array<string, mixed> $input
* @return array<string, mixed>
*/
public function editData(array $input): array;
/** /**
* @return array<string, mixed> * @return array<string, mixed>
*/ */
@ -43,10 +32,10 @@ public function getRepo(string $repo): mixed;
public function fullRepoUrl(string $repo, string $key): string; public function fullRepoUrl(string $repo, string $key): string;
/** /**
* @param array<mixed> $events * @param array<int, mixed> $events
* @return array<string, mixed> * @return array<string, mixed>
* *
* @throws \App\Exceptions\FailedToDeployGitHook * @throws FailedToDeployGitHook
*/ */
public function deployHook(string $repo, array $events, string $secret): array; 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; public function destroyHook(string $repo, string $hookId): void;
/** /**
* @return array<string, mixed> * @return array<string, mixed>|null
*/ */
public function getLastCommit(string $repo, string $branch): ?array; public function getLastCommit(string $repo, string $branch): ?array;

View File

@ -13,6 +13,7 @@
"ext-intl": "*", "ext-intl": "*",
"aws/aws-sdk-php": "^3.158", "aws/aws-sdk-php": "^3.158",
"inertiajs/inertia-laravel": "^2.0", "inertiajs/inertia-laravel": "^2.0",
"laradumps/laradumps": "^4.2",
"laravel/fortify": "^1.17", "laravel/fortify": "^1.17",
"laravel/framework": "^12.0", "laravel/framework": "^12.0",
"laravel/sanctum": "^4.0", "laravel/sanctum": "^4.0",
@ -25,7 +26,6 @@
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.9.1", "fakerphp/faker": "^1.9.1",
"knuckleswtf/scribe": "^5.0", "knuckleswtf/scribe": "^5.0",
"laradumps/laradumps": "^3.0",
"larastan/larastan": "^3.1", "larastan/larastan": "^3.1",
"laravel/pint": "^1.10", "laravel/pint": "^1.10",
"laravel/sail": "^1.18", "laravel/sail": "^1.18",

553
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "31d55d02539efe95d4e3447b817fcb00", "content-hash": "436252e5b1fca532bfad2c3eb094374c",
"packages": [ "packages": [
{ {
"name": "aws/aws-crt-php", "name": "aws/aws-crt-php",
@ -1377,6 +1377,147 @@
}, },
"time": "2025-04-10T15:08:36+00:00" "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", "name": "laravel/fortify",
"version": "v1.25.4", "version": "v1.25.4",
@ -4197,6 +4338,69 @@
], ],
"time": "2024-04-27T21:32:50+00:00" "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", "name": "spatie/laravel-route-attributes",
"version": "1.25.2", "version": "1.25.2",
@ -6497,6 +6701,78 @@
], ],
"time": "2025-04-09T08:14:01+00:00" "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", "name": "tightenco/ziggy",
"version": "v2.5.2", "version": "v2.5.2",
@ -7210,146 +7486,6 @@
], ],
"time": "2025-05-01T01:14:54+00:00" "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", "name": "larastan/larastan",
"version": "v3.4.0", "version": "v3.4.0",
@ -9561,69 +9697,6 @@
], ],
"time": "2024-02-20T11:51:46+00:00" "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", "name": "spatie/data-transfer-object",
"version": "3.9.1", "version": "3.9.1",
@ -10133,78 +10206,6 @@
], ],
"time": "2025-05-02T08:36:00+00:00" "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", "name": "theseer/tokenizer",
"version": "1.2.3", "version": "1.2.3",

View File

@ -462,6 +462,11 @@
'gitlab' => \App\SourceControlProviders\Gitlab::class, 'gitlab' => \App\SourceControlProviders\Gitlab::class,
'bitbucket' => \App\SourceControlProviders\Bitbucket::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 * available php extensions

View File

@ -1,5 +1,5 @@
import { type BreadcrumbItem, type NavItem } from '@/types'; 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 { ReactNode } from 'react';
import Layout from '@/layouts/app/layout'; import Layout from '@/layouts/app/layout';
@ -24,6 +24,11 @@ const sidebarNavItems: NavItem[] = [
href: route('server-providers'), href: route('server-providers'),
icon: CloudIcon, icon: CloudIcon,
}, },
{
title: 'Source Controls',
href: route('source-controls'),
icon: CodeIcon,
},
]; ];
export default function SettingsLayout({ children, breadcrumbs }: { children: ReactNode; breadcrumbs?: BreadcrumbItem[] }) { export default function SettingsLayout({ children, breadcrumbs }: { children: ReactNode; breadcrumbs?: BreadcrumbItem[] }) {

View File

@ -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 (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>Edit</DropdownMenuItem>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit {sourceControl.name}</DialogTitle>
<DialogDescription className="sr-only">Edit source control</DialogDescription>
</DialogHeader>
<Form id="edit-source-control-form" className="p-4" onSubmit={submit}>
<FormFields>
<FormField>
<Label htmlFor="name">Name</Label>
<Input type="text" id="name" name="name" value={form.data.name} onChange={(e) => form.setData('name', e.target.value)} />
<InputError message={form.errors.name} />
</FormField>
<FormField>
<div className="flex items-center space-x-3">
<Checkbox id="global" name="global" checked={form.data.global} onClick={() => form.setData('global', !form.data.global)} />
<Label htmlFor="global">Is global (accessible in all projects)</Label>
</div>
<InputError message={form.errors.global} />
</FormField>
</FormFields>
</Form>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button form="edit-source-control-form" disabled={form.processing} onClick={submit}>
{form.processing && <LoaderCircleIcon className="animate-spin" />}
<FormSuccessful successful={form.recentlySuccessful} />
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
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 (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<DropdownMenuItem variant="destructive" onSelect={(e) => e.preventDefault()}>
Delete
</DropdownMenuItem>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete {sourceControl.name}</DialogTitle>
<DialogDescription className="sr-only">Delete source control</DialogDescription>
</DialogHeader>
<div className="space-y-2 p-4">
<p>
Are you sure you want to delete <strong>{sourceControl.name}</strong>?
</p>
<InputError message={form.errors.provider} />
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button variant="destructive" disabled={form.processing} onClick={submit}>
{form.processing && <LoaderCircleIcon className="animate-spin" />}
<FormSuccessful successful={form.recentlySuccessful} />
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export const columns: ColumnDef<SourceControl>[] = [
{
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 <div>{row.original.global ? <Badge variant="success">yes</Badge> : <Badge variant="danger">no</Badge>}</div>;
},
},
{
accessorKey: 'created_at',
header: 'Created at',
enableColumnFilter: true,
enableSorting: true,
cell: ({ row }) => {
return <DateTime date={row.original.created_at} />;
},
},
{
id: 'actions',
enableColumnFilter: false,
enableSorting: false,
cell: ({ row }) => {
return (
<div className="flex items-center justify-end">
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreVerticalIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<Edit sourceControl={row.original} />
<DropdownMenuSeparator />
<Delete sourceControl={row.original} />
</DropdownMenuContent>
</DropdownMenu>
</div>
);
},
},
];

View File

@ -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<SharedData>();
const form = useForm<Required<SourceControlForm>>({
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 (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="sm:max-w-xl">
<DialogHeader>
<DialogTitle>Connect to source control</DialogTitle>
<DialogDescription className="sr-only">Connect to a new source control</DialogDescription>
</DialogHeader>
<Form id="create-source-control-form" onSubmit={submit} className="p-4">
<FormFields>
<FormField>
<Label htmlFor="provider">Provider</Label>
<Select
value={form.data.provider}
onValueChange={(value) => {
form.setData('provider', value);
form.clearErrors();
}}
>
<SelectTrigger id="provider">
<SelectValue placeholder="Select a provider" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{providers.map((provider) => (
<SelectItem key={provider} value={provider}>
{provider}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<InputError message={form.errors.provider} />
</FormField>
<FormField>
<Label htmlFor="name">Name</Label>
<Input
type="text"
name="name"
id="name"
placeholder="Name"
value={form.data.name}
onChange={(e) => form.setData('name', e.target.value)}
/>
<InputError message={form.errors.name} />
</FormField>
{page.props.configs.source_control_providers_custom_fields[form.data.provider]?.map((item: string) => (
<FormField key={item}>
<Label htmlFor={item} className="capitalize">
{item}
</Label>
<Input
type="text"
name={item}
id={item}
value={(form.data[item as keyof SourceControlForm] as string) ?? ''}
onChange={(e) => form.setData(item as keyof SourceControlForm, e.target.value)}
/>
<InputError message={form.errors[item as keyof SourceControlForm]} />
</FormField>
))}
<FormField>
<div className="flex items-center space-x-3">
<Checkbox id="global" name="global" checked={form.data.global} onClick={() => form.setData('global', !form.data.global)} />
<Label htmlFor="global">Is global (accessible in all projects)</Label>
</div>
<InputError message={form.errors.global} />
</FormField>
</FormFields>
</Form>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="outline">
Cancel
</Button>
</DialogClose>
<Button type="button" onClick={submit} disabled={form.processing}>
{form.processing && <LoaderCircle className="animate-spin" />}
Connect
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -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<Page>();
return (
<SettingsLayout>
<Head title="Source Controls" />
<Container className="max-w-5xl">
<div className="flex items-start justify-between">
<Heading title="Source Controls" description="Here you can manage all of the source control connectinos" />
<div className="flex items-center gap-2">
<ConnectSourceControl providers={page.props.configs.source_control_providers}>
<Button>Connect</Button>
</ConnectSourceControl>
</div>
</div>
<DataTable columns={columns} data={page.props.sourceControls.data} />
</Container>
</SettingsLayout>
);
}

View File

@ -33,6 +33,10 @@ export interface Configs {
server_providers_custom_fields: { server_providers_custom_fields: {
[provider: string]: string[]; [provider: string]: string[];
}; };
source_control_providers: string[];
source_control_providers_custom_fields: {
[provider: string]: string[];
};
operating_systems: string[]; operating_systems: string[];
service_versions: { service_versions: {
[service: string]: string[]; [service: string]: string[];

11
resources/js/types/source-control.d.ts vendored Normal file
View File

@ -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;
}

View File

@ -3,11 +3,8 @@
namespace Tests\Feature; namespace Tests\Feature;
use App\Models\SourceControl; 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\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Livewire\Livewire;
use Tests\TestCase; use Tests\TestCase;
class SourceControlsTest extends TestCase class SourceControlsTest extends TestCase
@ -15,6 +12,8 @@ class SourceControlsTest extends TestCase
use RefreshDatabase; use RefreshDatabase;
/** /**
* @param array<string, mixed> $input
*
* @dataProvider data * @dataProvider data
*/ */
public function test_connect_provider(string $provider, ?string $customUrl, array $input): void 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; $input['url'] = $customUrl;
} }
Livewire::test(Index::class) $this->post(route('source-controls.store'), $input);
->callAction('connect', $input)
->assertSuccessful();
$this->assertDatabaseHas('source_controls', [ $this->assertDatabaseHas('source_controls', [
'provider' => $provider, 'provider' => $provider,
@ -69,9 +66,9 @@ public function test_delete_provider(string $provider): void
'profile' => 'test', 'profile' => 'test',
]); ]);
Livewire::test(SourceControlsList::class) $this->delete(route('source-controls.destroy', $sourceControl))
->callTableAction('delete', $sourceControl->id) ->assertSessionDoesntHaveErrors()
->assertSuccessful(); ->assertRedirect(route('source-controls'));
$this->assertSoftDeleted('source_controls', [ $this->assertSoftDeleted('source_controls', [
'id' => $sourceControl->id, 'id' => $sourceControl->id,
@ -95,9 +92,10 @@ public function test_cannot_delete_provider(string $provider): void
'source_control_id' => $sourceControl->id, 'source_control_id' => $sourceControl->id,
]); ]);
Livewire::test(SourceControlsList::class) $this->delete(route('source-controls.destroy', $sourceControl))
->callTableAction('delete', $sourceControl->id) ->assertSessionHasErrors([
->assertNotified('This source control is being used by a site.'); 'source_control' => 'This source control is being used by a site.',
]);
$this->assertNotSoftDeleted('source_controls', [ $this->assertNotSoftDeleted('source_controls', [
'id' => $sourceControl->id, 'id' => $sourceControl->id,
@ -105,6 +103,8 @@ public function test_cannot_delete_provider(string $provider): void
} }
/** /**
* @param array<string, mixed> $input
*
* @dataProvider data * @dataProvider data
*/ */
public function test_edit_source_control(string $provider, ?string $url, array $input): void 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, 'url' => $url,
]); ]);
Livewire::test(SourceControlsList::class) $input['name'] = 'new-name';
->callTableAction('edit', $sourceControl->id, [
'name' => 'new-name', $this->patch(route('source-controls.update', $sourceControl), $input)
'token' => 'test', // for GitHub and Gitlab ->assertSessionDoesntHaveErrors();
'username' => 'test', // for Bitbucket
'password' => 'test', // for Bitbucket
'url' => $url, // for Gitlab
])
->assertSuccessful();
$sourceControl->refresh(); $sourceControl->refresh();
@ -136,6 +131,9 @@ public function test_edit_source_control(string $provider, ?string $url, array $
$this->assertEquals($url, $sourceControl->url); $this->assertEquals($url, $sourceControl->url);
} }
/**
* @return array<string, array<int, mixed>>
*/
public static function data(): array public static function data(): array
{ {
return [ return [