Skip to content

Commit 48a69e4

Browse files
authored
[Bug] Fix incorrectly formatted json data (#812)
1 parent 5b17328 commit 48a69e4

15 files changed

+508
-136
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
namespace App\Actions;
4+
5+
use App\Enums\ResultStatus;
6+
use App\Models\User;
7+
use App\Settings\DataMigrationSettings;
8+
use Filament\Notifications\Notification;
9+
use Illuminate\Support\Facades\DB;
10+
use Illuminate\Support\Facades\Log;
11+
use Illuminate\Support\Facades\Schema;
12+
use Lorisleiva\Actions\Concerns\AsAction;
13+
14+
class MigrateBadJsonResults
15+
{
16+
use AsAction;
17+
18+
public int $jobTimeout = 60 * 5;
19+
20+
public int $jobTries = 1;
21+
22+
public function handle(User $user)
23+
{
24+
$dataSettings = new DataMigrationSettings();
25+
26+
$tableName = 'results_bad_json';
27+
28+
if ($dataSettings->bad_json_migrated) {
29+
Notification::make()
30+
->title('❌ Hmmm it seems someone has already migrated the data!')
31+
->body('Check your results table and make sure you\'re not triggering a duplicate data migration.')
32+
->danger()
33+
->sendToDatabase($user);
34+
35+
return;
36+
}
37+
38+
if (! Schema::hasTable('results')) {
39+
Notification::make()
40+
->title('❌ Could not migrate bad json results!')
41+
->body('The "results" table is missing.')
42+
->danger()
43+
->sendToDatabase($user);
44+
45+
return;
46+
}
47+
48+
if (! Schema::hasTable($tableName)) {
49+
Notification::make()
50+
->title('❌ Could not migrate bad json results!')
51+
->body('The "results_bad_json" table is missing.')
52+
->danger()
53+
->sendToDatabase($user);
54+
55+
return;
56+
}
57+
58+
/**
59+
* Copy backup data to the new results table and reformat it.
60+
*/
61+
try {
62+
DB::table($tableName)->chunkById(100, function ($results) {
63+
foreach ($results as $result) {
64+
$record = [
65+
'service' => 'ookla',
66+
'ping' => $result->ping,
67+
'download' => $result->download,
68+
'upload' => $result->upload,
69+
'comments' => $result->comments,
70+
'data' => json_decode($result->data),
71+
'status' => match ($result->successful) {
72+
1 => ResultStatus::Completed,
73+
default => ResultStatus::Failed,
74+
},
75+
'scheduled' => $result->scheduled,
76+
'created_at' => $result->created_at,
77+
'updated_at' => now(),
78+
];
79+
80+
DB::table('results')->insert($record);
81+
}
82+
});
83+
} catch (\Throwable $e) {
84+
Log::error($e);
85+
86+
Notification::make()
87+
->title('There was an issue migrating the data!')
88+
->body('Check the logs for an output of the issue.')
89+
->danger()
90+
->sendToDatabase($user);
91+
92+
return;
93+
}
94+
95+
$dataSettings->bad_json_migrated = true;
96+
97+
$dataSettings->save();
98+
99+
Notification::make()
100+
->title('Data migration completed!')
101+
->body('Your history has been successfully migrated.')
102+
->success()
103+
->sendToDatabase($user);
104+
}
105+
}

app/Enums/ResultStatus.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace App\Enums;
4+
5+
use Filament\Support\Contracts\HasLabel;
6+
7+
enum ResultStatus: string implements HasLabel
8+
{
9+
case Completed = 'completed'; // a speedtest that ran successfully.
10+
case Failed = 'failed'; // a speedtest that failed to run successfully.
11+
case Started = 'started'; // a speedtest that has been started by a worker but has not finish running.
12+
13+
public function getLabel(): ?string
14+
{
15+
return $this->name;
16+
}
17+
}

app/Exports/ResultsSelectedBulkExport.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ public function array(): array
1919
return $this->results;
2020
}
2121

22+
/**
23+
* TODO: fix it
24+
*/
2225
public function headings(): array
2326
{
2427
return [

app/Filament/Resources/ResultResource.php

Lines changed: 105 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,29 @@
22

33
namespace App\Filament\Resources;
44

5+
use App\Actions\MigrateBadJsonResults;
6+
use App\Enums\ResultStatus;
57
use App\Exports\ResultsSelectedBulkExport;
68
use App\Filament\Resources\ResultResource\Pages;
9+
use App\Helpers\Number;
710
use App\Helpers\TimeZoneHelper;
811
use App\Models\Result;
12+
use App\Settings\DataMigrationSettings;
913
use App\Settings\GeneralSettings;
1014
use Carbon\Carbon;
1115
use Filament\Forms;
1216
use Filament\Forms\Components\TextInput;
1317
use Filament\Forms\Form;
18+
use Filament\Notifications\Notification;
1419
use Filament\Resources\Resource;
20+
use Filament\Support\Enums\Alignment;
1521
use Filament\Tables;
1622
use Filament\Tables\Actions\Action;
17-
use Filament\Tables\Columns\IconColumn;
18-
use Filament\Tables\Columns\TextColumn;
1923
use Filament\Tables\Table;
2024
use Illuminate\Database\Eloquent\Builder;
2125
use Illuminate\Database\Eloquent\Collection;
26+
use Illuminate\Support\Facades\Auth;
27+
use Illuminate\Support\HtmlString;
2228
use Maatwebsite\Excel\Facades\Excel;
2329

2430
class ResultResource extends Resource
@@ -52,17 +58,6 @@ public static function form(Form $form): Form
5258
$component->state(Carbon::parse($state)->format($settings->time_format ?? 'M j, Y G:i:s'));
5359
})
5460
->columnSpan(2),
55-
Forms\Components\TextInput::make('server_id')
56-
->label('Server ID'),
57-
Forms\Components\TextInput::make('server_name')
58-
->label('Server name')
59-
->columnSpan(2),
60-
Forms\Components\TextInput::make('server_host')
61-
->label('Server host')
62-
->columnSpan([
63-
'default' => 2,
64-
'md' => 3,
65-
]),
6661
Forms\Components\TextInput::make('download')
6762
->label('Download (Mbps)')
6863
->afterStateHydrated(function (TextInput $component, $state) {
@@ -75,11 +70,26 @@ public static function form(Form $form): Form
7570
}),
7671
Forms\Components\TextInput::make('ping')
7772
->label('Ping (Ms)'),
73+
Forms\Components\TextInput::make('data.download.latency.jitter')
74+
->label('Download Jitter (Ms)'),
75+
Forms\Components\TextInput::make('data.upload.latency.jitter')
76+
->label('Upload Jitter (Ms)'),
77+
Forms\Components\TextInput::make('data.ping.jitter')
78+
->label('Ping Jitter (Ms)'),
7879
])
7980
->columnSpan(2),
8081
Forms\Components\Section::make()
8182
->schema([
82-
Forms\Components\Checkbox::make('successful'),
83+
Forms\Components\Placeholder::make('service')
84+
->content(fn (Result $result): string => $result->service),
85+
Forms\Components\Placeholder::make('server_name')
86+
->content(fn (Result $result): string => $result->server_name),
87+
Forms\Components\Placeholder::make('server_id')
88+
->label('Server ID')
89+
->content(fn (Result $result): string => $result->server_id),
90+
Forms\Components\Placeholder::make('server_host')
91+
->label('Server ID')
92+
->content(fn (Result $result): string => $result->server_id),
8393
Forms\Components\Checkbox::make('scheduled'),
8494
])
8595
->columns(1)
@@ -88,65 +98,93 @@ public static function form(Form $form): Form
8898
'md' => 1,
8999
]),
90100
]),
91-
Forms\Components\Textarea::make('data')
92-
->rows(10)
93-
->columnSpan(2),
94101
]);
95102
}
96103

97104
public static function table(Table $table): Table
98105
{
106+
$dataSettings = new DataMigrationSettings();
107+
99108
$settings = new GeneralSettings();
100109

101110
return $table
102111
->columns([
103-
TextColumn::make('id')
112+
Tables\Columns\TextColumn::make('id')
104113
->label('ID')
105114
->sortable(),
106-
TextColumn::make('server')
107-
->getStateUsing(fn (Result $record): ?string => ! blank($record->server_id) ? $record->server_id.' ('.$record->server_name.')' : null)
115+
Tables\Columns\TextColumn::make('ip_address')
116+
->label('IP address')
108117
->toggleable()
118+
->toggledHiddenByDefault()
109119
->sortable(),
110-
IconColumn::make('successful')
111-
->boolean()
112-
->toggleable(),
113-
IconColumn::make('scheduled')
114-
->boolean()
115-
->toggleable(),
116-
TextColumn::make('download')
117-
->label('Download (Mbps)')
118-
->getStateUsing(fn (Result $record): ?string => ! blank($record->download) ? toBits(convertSize($record->download), 2) : null)
120+
Tables\Columns\TextColumn::make('service')
121+
->toggleable()
122+
->toggledHiddenByDefault()
123+
->sortable(),
124+
Tables\Columns\TextColumn::make('server_id')
125+
->label('Server ID')
126+
->toggleable()
127+
->sortable(),
128+
Tables\Columns\TextColumn::make('server_name')
129+
->toggleable()
130+
->sortable(),
131+
Tables\Columns\TextColumn::make('download')
132+
->getStateUsing(fn (Result $record): ?string => ! blank($record->download) ? Number::fileSizeBits(bits: $record->download, precision: 2, perSecond: true) : null)
119133
->sortable(),
120-
TextColumn::make('upload')
121-
->label('Upload (Mbps)')
122-
->getStateUsing(fn (Result $record): ?string => ! blank($record->upload) ? toBits(convertSize($record->upload), 2) : null)
134+
Tables\Columns\TextColumn::make('upload')
135+
->getStateUsing(fn (Result $record): ?string => ! blank($record->upload) ? Number::fileSizeBits(bits: $record->upload, precision: 2, perSecond: true) : null)
123136
->sortable(),
124-
TextColumn::make('ping')
125-
->label('Ping (Ms)')
137+
Tables\Columns\TextColumn::make('ping')
126138
->toggleable()
127139
->sortable(),
128-
TextColumn::make('download_jitter')
129-
->getStateUsing(fn (Result $record): ?string => json_decode($record->data, true)['download']['latency']['jitter'] ?? null)
140+
Tables\Columns\TextColumn::make('download_jitter')
130141
->toggleable()
131142
->toggledHiddenByDefault()
132143
->sortable(),
133-
TextColumn::make('upload_jitter')
134-
->getStateUsing(fn (Result $record): ?string => json_decode($record->data, true)['upload']['latency']['jitter'] ?? null)
144+
Tables\Columns\TextColumn::make('upload_jitter')
135145
->toggleable()
136146
->toggledHiddenByDefault()
137147
->sortable(),
138-
TextColumn::make('ping_jitter')
139-
->getStateUsing(fn (Result $record): ?string => json_decode($record->data, true)['ping']['jitter'] ?? null)
148+
Tables\Columns\TextColumn::make('ping_jitter')
140149
->toggleable()
141150
->toggledHiddenByDefault()
142151
->sortable(),
143-
TextColumn::make('created_at')
144-
->label('Created')
152+
Tables\Columns\TextColumn::make('status')
153+
->toggleable()
154+
->sortable(),
155+
Tables\Columns\IconColumn::make('scheduled')
156+
->boolean()
157+
->toggleable()
158+
->toggledHiddenByDefault()
159+
->alignment(Alignment::Center),
160+
Tables\Columns\TextColumn::make('created_at')
145161
->dateTime($settings->time_format ?? 'M j, Y G:i:s')
146162
->timezone(TimeZoneHelper::displayTimeZone($settings))
147-
->sortable(),
163+
->sortable()
164+
->alignment(Alignment::End),
165+
Tables\Columns\TextColumn::make('updated_at')
166+
->dateTime($settings->time_format ?? 'M j, Y G:i:s')
167+
->timezone(TimeZoneHelper::displayTimeZone($settings))
168+
->toggleable()
169+
->toggledHiddenByDefault()
170+
->sortable()
171+
->alignment(Alignment::End),
148172
])
149173
->filters([
174+
Tables\Filters\SelectFilter::make('ip_address')
175+
->label('IP address')
176+
->multiple()
177+
->options(function (): array {
178+
return Result::query()
179+
->select('data->interface->externalIp AS public_ip_address')
180+
->distinct()
181+
->get()
182+
->mapWithKeys(function (Result $item, int $key) {
183+
return [$item['public_ip_address'] => $item['public_ip_address']];
184+
})
185+
->toArray();
186+
})
187+
->attribute('data->interface->externalIp'),
150188
Tables\Filters\TernaryFilter::make('scheduled')
151189
->placeholder('-')
152190
->trueLabel('Only scheduled speedtests')
@@ -156,23 +194,17 @@ public static function table(Table $table): Table
156194
false: fn (Builder $query) => $query->where('scheduled', false),
157195
blank: fn (Builder $query) => $query,
158196
),
159-
Tables\Filters\TernaryFilter::make('successful')
160-
->placeholder('-')
161-
->trueLabel('Only successful speedtests')
162-
->falseLabel('Only failed speedtests')
163-
->queries(
164-
true: fn (Builder $query) => $query->where('successful', true),
165-
false: fn (Builder $query) => $query->where('successful', false),
166-
blank: fn (Builder $query) => $query,
167-
),
197+
Tables\Filters\SelectFilter::make('status')
198+
->multiple()
199+
->options(ResultStatus::class),
168200
])
169201
->actions([
170202
Tables\Actions\ActionGroup::make([
171203
Action::make('view result')
172204
->label('View on Speedtest.net')
173205
->icon('heroicon-o-link')
174-
->url(fn (Result $record): ?string => $record?->url)
175-
->hidden(fn (Result $record): bool => ! $record->is_successful)
206+
->url(fn (Result $record): ?string => $record->result_url)
207+
->hidden(fn (Result $record): bool => $record->status !== ResultStatus::Completed)
176208
->openUrlInNewTab(),
177209
Tables\Actions\ViewAction::make(),
178210
Tables\Actions\Action::make('updateComments')
@@ -206,14 +238,26 @@ public static function table(Table $table): Table
206238
}),
207239
Tables\Actions\DeleteBulkAction::make(),
208240
])
209-
->defaultSort('created_at', 'desc');
210-
}
241+
->headerActions([
242+
Tables\Actions\Action::make('migrate')
243+
->action(function (): void {
244+
Notification::make()
245+
->title('Starting data migration...')
246+
->body('This can take a little bit depending how much data you have.')
247+
->warning()
248+
->sendToDatabase(Auth::user());
211249

212-
public static function getRelations(): array
213-
{
214-
return [
215-
//
216-
];
250+
MigrateBadJsonResults::dispatch(Auth::user());
251+
})
252+
->hidden($dataSettings->bad_json_migrated)
253+
->requiresConfirmation()
254+
->modalHeading('Migrate History')
255+
->modalDescription(new HtmlString('<p>v0.16.0 archived the old <code>"results"</code> table, to migrate your history click the button below.</p><p>For more information read the <a href="#" target="_blank" rel="nofollow" class="underline">docs</a>.</p>'))
256+
->modalSubmitActionLabel('Yes, migrate it'),
257+
])
258+
->defaultSort('created_at', 'desc')
259+
->paginated([5, 15, 25, 50, 100])
260+
->defaultPaginationPageOption(15);
217261
}
218262

219263
public static function getPages(): array

0 commit comments

Comments
 (0)