Skip to content
Draft
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
67 changes: 67 additions & 0 deletions database/migrations/2026_08_12_000001_restore_tags.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

use Cachet\Models\Component;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
if (! Schema::hasTable('tags')) {
Schema::create('tags', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('slug')->unique();
$table->timestamps();
});
}

if (! Schema::hasTable('taggables')) {
Schema::create('taggables', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('tag_id')->index();
$table->morphs('taggable');
$table->timestamps();
});
}

DB::table('taggables')
->select('tag_id', 'taggable_id', 'taggable_type')
->groupBy('tag_id', 'taggable_id', 'taggable_type')
->havingRaw('count(*) > 1')
->orderBy('tag_id')
->orderBy('taggable_id')
->orderBy('taggable_type')
->each(function (object $duplicate): void {
$ids = DB::table('taggables')
->where('tag_id', $duplicate->tag_id)
->where('taggable_id', $duplicate->taggable_id)
->where('taggable_type', $duplicate->taggable_type)
->orderBy('id')
->pluck('id')
->all();

array_shift($ids);

DB::table('taggables')->whereIn('id', $ids)->delete();
});

Schema::table('taggables', function (Blueprint $table) {
$table->unique(['tag_id', 'taggable_id', 'taggable_type']);
});

DB::table('taggables')
->where('taggable_type', 'components')
->update(['taggable_type' => Component::class]);
}

public function down(): void
{
DB::table('taggables')
->where('taggable_type', Component::class)
->update(['taggable_type' => 'components']);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

use Spatie\LaravelSettings\Migrations\SettingsMigration;

return new class extends SettingsMigration
{
public function up(): void
{
rescue(fn () => $this->migrator->add('app.show_component_tags', false));
}
};
2 changes: 2 additions & 0 deletions resources/lang/en/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
'dynamic_favicon_helper' => 'Update the favicon to reflect the current status of your systems.',
'show_component_group_status' => 'Show component group status',
'show_component_group_status_helper' => 'Display each group’s most severe component status on the public status page.',
'show_component_tags' => 'Show component tags',
'show_component_tags_helper' => 'Display tags underneath component names on the public status page.',
],
'display_settings_title' => 'Display settings',
'api_settings_title' => 'API settings',
Expand Down
12 changes: 10 additions & 2 deletions resources/views/components/component.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,16 @@
<div x-popover:panel x-cloak x-transition.opacity x-anchor.right.offset.8="$refs.anchor" class="z-10 w-max max-w-sm rounded-md bg-zinc-900 px-3 py-2 text-xs font-medium text-white shadow-lg dark:bg-zinc-100 dark:text-zinc-900">
<span class="pointer-events-none absolute -left-1 top-2 size-2 rotate-45 bg-zinc-900 dark:bg-zinc-100" aria-hidden="true"></span>
<p class="relative">{!! $component->formattedDescription() !!}</p>
</div>
</div>
</div>

@if (app(\Cachet\Settings\AppSettings::class)->show_component_tags && $component->tags->isNotEmpty())
<div class="mt-2 flex flex-wrap gap-1.5">
@foreach ($component->tags as $tag)
<span class="rounded-full bg-zinc-100 px-2 py-0.5 text-xs font-medium text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300">{{ $tag->name }}</span>
@endforeach
</div>
@endif
</div>
@endif
</div>

Expand Down
3 changes: 2 additions & 1 deletion src/Actions/Component/CreateComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ class CreateComponent
*/
public function handle(CreateComponentRequestData $component): Component
{
return tap(Component::create($component->except('meta')->toArray()), function (Component $model) use ($component) {
return tap(Component::create($component->except('meta', 'tags')->toArray()), function (Component $model) use ($component) {
$model->syncMeta($component->meta ?? []);
$model->syncTags($component->tags);
});
}
}
6 changes: 5 additions & 1 deletion src/Actions/Component/UpdateComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function __construct(private ChangeComponentStatus $changeComponentStatus
public function handle(Component $component, UpdateComponentRequestData $data, ?Authenticatable $user = null): Component
{
DB::transaction(function () use ($component, $data, $user): void {
$attributes = $data->except('meta', 'status')->toArray();
$attributes = $data->except('meta', 'status', 'tags')->toArray();

if ($data->status === null) {
$component->update($attributes);
Expand All @@ -38,6 +38,10 @@ public function handle(Component $component, UpdateComponentRequestData $data, ?
if ($data->meta !== null) {
$component->syncMeta($data->meta);
}

if ($data->tags !== null) {
$component->syncTags($data->tags);
}
});

return $component->fresh();
Expand Down
3 changes: 2 additions & 1 deletion src/Actions/Incident/CreateIncident.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public function handle(CreateIncidentRequestData $data): Incident
$incident = DB::transaction(function () use ($data): Incident {
return tap(Incident::create(array_merge(
['guid' => Str::uuid()],
$data->except('components', 'meta')->toArray()
$data->except('components', 'meta', 'tags')->toArray()
)), function (Incident $incident) use ($data) {
$components = collect($data->components)
->mapWithKeys(fn (IncidentComponentRequestData $component) => [
Expand All @@ -52,6 +52,7 @@ public function handle(CreateIncidentRequestData $data): Incident
}

$incident->syncMeta($data->meta ?? []);
$incident->syncTags($data->tags);
});
});

Expand Down
6 changes: 5 additions & 1 deletion src/Actions/Incident/UpdateIncident.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@ class UpdateIncident
*/
public function handle(Incident $incident, UpdateIncidentRequestData $data): Incident
{
$incident->update($data->except('meta')->toArray());
$incident->update($data->except('meta', 'tags')->toArray());

if ($data->meta !== null) {
$incident->syncMeta($data->meta);
}

if ($data->tags !== null) {
$incident->syncTags($data->tags);
}

return $incident->fresh();
}
}
2 changes: 1 addition & 1 deletion src/Actions/Metric/CreateMetric.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ class CreateMetric
*/
public function handle(CreateMetricRequestData $data): Metric
{
return Metric::create($data->toArray());
return tap(Metric::create($data->except('tags')->toArray()), fn (Metric $metric) => $metric->syncTags($data->tags));
}
}
6 changes: 5 additions & 1 deletion src/Actions/Metric/UpdateMetric.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ class UpdateMetric
*/
public function handle(Metric $metric, UpdateMetricRequestData $data): Metric
{
$metric->update($data->toArray());
$metric->update($data->except('tags')->toArray());

if ($data->tags !== null) {
$metric->syncTags($data->tags);
}

return $metric->fresh();
}
Expand Down
3 changes: 2 additions & 1 deletion src/Actions/Schedule/CreateSchedule.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function handle(CreateScheduleRequestData $data): Schedule
{
/** @phpstan-ignore-next-line argument.type */
$schedule = DB::transaction(function () use ($data): Schedule {
return tap(Schedule::create($data->except('components', 'meta')->toArray()), function (Schedule $schedule) use ($data) {
return tap(Schedule::create($data->except('components', 'meta', 'tags')->toArray()), function (Schedule $schedule) use ($data) {
if ($data->components) {
$components = collect($data->components)
->mapWithKeys(fn (ScheduleComponentRequestData $component) => [
Expand All @@ -33,6 +33,7 @@ public function handle(CreateScheduleRequestData $data): Schedule
}

$schedule->syncMeta($data->meta ?? []);
$schedule->syncTags($data->tags);
});
});

Expand Down
6 changes: 5 additions & 1 deletion src/Actions/Schedule/UpdateSchedule.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ class UpdateSchedule
*/
public function handle(Schedule $schedule, UpdateScheduleRequestData $data): Schedule
{
$schedule->update($data->except('components', 'meta')->toArray());
$schedule->update($data->except('components', 'meta', 'tags')->toArray());

if ($data->meta !== null) {
$schedule->syncMeta($data->meta);
}

if ($data->tags !== null) {
$schedule->syncTags($data->tags);
}

if ($data->components) {
$components = collect($data->components)
->mapWithKeys(fn (ScheduleComponentRequestData $component) => [
Expand Down
49 changes: 49 additions & 0 deletions src/Concerns/HasTags.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace Cachet\Concerns;

use Cachet\Models\Tag;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Support\Str;

trait HasTags
{
/**
* @return MorphToMany<Tag, $this>
*/
public function tags(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable');
}

/**
* @param list<string> $names
*/
public function syncTags(array $names): void
{
$names = collect($names)
->filter(fn (string $name): bool => trim($name) !== '')
->map(fn (string $name): string => trim($name))
->unique(fn (string $name): string => mb_strtolower($name))
->values();

$this->tags()->sync($names->map(fn (string $name): int => Tag::query()
->where('slug', Str::slug($name))
->firstOrCreate(['name' => $name])
->id));
}

/**
* @param list<string> $names
* @param Builder<static> $query
*/
public function scopeWithAnyTags(Builder $query, array $names): void
{
$slugs = collect($names)->map(fn (string $name): string => Str::slug(trim($name)))->filter();

if ($slugs->isNotEmpty()) {
$query->whereHas('tags', fn (Builder $tags) => $tags->whereIn('slug', $slugs));
}
}
}
4 changes: 4 additions & 0 deletions src/Data/Requests/Component/CreateComponentRequestData.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public function __construct(
public readonly ?int $order = null,
public readonly bool $enabled = true,
public readonly ?int $componentGroupId = null,
/** @var list<string> */
public readonly array $tags = [],
/** @var array<string, mixed>|null */
public readonly ?array $meta = null,
) {}
Expand All @@ -31,6 +33,8 @@ public static function rules(ValidationContext $context): array
'order' => ['int', 'min:0'],
'enabled' => ['boolean'],
'component_group_id' => ['int', 'min:0', Rule::exists('component_groups', 'id')],
'tags' => ['array'],
'tags.*' => ['string', 'max:255'],
/**
* Key/value metadata to store against the resource.
*
Expand Down
4 changes: 4 additions & 0 deletions src/Data/Requests/Component/UpdateComponentRequestData.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public function __construct(
public readonly ?int $order = null,
public readonly ?bool $enabled = null,
public readonly ?int $componentGroupId = null,
/** @var list<string>|null */
public readonly ?array $tags = null,
/** @var array<string, mixed>|null */
public readonly ?array $meta = null,
) {}
Expand All @@ -31,6 +33,8 @@ public static function rules(ValidationContext $context): array
'order' => ['int', 'min:0'],
'component_group_id' => ['int', 'min:0', Rule::exists('component_groups', 'id')],
'enabled' => ['boolean'],
'tags' => ['nullable', 'array'],
'tags.*' => ['string', 'max:255'],
/**
* Key/value metadata to store against the resource.
*
Expand Down
5 changes: 5 additions & 0 deletions src/Data/Requests/Incident/CreateIncidentRequestData.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public function __construct(
public readonly ?ComponentStatusEnum $componentStatus = null,
#[DataCollectionOf(IncidentComponentRequestData::class)]
public readonly ?array $components = null,
/** @var list<string> */
public readonly array $tags = [],
/** @var array<string, mixed>|null */
public readonly ?array $meta = null,
) {}
Expand Down Expand Up @@ -72,6 +74,8 @@ public static function rules(ValidationContext $context): array
'components' => ['array'],
'components.*.id' => ['required', 'int', 'distinct', 'exists:components,id'],
'components.*.status' => ['required', 'int', Rule::enum(ComponentStatusEnum::class)],
'tags' => ['array'],
'tags.*' => ['string', 'max:255'],
/**
* Key/value metadata to store against the resource.
*
Expand Down Expand Up @@ -99,6 +103,7 @@ public function withMessage(string $message): self
componentId: $this->componentId,
componentStatus: $this->componentStatus,
components: $this->components,
tags: $this->tags,
meta: $this->meta,
);
}
Expand Down
4 changes: 4 additions & 0 deletions src/Data/Requests/Incident/UpdateIncidentRequestData.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public function __construct(
public readonly ?bool $notifications = null,
public readonly ?string $occurredAt = null,
public readonly ?string $publishedAt = null,
/** @var list<string>|null */
public readonly ?array $tags = null,
/** @var array<string, mixed>|null */
public readonly ?array $meta = null,
) {}
Expand All @@ -40,6 +42,8 @@ public static function rules(ValidationContext $context): array
* The date/time to publish the incident, e.g. "2023-11-07 05:31:56" or ISO 8601. While set in the future the incident is hidden from the status page and public API.
*/
'published_at' => ['nullable', 'date'],
'tags' => ['nullable', 'array'],
'tags.*' => ['string', 'max:255'],
/**
* Key/value metadata to store against the resource.
*
Expand Down
4 changes: 4 additions & 0 deletions src/Data/Requests/Metric/CreateMetricRequestData.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public function __construct(
public readonly ?bool $displayChart = null,
public readonly ?int $threshold = null,
public readonly ?int $places = null,
/** @var list<string> */
public readonly array $tags = [],
) {}

public static function rules(ValidationContext $context): array
Expand All @@ -32,6 +34,8 @@ public static function rules(ValidationContext $context): array
'display_chart' => ['nullable', 'boolean'],
'threshold' => ['int', 'min:0', 'max:60', new FactorOfSixty],
'places' => ['int', 'min:0', 'max:4'],
'tags' => ['array'],
'tags.*' => ['string', 'max:255'],
];
}
}
4 changes: 4 additions & 0 deletions src/Data/Requests/Metric/UpdateMetricRequestData.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public function __construct(
public readonly ?string $description = null,
public readonly ?float $defaultValue = null,
public readonly ?int $threshold = null,
/** @var list<string>|null */
public readonly ?array $tags = null,
) {}

public static function rules(ValidationContext $context): array
Expand All @@ -24,6 +26,8 @@ public static function rules(ValidationContext $context): array
'description' => ['string'],
'default_value' => ['decimal:1,2'],
'threshold' => ['int', 'min:0', 'max:60', new FactorOfSixty],
'tags' => ['nullable', 'array'],
'tags.*' => ['string', 'max:255'],
];
}
}
Loading
Loading