Skip to content

Commit

Permalink
Merge pull request #372 from dshoreman/project-progress
Browse files Browse the repository at this point in the history
  • Loading branch information
dshoreman authored Apr 19, 2021
2 parents 8a93a7a + 9b3c8be commit 622be73
Show file tree
Hide file tree
Showing 21 changed files with 567 additions and 291 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0


## [Unreleased]
### Added
* New progress modal to show steps and progress of project creation

### Fixed
* Removed a rogue '>' from the New Project confirmation page
* Projects no longer prompt to discard changes on successful creation


## [0.12.0] - 2021-04-04
Expand Down
26 changes: 1 addition & 25 deletions app/Http/Controllers/Projects/CreateProject.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,16 @@
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
use Servidor\Http\Requests\CreateProjectRequest;
use Servidor\Projects\Application;
use Servidor\Projects\Project;
use Servidor\Projects\Redirect;

class CreateProject extends Controller
{
public function __invoke(CreateProjectRequest $request): JsonResponse
{
$data = $request->validated();
$project = new Project($request->validated());

$project = new Project([
'name' => $data['name'],
'is_enabled' => $data['is_enabled'] ?? false,
]);
$project->save();

$project->applications()->saveMany(array_map(function (array $app): Application {
return new Application([
'template' => $app['template'] ?? '',
'domain_name' => $app['domain'] ?? '',
'source_provider' => $app['provider'] ?? '',
'source_repository' => $app['repository'] ?? '',
'source_branch' => $app['branch'] ?? '',
]);
}, (array) ($data['applications'] ?? [])));

$project->redirects()->saveMany(array_map(function (array $redirect): Redirect {
return new Redirect([
'domain_name' => $redirect['domain'],
'target' => $redirect['target'],
'type' => $redirect['type'],
]);
}, (array) ($data['redirects'] ?? [])));

return response()->json(
$project->load(['applications', 'redirects']),
Response::HTTP_CREATED
Expand Down
21 changes: 21 additions & 0 deletions app/Http/Controllers/Projects/CreateProjectApp.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Servidor\Http\Controllers\Projects;

use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
use Servidor\Http\Requests\Projects\NewProjectApp;
use Servidor\Projects\Application;
use Servidor\Projects\Project;

class CreateProjectApp extends Controller
{
public function __invoke(NewProjectApp $request, Project $project): JsonResponse
{
$app = new Application($request->validated());

$project->applications()->save($app);

return response()->json($app, Response::HTTP_CREATED);
}
}
21 changes: 21 additions & 0 deletions app/Http/Controllers/Projects/CreateProjectRedirect.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Servidor\Http\Controllers\Projects;

use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
use Servidor\Http\Requests\Projects\NewProjectRedirect;
use Servidor\Projects\Project;
use Servidor\Projects\Redirect;

class CreateProjectRedirect extends Controller
{
public function __invoke(NewProjectRedirect $request, Project $project): JsonResponse
{
$redirect = new Redirect($request->validated());

$project->redirects()->save($redirect);

return response()->json($redirect, Response::HTTP_CREATED);
}
}
67 changes: 0 additions & 67 deletions app/Http/Requests/CreateProjectRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,81 +3,14 @@
namespace Servidor\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Validator;
use Servidor\Projects\Application;
use Servidor\Rules\Domain;

class CreateProjectRequest extends FormRequest
{
private const BRANCH_CMD = 'git ls-remote --heads --exit-code "%s" %s';

private const ERR_NO_REFS = "This branch doesn't exist.";
private const ERR_NON_ZERO = 'Branch listing failed. Is this repo valid?';
private const ERR_NOT_FOUND = "This repo couldn't be found. Does it require auth?";

private const GIT_NO_REFS = 2;
private const GIT_NOT_FOUND = 128;

public function authorize(): bool
{
return (bool) $this->user();
}

public function rules(): array
{
return [
'name' => 'required|string|unique:projects,name',
'applications' => 'array',
'applications.*.template' => 'required|in:html,php,laravel',
'applications.*.domain' => [new Domain()],
'applications.*.provider' => 'required|in:github,bitbucket',
'applications.*.repository' => 'required|nullable|regex:_^([a-z-]+)/([a-z-]+)$_i',
'applications.*.branch' => 'nullable|string',
'redirects' => 'array',
'redirects.*.domain' => ['required', new Domain()],
'redirects.*.target' => 'required|string',
'redirects.*.type' => 'required|integer',
'is_enabled' => 'boolean',
];
}

public function withValidator(Validator $validator): void
{
$validator->after(function (Validator $validator): void {
$apps = $validator->getData()['applications'] ?? [];

if (!is_array($apps) || empty($apps)) {
return;
}

/**
* @var array{provider: string, repository: string, branch?: string}
*/
$app = $apps[0];

if (isset($app['repository'], $app['provider'])) {
$this->validateAppRepository($validator, $app);
}
});
}

/** @param array{provider: string, repository: string, branch?: string} $app */
private function validateAppRepository(Validator $validator, array $app): void
{
$branch = $app['branch'] ?? '';
$branch = $branch ? escapeshellarg($branch) : '';
$repo = Application::SOURCE_PROVIDERS[$app['provider']];
$repo = str_replace('{repo}', $app['repository'], $repo);

exec(sprintf(self::BRANCH_CMD, $repo, $branch), $_, $status);

if (self::GIT_NO_REFS === $status) {
$validator->errors()->add('applications.0.branch', self::ERR_NO_REFS);
} elseif (0 !== $status) {
$message = self::GIT_NOT_FOUND === $status
? self::ERR_NOT_FOUND : self::ERR_NON_ZERO;

$validator->errors()->add('applications.0.repository', $message);
}
}
}
78 changes: 78 additions & 0 deletions app/Http/Requests/Projects/NewProjectApp.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

namespace Servidor\Http\Requests\Projects;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Validator;
use Servidor\Projects\Application;
use Servidor\Rules\Domain;

class NewProjectApp extends FormRequest
{
private const BRANCH_CMD = 'git ls-remote --heads --exit-code "%s" %s';

private const ERR_NO_REFS = "This branch doesn't exist.";
private const ERR_NON_ZERO = 'Branch listing failed. Is this repo valid?';
private const ERR_NOT_FOUND = "This repo couldn't be found. Does it require auth?";

private const GIT_NO_REFS = 2;
private const GIT_NOT_FOUND = 128;

public function rules(): array
{
return [
'template' => 'required|in:html,php,laravel',
'domain' => [new Domain()],
'provider' => 'required|in:github,bitbucket',
'repository' => 'required|nullable|regex:_^([a-z-]+)/([a-z-]+)$_i',
'branch' => 'nullable|string',
];
}

public function validated(): array
{
$data = parent::validated();

return [
'template' => $data['template'],
'domain_name' => $data['domain'],
'source_provider' => $data['provider'],
'source_repository' => $data['repository'],
'source_branch' => $data['branch'] ?? '',
];
}

public function withValidator(Validator $validator): void
{
$validator->after(function (Validator $validator): void {
/**
* @var array{provider: string, repository: string, branch?: string}
*/
$app = $validator->getData();

if (isset($app['repository'], $app['provider'])) {
$this->validateAppRepository($validator, $app);
}
});
}

/** @param array{provider: string, repository: string, branch?: string} $app */
private function validateAppRepository(Validator $validator, array $app): void
{
$branch = $app['branch'] ?? '';
$branch = $branch ? escapeshellarg($branch) : '';
$repo = Application::SOURCE_PROVIDERS[$app['provider']];
$repo = str_replace('{repo}', $app['repository'], $repo);

exec(sprintf(self::BRANCH_CMD, $repo, $branch), $_, $status);

if (self::GIT_NO_REFS === $status) {
$validator->errors()->add('branch', self::ERR_NO_REFS);
} elseif (0 !== $status) {
$message = self::GIT_NOT_FOUND === $status
? self::ERR_NOT_FOUND : self::ERR_NON_ZERO;

$validator->errors()->add('repository', $message);
}
}
}
28 changes: 28 additions & 0 deletions app/Http/Requests/Projects/NewProjectRedirect.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Servidor\Http\Requests\Projects;

use Illuminate\Foundation\Http\FormRequest;
use Servidor\Rules\Domain;

class NewProjectRedirect extends FormRequest
{
public function rules(): array
{
return [
'domain' => ['required', new Domain()],
'target' => 'required|string',
'type' => 'required|integer',
];
}

public function validated(): array
{
$data = parent::validated();

$data['domain_name'] = (string) $data['domain'];
unset($data['domain']);

return $data;
}
}
31 changes: 31 additions & 0 deletions resources/js/components/ProgressModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<template>
<sui-modal size="tiny" v-model="visible">
<sui-modal-header>{{ title }}</sui-modal-header>
<sui-progress attached top :percent="done" />
<sui-modal-content>
<sui-list>
<sui-list-item v-for="step in steps" :key="step.name">
<sui-icon :name="step.icon" color="green" size="large" />
<sui-list-content>
{{ step.text }}
</sui-list-content>
</sui-list-item>
</sui-list>
</sui-modal-content>
</sui-modal>
</template>

<script>
import { mapGetters } from 'vuex';
export default {
computed: {
...mapGetters({
done: 'progress/done',
title: 'progress/title',
steps: 'progress/steps',
visible: 'progress/visible',
}),
},
};
</script>
2 changes: 1 addition & 1 deletion resources/js/components/Projects/ConfirmationText.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div v-if="app.template != 'Clean Slate'">>
<div v-if="app.template != 'Clean Slate'">
<p>
When you continue, the new project will be created with a
<strong>{{ app.template }}</strong> application.
Expand Down
5 changes: 5 additions & 0 deletions resources/js/pages/Projects/NewProject.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
@created="create" />
</sui-segment>

<progress-modal />

</sui-grid-column>

<discard-prompt ref="discardProject" @leavebypass="bypassLeaveHandler = true" />
Expand All @@ -54,6 +56,7 @@ import ConfirmationForm from '../../components/Projects/ConfirmationForm';
import ConfirmationText from '../../components/Projects/ConfirmationText';
import DiscardPrompt from '../../components/Projects/DiscardPrompt.vue';
import DomainForm from '../../components/Projects/Apps/DomainForm';
import ProgressModal from '../../components/ProgressModal';
import RedirectForm from '../../components/Projects/Apps/RedirectForm';
import SourceSelector from '../../components/Projects/Apps/SourceSelector';
import StepList from '../../components/Projects/StepList';
Expand All @@ -68,6 +71,7 @@ export default {
ConfirmationText,
DiscardPrompt,
DomainForm,
ProgressModal,
RedirectForm,
StepList,
SourceSelector,
Expand Down Expand Up @@ -224,6 +228,7 @@ export default {
}
this.$store.dispatch('projects/create', this.project).then(response => {
this.bypassLeaveHandler = true;
this.$router.push({ name: 'projects.view', params: { id: response.data.id }});
}).catch(error => {
const res = error.response, validationError = 422;
Expand Down
2 changes: 2 additions & 0 deletions resources/js/store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Database from './modules/Database';
import FileEditor from './modules/FileEditor';
import FileManager from './modules/FileManager';
import Group from './modules/System/Group';
import Progress from './modules/Progress';
import Project from './modules/Project';
import User from './modules/System/User';
import Vue from 'vue';
Expand All @@ -13,6 +14,7 @@ Vue.use(VueX);
export default new VueX.Store({
modules: {
Auth,
progress: Progress,
projects: Project,
databases: Database,
files: FileManager,
Expand Down
Loading

0 comments on commit 622be73

Please sign in to comment.