Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions app/Http/Controllers/CreateProjectController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace App\Http\Controllers;

use App\Models\Project;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\View\View;

final class CreateProjectController extends AbstractController
{
public function __invoke(Request $request): View
{
Gate::authorize('create', Project::class);

return $this->vue('create-project-page', 'Create Project', [
'max-project-visibility' => $request->user()->admin ?? false ? 'PUBLIC' : config('cdash.max_project_visibility'),
]);
}
}
2 changes: 1 addition & 1 deletion app/cdash/app/Model/Project.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public function Save(): bool
$project = EloquentProject::findOrNew($this->Id);
$project->fill([
'name' => $this->Name ?? '',
'description' => $this->Description ?? '',
'description' => $this->Description,
'homeurl' => $this->HomeUrl ?? '',
'cvsurl' => $this->CvsUrl ?? '',
'documentationurl' => $this->DocumentationUrl ?? '',
Expand Down
2 changes: 2 additions & 0 deletions app/cdash/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,8 @@ add_browser_test(/Browser/Pages/ProjectMembersPageTest)

add_browser_test(/Browser/Pages/ProjectBuildsPageTest)

add_browser_test(/Browser/Pages/CreateProjectPageTest)

add_browser_test(/Browser/Pages/ProjectsPageTest)
set_property(TEST /Browser/Pages/ProjectsPageTest APPEND PROPERTY RUN_SERIAL TRUE)

Expand Down
1 change: 1 addition & 0 deletions config/cdash.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
'allow_submit_only_tokens' => env('ALLOW_SUBMIT_ONLY_TOKENS', true),
'unlimited_projects' => $unlimited_projects,
'user_create_projects' => env('USER_CREATE_PROJECTS', false),
// Options: PUBLIC, PROTECTED, PRIVATE
// Defaults to public. Only meaningful if USER_CREATE_PROJECT=true.
'max_project_visibility' => env('MAX_PROJECT_VISIBILITY', 'PUBLIC'),
'require_authenticated_submissions' => env('REQUIRE_AUTHENTICATED_SUBMISSIONS', false),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

use Illuminate\Database\Migrations\Migration;

return new class extends Migration {
public function up(): void
{
DB::statement('ALTER TABLE project ALTER COLUMN description DROP DEFAULT');
DB::statement('ALTER TABLE project ALTER COLUMN description DROP NOT NULL');
DB::update("UPDATE project SET description = NULL WHERE description = ''");
}

public function down(): void
{
}
};
4 changes: 2 additions & 2 deletions graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ type Project {
name: String! @filterable

"Description."
description: String!
description: String

"Homepage for this project."
homeurl: Url @deprecated(reason: "Use 'homeUrl' instead.")
Expand Down Expand Up @@ -213,7 +213,7 @@ input CreateProjectInput {
name: String! @rules(apply: ["App\\Rules\\ProjectNameRule"])

"Description."
description: String!
description: String

"Project homepage"
homeurl: Url @deprecated(reason: "Use 'homeUrl' instead.") @rules(apply: ["prohibits:homeUrl"])
Expand Down
2 changes: 2 additions & 0 deletions resources/js/vue/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const BuildTargetsPage = Vue.defineAsyncComponent(() => import('./components/Bui
const BuildCommandsPage = Vue.defineAsyncComponent(() => import('./components/BuildCommandsPage.vue'));
const CoverageFilePage = Vue.defineAsyncComponent(() => import('./components/CoverageFilePage.vue'));
const BuildCoveragePage = Vue.defineAsyncComponent(() => import('./components/BuildCoveragePage.vue'));
const CreateProjectPage = Vue.defineAsyncComponent(() => import('./components/CreateProjectPage.vue'));

const cdash_components = {
BuildConfigure,
Expand Down Expand Up @@ -59,6 +60,7 @@ const cdash_components = {
BuildCommandsPage,
CoverageFilePage,
BuildCoveragePage,
CreateProjectPage,
};

const app = Vue.createApp({
Expand Down
239 changes: 239 additions & 0 deletions resources/js/vue/components/CreateProjectPage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
<template>
<div
class="tw-flex tw-flex-row tw-w-full tw-justify-center"
data-test="create-project-page"
>
<div class="tw-w-full sm:tw-w-3/4 lg:tw-w-1/2 tw-flex tw-flex-col tw-gap-2">
<div
v-if="fatalError"
role="alert"
class="tw-alert tw-alert-error"
>
<font-awesome-icon
:icon="FA.faCircleXmark"
class="tw-h-6 tw-w-6"
/>
<span>{{ fatalError }}</span>
</div>
<label class="tw-form-control tw-w-full">
<span class="tw-label tw-label-text tw-font-bold">
Project Name
</span>
<input
v-model="name"
type="text"
class="tw-input tw-input-bordered tw-w-full"
:class="{'tw-input-error': validationErrors.name}"
data-test="project-name-input"
>
<span
v-if="validationErrors.name"
class="tw-label tw-text-error"
data-test="project-name-validation-errors"
>
{{ validationErrors.name[0] }}
</span>
</label>
<label class="tw-form-control tw-w-full">
<span class="tw-label tw-label-textt tw-font-bold">
Description
</span>
<textarea
v-model="description"
class="tw-textarea tw-textarea-bordered tw-h-24 tw-w-full"
:class="{'tw-textarea-error': validationErrors.description}"
data-test="project-description-input"
/>
<span
v-if="validationErrors.description"
class="tw-label tw-text-error"
>
{{ validationErrors.description[0] }}
</span>
</label>
<div class="tw-form-control tw-w-full">
<span class="tw-label tw-label-text tw-font-bold">
Visibility
</span>
<div class="tw-w-full tw-flex tw-flex-col tw-gap-1">
<label class="tw-flex tw-items-center tw-gap-2">
<input
v-model="visibility"
type="radio"
name="visibility"
class="tw-radio"
value="PUBLIC"
data-test="project-visibility-public"
:disabled="maxProjectVisibility === 'PRIVATE' || maxProjectVisibility === 'PROTECTED'"
>
<div>
<span class="tw-label-text">
<font-awesome-icon :icon="FA.faEarthAmericas" /> Public
</span>
<div class="tw-text-xs tw-text-neutral-500">
Does not require authentication to access.
</div>
</div>
</label>
<label class="tw-flex tw-items-center tw-gap-2">
<input
v-model="visibility"
type="radio"
name="visibility"
class="tw-radio"
value="PROTECTED"
data-test="project-visibility-protected"
:disabled="maxProjectVisibility === 'PRIVATE'"
>
<div>
<span class="tw-label-text">
<font-awesome-icon :icon="FA.faShieldHalved" /> Protected
</span>
<div class="tw-text-xs tw-text-neutral-500">
Access limited to authenticated users.
</div>
</div>
</label>
<label class="tw-flex tw-items-center tw-gap-2">
<input
v-model="visibility"
type="radio"
name="visibility"
class="tw-radio"
value="PRIVATE"
data-test="project-visibility-private"
>
<div>
<span class="tw-label-text">
<font-awesome-icon :icon="FA.faLock" /> Private
</span>
<div class="tw-text-xs tw-text-neutral-500">
Requires access to be granted explicitly.
</div>
</div>
</label>
</div>
</div>
<div class="tw-form-control tw-w-full">
<span class="tw-label tw-label-text tw-font-bold">
Submission Authentication
</span>
<label class="tw-flex tw-items-center tw-gap-2">
<input
v-model="requireAuthenticatedSubmissions"
type="checkbox"
class="tw-checkbox"
data-test="project-authenticated-submissions-input"
>
<span class="tw-label-text">
Require submissions to provide a valid authentication token.
</span>
</label>
</div>
<div class="tw-flex tw-justify-start tw-mt-4">
<button
class="tw-btn tw-btn-success"
data-test="create-project-button"
@click="createProject"
>
Create Project
</button>
</div>
</div>
</div>
</template>

<script>
import {
faCircleXmark,
faEarthAmericas,
faShieldHalved,
faLock,
} from '@fortawesome/free-solid-svg-icons';
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome';
import gql from 'graphql-tag';

export default {
components: {FontAwesomeIcon},

props: {
maxProjectVisibility: {
type: String,
required: true,
},
},

data() {
return {
name: '',
description: '',
visibility: 'PRIVATE',
requireAuthenticatedSubmissions: false,
validationErrors: {},
fatalError: null,
};
},

computed: {
FA() {
return {
faCircleXmark,
faEarthAmericas,
faShieldHalved,
faLock,
};
},
},

methods: {
async createProject() {
this.validationErrors = {};
this.fatalError = null;
try {
const response = await this.$apollo.mutate({
mutation: gql`
mutation createProject($name: String!, $description: String, $visibility: ProjectVisibility!, $authenticateSubmissions: Boolean!) {
createProject(input: {
name: $name,
description: $description,
visibility: $visibility,
authenticateSubmissions: $authenticateSubmissions,
}) {
id
}
}
`,
variables: {
name: this.name,
description: this.description,
visibility: this.visibility,
authenticateSubmissions: this.requireAuthenticatedSubmissions,
},
});

if (response.data.createProject) {
window.location.href = `${this.$baseURL}/projects/${response.data.createProject.id}/edit`;
}
}
catch (error) {
if (error.graphQLErrors) {
error.graphQLErrors.forEach(e => {
if (e.extensions && e.extensions.validation) {
this.validationErrors = Object.keys(e.extensions.validation).reduce((acc, key) => {
acc[key.replace('input.', '')] = e.extensions.validation[key];
return acc;
}, {});
}
else {
this.fatalError = e.message;
}
});
}
else {
this.fatalError = error.message;
}
}
},
},
};
</script>
3 changes: 2 additions & 1 deletion routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/

use App\Http\Controllers\CoverageFileController;
use App\Http\Controllers\CreateProjectController;
use App\Http\Controllers\GlobalInvitationController;
use App\Http\Controllers\ProjectInvitationController;
use App\Models\DynamicAnalysis;
Expand Down Expand Up @@ -163,7 +164,7 @@
->whereNumber('id');
Route::permanentRedirect('/project/{id}/edit', url('/projects/{id}/edit'));

Route::get('/projects/new', 'EditProjectController@create');
Route::get('/projects/new', CreateProjectController::class);
Route::permanentRedirect('/project/new', url('/projects/new'));

Route::get('/projects/{id}/testmeasurements', 'ManageMeasurementsController@show')
Expand Down
Loading