mirror of
https://github.com/pelican-dev/panel.git
synced 2025-05-19 22:14:45 +02:00
File manager improvements (#936)
* add separate button for "save & close" * make language selection for editor work * fix download url * add info banner for .pelicanignore files * small cleanup * fix import * Move File Lang * add `ctrl+shift+s` for save & close * fix keybind * cleanup and fix default value for edit * remove unnecessary File::get & trait * More EditorLanguages not matching their names * mdx has its own highlighter --------- Co-authored-by: notCharles <charles@pelican.dev> Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
This commit is contained in:
parent
401026efa1
commit
71f3abe464
@ -36,6 +36,7 @@ enum EditorLanguages: string implements HasLabel
|
||||
case java = 'java';
|
||||
case javascript = 'javascript';
|
||||
case julia = 'julia';
|
||||
case json = 'json';
|
||||
case kotlin = 'kotlin';
|
||||
case less = 'less';
|
||||
case lexon = 'lexon';
|
||||
@ -89,7 +90,49 @@ enum EditorLanguages: string implements HasLabel
|
||||
case wgsl = 'wgsl';
|
||||
case xml = 'xml';
|
||||
case yaml = 'yaml';
|
||||
case json = 'json';
|
||||
|
||||
public static function fromWithAlias(string $match): self
|
||||
{
|
||||
return match ($match) {
|
||||
'h' => self::c,
|
||||
|
||||
'cc', 'hpp' => self::cpp,
|
||||
|
||||
'cs' => self::csharp,
|
||||
|
||||
'class' => self::java,
|
||||
|
||||
'htm' => self::html,
|
||||
|
||||
'js', 'mjs', 'cjs' => self::javascript,
|
||||
|
||||
'kt', 'kts' => self::kotlin,
|
||||
|
||||
'md' => self::markdown,
|
||||
|
||||
'm' => self::objectivec,
|
||||
|
||||
'pl', 'pm' => self::perl,
|
||||
|
||||
'php3', 'php4', 'php5', 'phtml' => self::php,
|
||||
|
||||
'py', 'pyc', 'pyo', 'pyi' => self::python,
|
||||
|
||||
'rdata', 'rds' => self::r,
|
||||
|
||||
'rb', 'erb' => self::ruby,
|
||||
|
||||
'sc' => self::scala,
|
||||
|
||||
'sh', 'zsh' => self::shell,
|
||||
|
||||
'ts', 'tsx' => self::typescript,
|
||||
|
||||
'yml' => self::yaml,
|
||||
|
||||
default => self::tryFrom($match) ?? self::plaintext,
|
||||
};
|
||||
}
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
|
@ -56,6 +56,7 @@ class FileResource extends Resource
|
||||
return [
|
||||
'edit' => Pages\EditFiles::route('/edit/{path}'),
|
||||
'search' => Pages\SearchFiles::route('/search/{searchTerm}'), // TODO: find better way?
|
||||
'download' => Pages\DownloadFiles::route('/download/{path}'),
|
||||
'index' => Pages\ListFiles::route('/{path?}'),
|
||||
];
|
||||
}
|
||||
|
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\FileResource\Pages;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Server\Resources\FileResource;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Services\Nodes\NodeJWTService;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Panel;
|
||||
use Filament\Resources\Pages\Page;
|
||||
use Filament\Resources\Pages\PageRegistration;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Facades\Route as RouteFacade;
|
||||
use Livewire\Attributes\Locked;
|
||||
|
||||
class DownloadFiles extends Page
|
||||
{
|
||||
protected static string $resource = FileResource::class;
|
||||
|
||||
#[Locked]
|
||||
public string $path;
|
||||
|
||||
public function mount(string $path, NodeJWTService $service): void
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
$token = $service
|
||||
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
|
||||
->setUser(auth()->user())
|
||||
->setClaims([
|
||||
'file_path' => rawurldecode($path),
|
||||
'server_uuid' => $server->uuid,
|
||||
])
|
||||
->handle($server->node, auth()->user()->id . $server->uuid);
|
||||
|
||||
Activity::event('server:file.download')
|
||||
->property('file', $path)
|
||||
->log();
|
||||
|
||||
redirect()->away($server->node->getConnectionAddress() . '/download/file?token=' . $token->toString());
|
||||
}
|
||||
|
||||
protected function authorizeAccess(): void
|
||||
{
|
||||
abort_unless(auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, Filament::getTenant()), 403);
|
||||
}
|
||||
|
||||
public static function route(string $path): PageRegistration
|
||||
{
|
||||
return new PageRegistration(
|
||||
page: static::class,
|
||||
route: fn (Panel $panel): Route => RouteFacade::get($path, static::class)
|
||||
->middleware(static::getRouteMiddleware($panel))
|
||||
->withoutMiddleware(static::getWithoutRouteMiddleware($panel))
|
||||
->where('path', '.*'),
|
||||
);
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
||||
use App\Enums\EditorLanguages;
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Server\Resources\FileResource;
|
||||
use App\Models\File;
|
||||
use App\Livewire\AlertBanner;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonFileRepository;
|
||||
@ -18,7 +18,6 @@ use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Concerns\HasUnsavedDataChangesAlert;
|
||||
use Filament\Pages\Concerns\InteractsWithFormActions;
|
||||
use Filament\Panel;
|
||||
use Filament\Resources\Pages\Page;
|
||||
@ -34,7 +33,6 @@ use Livewire\Attributes\Locked;
|
||||
*/
|
||||
class EditFiles extends Page
|
||||
{
|
||||
use HasUnsavedDataChangesAlert;
|
||||
use InteractsWithFormActions;
|
||||
use InteractsWithForms;
|
||||
|
||||
@ -54,27 +52,34 @@ class EditFiles extends Page
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
File::get($server, dirname($this->path))->orderByDesc('is_directory')->orderBy('name');
|
||||
|
||||
return $form
|
||||
->schema([
|
||||
Select::make('lang')
|
||||
->live()
|
||||
->label('')
|
||||
->placeholder('File Language')
|
||||
->options(EditorLanguages::class)
|
||||
->hidden() //TODO Fix Dis
|
||||
->default(function () {
|
||||
$ext = pathinfo($this->path, PATHINFO_EXTENSION);
|
||||
|
||||
if ($ext === 'yml') {
|
||||
return 'yaml';
|
||||
}
|
||||
|
||||
return $ext;
|
||||
}),
|
||||
Section::make('Editing: ' . $this->path)
|
||||
->footerActions([
|
||||
Action::make('save_and_close')
|
||||
->label('Save & Close')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->icon('tabler-device-floppy')
|
||||
->keyBindings('mod+shift+s')
|
||||
->action(function (DaemonFileRepository $fileRepository) use ($server) {
|
||||
$data = $this->form->getState();
|
||||
|
||||
$fileRepository
|
||||
->setServer($server)
|
||||
->putContent($this->path, $data['editor'] ?? '');
|
||||
|
||||
Activity::event('server:file.write')
|
||||
->property('file', $this->path)
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('File saved')
|
||||
->body(fn () => $this->path)
|
||||
->send();
|
||||
|
||||
$this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
|
||||
}),
|
||||
Action::make('save')
|
||||
->label('Save')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
@ -93,12 +98,9 @@ class EditFiles extends Page
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->duration(5000) // 5 seconds
|
||||
->title('Saved File')
|
||||
->title('File saved')
|
||||
->body(fn () => $this->path)
|
||||
->send();
|
||||
|
||||
$this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
|
||||
}),
|
||||
Action::make('cancel')
|
||||
->label('Cancel')
|
||||
@ -108,10 +110,17 @@ class EditFiles extends Page
|
||||
])
|
||||
->footerActionsAlignment(Alignment::End)
|
||||
->schema([
|
||||
Select::make('lang')
|
||||
->label('Syntax Highlighting')
|
||||
->live()
|
||||
->options(EditorLanguages::class)
|
||||
->selectablePlaceholder(false)
|
||||
->afterStateUpdated(fn ($state) => $this->dispatch('setLanguage', lang: $state))
|
||||
->default(fn () => EditorLanguages::fromWithAlias(pathinfo($this->path, PATHINFO_EXTENSION))),
|
||||
MonacoEditor::make('editor')
|
||||
->label('')
|
||||
->placeholderText('')
|
||||
->formatStateUsing(function (DaemonFileRepository $fileRepository) use ($server) {
|
||||
->default(function (DaemonFileRepository $fileRepository) use ($server) {
|
||||
try {
|
||||
return $fileRepository
|
||||
->setServer($server)
|
||||
@ -120,7 +129,7 @@ class EditFiles extends Page
|
||||
abort(404, $this->path . ' not found.');
|
||||
}
|
||||
})
|
||||
->language(fn (Get $get) => $get('lang') ?? 'plaintext')
|
||||
->language(fn (Get $get) => $get('lang'))
|
||||
->view('filament.plugins.monaco-editor'),
|
||||
]),
|
||||
]);
|
||||
@ -133,6 +142,15 @@ class EditFiles extends Page
|
||||
$this->path = $path;
|
||||
|
||||
$this->form->fill();
|
||||
|
||||
if (str($path)->endsWith('.pelicanignore')) {
|
||||
AlertBanner::make()
|
||||
->title('You\'re editing a <code>.pelicanignore</code> file!')
|
||||
->body('Any files or directories listed in here will be excluded from backups. Wildcards are supported by using an asterisk (<code>*</code>).<br>You can negate a prior rule by prepending an exclamation point (<code>!</code>).')
|
||||
->info()
|
||||
->closable()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
protected function authorizeAccess(): void
|
||||
|
@ -10,10 +10,8 @@ use App\Models\File;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonFileRepository;
|
||||
use App\Services\Nodes\NodeJWTService;
|
||||
use App\Filament\Components\Tables\Columns\BytesColumn;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Filament\Actions\Action as HeaderAction;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
@ -21,6 +19,7 @@ use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Notifications\Notification;
|
||||
@ -175,22 +174,7 @@ class ListFiles extends ListRecords
|
||||
->label('Download')
|
||||
->icon('tabler-download')
|
||||
->visible(fn (File $file) => $file->is_file)
|
||||
->action(function (File $file, NodeJWTService $service) use ($server) {
|
||||
$token = $service
|
||||
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
|
||||
->setUser(auth()->user())
|
||||
->setClaims([
|
||||
'file_path' => rawurldecode(join_paths($this->path, $file->name)),
|
||||
'server_uuid' => $server->uuid,
|
||||
])
|
||||
->handle($server->node, auth()->user()->id . $server->uuid);
|
||||
|
||||
Activity::event('server:file.download')
|
||||
->property('file', join_paths($this->path, $file->name))
|
||||
->log();
|
||||
|
||||
return redirect()->away(sprintf('%s/download/file?token=%s', $server->node->getConnectionAddress(), $token->toString())); // TODO: download works, but breaks modals
|
||||
}),
|
||||
->url(fn () => DownloadFiles::getUrl(['path' => $this->path]), true),
|
||||
Action::make('move')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->label('Move')
|
||||
@ -448,15 +432,16 @@ class ListFiles extends ListRecords
|
||||
->label('File Name')
|
||||
->required(),
|
||||
Select::make('lang')
|
||||
->label('Syntax Highlighting')
|
||||
->live()
|
||||
->hidden() //TODO: Make file language selection work
|
||||
->label('Language')
|
||||
->placeholder('File Language')
|
||||
->options(EditorLanguages::class),
|
||||
->options(EditorLanguages::class)
|
||||
->selectablePlaceholder(false)
|
||||
->afterStateUpdated(fn ($state) => $this->dispatch('setLanguage', lang: $state))
|
||||
->default(EditorLanguages::plaintext->value),
|
||||
MonacoEditor::make('editor')
|
||||
->label('')
|
||||
->view('filament.plugins.monaco-editor')
|
||||
->language(fn (Get $get) => $get('lang')),
|
||||
->language(fn (Get $get) => $get('lang') ?? 'plaintext'),
|
||||
]),
|
||||
HeaderAction::make('new_folder')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||
@ -504,13 +489,12 @@ class ListFiles extends ListRecords
|
||||
}
|
||||
|
||||
return redirect(ListFiles::getUrl(['path' => $this->path]));
|
||||
|
||||
})
|
||||
->form([
|
||||
Tabs::make()
|
||||
->contained(false)
|
||||
->schema([
|
||||
Tabs\Tab::make('Upload Files')
|
||||
Tab::make('Upload Files')
|
||||
->live()
|
||||
->schema([
|
||||
FileUpload::make('files')
|
||||
@ -520,7 +504,7 @@ class ListFiles extends ListRecords
|
||||
->preserveFilenames()
|
||||
->multiple(),
|
||||
]),
|
||||
Tabs\Tab::make('Upload From URL')
|
||||
Tab::make('Upload From URL')
|
||||
->live()
|
||||
->disabled(fn (Get $get) => count($get('files')) > 0)
|
||||
->schema([
|
||||
|
@ -6,7 +6,7 @@ return [
|
||||
'show-full-screen-toggle' => true,
|
||||
'show-placeholder' => true,
|
||||
'placeholder-text' => 'Your code here...',
|
||||
'show-loader' => true,
|
||||
'show-loader' => false,
|
||||
'font-size' => '16px',
|
||||
'line-numbers-min-chars' => true,
|
||||
'automatic-layout' => true,
|
||||
|
@ -1,3 +1,11 @@
|
||||
@script
|
||||
<script>
|
||||
$wire.on('setLanguage', ({ lang }) => {
|
||||
monaco.editor.setModelLanguage(document.getElementById('{{ $getId() }}').editor.getModel(), lang);
|
||||
});
|
||||
</script>
|
||||
@endscript
|
||||
|
||||
<x-dynamic-component :component="$getFieldWrapperView()" :field="$field" class="overflow-hidden">
|
||||
|
||||
<div x-data="{
|
||||
@ -12,7 +20,7 @@
|
||||
monacoFontSize: '{{ $getFontSize() }}',
|
||||
lineNumbersMinChars: {{ $getLineNumbersMinChars() }},
|
||||
automaticLayout: {{ (int) $getAutomaticLayout() }},
|
||||
monacoId: $id('monaco-editor'),
|
||||
monacoId: '{{ $getId() }}',
|
||||
|
||||
toggleFullScreenMode() {
|
||||
this.fullScreenModeEnabled = !this.fullScreenModeEnabled;
|
||||
|
@ -6,6 +6,4 @@
|
||||
>
|
||||
{{ $this->form }}
|
||||
</x-filament-panels::form>
|
||||
|
||||
<x-filament-panels::page.unsaved-data-changes-alert />
|
||||
</x-filament-panels::page>
|
||||
|
Loading…
x
Reference in New Issue
Block a user