Skip to content

Commit d0bf806

Browse files
authored
chore: add admin form (#65)
* chore: add admin form * chore: add create polydock app instance - filament test * feat: implement dynamic app class discovery and custom configuration schemas - Introduced `PolydockAppClassDiscovery` service to dynamically locate and register `PolydockAppInterface` implementations via Composer classmaps. - Enhanced Filament admin resources (`PolydockStoreApp` and `PolydockAppInstance`) to render dynamic forms and infolists based on class-defined schemas. - Integrated `PolydockVariable` storage for app and instance-specific configurations, including support for field-level encryption. - Updated the trial registration flow in `ProcessUserRemoteRegistration` to capture and store instance configuration fields. - Added validation to `PolydockEngine` to ensure discovered classes correctly implement the required interfaces. - Applied `declare(strict_types=1);` and improved type hinting across modified core files. - Bumped `freedomtech-hosting` and `amazeeio` package dependencies to latest versions. - Added comprehensive unit tests for the new discovery service. * fix: exception logging * chore: fixup env vars * chore: require amazeeio/lagoon-logs * fix: additional null check return string, and ensure polydock_store_id is set for create webhook store
1 parent 2814a82 commit d0bf806

29 files changed

Lines changed: 2420 additions & 324 deletions

.lagoon.env

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,16 @@ QUEUE_CONNECTION=redis
3737
# No environment vars
3838

3939
# Freedom Tech PHP Lagoon Client Settings
40-
FTLAGOON_PRIVATE_KEY_FILE=storage/ftlagoon/.ssh/id_rsa
41-
FTLAGOON_TOKEN_CACHE_DIR=storage/ftlagoon/.tokencache/
40+
FTLAGOON_PRIVATE_KEY_FILE=/app/storage/ftlagoon/.ssh/polydock
41+
FTLAGOON_TOKEN_CACHE_DIR=/app/storage/ftlagoon/.tokencache/
4242

43-
FTLAGOON_ENDPOINT=https://api.main.lagoon-core.test6.amazee.io/graphql
44-
FTLAGOON_SSH_USER=lagoon
45-
FTLAGOON_SSH_PORT=22
46-
FTLAGOON_SSH_SERVER=ssh.main.lagoon-core.test6.amazee.io
47-
FTLAGOON_SSH_PRIVATE_KEY_FILE=/home/bryan/.ssh/id_rsa
43+
# FTLAGOON_ENDPOINT=https://api.main.lagoon-core.test6.amazee.io/graphql
44+
# FTLAGOON_SSH_USER=lagoon
45+
# FTLAGOON_SSH_PORT=22
46+
# FTLAGOON_SSH_SERVER=ssh.main.lagoon-core.test6.amazee.io
4847

48+
# FTLAGOON_PRIVATE_KEY_FILE=storage/ftlagoon/.ssh/polydock
4949
# FTLAGOON_ENDPOINT=https://api.lagoon.amazeeio.cloud/graphql
5050
# FTLAGOON_SSH_USER=lagoon
5151
# FTLAGOON_SSH_PORT=32222
5252
# FTLAGOON_SSH_SERVER=ssh.lagoon.amazeeio.cloud
53-
# FTLAGOON_SSH_PRIVATE_KEY_FILE=/home/bryan/.ssh/id_rsa
54-

app/Console/Commands/CreateStoreApp.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use App\Enums\PolydockStoreAppStatusEnum;
66
use App\Models\PolydockStore;
77
use App\Models\PolydockStoreApp;
8+
use App\Services\PolydockAppClassDiscovery;
89
use Illuminate\Console\Command;
910

1011
class CreateStoreApp extends Command
@@ -72,7 +73,23 @@ public function handle(): int
7273

7374
// Gather app information
7475
$name = $this->option('name') ?? $this->ask('App name');
75-
$appClass = $this->option('app-class') ?? $this->ask('Polydock app class');
76+
$discovery = app(PolydockAppClassDiscovery::class);
77+
$availableClasses = $discovery->getAvailableAppClasses();
78+
$appClass = $this->option('app-class');
79+
if ($appClass) {
80+
if (! $discovery->isValidAppClass($appClass)) {
81+
$this->error("Invalid app class: {$appClass}. Must be a concrete PolydockAppInterface implementation.");
82+
83+
return 1;
84+
}
85+
} else {
86+
if (empty($availableClasses)) {
87+
$this->error('No Polydock app classes found. Ensure packages are installed correctly.');
88+
89+
return 1;
90+
}
91+
$appClass = $this->choice('Select Polydock app class', array_keys($availableClasses));
92+
}
7693
$description = $this->option('description') ?? $this->ask('App description');
7794
$author = $this->option('author') ?? $this->ask('Author');
7895
$website = $this->option('website') ?? $this->ask('Author website');

app/Filament/Admin/Resources/PolydockAppInstanceResource.php

Lines changed: 110 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use App\Models\PolydockAppInstance;
99
use App\PolydockEngine\Helpers\AmazeeAiBackendHelper;
1010
use App\PolydockEngine\Helpers\LagoonHelper;
11+
use App\Services\PolydockAppClassDiscovery;
1112
use Filament\Forms;
1213
use Filament\Forms\Form;
1314
use Filament\Infolists\Infolist;
@@ -17,6 +18,7 @@
1718
use Filament\Tables\Columns\TextColumn;
1819
use Filament\Tables\Filters\SelectFilter;
1920
use Filament\Tables\Table;
21+
use FreedomtechHosting\PolydockApp\Attributes\PolydockAppInstanceFields;
2022
use FreedomtechHosting\PolydockApp\Enums\PolydockAppInstanceStatus;
2123

2224
class PolydockAppInstanceResource extends Resource
@@ -70,20 +72,28 @@ public static function table(Table $table): Table
7072
TextColumn::make('send_midtrial_email_at')
7173
->label('Midtrial Email')
7274
->description(fn ($record) => $record->midtrial_email_sent ? 'Sent' : 'Pending')
73-
->state(fn ($record) => $record->send_midtrial_email_at ? $record->send_midtrial_email_at->format('Y-m-d H:i:s') : ''),
75+
->state(fn ($record) => $record->send_midtrial_email_at
76+
? $record->send_midtrial_email_at->format('Y-m-d H:i:s')
77+
: ''),
7478
TextColumn::make('send_one_day_left_email_at')
7579
->label('1D Left Email')
7680
->description(fn ($record) => $record->one_day_left_email_sent ? 'Sent' : 'Pending')
77-
->state(fn ($record) => $record->send_one_day_left_email_at ? $record->send_one_day_left_email_at->format('Y-m-d H:i:s') : ''),
81+
->state(fn ($record) => $record->send_one_day_left_email_at
82+
? $record->send_one_day_left_email_at->format('Y-m-d H:i:s')
83+
: ''),
7884
TextColumn::make('trial_complete_email_sent')
7985
->label('Trial Complete Email')
80-
->state(fn ($record) => ($record->is_trial && $record->trial_complete_email_sent) ? 'Sent' : ($record->is_trial ? 'Pending' : '')),
86+
->state(fn ($record) => $record->is_trial && $record->trial_complete_email_sent
87+
? 'Sent'
88+
: ($record->is_trial ? 'Pending' : '')),
8189
])
8290
->filters([
8391
SelectFilter::make('status')
84-
->options(collect(PolydockAppInstanceStatus::cases())
85-
->mapWithKeys(fn ($status) => [$status->value => $status->getLabel()])
86-
->toArray())
92+
->options(
93+
collect(PolydockAppInstanceStatus::cases())
94+
->mapWithKeys(fn ($status) => [$status->value => $status->getLabel()])
95+
->toArray(),
96+
)
8797
->multiple()
8898
->label('Instance Status')
8999
->indicator('Status'),
@@ -165,7 +175,8 @@ public static function table(Table $table): Table
165175
->actions([
166176
Tables\Actions\ViewAction::make(),
167177
Tables\Actions\EditAction::make(),
168-
])->headerActions([
178+
])
179+
->headerActions([
169180
ExportAction::make()
170181
->label('Export registrations')
171182
->exporter(UserRemoteRegistrationExporter::class),
@@ -205,10 +216,19 @@ public static function infolist(Infolist $infolist): Infolist
205216
->schema([
206217
\Filament\Infolists\Components\TextEntry::make('storeApp.lagoon_deploy_region_id_ext')
207218
->label('Deploy Region')
208-
->formatStateUsing(fn ($state) => LagoonHelper::getLagoonCodeDataValueForRegion($state, 'name')),
209-
\Filament\Infolists\Components\TextEntry::make('storeApp.amazee_ai_backend_region_id_ext')
219+
->formatStateUsing(
220+
fn ($state) => LagoonHelper::getLagoonCodeDataValueForRegion($state, 'name'),
221+
),
222+
\Filament\Infolists\Components\TextEntry::make(
223+
'storeApp.amazee_ai_backend_region_id_ext',
224+
)
210225
->label('AI Backend Region')
211-
->formatStateUsing(fn ($state) => AmazeeAiBackendHelper::getAmazeeAiBackendCodeDataValueForRegion($state, 'name')),
226+
->formatStateUsing(
227+
fn ($state) => AmazeeAiBackendHelper::getAmazeeAiBackendCodeDataValueForRegion(
228+
$state,
229+
'name',
230+
),
231+
),
212232
]),
213233
])
214234
->columnSpan(2),
@@ -229,6 +249,13 @@ public static function infolist(Infolist $infolist): Infolist
229249
])
230250
->columnSpan(1),
231251

252+
\Filament\Infolists\Components\Section::make('Instance Configuration')
253+
->description('Instance-specific settings configured at creation.')
254+
->schema(fn ($record) => self::getRenderedInstanceConfigForRecord($record))
255+
->visible(fn ($record) => self::hasInstanceConfigFields($record))
256+
->columnSpan(3)
257+
->collapsible(),
258+
232259
\Filament\Infolists\Components\Section::make('Instance Data')
233260
->description('Safe data that can be shared with webhooks')
234261
->schema(fn ($record) => self::getRenderedSafeDataForRecord($record))
@@ -249,7 +276,7 @@ public static function getRenderedSafeDataForRecord(PolydockAppInstance $record)
249276
}
250277

251278
if ($value === null) {
252-
$value = '';
279+
$value = 'N/A';
253280
}
254281

255282
$renderKey = 'webhook_data_'.$key;
@@ -271,6 +298,76 @@ public static function getRenderedSafeDataForRecord(PolydockAppInstance $record)
271298
return $renderedArray;
272299
}
273300

301+
/**
302+
* Check if the record's app class defines instance configuration fields.
303+
*/
304+
public static function hasInstanceConfigFields(PolydockAppInstance $record): bool
305+
{
306+
$storeApp = $record->storeApp;
307+
if (! $storeApp || empty($storeApp->polydock_app_class)) {
308+
return false;
309+
}
310+
311+
$discovery = app(PolydockAppClassDiscovery::class);
312+
313+
return ! empty($discovery->getAppInstanceInfolistSchema($storeApp->polydock_app_class));
314+
}
315+
316+
/**
317+
* Get rendered infolist components for instance configuration fields.
318+
*
319+
* Values are loaded from PolydockVariables associated with the app instance.
320+
*/
321+
public static function getRenderedInstanceConfigForRecord(PolydockAppInstance $record): array
322+
{
323+
$storeApp = $record->storeApp;
324+
if (! $storeApp || empty($storeApp->polydock_app_class)) {
325+
return [];
326+
}
327+
328+
$discovery = app(PolydockAppClassDiscovery::class);
329+
$fieldNames = $discovery->getAppInstanceFormFieldNames($storeApp->polydock_app_class);
330+
331+
if (empty($fieldNames)) {
332+
return [];
333+
}
334+
335+
// Build a simple display of instance config values from PolydockVariables
336+
$renderedArray = [];
337+
$instanceConfigPrefix = PolydockAppInstanceFields::FIELD_PREFIX;
338+
339+
foreach ($fieldNames as $fieldName) {
340+
$value = $record->getPolydockVariableValue($fieldName);
341+
342+
// Create a human-readable label from the field name
343+
// e.g., "instance_config_ai_model_override" -> "Ai Model Override"
344+
$labelName = str_replace($instanceConfigPrefix, '', $fieldName);
345+
$labelName = str_replace('_', ' ', $labelName);
346+
$labelName = ucwords($labelName);
347+
348+
// Check if value should be masked (for encrypted fields)
349+
$isEncrypted = $record->isPolydockVariableEncrypted($fieldName);
350+
351+
$renderedItem = \Filament\Infolists\Components\TextEntry::make('instance_config_display_'.$fieldName)
352+
->label($labelName);
353+
354+
if ($isEncrypted && $value !== null && $value !== '') {
355+
// Mask encrypted values
356+
$renderedItem->state('********');
357+
} elseif ($value === null || $value === '') {
358+
$renderedItem->state('Not configured')
359+
->color('gray');
360+
} else {
361+
$renderedItem->state($value);
362+
}
363+
364+
$renderedArray[] = $renderedItem;
365+
}
366+
367+
return $renderedArray;
368+
}
369+
370+
#[\Override]
274371
public static function getRelations(): array
275372
{
276373
return [
@@ -281,14 +378,15 @@ public static function getRelations(): array
281378
#[\Override]
282379
public static function canCreate(): bool
283380
{
284-
return false;
381+
return true;
285382
}
286383

287384
#[\Override]
288385
public static function getPages(): array
289386
{
290387
return [
291388
'index' => Pages\ListPolydockAppInstances::route('/'),
389+
'create' => Pages\CreatePolydockAppInstance::route('/create'),
292390
'view' => Pages\ViewPolydockAppInstance::route('/{record}'),
293391
'edit' => Pages\EditPolydockAppInstance::route('/{record}/edit'),
294392
];

0 commit comments

Comments
 (0)