diff --git a/app/DTO/WorkflowActorDTO.php b/app/DTO/WorkflowActorDTO.php new file mode 100644 index 00000000..f97ecd6e --- /dev/null +++ b/app/DTO/WorkflowActorDTO.php @@ -0,0 +1,23 @@ +get(); + $data = AuthorResource::collection($actors); + + return Inertia::render("Authors", ["data" => $data]); + } +} diff --git a/app/Http/Controllers/RepositoriesController.php b/app/Http/Controllers/RepositoriesController.php new file mode 100644 index 00000000..853c8cb3 --- /dev/null +++ b/app/Http/Controllers/RepositoriesController.php @@ -0,0 +1,23 @@ +get(); + $data = RepositoryResource::collection($repositories); + + return Inertia::render("Repositories", ["data" => $data]); + } +} diff --git a/app/Http/Controllers/TableController.php b/app/Http/Controllers/TableController.php new file mode 100644 index 00000000..5d3bcf89 --- /dev/null +++ b/app/Http/Controllers/TableController.php @@ -0,0 +1,23 @@ +get(); + $data = WorkflowJobResource::collection($jobs); + + return Inertia::render("Table", ["data" => $data]); + } +} diff --git a/app/Http/Integrations/Requests/GetWorkflowJobsRequest.php b/app/Http/Integrations/Requests/GetWorkflowJobsRequest.php index 5ef46c76..173eca9b 100644 --- a/app/Http/Integrations/Requests/GetWorkflowJobsRequest.php +++ b/app/Http/Integrations/Requests/GetWorkflowJobsRequest.php @@ -41,6 +41,10 @@ public function createDtoFromResponse(Response $response): Collection if ($response->json() !== null) { foreach ($response->json()["jobs"] as $data) { + if ($data["conclusion"] === "skipped") { + continue; + } + $jobTime = $calculateJobTimeService->calculate($data["started_at"], $data["completed_at"]); $runnerData = $getRunnerDataService->getRunnerData($data["labels"]); diff --git a/app/Http/Integrations/Requests/GetWorkflowRunsRequest.php b/app/Http/Integrations/Requests/GetWorkflowRunsRequest.php index 7ea059de..583cff70 100644 --- a/app/Http/Integrations/Requests/GetWorkflowRunsRequest.php +++ b/app/Http/Integrations/Requests/GetWorkflowRunsRequest.php @@ -5,6 +5,7 @@ namespace App\Http\Integrations\Requests; use App\DTO\RepositoryDTO; +use App\DTO\WorkflowActorDTO; use App\DTO\WorkflowRunDTO; use App\Models\Organization; use App\Models\Repository; @@ -46,6 +47,7 @@ public function createDtoFromResponse(Response $response): Collection name: $data["name"], repositoryId: $repository->id, createdAt: new DateTime($data["created_at"]), + actor: WorkflowActorDTO::createFromArray($data["actor"]), )); } } diff --git a/app/Http/Resources/AuthorResource.php b/app/Http/Resources/AuthorResource.php new file mode 100644 index 00000000..850c4176 --- /dev/null +++ b/app/Http/Resources/AuthorResource.php @@ -0,0 +1,26 @@ + + */ + public function toArray(Request $request): array + { + return [ + "id" => $this->id, + "name" => $this->name, + "github_id" => $this->github_id, + "avatar_url" => $this->avatar_url, + "minutes" => $this->totalMinutes, + "price" => $this->totalPrice, + ]; + } +} diff --git a/app/Http/Resources/RepositoryResource.php b/app/Http/Resources/RepositoryResource.php new file mode 100644 index 00000000..97582fb5 --- /dev/null +++ b/app/Http/Resources/RepositoryResource.php @@ -0,0 +1,26 @@ + + */ + public function toArray(Request $request): array + { + return [ + "id" => $this->id, + "name" => $this->name, + "organization" => $this->organization->name, + "avatar_url" => $this->organization->avatar_url, + "minutes" => $this->totalMinutes, + "price" => $this->totalPrice, + ]; + } +} diff --git a/app/Http/Resources/WorkflowJobResource.php b/app/Http/Resources/WorkflowJobResource.php new file mode 100644 index 00000000..d5b54c25 --- /dev/null +++ b/app/Http/Resources/WorkflowJobResource.php @@ -0,0 +1,31 @@ + + */ + public function toArray(Request $request): array + { + return [ + "id" => $this->workflowRun->id, + "date" => $this->workflowRun->github_created_at, + "organization" => $this->workflowRun->repository->organization->name, + "repository" => $this->workflowRun->repository->name, + "repository_id" => $this->workflowRun->repository->id, + "minutes" => $this->minutes, + "price_per_minute" => $this->price_per_unit, + "total_price" => $this->price, + "workflow" => $this->fullName, + "os" => $this->runner_os, + "actor" => $this->workflowRun->workflowActor, + ]; + } +} diff --git a/app/Models/Organization.php b/app/Models/Organization.php index feb9ce1d..56b191f2 100644 --- a/app/Models/Organization.php +++ b/app/Models/Organization.php @@ -4,11 +4,24 @@ namespace App\Models; +use Carbon\Carbon; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; +/** + * @property int $id + * @property int $github_id + * @property string $name + * @property string $avatar_url + * @property Carbon $created_at + * @property Carbon $updated_at + * + * @property Collection $users + * @property Collection $repositories + */ class Organization extends Model { use HasFactory; diff --git a/app/Models/Repository.php b/app/Models/Repository.php index ad07e5f0..32a70f2a 100644 --- a/app/Models/Repository.php +++ b/app/Models/Repository.php @@ -4,10 +4,31 @@ namespace App\Models; +use Carbon\Carbon; +use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasManyThrough; +/** + * @property int $id + * @property int $github_id + * @property string $name + * @property int $organization_id + * @property boolean $is_private + * @property Carbon $created_at + * @property Carbon $updated_at + * + * @property float $totalMinutes + * @property float $totalPrice + * + * @property Organization $organization + * @property Collection $workflowRuns + * @property Collection $workflowJobs + */ class Repository extends Model { use HasFactory; @@ -23,4 +44,24 @@ public function workflowRuns(): HasMany { return $this->HasMany(WorkflowRun::class); } + + public function workflowJobs(): HasManyThrough + { + return $this->HasManyThrough(WorkflowJob::class, WorkflowRun::class); + } + + public function organization(): BelongsTo + { + return $this->BelongsTo(Organization::class); + } + + protected function totalMinutes(): Attribute + { + return Attribute::get(fn(): float => $this->workflowJobs->sum("minutes")); + } + + protected function totalPrice(): Attribute + { + return Attribute::get(fn(): float => $this->workflowJobs->sum("price")); + } } diff --git a/app/Models/User.php b/app/Models/User.php index 46339a8f..f890ffc9 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -5,6 +5,7 @@ namespace App\Models; use Carbon\Carbon; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Foundation\Auth\User as Authenticatable; @@ -18,6 +19,8 @@ * @property Carbon $email_verified_at * @property Carbon $created_at * @property Carbon $updated_at + * + * @property Collection $organizations */ class User extends Authenticatable { diff --git a/app/Models/WorkflowActor.php b/app/Models/WorkflowActor.php new file mode 100644 index 00000000..fb72b3a3 --- /dev/null +++ b/app/Models/WorkflowActor.php @@ -0,0 +1,58 @@ + $workflowRuns + * @property Collection $workflowJobs + */ +class WorkflowActor extends Model +{ + use HasFactory; + + protected $fillable = [ + "name", + "github_id", + "avatar_url", + ]; + + public function workflowRuns(): HasMany + { + return $this->hasMany(WorkflowRun::class); + } + + public function workflowJobs(): HasManyThrough + { + return $this->HasManyThrough(WorkflowJob::class, WorkflowRun::class); + } + + protected function totalMinutes(): Attribute + { + return Attribute::get(fn(): float => $this->workflowJobs->sum("minutes")); + } + + protected function totalPrice(): Attribute + { + return Attribute::get(fn(): float => $this->workflowJobs->sum("price")); + } +} diff --git a/app/Models/WorkflowJob.php b/app/Models/WorkflowJob.php index 6dd0e3c0..945874bf 100644 --- a/app/Models/WorkflowJob.php +++ b/app/Models/WorkflowJob.php @@ -4,9 +4,30 @@ namespace App\Models; +use Carbon\Carbon; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +/** + * @property int $id + * @property int $github_id + * @property string $name + * @property int $workflow_run_id + * @property string $runner_os + * @property string $runner_type + * @property int $minutes + * @property int $multiplier + * @property float $price_per_unit + * @property float $price + * @property Carbon $created_at + * @property Carbon $updated_at + * + * @property string $fullName + * + * @property WorkflowRun $workflowRun + */ class WorkflowJob extends Model { use HasFactory; @@ -21,4 +42,19 @@ class WorkflowJob extends Model "multiplier", "price_per_unit", ]; + + public function workflowRun(): BelongsTo + { + return $this->belongsTo(WorkflowRun::class); + } + + protected function price(): Attribute + { + return Attribute::get(fn(): float => $this->minutes * $this->price_per_unit); + } + + protected function fullName(): Attribute + { + return Attribute::get(fn(): string => $this->workflowRun->name . " - " . $this->name); + } } diff --git a/app/Models/WorkflowRun.php b/app/Models/WorkflowRun.php index 9966c9bf..c3d44e4d 100644 --- a/app/Models/WorkflowRun.php +++ b/app/Models/WorkflowRun.php @@ -4,10 +4,27 @@ namespace App\Models; +use Carbon\Carbon; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +/** + * @property int $id + * @property int $github_id + * @property string $name + * @property int $repository_id + * @property int $workflow_actor_id + * @property Carbon $github_created_at + * @property Carbon $created_at + * @property Carbon $updated_at + * + * @property Repository $repository + * @property WorkflowActor $workflowActor + * @property Collection $workflowJobs + */ class WorkflowRun extends Model { use HasFactory; @@ -17,8 +34,19 @@ class WorkflowRun extends Model "name", "repository_id", "github_created_at", + "workflow_actor_id", ]; + public function repository(): BelongsTo + { + return $this->belongsTo(Repository::class); + } + + public function workflowActor(): BelongsTo + { + return $this->belongsTo(WorkflowActor::class); + } + public function workflowJobs(): HasMany { return $this->HasMany(WorkflowJob::class); diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index dfdc1c7a..fcd238e8 100755 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,6 +4,7 @@ namespace App\Providers; +use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -14,5 +15,6 @@ public function register(): void public function boot(): void { + JsonResource::withoutWrapping(); } } diff --git a/app/Services/FetchWorkflowRunsService.php b/app/Services/FetchWorkflowRunsService.php index 9b676129..e5463eb0 100644 --- a/app/Services/FetchWorkflowRunsService.php +++ b/app/Services/FetchWorkflowRunsService.php @@ -5,11 +5,13 @@ namespace App\Services; use App\DTO\RepositoryDTO; +use App\DTO\WorkflowRunDTO; use App\Exceptions\FetchingWorkflowRunsErrorException; use App\Http\Integrations\GithubConnector; use App\Http\Integrations\Requests\GetWorkflowRunsRequest; use App\Models\Organization; use App\Models\User; +use App\Models\WorkflowActor; use App\Models\WorkflowRun; use Exception; use Illuminate\Support\Collection; @@ -51,14 +53,24 @@ public function fetchWorkflowRuns(RepositoryDTO $repositoryDto, int $userId): Co } } + /** + * @param Collection $workflowRuns + */ public function storeWorkflowRuns(Collection $workflowRuns): void { if (!$workflowRuns->isEmpty()) { foreach ($workflowRuns as $workflowRunDto) { + $actor = WorkflowActor::firstOrCreate(["github_id" => $workflowRunDto->actor->githubId], [ + "github_id" => $workflowRunDto->actor->githubId, + "name" => $workflowRunDto->actor->name, + "avatar_url" => $workflowRunDto->actor->avatarUrl, + ]); + WorkflowRun::firstOrCreate([ "github_id" => $workflowRunDto->githubId, "name" => $workflowRunDto->name, "repository_id" => $workflowRunDto->repositoryId, + "workflow_actor_id" => $actor->id, "github_created_at" => $workflowRunDto->createdAt, ]); } diff --git a/database/factories/WorkflowActorFactory.php b/database/factories/WorkflowActorFactory.php new file mode 100644 index 00000000..43d23f5d --- /dev/null +++ b/database/factories/WorkflowActorFactory.php @@ -0,0 +1,19 @@ + $this->faker->unique()->randomNumber(), + "name" => $this->faker->word(), + "avatar_url" => $this->faker->url(), + ]; + } +} diff --git a/database/factories/WorkflowRunFactory.php b/database/factories/WorkflowRunFactory.php index 32fb1d75..9aefe86c 100644 --- a/database/factories/WorkflowRunFactory.php +++ b/database/factories/WorkflowRunFactory.php @@ -5,6 +5,7 @@ namespace Database\Factories; use App\Models\Repository; +use App\Models\WorkflowActor; use Illuminate\Database\Eloquent\Factories\Factory; class WorkflowRunFactory extends Factory @@ -16,6 +17,7 @@ public function definition(): array "name" => $this->faker->word(), "repository_id" => Repository::factory(), "github_created_at" => $this->faker->iso8601(), + "workflow_actor_id" => WorkflowActor::factory(), ]; } } diff --git a/database/migrations/2024_08_05_062019_create_workflow_actors_table.php b/database/migrations/2024_08_05_062019_create_workflow_actors_table.php new file mode 100644 index 00000000..3093adfe --- /dev/null +++ b/database/migrations/2024_08_05_062019_create_workflow_actors_table.php @@ -0,0 +1,25 @@ +id(); + $table->bigInteger("github_id"); + $table->string("name"); + $table->string("avatar_url"); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists("workflow_actors"); + } +}; diff --git a/database/migrations/2024_08_05_062055_add_workflow_actor_id_to_workflow_runs_table.php b/database/migrations/2024_08_05_062055_add_workflow_actor_id_to_workflow_runs_table.php new file mode 100644 index 00000000..a323c5f8 --- /dev/null +++ b/database/migrations/2024_08_05_062055_add_workflow_actor_id_to_workflow_runs_table.php @@ -0,0 +1,24 @@ +bigInteger("workflow_actor_id"); + $table->foreign("workflow_actor_id")->references("id")->on("workflow_actors")->onDelete("cascade"); + }); + } + + public function down(): void + { + Schema::table("workflow_runs", function (Blueprint $table): void { + $table->dropColumn("workflow_actor_id"); + }); + } +}; diff --git a/package-lock.json b/package-lock.json index 74d051a5..ed33cf73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@tailwindcss/typography": "^0.5.10", "laravel-vite-plugin": "^1.0.2", "lodash": "^4.17.21", + "moment": "^2.30.1", "tailwindcss": "^3.4.1", "vue": "^3.4.21" }, @@ -4260,6 +4261,14 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/package.json b/package.json index 7e6915cc..9466db82 100755 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@tailwindcss/typography": "^0.5.10", "laravel-vite-plugin": "^1.0.2", "lodash": "^4.17.21", + "moment": "^2.30.1", "tailwindcss": "^3.4.1", "vue": "^3.4.21" }, diff --git a/resources/assets/images/icon.png b/resources/assets/images/icon.png new file mode 100644 index 00000000..7f58f57e Binary files /dev/null and b/resources/assets/images/icon.png differ diff --git a/resources/assets/images/units/ubuntu.png b/resources/assets/images/units/ubuntu.png new file mode 100644 index 00000000..3737cd65 Binary files /dev/null and b/resources/assets/images/units/ubuntu.png differ diff --git a/resources/assets/images/units/windows.png b/resources/assets/images/units/windows.png new file mode 100644 index 00000000..36cafac9 Binary files /dev/null and b/resources/assets/images/units/windows.png differ diff --git a/resources/data/colors.json b/resources/data/colors.json new file mode 100644 index 00000000..f6c392fc --- /dev/null +++ b/resources/data/colors.json @@ -0,0 +1,10 @@ +[ +"bg-gray-400", +"bg-blue-400", +"bg-red-400", +"bg-green-400", +"bg-yellow-400", +"bg-indigo-400", +"bg-purple-400", +"bg-pink-400" +] diff --git a/resources/data/sampleLogs.json b/resources/data/sampleLogs.json new file mode 100644 index 00000000..978deca8 --- /dev/null +++ b/resources/data/sampleLogs.json @@ -0,0 +1,3 @@ +{ + "sample": "Date,Product,SKU,Quantity,Unit Type,Price Per Unit ($),Multiplier,Owner,Repository Slug,Username,Actions Workflow,Notes\n2022-05-20,Actions,Compute - UBUNTU,2,minute,0.008,1.0,galaxy,trantor,octocat,.github/workflows/check-pr.yml,\n2022-05-20,Actions,Compute - UBUNTU,9,minute,0.008,1.0,galaxy,caprica,octocat,.github/workflows/check-pr.yml,\n2022-05-20,Actions,Compute - UBUNTU,8,minute,0.008,1.0,galaxy,mustafar,octocat,.github/workflows/test.yml,\n2022-05-20,Actions,Compute - UBUNTU,9,minute,0.008,1.0,galaxy,caprica,octocat,.github/workflows/lint.yml,\n2022-05-20,Actions,Compute - UBUNTU,3,minute,0.008,1.0,galaxy,trantor,octocat,.github/workflows/behat.yml,\n2022-05-20,Actions,Compute - UBUNTU,3,minute,0.008,1.0,galaxy,mustafar,octocat,.github/workflows/check-pr.yml,\n2022-05-20,Actions,Compute - UBUNTU,2,minute,0.008,1.0,galaxy,trantor,octocat,.github/workflows/behat.yml,\n2022-05-20,Actions,Compute - UBUNTU,15,minute,0.008,1.0,galaxy,caprica,octocat,.github/workflows/lint.yml,\n2022-05-20,Actions,Compute - WINDOWS,7,minute,0.016,1.0,galaxy,mustafar,octocat,.github/workflows/test-windows.yml,\n2022-05-20,Actions,Compute - UBUNTU,11,minute,0.008,1.0,galaxy,caprica,octocat,.github/workflows/check-pr.yml,\n2022-05-20,Actions,Compute - UBUNTU,2,minute,0.008,1.0,galaxy,mustafar,octocat,.github/workflows/test.yml,\n2022-05-20,Actions,Compute - UBUNTU,1,minute,0.008,1.0,galaxy,trantor,dependabot[bot],.github/workflows/check-pr.yml,\n2022-05-20,Actions,Compute - UBUNTU,7,minute,0.008,1.0,galaxy,trantor,octocat,.github/workflows/behat.yml," +} diff --git a/resources/js/Components/Github.vue b/resources/js/Components/Github.vue new file mode 100644 index 00000000..df642ea6 --- /dev/null +++ b/resources/js/Components/Github.vue @@ -0,0 +1,13 @@ + + + diff --git a/resources/js/Components/Navbar.vue b/resources/js/Components/Navbar.vue new file mode 100644 index 00000000..57ed33ee --- /dev/null +++ b/resources/js/Components/Navbar.vue @@ -0,0 +1,29 @@ + + + diff --git a/resources/js/Components/Repository.vue b/resources/js/Components/Repository.vue new file mode 100644 index 00000000..418cedcd --- /dev/null +++ b/resources/js/Components/Repository.vue @@ -0,0 +1,26 @@ + + + diff --git a/resources/js/Components/SortableTable.vue b/resources/js/Components/SortableTable.vue new file mode 100644 index 00000000..dcc228d4 --- /dev/null +++ b/resources/js/Components/SortableTable.vue @@ -0,0 +1,47 @@ + + + + diff --git a/resources/js/Layouts/DefaultLayout.vue b/resources/js/Layouts/DefaultLayout.vue new file mode 100644 index 00000000..44f965dc --- /dev/null +++ b/resources/js/Layouts/DefaultLayout.vue @@ -0,0 +1,21 @@ + + + diff --git a/resources/js/Pages/Authors.vue b/resources/js/Pages/Authors.vue new file mode 100644 index 00000000..528f9745 --- /dev/null +++ b/resources/js/Pages/Authors.vue @@ -0,0 +1,56 @@ + + + diff --git a/resources/js/Pages/Repositories.vue b/resources/js/Pages/Repositories.vue new file mode 100644 index 00000000..000b7995 --- /dev/null +++ b/resources/js/Pages/Repositories.vue @@ -0,0 +1,57 @@ + + + diff --git a/resources/js/Pages/Table.vue b/resources/js/Pages/Table.vue new file mode 100644 index 00000000..38468a80 --- /dev/null +++ b/resources/js/Pages/Table.vue @@ -0,0 +1,87 @@ + + + diff --git a/resources/js/Pages/Welcome.vue b/resources/js/Pages/Welcome.vue deleted file mode 100755 index 80004bdb..00000000 --- a/resources/js/Pages/Welcome.vue +++ /dev/null @@ -1,286 +0,0 @@ - - - - - diff --git a/resources/js/Types/Actor.d.ts b/resources/js/Types/Actor.d.ts new file mode 100644 index 00000000..a6980f3a --- /dev/null +++ b/resources/js/Types/Actor.d.ts @@ -0,0 +1,6 @@ +export interface Actor { + id: number + name: string + github_id: number + avatar_url: string +} diff --git a/resources/js/Types/Author.d.ts b/resources/js/Types/Author.d.ts new file mode 100644 index 00000000..993baa30 --- /dev/null +++ b/resources/js/Types/Author.d.ts @@ -0,0 +1,8 @@ +export interface Author { + id: number + name: string + github_id: number + avatar_url: string + minutes: number + price: number +} diff --git a/resources/js/Types/Repository.d.ts b/resources/js/Types/Repository.d.ts new file mode 100644 index 00000000..ba7b1ffa --- /dev/null +++ b/resources/js/Types/Repository.d.ts @@ -0,0 +1,8 @@ +export interface Repository { + id: number + name: string + organization: string + avatar_url: string + minutes: number + price: number +} diff --git a/resources/js/Types/WorkflowRun.d.ts b/resources/js/Types/WorkflowRun.d.ts new file mode 100644 index 00000000..262b65b8 --- /dev/null +++ b/resources/js/Types/WorkflowRun.d.ts @@ -0,0 +1,15 @@ +import type {Actor} from '@/Types/Actor' + +export interface WorkflowRun { + id: number + date: number + organization: string + repository: string + repository_id: number + minutes: number + price_per_minute: number + total_price: number + workflow: string + os: string + actor: Actor +} diff --git a/resources/js/Utils/sort.ts b/resources/js/Utils/sort.ts new file mode 100644 index 00000000..f1592b1e --- /dev/null +++ b/resources/js/Utils/sort.ts @@ -0,0 +1,60 @@ +import { type Ref, computed, ref, watch } from 'vue' + +interface Sortable { + items: T[] + sort: keyof T + order: 'asc' | 'desc' +} + +export const withSort = (items: T[], sortByKey: keyof T) => { + const data = ref({ + items, + sort: sortByKey, + order: 'desc', + }) as Ref> + + watch (items, () => { + data.value.items = items + }) + + const sorted = computed(() => { + let items = [...data.value.items] + + if (data.value.sort) { + items = items.sort((a, b) => { + const key = data.value.sort + + const valueA = a[key] + const valueB = b[key] + + if (typeof valueA === 'number' && typeof valueB === 'number') { + return valueA - valueB + } + + if (typeof valueA === 'string' && typeof valueB === 'string') { + return valueA.localeCompare(valueB) + } + + return 0 + }) + } + + if (data.value.order === 'desc') { + items.reverse() + } + + return items + }) + + const sortBy = (tag: keyof T) => { + if (data.value.sort === tag) { + data.value.order = data.value.order === 'desc' ? 'asc' : 'desc' + } + else { + data.value.sort = tag + data.value.order = 'desc' + } + } + + return { sorted, data, sortBy } +} diff --git a/resources/js/app.ts b/resources/js/app.ts index 45715ff2..90415c7b 100755 --- a/resources/js/app.ts +++ b/resources/js/app.ts @@ -2,12 +2,19 @@ import '../css/app.css' import { createApp, h, type DefineComponent } from 'vue' import { createInertiaApp } from '@inertiajs/vue3' import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers' +import DefaultLayout from './Layouts/DefaultLayout.vue' const appName = import.meta.env.VITE_APP_NAME || 'Laravel' createInertiaApp({ title: (title) => `${title} - ${appName}`, - resolve: async (name) => await resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')), + resolve: async (name) => { + const page = await resolvePageComponent( + `./Pages/${name}.vue`, + import.meta.glob('./Pages/**/*.vue')) + page.default.layout ??= DefaultLayout + return page + }, setup({ el, App, props, plugin }) { createApp({ render: () => h(App, props) }) .use(plugin) diff --git a/routes/api.php b/routes/api.php index b44ee6b5..ac0d02a7 100755 --- a/routes/api.php +++ b/routes/api.php @@ -3,8 +3,11 @@ declare(strict_types=1); use App\Http\Controllers\Api\GithubWebhookController; +use App\Http\Controllers\LogsController; use App\Http\Middleware\ValidateGithubWebhook; use Illuminate\Support\Facades\Route; Route::post("/webhook", GithubWebhookController::class) ->middleware(ValidateGithubWebhook::class); + +Route::get("/data/sampleLogs", [LogsController::class, "getSampleLogs"]); diff --git a/routes/web.php b/routes/web.php index 463b1209..5382c5ea 100755 --- a/routes/web.php +++ b/routes/web.php @@ -2,14 +2,21 @@ declare(strict_types=1); +use App\Http\Controllers\AuthorsController; use App\Http\Controllers\GithubController; +use App\Http\Controllers\RepositoriesController; +use App\Http\Controllers\TableController; use Illuminate\Support\Facades\Route; -use Inertia\Response; -Route::get("/", fn(): Response => inertia("Welcome")); -Route::get("/auth/login", [GithubController::class, "login"])->name("login"); +Route::middleware("auth")->group(function (): void { + Route::get("/table", [TableController::class, "show"]); + Route::get("/repositories", [RepositoriesController::class, "show"]); + Route::get("/authors", [AuthorsController::class, "show"]); + + Route::get("/{organizationId}/fetch", [GithubController::class, "fetchData"])->middleware("auth"); +}); +Route::redirect("/", "table"); +Route::get("/auth/login", [GithubController::class, "login"])->name("login"); Route::get("/auth/redirect", [GithubController::class, "redirect"]); Route::get("/auth/callback", [GithubController::class, "callback"]); - -Route::get("/{organizationId}/fetch", [GithubController::class, "fetchData"])->middleware("auth"); diff --git a/tailwind.config.js b/tailwind.config.js index 2c9a037b..2c2719d4 100755 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,11 +1,18 @@ -module.exports = { +import colors from "./resources/data/colors.json" + +export default { content: [ "./resources/**/*.blade.php", "./resources/**/*.js", "./resources/**/*.vue", ], + safelist: [ + ...colors + ], theme: { - extend: {}, + extend: { + + }, }, plugins: [], } diff --git a/tests/Feature/FetchWorkflowJobsTest.php b/tests/Feature/FetchWorkflowJobsTest.php index fde3705f..0534bf5d 100644 --- a/tests/Feature/FetchWorkflowJobsTest.php +++ b/tests/Feature/FetchWorkflowJobsTest.php @@ -4,6 +4,7 @@ namespace Tests\Feature; +use App\DTO\WorkflowActorDTO; use App\DTO\WorkflowRunDTO; use App\Exceptions\FetchingWorkflowJobsErrorException; use App\Http\Integrations\GithubConnector; @@ -11,6 +12,7 @@ use App\Models\Organization; use App\Models\Repository; use App\Models\User; +use App\Models\WorkflowActor; use App\Models\WorkflowJob; use App\Models\WorkflowRun; use App\Services\FetchWorkflowJobsService; @@ -29,10 +31,12 @@ class FetchWorkflowJobsTest extends TestCase protected User $user; protected Repository $repository; protected WorkflowRun $workflowRun; + protected WorkflowActor $workflowActor; protected Organization $organization; protected FetchWorkflowJobsService $fetchWorkflowJobsService; protected GithubConnector $githubConnector; protected WorkflowRunDTO $workflowRunDto; + protected WorkflowActorDTO $workflowActorDto; protected function setUp(): void { @@ -41,15 +45,23 @@ protected function setUp(): void $this->githubConnector = new GithubConnector(); $this->user = User::factory()->create(); + $this->workflowRun = WorkflowRun::factory()->create(); + $this->workflowActorDto = new WorkflowActorDTO( + $this->workflowRun->workflowActor->github_id, + $this->workflowRun->workflowActor->name, + $this->workflowRun->workflowActor->avatar_url, + ); + $this->workflowRunDto = new WorkflowRunDTO( $this->workflowRun->github_id, $this->workflowRun->name, $this->workflowRun->repository_id, new DateTime($this->workflowRun->github_created_at), + $this->workflowActorDto, ); $this->repository = Repository::query()->where("id", $this->workflowRun->repository_id)->firstOrFail(); - $this->fetchWorkflowJobsService = new FetchWorkflowJobsService($this->githubConnector, $this->user->id); + $this->fetchWorkflowJobsService = new FetchWorkflowJobsService($this->githubConnector); $this->actingAs($this->user); MockClient::destroyGlobal(); @@ -67,6 +79,7 @@ public function testFetchWorkflowJobsWithAdminUser(): void "name" => "job1", "started_at" => "2024-06-19T08:25:09Z", "completed_at" => "2024-06-19T08:26:09Z", + "conclusion" => "success", "labels" => [ "ubuntu-latest", ], @@ -106,6 +119,44 @@ public function testFetchWorkflowJobsIfAlreadyFetched(): void "name" => "job1", "started_at" => "2024-06-19T08:25:09Z", "completed_at" => "2024-06-19T08:26:09Z", + "conclusion" => "success", + "labels" => [ + "ubuntu-latest", + ], + ], + ], + ], 200), + ]); + + $this->githubConnector->withMockClient($mockClient); + + $this->fetchWorkflowJobsService->fetchWorkflowJobs($this->workflowRunDto, $this->user->id); + + $this->assertDatabaseMissing("workflow_jobs", [ + "github_id" => 123, + "name" => "job1", + "workflow_run_id" => $this->workflowRun->id, + "runner_os" => "ubuntu", + "runner_type" => "standard", + "minutes" => 1, + "multiplier" => 1, + "price_per_unit" => 0.008, + ]); + } + + public function testFetchWorkflowJobsWithSkippedJob(): void + { + $this->user->organizations()->attach($this->repository->organization_id, ["is_admin" => true]); + + $mockClient = new MockClient([ + GetWorkflowJobsRequest::class => MockResponse::make([ + "jobs" => [ + [ + "id" => 123, + "name" => "job1", + "started_at" => "2024-06-19T08:25:09Z", + "completed_at" => "2024-06-19T08:26:09Z", + "conclusion" => "skipped", "labels" => [ "ubuntu-latest", ], @@ -142,6 +193,7 @@ public function testFetchWorkflowJobsWithMemberUser(): void "name" => "job1", "started_at" => "2024-06-19T08:25:09Z", "completed_at" => "2024-06-19T08:26:09Z", + "conclusion" => "success", "labels" => [ "ubuntu-latest", ], @@ -178,6 +230,7 @@ public function testFetchWorkflowJobsWithUserNotInOrganization(): void "name" => "job1", "started_at" => "2024-06-19T08:25:09Z", "completed_at" => "2024-06-19T08:26:09Z", + "conclusion" => "success", "labels" => [ "ubuntu-latest", ], diff --git a/tests/Feature/FetchWorkflowRunsTest.php b/tests/Feature/FetchWorkflowRunsTest.php index f8e92983..5b461546 100644 --- a/tests/Feature/FetchWorkflowRunsTest.php +++ b/tests/Feature/FetchWorkflowRunsTest.php @@ -11,6 +11,7 @@ use App\Models\Organization; use App\Models\Repository; use App\Models\User; +use App\Models\WorkflowActor; use App\Services\FetchWorkflowRunsService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Validation\UnauthorizedException; @@ -24,6 +25,7 @@ class FetchWorkflowRunsTest extends TestCase use RefreshDatabase; protected User $user; + protected WorkflowActor $actor; protected Repository $repository; protected Organization $organization; protected FetchWorkflowRunsService $fetchWorkflowRunsService; @@ -37,6 +39,7 @@ protected function setUp(): void $this->githubConnector = new GithubConnector(); $this->user = User::factory()->create(); + $this->actor = WorkflowActor::factory()->create(); $this->repository = Repository::factory()->create(); $this->repositoryDto = new RepositoryDTO( $this->repository->github_id, @@ -61,6 +64,11 @@ public function testFetchWorkflowRunsWithAdminUser(): void "id" => 123, "name" => "run1", "created_at" => "2024-06-19T08:25:09Z", + "actor" => [ + "id" => 321, + "login" => "actor21", + "avatar_url" => "http://localhost/actor21.png", + ], ], ], ], 200), @@ -76,6 +84,49 @@ public function testFetchWorkflowRunsWithAdminUser(): void "repository_id" => $this->repository->id, "github_created_at" => "2024-06-19T08:25:09Z", ]); + + $this->assertDatabaseCount("workflow_actors", 2); + + $this->assertDatabaseHas("workflow_actors", [ + "github_id" => 321, + "name" => "actor21", + "avatar_url" => "http://localhost/actor21.png", + ]); + } + + public function testFetchWorkflowRunsWithKnownActor(): void + { + $this->user->organizations()->attach($this->repository->organization_id, ["is_admin" => true]); + + $mockClient = new MockClient([ + GetWorkflowRunsRequest::class => MockResponse::make([ + "workflow_runs" => [ + [ + "id" => 123, + "name" => "run1", + "created_at" => "2024-06-19T08:25:09Z", + "actor" => [ + "id" => $this->actor->github_id, + "login" => $this->actor->name, + "avatar_url" => $this->actor->avatar_url, + ], + ], + ], + ], 200), + ]); + + $this->githubConnector->withMockClient($mockClient); + + $this->fetchWorkflowRunsService->fetchWorkflowRuns($this->repositoryDto, $this->user->id); + + $this->assertDatabaseHas("workflow_runs", [ + "github_id" => 123, + "name" => "run1", + "repository_id" => $this->repository->id, + "github_created_at" => "2024-06-19T08:25:09Z", + ]); + + $this->assertDatabaseCount("workflow_actors", 1); } public function testFetchWorkflowRunsWithMemberUser(): void @@ -89,6 +140,11 @@ public function testFetchWorkflowRunsWithMemberUser(): void "id" => 123, "name" => "run1", "created_at" => "2024-06-19T08:25:09Z", + "actor" => [ + "id" => 321, + "login" => "actor21", + "avatar_url" => "http://localhost/actor21.png", + ], ], ], ], 200), @@ -106,6 +162,12 @@ public function testFetchWorkflowRunsWithMemberUser(): void "repository_id" => $this->repository->id, "github_created_at" => "2024-06-19T08:25:09Z", ]); + + $this->assertDatabaseMissing("workflow_actors", [ + "github_id" => 321, + "name" => "actor21", + "avatar_url" => "http://localhost/actor21.png", + ]); } public function testFetchWorkflowRunsWithUserNotInOrganization(): void @@ -117,6 +179,11 @@ public function testFetchWorkflowRunsWithUserNotInOrganization(): void "id" => 123, "name" => "run1", "created_at" => "2024-06-19T08:25:09Z", + "actor" => [ + "id" => 321, + "login" => "actor21", + "avatar_url" => "http://localhost/actor21.png", + ], ], ], ], 200), @@ -133,6 +200,17 @@ public function testFetchWorkflowRunsWithUserNotInOrganization(): void "name" => "run1", "repository_id" => $this->repository->id, "github_created_at" => "2024-06-19T08:25:09Z", + "actor" => [ + "id" => 321, + "login" => "actor21", + "avatar_url" => "http://localhost/actor21.png", + ], + ]); + + $this->assertDatabaseMissing("workflow_actors", [ + "github_id" => 321, + "name" => "actor21", + "avatar_url" => "http://localhost/actor21.png", ]); }