mirror of
https://github.com/pelican-dev/panel.git
synced 2025-05-20 04:04: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 java = 'java';
|
||||||
case javascript = 'javascript';
|
case javascript = 'javascript';
|
||||||
case julia = 'julia';
|
case julia = 'julia';
|
||||||
|
case json = 'json';
|
||||||
case kotlin = 'kotlin';
|
case kotlin = 'kotlin';
|
||||||
case less = 'less';
|
case less = 'less';
|
||||||
case lexon = 'lexon';
|
case lexon = 'lexon';
|
||||||
@ -89,7 +90,49 @@ enum EditorLanguages: string implements HasLabel
|
|||||||
case wgsl = 'wgsl';
|
case wgsl = 'wgsl';
|
||||||
case xml = 'xml';
|
case xml = 'xml';
|
||||||
case yaml = 'yaml';
|
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
|
public function getLabel(): string
|
||||||
{
|
{
|
||||||
|
@ -56,6 +56,7 @@ class FileResource extends Resource
|
|||||||
return [
|
return [
|
||||||
'edit' => Pages\EditFiles::route('/edit/{path}'),
|
'edit' => Pages\EditFiles::route('/edit/{path}'),
|
||||||
'search' => Pages\SearchFiles::route('/search/{searchTerm}'), // TODO: find better way?
|
'search' => Pages\SearchFiles::route('/search/{searchTerm}'), // TODO: find better way?
|
||||||
|
'download' => Pages\DownloadFiles::route('/download/{path}'),
|
||||||
'index' => Pages\ListFiles::route('/{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\Enums\EditorLanguages;
|
||||||
use App\Facades\Activity;
|
use App\Facades\Activity;
|
||||||
use App\Filament\Server\Resources\FileResource;
|
use App\Filament\Server\Resources\FileResource;
|
||||||
use App\Models\File;
|
use App\Livewire\AlertBanner;
|
||||||
use App\Models\Permission;
|
use App\Models\Permission;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Repositories\Daemon\DaemonFileRepository;
|
use App\Repositories\Daemon\DaemonFileRepository;
|
||||||
@ -18,7 +18,6 @@ use Filament\Forms\Concerns\InteractsWithForms;
|
|||||||
use Filament\Forms\Form;
|
use Filament\Forms\Form;
|
||||||
use Filament\Forms\Get;
|
use Filament\Forms\Get;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Pages\Concerns\HasUnsavedDataChangesAlert;
|
|
||||||
use Filament\Pages\Concerns\InteractsWithFormActions;
|
use Filament\Pages\Concerns\InteractsWithFormActions;
|
||||||
use Filament\Panel;
|
use Filament\Panel;
|
||||||
use Filament\Resources\Pages\Page;
|
use Filament\Resources\Pages\Page;
|
||||||
@ -34,7 +33,6 @@ use Livewire\Attributes\Locked;
|
|||||||
*/
|
*/
|
||||||
class EditFiles extends Page
|
class EditFiles extends Page
|
||||||
{
|
{
|
||||||
use HasUnsavedDataChangesAlert;
|
|
||||||
use InteractsWithFormActions;
|
use InteractsWithFormActions;
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
|
|
||||||
@ -54,27 +52,34 @@ class EditFiles extends Page
|
|||||||
/** @var Server $server */
|
/** @var Server $server */
|
||||||
$server = Filament::getTenant();
|
$server = Filament::getTenant();
|
||||||
|
|
||||||
File::get($server, dirname($this->path))->orderByDesc('is_directory')->orderBy('name');
|
|
||||||
|
|
||||||
return $form
|
return $form
|
||||||
->schema([
|
->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)
|
Section::make('Editing: ' . $this->path)
|
||||||
->footerActions([
|
->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')
|
Action::make('save')
|
||||||
->label('Save')
|
->label('Save')
|
||||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||||
@ -93,12 +98,9 @@ class EditFiles extends Page
|
|||||||
|
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->success()
|
->success()
|
||||||
->duration(5000) // 5 seconds
|
->title('File saved')
|
||||||
->title('Saved File')
|
|
||||||
->body(fn () => $this->path)
|
->body(fn () => $this->path)
|
||||||
->send();
|
->send();
|
||||||
|
|
||||||
$this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
|
|
||||||
}),
|
}),
|
||||||
Action::make('cancel')
|
Action::make('cancel')
|
||||||
->label('Cancel')
|
->label('Cancel')
|
||||||
@ -108,10 +110,17 @@ class EditFiles extends Page
|
|||||||
])
|
])
|
||||||
->footerActionsAlignment(Alignment::End)
|
->footerActionsAlignment(Alignment::End)
|
||||||
->schema([
|
->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')
|
MonacoEditor::make('editor')
|
||||||
->label('')
|
->label('')
|
||||||
->placeholderText('')
|
->placeholderText('')
|
||||||
->formatStateUsing(function (DaemonFileRepository $fileRepository) use ($server) {
|
->default(function (DaemonFileRepository $fileRepository) use ($server) {
|
||||||
try {
|
try {
|
||||||
return $fileRepository
|
return $fileRepository
|
||||||
->setServer($server)
|
->setServer($server)
|
||||||
@ -120,7 +129,7 @@ class EditFiles extends Page
|
|||||||
abort(404, $this->path . ' not found.');
|
abort(404, $this->path . ' not found.');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
->language(fn (Get $get) => $get('lang') ?? 'plaintext')
|
->language(fn (Get $get) => $get('lang'))
|
||||||
->view('filament.plugins.monaco-editor'),
|
->view('filament.plugins.monaco-editor'),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
@ -133,6 +142,15 @@ class EditFiles extends Page
|
|||||||
$this->path = $path;
|
$this->path = $path;
|
||||||
|
|
||||||
$this->form->fill();
|
$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
|
protected function authorizeAccess(): void
|
||||||
|
@ -10,10 +10,8 @@ use App\Models\File;
|
|||||||
use App\Models\Permission;
|
use App\Models\Permission;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Repositories\Daemon\DaemonFileRepository;
|
use App\Repositories\Daemon\DaemonFileRepository;
|
||||||
use App\Services\Nodes\NodeJWTService;
|
|
||||||
use App\Filament\Components\Tables\Columns\BytesColumn;
|
use App\Filament\Components\Tables\Columns\BytesColumn;
|
||||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||||
use Carbon\CarbonImmutable;
|
|
||||||
use Filament\Actions\Action as HeaderAction;
|
use Filament\Actions\Action as HeaderAction;
|
||||||
use Filament\Facades\Filament;
|
use Filament\Facades\Filament;
|
||||||
use Filament\Forms\Components\CheckboxList;
|
use Filament\Forms\Components\CheckboxList;
|
||||||
@ -21,6 +19,7 @@ use Filament\Forms\Components\FileUpload;
|
|||||||
use Filament\Forms\Components\Placeholder;
|
use Filament\Forms\Components\Placeholder;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Components\Tabs;
|
use Filament\Forms\Components\Tabs;
|
||||||
|
use Filament\Forms\Components\Tabs\Tab;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Get;
|
use Filament\Forms\Get;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
@ -175,22 +174,7 @@ class ListFiles extends ListRecords
|
|||||||
->label('Download')
|
->label('Download')
|
||||||
->icon('tabler-download')
|
->icon('tabler-download')
|
||||||
->visible(fn (File $file) => $file->is_file)
|
->visible(fn (File $file) => $file->is_file)
|
||||||
->action(function (File $file, NodeJWTService $service) use ($server) {
|
->url(fn () => DownloadFiles::getUrl(['path' => $this->path]), true),
|
||||||
$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
|
|
||||||
}),
|
|
||||||
Action::make('move')
|
Action::make('move')
|
||||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||||
->label('Move')
|
->label('Move')
|
||||||
@ -448,15 +432,16 @@ class ListFiles extends ListRecords
|
|||||||
->label('File Name')
|
->label('File Name')
|
||||||
->required(),
|
->required(),
|
||||||
Select::make('lang')
|
Select::make('lang')
|
||||||
|
->label('Syntax Highlighting')
|
||||||
->live()
|
->live()
|
||||||
->hidden() //TODO: Make file language selection work
|
->options(EditorLanguages::class)
|
||||||
->label('Language')
|
->selectablePlaceholder(false)
|
||||||
->placeholder('File Language')
|
->afterStateUpdated(fn ($state) => $this->dispatch('setLanguage', lang: $state))
|
||||||
->options(EditorLanguages::class),
|
->default(EditorLanguages::plaintext->value),
|
||||||
MonacoEditor::make('editor')
|
MonacoEditor::make('editor')
|
||||||
->label('')
|
->label('')
|
||||||
->view('filament.plugins.monaco-editor')
|
->view('filament.plugins.monaco-editor')
|
||||||
->language(fn (Get $get) => $get('lang')),
|
->language(fn (Get $get) => $get('lang') ?? 'plaintext'),
|
||||||
]),
|
]),
|
||||||
HeaderAction::make('new_folder')
|
HeaderAction::make('new_folder')
|
||||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
->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]));
|
return redirect(ListFiles::getUrl(['path' => $this->path]));
|
||||||
|
|
||||||
})
|
})
|
||||||
->form([
|
->form([
|
||||||
Tabs::make()
|
Tabs::make()
|
||||||
->contained(false)
|
->contained(false)
|
||||||
->schema([
|
->schema([
|
||||||
Tabs\Tab::make('Upload Files')
|
Tab::make('Upload Files')
|
||||||
->live()
|
->live()
|
||||||
->schema([
|
->schema([
|
||||||
FileUpload::make('files')
|
FileUpload::make('files')
|
||||||
@ -520,7 +504,7 @@ class ListFiles extends ListRecords
|
|||||||
->preserveFilenames()
|
->preserveFilenames()
|
||||||
->multiple(),
|
->multiple(),
|
||||||
]),
|
]),
|
||||||
Tabs\Tab::make('Upload From URL')
|
Tab::make('Upload From URL')
|
||||||
->live()
|
->live()
|
||||||
->disabled(fn (Get $get) => count($get('files')) > 0)
|
->disabled(fn (Get $get) => count($get('files')) > 0)
|
||||||
->schema([
|
->schema([
|
||||||
|
@ -6,7 +6,7 @@ return [
|
|||||||
'show-full-screen-toggle' => true,
|
'show-full-screen-toggle' => true,
|
||||||
'show-placeholder' => true,
|
'show-placeholder' => true,
|
||||||
'placeholder-text' => 'Your code here...',
|
'placeholder-text' => 'Your code here...',
|
||||||
'show-loader' => true,
|
'show-loader' => false,
|
||||||
'font-size' => '16px',
|
'font-size' => '16px',
|
||||||
'line-numbers-min-chars' => true,
|
'line-numbers-min-chars' => true,
|
||||||
'automatic-layout' => 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">
|
<x-dynamic-component :component="$getFieldWrapperView()" :field="$field" class="overflow-hidden">
|
||||||
|
|
||||||
<div x-data="{
|
<div x-data="{
|
||||||
@ -12,7 +20,7 @@
|
|||||||
monacoFontSize: '{{ $getFontSize() }}',
|
monacoFontSize: '{{ $getFontSize() }}',
|
||||||
lineNumbersMinChars: {{ $getLineNumbersMinChars() }},
|
lineNumbersMinChars: {{ $getLineNumbersMinChars() }},
|
||||||
automaticLayout: {{ (int) $getAutomaticLayout() }},
|
automaticLayout: {{ (int) $getAutomaticLayout() }},
|
||||||
monacoId: $id('monaco-editor'),
|
monacoId: '{{ $getId() }}',
|
||||||
|
|
||||||
toggleFullScreenMode() {
|
toggleFullScreenMode() {
|
||||||
this.fullScreenModeEnabled = !this.fullScreenModeEnabled;
|
this.fullScreenModeEnabled = !this.fullScreenModeEnabled;
|
||||||
|
@ -6,6 +6,4 @@
|
|||||||
>
|
>
|
||||||
{{ $this->form }}
|
{{ $this->form }}
|
||||||
</x-filament-panels::form>
|
</x-filament-panels::form>
|
||||||
|
|
||||||
<x-filament-panels::page.unsaved-data-changes-alert />
|
|
||||||
</x-filament-panels::page>
|
</x-filament-panels::page>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user