Feat: Updating Data Erasure and Data Export Views

This commit is contained in:
2026-09-16 00:46:13 +03:00
parent c084eb47cb
commit 027f7e8982
3 changed files with 159 additions and 66 deletions
@@ -5,11 +5,11 @@ namespace Modules\Core\Privacy\Filament\Resources;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Filament\Actions\ViewAction; use Filament\Actions\ViewAction;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Infolists\Components\KeyValueEntry;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Section;
use Modules\Core\Privacy\Filament\Resources\DataErasureRequestResource\Pages\ListDataErasureRequests; use Modules\Core\Privacy\Filament\Resources\DataErasureRequestResource\Pages\ListDataErasureRequests;
use Modules\Core\Privacy\Filament\Resources\DataErasureRequestResource\Pages\ViewDataErasureRequest; use Modules\Core\Privacy\Filament\Resources\DataErasureRequestResource\Pages\ViewDataErasureRequest;
use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\SelectFilter;
@@ -40,45 +40,86 @@ class DataErasureRequestResource extends Resource
protected static ?string $pluralModelLabel = 'Erasure Requests'; protected static ?string $pluralModelLabel = 'Erasure Requests';
public static function form(Schema $schema): Schema /**
* A real infolist, not form()'s disabled inputs/Placeholders — ViewRecord
* falls back to rendering form() in read-only mode when a resource has no
* infolist() at all (Filament\Resources\Pages\ViewRecord::hasInfolist()),
* which is what this resource did before: every field rendered as a
* plain, unstyled label/value pair with no grouping, badges, or icons.
*/
public static function infolist(Schema $schema): Schema
{ {
return $schema->components([ return $schema->components([
Placeholder::make('subject') Section::make('Request')
->icon('heroicon-o-shield-exclamation')
->columns(4)
->components([
TextEntry::make('subject')
->label('Subject') ->label('Subject')
->content(fn (DataErasureRequest $record) => sprintf( ->state(fn (DataErasureRequest $record) => DataErasureRequest::displayNameFor($record->subject))
'%s (%s)', ->weight('bold')
DataErasureRequest::displayNameFor($record->subject), ->size('lg'),
$record->isForCustomer() ? 'Customer account' : 'Individual user' TextEntry::make('subject_type')
)), ->label('Scope')
Placeholder::make('requested_by') ->formatStateUsing(fn (DataErasureRequest $record) => $record->isForCustomer() ? 'Customer account' : 'Individual user')
->label('Requested by') ->badge()
->content(fn (DataErasureRequest $record) => DataErasureRequest::displayNameFor($record->requestedBy)), ->icon(fn (DataErasureRequest $record) => $record->isForCustomer() ? 'heroicon-o-building-office' : 'heroicon-o-user')
TextInput::make('email') ->color(fn (DataErasureRequest $record) => $record->isForCustomer() ? 'info' : 'warning'),
TextEntry::make('email')
->label('Email (snapshot at request time)') ->label('Email (snapshot at request time)')
->disabled(), ->icon('heroicon-o-envelope')
Placeholder::make('status') ->copyable(),
->content(fn (DataErasureRequest $record) => $record->status->value), TextEntry::make('requested_by')
Placeholder::make('scheduled_for') ->label('Requested by')
->state(fn (DataErasureRequest $record) => DataErasureRequest::displayNameFor($record->requestedBy))
->icon('heroicon-o-user-circle'),
TextEntry::make('status')
->badge()
->formatStateUsing(fn (ErasureRequestStatus $state) => ucfirst($state->value))
->color(fn (ErasureRequestStatus $state) => match ($state) {
ErasureRequestStatus::Pending => 'warning',
ErasureRequestStatus::Cancelled => 'gray',
ErasureRequestStatus::Completed => 'success',
}),
TextEntry::make('created_at')
->label('Requested at')
->dateTime()
->icon('heroicon-o-calendar'),
TextEntry::make('scheduled_for')
->label('Scheduled for') ->label('Scheduled for')
->content(fn (DataErasureRequest $record) => $record->scheduled_for->toDayDateTimeString()), ->dateTime()
Placeholder::make('cancelled_at') ->icon('heroicon-o-calendar-days'),
->label('Cancelled at') TextEntry::make('completed_at')
->content(fn (DataErasureRequest $record) => $record->cancelled_at?->toDayDateTimeString() ?? '—'),
Placeholder::make('completed_at')
->label('Completed at') ->label('Completed at')
->content(fn (DataErasureRequest $record) => $record->completed_at?->toDayDateTimeString() ?? '—'), ->dateTime()
Placeholder::make('caused_by') ->placeholder('—')
->label('Caused by (cascade)') ->icon('heroicon-o-check-circle')
->content(fn (DataErasureRequest $record) => $record->causedBy ->color(fn (DataErasureRequest $record) => $record->completed_at ? 'success' : 'gray'),
? "Request #{$record->causedBy->id} (".DataErasureRequest::displayNameFor($record->causedBy->subject).')' TextEntry::make('cancelled_at')
: 'Not a cascade — directly requested') ->label('Cancelled at')
->visible(fn (DataErasureRequest $record) => $record->caused_by_request_id !== null), ->dateTime()
KeyValue::make('report') ->placeholder('—')
->label('Per-provider outcome') ->icon('heroicon-o-x-circle')
->disabled() ->color(fn (DataErasureRequest $record) => $record->cancelled_at ? 'danger' : 'gray')
->visible(fn (DataErasureRequest $record) => $record->cancelled_at !== null),
TextEntry::make('caused_by')
->label('Cascade')
->icon('heroicon-o-arrow-turn-down-right')
->state(fn (DataErasureRequest $record) => $record->causedBy
? "From request #{$record->causedBy->id} (".DataErasureRequest::displayNameFor($record->causedBy->subject).')'
: 'Directly requested')
->color(fn (DataErasureRequest $record) => $record->caused_by_request_id !== null ? 'info' : 'gray'),
]),
Section::make('Outcome')
->description('Each provider\'s outcome once the erasure completed — see docs/privacy.md.')
->icon('heroicon-o-document-check')
->visible(fn (DataErasureRequest $record) => $record->report !== null) ->visible(fn (DataErasureRequest $record) => $record->report !== null)
->helperText('Each provider\'s outcome once the erasure completed — see docs/privacy.md.'), ->components([
])->columns(2); KeyValueEntry::make('report')
->label(''),
]),
]);
} }
public static function table(Table $table): Table public static function table(Table $table): Table
@@ -5,10 +5,10 @@ namespace Modules\Core\Privacy\Filament\Resources;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Filament\Actions\ViewAction; use Filament\Actions\ViewAction;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Section;
use Modules\Core\Privacy\Filament\Resources\DataExportRequestResource\Pages\ListDataExportRequests; use Modules\Core\Privacy\Filament\Resources\DataExportRequestResource\Pages\ListDataExportRequests;
use Modules\Core\Privacy\Filament\Resources\DataExportRequestResource\Pages\ViewDataExportRequest; use Modules\Core\Privacy\Filament\Resources\DataExportRequestResource\Pages\ViewDataExportRequest;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\SelectFilter;
@@ -35,28 +35,77 @@ class DataExportRequestResource extends Resource
protected static ?string $pluralModelLabel = 'Export Requests'; protected static ?string $pluralModelLabel = 'Export Requests';
public static function form(Schema $schema): Schema /**
* A real infolist, not form()'s disabled inputs/Placeholders — see
* DataErasureRequestResource::infolist()'s own docblock for why.
*/
public static function infolist(Schema $schema): Schema
{ {
return $schema->components([ return $schema->components([
Placeholder::make('subject') Section::make('Request')
->icon('heroicon-o-arrow-down-tray')
->columns(4)
->components([
TextEntry::make('subject')
->label('Subject') ->label('Subject')
->content(fn (DataExportRequest $record) => sprintf( ->state(fn (DataExportRequest $record) => DataErasureRequest::displayNameFor($record->subject))
'%s (%s)', ->weight('bold')
DataErasureRequest::displayNameFor($record->subject), ->size('lg'),
$record->isForCustomer() ? 'Customer account' : 'Individual user' TextEntry::make('subject_type')
)), ->label('Scope')
TextInput::make('email') ->formatStateUsing(fn (DataExportRequest $record) => $record->isForCustomer() ? 'Customer account' : 'Individual user')
->badge()
->icon(fn (DataExportRequest $record) => $record->isForCustomer() ? 'heroicon-o-building-office' : 'heroicon-o-user')
->color(fn (DataExportRequest $record) => $record->isForCustomer() ? 'info' : 'warning'),
TextEntry::make('email')
->label('Email (snapshot at request time)') ->label('Email (snapshot at request time)')
->disabled(), ->icon('heroicon-o-envelope')
Placeholder::make('status') ->copyable(),
->content(fn (DataExportRequest $record) => $record->status->value), TextEntry::make('status')
Placeholder::make('completed_at') ->badge()
->formatStateUsing(fn (ExportRequestStatus $state) => ucfirst($state->value))
->color(fn (ExportRequestStatus $state) => match ($state) {
ExportRequestStatus::Pending => 'warning',
ExportRequestStatus::Failed => 'danger',
ExportRequestStatus::Completed => 'success',
}),
TextEntry::make('created_at')
->label('Requested at')
->dateTime()
->icon('heroicon-o-calendar'),
TextEntry::make('completed_at')
->label('Completed at') ->label('Completed at')
->content(fn (DataExportRequest $record) => $record->completed_at?->toDayDateTimeString() ?? '—'), ->dateTime()
Placeholder::make('file_path') ->placeholder('Not generated yet')
->label('Export file') ->icon('heroicon-o-check-circle')
->content(fn (DataExportRequest $record) => $record->file_path ?? 'Not generated yet'), ->color(fn (DataExportRequest $record) => $record->completed_at ? 'success' : 'gray'),
])->columns(2); TextEntry::make('file_path')
->label('File')
// Just the filename, not the full server path — a raw
// filesystem path (/var/www/.../export_2_....zip) isn't
// actionable for staff and previously rendered as if it
// were a clickable link. The actual download is the
// "Download" header action below (self::downloadAction()),
// shared with the table's row action.
->state(fn (DataExportRequest $record) => $record->file_path ? basename($record->file_path) : 'Not generated yet')
->icon('heroicon-o-document')
->color(fn (DataExportRequest $record) => $record->file_path ? 'success' : 'gray'),
]),
]);
}
/**
* Shared by the table's row action and the view page's header action
* (ViewDataExportRequest::getHeaderActions()) so "is this downloadable"
* and the download itself are defined in exactly one place.
*/
public static function downloadAction(): Action
{
return Action::make('download')
->label('Download')
->icon('heroicon-o-arrow-down-tray')
->visible(fn (DataExportRequest $record) => $record->status === ExportRequestStatus::Completed && $record->file_path && file_exists($record->file_path))
->action(fn (DataExportRequest $record) => response()->download($record->file_path));
} }
public static function table(Table $table): Table public static function table(Table $table): Table
@@ -104,11 +153,7 @@ class DataExportRequestResource extends Resource
]) ])
->recordActions([ ->recordActions([
ViewAction::make(), ViewAction::make(),
Action::make('download') self::downloadAction(),
->label('Download')
->icon('heroicon-o-arrow-down-tray')
->visible(fn (DataExportRequest $record) => $record->status === ExportRequestStatus::Completed && $record->file_path && file_exists($record->file_path))
->action(fn (DataExportRequest $record) => response()->download($record->file_path)),
]); ]);
} }
@@ -8,4 +8,11 @@ use Modules\Core\Privacy\Filament\Resources\DataExportRequestResource;
class ViewDataExportRequest extends ViewRecord class ViewDataExportRequest extends ViewRecord
{ {
protected static string $resource = DataExportRequestResource::class; protected static string $resource = DataExportRequestResource::class;
protected function getHeaderActions(): array
{
return [
DataExportRequestResource::downloadAction(),
];
}
} }