mirror of
https://github.com/pelican-dev/panel.git
synced 2025-05-19 21:04:44 +02:00
feat: Client UI translate to Filament (from React) (#416)
* Add new panel * Add some basic resource pages * Wip * Wip terminal * Wip * Add new panel * Add some basic resource pages * Wip * [Sub-Users] Add Invite TODO: The logic with permissions * [Sub-Users] Fix Creation * [Cron] Add basics * Add basic auth and messages * Add basic buttons * WIP on issue/353 * WIP on issue/353 * Add Database page * Update Database Page * Start of Backup Page * Composer Update * Changes * Send input * Remove this includes * Better offline handling * Consolidate top nav config * Update Backups Page * Update Backups * Change name * Add Assign All, Layout Fixes. * conflict * update schedule pages * fix phpstan * update pint.json * add cron presets to schedule * fix tests * fix task creation * schedules: disable task creation if limit is reached & disable backup action if backup limit is 0 * update activity pages * update resources * Update Edit User TODO: actually save permissions when they're changed. TODO: Figure out why Control does not update it's state... but the rest do... * .... Sure it works. TODO: Update permissions when you save editing a sub user. * user: update canAccessPanel & canAccessTenant * add helper to convert bytes into readable format * very basic file explorer * files: fix some stuff & remove dummy data * files: better error handling * files: basic file editor * files: add some actions * File manager updates * files: fix paths * Revery Composer Upgrade, Fixes SQLite * fix: Pint (#517) feat: MenuItems to and from admin * Update File Editing Updated File Editing to its own page, Added Permission checks for file manager. Co-authored-by: Boy132 <Boy132@users.noreply.github.com> * add enum for editor langs * files: add upload & pull actions * fix build * files: handle images * Update to Filament v3.2.98 * files: add remaining actions * use `authorize` instead of `hidden` * fix canAccessTenant * update date columns * files: testing & fixes * Fix File Names Co-authored-by: lancepioch <git@lance.sh> * Combine Pull/Upload * Fix BulkDelete * Uncontained tabs * Hide Lang Selection, Move Actions * Update Monaco, more custom * Add livewire config livewire limits uploads to 12MB... who knows why... Fixed uploading a single files failing * files: fix record url * basic setup for settings & startup page * make abstract class for simple app pages * Basic Startup Page * Update nav sort * small cleanup * startup: fix shouldHideComponent & getSelectOptionsFromRules * startup: fix non editable fields & set default value * startup: add todo for save button * Save Variables after update & off click Variables update when the user clicks off the input. * Notifications are cool * Add rule validation * Sort variables by sortid * pint * Settings Page + Startup Changes * settings: cleanup * refactor: use server model for ServerFormPage (formerly known as SimplePage) * Use Repeater for variables * Add Network, Remove breadcrumbs * Add paginated to file explorer * Fix updating variables * Add link to go to new client area * fix after merge * Add graphs to console page Graphs still need to get the data from the web socket. * fix pint & phpstan * fix authorizeAccess for EditFiles and Startup page * Fix rules on startup page * Update console size * Fix node name * add "global search" to files list requires https://github.com/pelican-dev/wings/pull/44 * remove debug dummy data * update view action on ListServers * enable SPA mode for app panel * remove colors from app panel they are defined globally in AppServiceProvider * update global search ui a bit (to be replaced with a custom page that is similar to the list files table) * add own page for global search untested - and route needs cleanup (if possible) * fix File getRows * remove "path" from SearchFiles (for now) * fix caching for searched files * add title and breadcrumbs to global search page * make cpu & memory charts on console page working * fix phpstan * add missing import * cleanup console views & widgets * add overview stats to console * don't be so lazy, console! * make history working * decode data to get array * add missing On * fix json_decode * change polling to 1 sec * hide "0" cpu/ memory * add data to network chart * Remove data labels * fix data on network chart * fix data on network chart (2nd try) * WIP Network Stats * Remove test * Change MaxWidth * run pint * fix phpstan * Fix storeStats cast * make $data a string this time for real * update visible check for "admin" menu item * remove account widget * rebrand "Dashboard" to "Server List" WIP - doesn't look good but is somewhat working * fix canAccessPanel * separate server list into own panel * change path to avoid conflicts with old client area (and remove sidebar width) * display correct icon and color on server list entries * show total memory if server is offline * replace custom server list page with ListRecords page * fix tests * fix namespace * remove "open" button and make whole column clickable * Update EditProfile * run pint * fix access to server list * add new login page to panels * fix next_run_at for new schedules * use new DateTimeColumn * add own column for file bytes * return to server list when clicking title * fix console loading * handle server with "conflict state" * add banner if server is in "conflict state" * fix phpstan * update docker image select * fix permission checks on Settings & Startup pages * fix query for activity log page * fix activity log not being logged * adjust ListActivities * fix phpstan * fix pint * fix profile menu item link on server panel * add ip tooltip to activity logs (and role permission) * change backup icon * update navigation sort * general code cleanup * more cleanup * Disable Restart/Stop if server is offline * Change rename notification * Remove negation on abort_unless * Add notification on save * Single disabled closure & comment unused import * Add required to Server Name & Nullable to description * mutateFormDataBeforeSave doesn't work since we use forceFill * Fix web socket connection not existing. * Fix some subuser permissions * add permission checks to resources * do not allow self-deletion * Update editing file permissions * Fix of the previous fix * add service for subuser updating * Only allow save if they have file_update * Remove unused import * Update backup delete button * Add Delete, remove bulks * Update Database page * Use Allocation Permissions * add canAccess check to startup * Add Permission checks to Settings page * add service for subuser deletion * Remove Kill permission * Updates * fix move files * add redirects * fix phpstan * activity: remove properties from tans for now * If alias, use that, else ip --------- Co-authored-by: notCharles <charles@pelican.dev> Co-authored-by: Boy132 <mail@boy132.de> Co-authored-by: Senna <62171904+Poseidon281@users.noreply.github.com> Co-authored-by: Boy132 <Boy132@users.noreply.github.com> Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
This commit is contained in:
parent
e0c6137b92
commit
fea1c51337
98
app/Enums/EditorLanguages.php
Normal file
98
app/Enums/EditorLanguages.php
Normal file
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use Filament\Support\Contracts\HasLabel;
|
||||
|
||||
enum EditorLanguages: string implements HasLabel
|
||||
{
|
||||
case plaintext = 'plaintext';
|
||||
case abap = 'abap';
|
||||
case apex = 'apex';
|
||||
case azcali = 'azcali';
|
||||
case bat = 'bat';
|
||||
case bicep = 'bicep';
|
||||
case cameligo = 'cameligo';
|
||||
case coljure = 'coljure';
|
||||
case coffeescript = 'coffeescript';
|
||||
case c = 'c';
|
||||
case cpp = 'cpp';
|
||||
case csharp = 'csharp';
|
||||
case csp = 'csp';
|
||||
case css = 'css';
|
||||
case cypher = 'cypher';
|
||||
case dart = 'dart';
|
||||
case dockerfile = 'dockerfile';
|
||||
case ecl = 'ecl';
|
||||
case elixir = 'elixir';
|
||||
case flow9 = 'flow9';
|
||||
case fsharp = 'fsharp';
|
||||
case go = 'go';
|
||||
case graphql = 'graphql';
|
||||
case handlebars = 'handlebars';
|
||||
case hcl = 'hcl';
|
||||
case html = 'html';
|
||||
case ini = 'ini';
|
||||
case java = 'java';
|
||||
case javascript = 'javascript';
|
||||
case julia = 'julia';
|
||||
case kotlin = 'kotlin';
|
||||
case less = 'less';
|
||||
case lexon = 'lexon';
|
||||
case lua = 'lua';
|
||||
case liquid = 'liquid';
|
||||
case m3 = 'm3';
|
||||
case markdown = 'markdown';
|
||||
case mdx = 'mdx';
|
||||
case mips = 'mips';
|
||||
case msdax = 'msdax';
|
||||
case mysql = 'mysql';
|
||||
case objectivec = 'objective-c';
|
||||
case pascal = 'pascal';
|
||||
case pascaligo = 'pascaligo';
|
||||
case perl = 'perl';
|
||||
case pgsql = 'pgsql';
|
||||
case php = 'php';
|
||||
case pla = 'pla';
|
||||
case postiats = 'postiats';
|
||||
case powerquery = 'powerquery';
|
||||
case powershell = 'powershell';
|
||||
case proto = 'proto';
|
||||
case pug = 'pug';
|
||||
case python = 'python';
|
||||
case qsharp = 'qsharp';
|
||||
case r = 'r';
|
||||
case razor = 'razor';
|
||||
case redis = 'redis';
|
||||
case redshift = 'redshift';
|
||||
case restructuredtext = 'restructuredtext';
|
||||
case ruby = 'ruby';
|
||||
case rust = 'rust';
|
||||
case sb = 'sb';
|
||||
case scala = 'scala';
|
||||
case scheme = 'scheme';
|
||||
case scss = 'scss';
|
||||
case shell = 'shell';
|
||||
case sol = 'sol';
|
||||
case aes = 'aes';
|
||||
case sparql = 'sparql';
|
||||
case sql = 'sql';
|
||||
case st = 'st';
|
||||
case swift = 'swift';
|
||||
case systemverilog = 'systemverilog';
|
||||
case verilog = 'verilog';
|
||||
case tcl = 'tcl';
|
||||
case twig = 'twig';
|
||||
case typescript = 'typescript';
|
||||
case typespec = 'typespec';
|
||||
case vb = 'vb';
|
||||
case wgsl = 'wgsl';
|
||||
case xml = 'xml';
|
||||
case yaml = 'yaml';
|
||||
case json = 'json';
|
||||
|
||||
public function getLabel(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
28
app/Filament/App/Resources/ServerResource.php
Normal file
28
app/Filament/App/Resources/ServerResource.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\App\Resources;
|
||||
|
||||
use App\Filament\App\Resources\ServerResource\Pages;
|
||||
use App\Models\Server;
|
||||
use Filament\Resources\Resource;
|
||||
|
||||
class ServerResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Server::class;
|
||||
|
||||
protected static ?string $slug = '/';
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListServers::route('/'),
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\App\Resources\ServerResource\Pages;
|
||||
|
||||
use App\Filament\App\Resources\ServerResource;
|
||||
use App\Filament\Server\Pages\Console;
|
||||
use App\Models\Server;
|
||||
use App\Tables\Columns\ServerEntryColumn;
|
||||
use Carbon\CarbonInterface;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Columns\Layout\Stack;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Number;
|
||||
|
||||
class ListServers extends ListRecords
|
||||
{
|
||||
protected static string $resource = ServerResource::class;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->paginated(false)
|
||||
->query(fn () => auth()->user()->can('viewList server') ? Server::query() : auth()->user()->accessibleServers())
|
||||
->columns([
|
||||
Stack::make([
|
||||
ServerEntryColumn::make('server_entry')
|
||||
->searchable(['name']),
|
||||
]),
|
||||
])
|
||||
->contentGrid([
|
||||
'default' => 1,
|
||||
'xl' => 2,
|
||||
])
|
||||
->recordUrl(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server))
|
||||
->emptyStateIcon('tabler-brand-docker')
|
||||
->emptyStateDescription('')
|
||||
->emptyStateHeading('You don\'t have access to any servers!');
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
private function uptime(Server $server): string
|
||||
{
|
||||
$uptime = collect(cache()->get("servers.{$server->id}.uptime"))->last() ?? 0;
|
||||
|
||||
if ($uptime === 0) {
|
||||
return 'Offline';
|
||||
}
|
||||
|
||||
return now()->subMillis($uptime)->diffForHumans(syntax: CarbonInterface::DIFF_ABSOLUTE, short: true, parts: 2);
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
private function cpu(Server $server): string
|
||||
{
|
||||
$cpu = Number::format(collect(cache()->get("servers.{$server->id}.cpu_absolute"))->last() ?? 0, maxPrecision: 2, locale: auth()->user()->language) . '%';
|
||||
$max = Number::format($server->cpu, locale: auth()->user()->language) . '%';
|
||||
|
||||
return $cpu . ($server->cpu > 0 ? ' Of ' . $max : '');
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
private function memory(Server $server): string
|
||||
{
|
||||
$latestMemoryUsed = collect(cache()->get("servers.{$server->id}.memory_bytes"))->last() ?? 0;
|
||||
$totalMemory = collect(cache()->get("servers.{$server->id}.memory_limit_bytes"))->last() ?? 0;
|
||||
|
||||
$used = config('panel.use_binary_prefix')
|
||||
? Number::format($latestMemoryUsed / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($latestMemoryUsed / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
|
||||
if ($totalMemory === 0) {
|
||||
$total = config('panel.use_binary_prefix')
|
||||
? Number::format($server->memory / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($server->memory / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
} else {
|
||||
$total = config('panel.use_binary_prefix')
|
||||
? Number::format($totalMemory / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($totalMemory / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
}
|
||||
|
||||
return $used . ($server->memory > 0 ? ' Of ' . $total : '');
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
private function disk(Server $server): string
|
||||
{
|
||||
$usedDisk = collect(cache()->get("servers.{$server->id}.disk_bytes"))->last() ?? 0;
|
||||
|
||||
$used = config('panel.use_binary_prefix')
|
||||
? Number::format($usedDisk / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($usedDisk / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
|
||||
$total = config('panel.use_binary_prefix')
|
||||
? Number::format($server->disk / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($server->disk / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
|
||||
return $used . ($server->disk > 0 ? ' Of ' . $total : '');
|
||||
}
|
||||
}
|
@ -82,6 +82,7 @@ class CreateDatabaseHost extends CreateRecord
|
||||
Select::make('node_id')
|
||||
->searchable()
|
||||
->preload()
|
||||
->unique()
|
||||
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
|
||||
->label('Linked Node')
|
||||
->relationship('node', 'name'),
|
||||
|
@ -77,6 +77,7 @@ class EditDatabaseHost extends EditRecord
|
||||
Select::make('node_id')
|
||||
->searchable()
|
||||
->preload()
|
||||
->unique()
|
||||
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
|
||||
->label('Linked Node')
|
||||
->relationship('node', 'name'),
|
||||
|
@ -91,6 +91,8 @@ class RoleResource extends Resource
|
||||
$icon = ('\App\Filament\Resources\\' . $model . 'Resource')::getNavigationIcon();
|
||||
} elseif (class_exists('\App\Filament\Pages\\' . $model)) {
|
||||
$icon = ('\App\Filament\Pages\\' . $model)::getNavigationIcon();
|
||||
} elseif (class_exists('\App\Filament\Server\Resources\\' . $model . 'Resource')) {
|
||||
$icon = ('\App\Filament\Server\Resources\\' . $model . 'Resource')::getNavigationIcon();
|
||||
}
|
||||
|
||||
return Section::make(Str::headline(Str::plural($model)))
|
||||
|
@ -512,23 +512,19 @@ class EditServer extends EditRecord
|
||||
->label('Startup Command')
|
||||
->required()
|
||||
->columnSpan(6)
|
||||
->rows(function ($state) {
|
||||
return str($state)->explode("\n")->reduce(
|
||||
fn (int $carry, $line) => $carry + floor(strlen($line) / 125),
|
||||
0
|
||||
);
|
||||
}),
|
||||
->autosize(),
|
||||
|
||||
Textarea::make('defaultStartup')
|
||||
->hintAction(CopyAction::make())
|
||||
->label('Default Startup Command')
|
||||
->disabled()
|
||||
->autosize()
|
||||
->columnSpan(6)
|
||||
->formatStateUsing(function ($state, Get $get) {
|
||||
$egg = Egg::query()->find($get('egg_id'));
|
||||
|
||||
return $egg->startup;
|
||||
})
|
||||
->columnSpan(6),
|
||||
}),
|
||||
|
||||
Repeater::make('server_variables')
|
||||
->relationship('serverVariables', function (Builder $query) {
|
||||
@ -868,7 +864,7 @@ class EditServer extends EditRecord
|
||||
->action(function (Server $server, ServerDeletionService $service) {
|
||||
$service->handle($server);
|
||||
|
||||
return redirect(ListServers::getUrl());
|
||||
return redirect(ListServers::getUrl(panel: 'admin'));
|
||||
})
|
||||
->authorize(fn (Server $server) => auth()->user()->can('delete server', $server)),
|
||||
Actions\Action::make('console')
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Filament\Resources\ServerResource\Pages;
|
||||
|
||||
use App\Filament\Server\Pages\Console;
|
||||
use App\Filament\Resources\ServerResource;
|
||||
use App\Models\Server;
|
||||
use Filament\Actions;
|
||||
@ -82,8 +83,8 @@ class ListServers extends ListRecords
|
||||
->actions([
|
||||
Action::make('View')
|
||||
->icon('tabler-terminal')
|
||||
->url(fn (Server $server) => "/server/$server->uuid_short")
|
||||
->authorize(fn () => auth()->user()->can('view server')),
|
||||
->url(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server))
|
||||
->authorize(fn (Server $server) => auth()->user()->canAccessTenant($server)),
|
||||
EditAction::make(),
|
||||
])
|
||||
->emptyStateIcon('tabler-brand-docker')
|
||||
|
@ -31,6 +31,7 @@ use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
@ -51,6 +52,11 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
$this->toggleTwoFactorService = $toggleTwoFactorService;
|
||||
}
|
||||
|
||||
public function getMaxWidth(): MaxWidth|string
|
||||
{
|
||||
return MaxWidth::SevenExtraLarge;
|
||||
}
|
||||
|
||||
protected function getForms(): array
|
||||
{
|
||||
return [
|
||||
@ -368,4 +374,17 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
$stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
|
||||
}
|
||||
}
|
||||
|
72
app/Filament/Server/Pages/Console.php
Normal file
72
app/Filament/Server/Pages/Console.php
Normal file
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Pages;
|
||||
|
||||
use App\Filament\Server\Widgets\ServerConsole;
|
||||
use App\Filament\Server\Widgets\ServerCpuChart;
|
||||
use App\Filament\Server\Widgets\ServerMemoryChart;
|
||||
// use App\Filament\Server\Widgets\ServerNetworkChart;
|
||||
use App\Filament\Server\Widgets\ServerOverview;
|
||||
use App\Models\Server;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Pages\Page;
|
||||
|
||||
class Console extends Page
|
||||
{
|
||||
protected static ?string $navigationIcon = 'tabler-brand-tabler';
|
||||
|
||||
protected static ?int $navigationSort = 1;
|
||||
|
||||
protected static string $view = 'filament.server.pages.console';
|
||||
|
||||
public function getWidgetData(): array
|
||||
{
|
||||
return [
|
||||
'server' => Filament::getTenant(),
|
||||
'user' => auth()->user(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
ServerOverview::class,
|
||||
ServerConsole::class,
|
||||
ServerCpuChart::class,
|
||||
ServerMemoryChart::class,
|
||||
//ServerNetworkChart::class, TODO: convert units.
|
||||
];
|
||||
}
|
||||
|
||||
public function getVisibleWidgets(): array
|
||||
{
|
||||
return $this->filterVisibleWidgets($this->getWidgets());
|
||||
}
|
||||
|
||||
public function getColumns(): int|string|array
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return [
|
||||
Action::make('start')
|
||||
->color('primary')
|
||||
->action(fn () => $this->dispatch('setServerState', state: 'start'))
|
||||
->disabled(fn () => $server->isInConflictState()),
|
||||
Action::make('restart')
|
||||
->color('gray')
|
||||
->action(fn () => $this->dispatch('setServerState', state: 'restart'))
|
||||
->disabled(fn () => $server->isInConflictState() || $server->retrieveStatus() == 'offline'),
|
||||
Action::make('stop')
|
||||
->color('danger')
|
||||
->action(fn () => $this->dispatch('setServerState', state: 'stop'))
|
||||
->disabled(fn () => $server->isInConflictState() || $server->retrieveStatus() == 'offline'),
|
||||
];
|
||||
}
|
||||
}
|
79
app/Filament/Server/Pages/ServerFormPage.php
Normal file
79
app/Filament/Server/Pages/ServerFormPage.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Pages;
|
||||
|
||||
use App\Models\Server;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Pages\Concerns\InteractsWithFormActions;
|
||||
use Filament\Pages\Page;
|
||||
|
||||
/**
|
||||
* @property Form $form
|
||||
*/
|
||||
abstract class ServerFormPage extends Page
|
||||
{
|
||||
use InteractsWithFormActions;
|
||||
use InteractsWithForms;
|
||||
|
||||
protected static string $view = 'filament.server.pages.server-form-page';
|
||||
|
||||
public ?array $data = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
|
||||
$this->fillForm();
|
||||
}
|
||||
|
||||
protected function authorizeAccess(): void {}
|
||||
|
||||
protected function fillForm(): void
|
||||
{
|
||||
$data = $this->getRecord()->attributesToArray();
|
||||
$this->form->fill($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int | string, string | Form>
|
||||
*/
|
||||
protected function getForms(): array
|
||||
{
|
||||
return [
|
||||
'form' => $this->form($this->makeForm()
|
||||
->model($this->getRecord())
|
||||
->statePath($this->getFormStatePath())
|
||||
->columns($this->hasInlineLabels() ? 1 : 2)
|
||||
->inlineLabel($this->hasInlineLabels()),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
public function getFormStatePath(): ?string
|
||||
{
|
||||
return 'data';
|
||||
}
|
||||
|
||||
public function getRecord(): Server
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $server;
|
||||
}
|
||||
|
||||
// TODO: find better way handle server conflict state
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
if ($server->isInConflictState()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::canAccess();
|
||||
}
|
||||
}
|
271
app/Filament/Server/Pages/Settings.php
Normal file
271
app/Filament/Server/Pages/Settings.php
Normal file
@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Pages;
|
||||
|
||||
use App\Enums\ServerState;
|
||||
use App\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
use App\Facades\Activity;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use Exception;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\Alignment;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class Settings extends ServerFormPage
|
||||
{
|
||||
protected static ?string $navigationIcon = 'tabler-settings';
|
||||
|
||||
protected static ?int $navigationSort = 10;
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $form
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->schema([
|
||||
Section::make('Server Information')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->schema([
|
||||
Fieldset::make('Server')
|
||||
->label('Information')
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Server Name')
|
||||
->disabled(!auth()->user()->can(Permission::ACTION_SETTINGS_RENAME, $server))
|
||||
->required()
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, Server $server) => $this->updateName($state, $server)),
|
||||
Textarea::make('description')
|
||||
->label('Server Description')
|
||||
->disabled(!auth()->user()->can(Permission::ACTION_SETTINGS_RENAME, $server))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->autosize()
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, Server $server) => $this->updateDescription($state ?? '', $server)),
|
||||
TextInput::make('uuid')
|
||||
->label('Server UUID')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 3,
|
||||
'lg' => 5,
|
||||
])
|
||||
->disabled(),
|
||||
TextInput::make('id')
|
||||
->label('Server ID')
|
||||
->disabled()
|
||||
->columnSpan(1),
|
||||
]),
|
||||
Fieldset::make('Limits')
|
||||
->label('Limits')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 3,
|
||||
'lg' => 3,
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('backup_limit')
|
||||
->label('Backup Limit')
|
||||
->columnSpan(1)
|
||||
->disabled()
|
||||
->formatStateUsing(fn ($state, Server $server) => !$state ? 'No Backups can be created' : $server->backups->count() . ' of ' . $state),
|
||||
TextInput::make('database_limit')
|
||||
->label('Database Limit')
|
||||
->columnSpan(1)
|
||||
->disabled()
|
||||
->formatStateUsing(fn ($state, Server $server) => !$state ? 'No Databases can be created' : $server->databases->count() . ' of ' . $state),
|
||||
TextInput::make('allocation_limit')
|
||||
->label('Allocation Limit')
|
||||
->columnSpan(1)
|
||||
->disabled()
|
||||
->formatStateUsing(fn ($state, Server $server) => !$state ? 'No additional Allocations can be created' : $server->allocations->count() . ' of ' . ($state + 1)),
|
||||
]),
|
||||
]),
|
||||
Section::make('Node Information')
|
||||
->schema([
|
||||
TextInput::make('node.name')
|
||||
->label('Node Name')
|
||||
->formatStateUsing(fn (Server $server) => $server->node->name)
|
||||
->disabled(),
|
||||
Fieldset::make('SFTP Information')
|
||||
->hidden(fn () => !auth()->user()->can(Permission::ACTION_FILE_SFTP, $server))
|
||||
->label('SFTP Information')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 3,
|
||||
'lg' => 3,
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('connection')
|
||||
->label('Connection')
|
||||
->columnSpan(1)
|
||||
->disabled()
|
||||
->hintActions([
|
||||
Action::make('connect_sftp')
|
||||
->label('Connect to SFTP')
|
||||
->color('success')
|
||||
->icon('tabler-plug')
|
||||
->url(function (Server $server) {
|
||||
$fqdn = $server->node->daemon_sftp_alias ?? $server->node->fqdn;
|
||||
|
||||
return 'sftp://' . auth()->user()->username . '.' . $server->uuid_short . '@' . $fqdn . ':' . $server->node->daemon_sftp;
|
||||
}),
|
||||
])
|
||||
->formatStateUsing(function (Server $server) {
|
||||
$fqdn = $server->node->daemon_sftp_alias ?? $server->node->fqdn;
|
||||
|
||||
return 'sftp://' . auth()->user()->username . '.' . $server->uuid_short . '@' . $fqdn . ':' . $server->node->daemon_sftp;
|
||||
}),
|
||||
TextInput::make('username')
|
||||
->label('Username')
|
||||
->columnSpan(1)
|
||||
->disabled()
|
||||
->formatStateUsing(fn (Server $server) => auth()->user()->username . '.' . $server->uuid_short),
|
||||
Placeholder::make('password')
|
||||
->columnSpan(1)
|
||||
->content('Your SFTP password is the same as the password you use to access this panel.'),
|
||||
]),
|
||||
]),
|
||||
Section::make('Reinstall Server')
|
||||
->hidden(fn () => !auth()->user()->can(Permission::ACTION_SETTINGS_REINSTALL, $server))
|
||||
->collapsible()->collapsed()
|
||||
->footerActions([
|
||||
Action::make('reinstall')
|
||||
->color('danger')
|
||||
->disabled(!auth()->user()->can(Permission::ACTION_SETTINGS_REINSTALL, $server))
|
||||
->label('Reinstall')
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Are you sure you want to reinstall the server?')
|
||||
->modalDescription('Some files may be deleted or modified during this process, please back up your data before continuing.')
|
||||
->modalSubmitActionLabel('Yes, Reinstall')
|
||||
->action(function (Server $server) {
|
||||
abort_unless(auth()->user()->can(Permission::ACTION_SETTINGS_REINSTALL, $server), 403);
|
||||
|
||||
$server->fill(['status' => ServerState::Installing])->save();
|
||||
try {
|
||||
Http::daemon($server->node)->post(sprintf(
|
||||
'/api/servers/%s/reinstall',
|
||||
$server->uuid
|
||||
));
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
|
||||
Activity::event('server:settings.reinstall')
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Server Reinstall Started')
|
||||
->send();
|
||||
}),
|
||||
])
|
||||
->footerActionsAlignment(Alignment::Right)
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->label('Reinstalling your server will stop it, and then re-run the installation script that initially set it up.'),
|
||||
Placeholder::make('')
|
||||
->label('Some files may be deleted or modified during this process, please back up your data before continuing.'),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateName(string $name, Server $server): void
|
||||
{
|
||||
abort_unless(auth()->user()->can(Permission::ACTION_SETTINGS_RENAME, $server), 403);
|
||||
|
||||
$original = $server->name;
|
||||
|
||||
try {
|
||||
$server->forceFill([
|
||||
'name' => $name,
|
||||
])->saveOrFail();
|
||||
|
||||
if ($original !== $name) {
|
||||
Activity::event('server:settings.rename')
|
||||
->property(['old' => $original, 'new' => $name])
|
||||
->log();
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->duration(5000) // 5 seconds
|
||||
->title('Updated Server Name')
|
||||
->body(fn () => $original . ' -> ' . $name)
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title('Failed')
|
||||
->body($exception->getMessage())
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
public function updateDescription(string $description, Server $server): void
|
||||
{
|
||||
abort_unless(auth()->user()->can(Permission::ACTION_SETTINGS_RENAME, $server), 403);
|
||||
|
||||
$original = $server->description;
|
||||
|
||||
try {
|
||||
$server->forceFill([
|
||||
'description' => $description,
|
||||
])->saveOrFail();
|
||||
|
||||
if ($original !== $description) {
|
||||
Activity::event('server:settings.description')
|
||||
->property(['old' => $original, 'new' => $description])
|
||||
->log();
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->duration(5000) // 5 seconds
|
||||
->title('Updated Server Description')
|
||||
->body(fn () => $original . ' -> ' . $description)
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title('Failed')
|
||||
->body($exception->getMessage())
|
||||
->send();
|
||||
}
|
||||
}
|
||||
}
|
238
app/Filament/Server/Pages/Startup.php
Normal file
238
app/Filament/Server/Pages/Startup.php
Normal file
@ -0,0 +1,238 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Pages;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerVariable;
|
||||
use Closure;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Component;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class Startup extends ServerFormPage
|
||||
{
|
||||
protected static ?string $navigationIcon = 'tabler-player-play';
|
||||
|
||||
protected static ?int $navigationSort = 9;
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $form
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->schema([
|
||||
Textarea::make('startup')
|
||||
->label('Startup Command')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 2,
|
||||
'lg' => 4,
|
||||
])
|
||||
->autosize()
|
||||
->readOnly(),
|
||||
TextInput::make('custom_image')
|
||||
->label('Docker Image')
|
||||
->readOnly()
|
||||
->visible(fn (Server $server) => !in_array($server->image, $server->egg->docker_images))
|
||||
->formatStateUsing(fn (Server $server) => $server->image)
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 2,
|
||||
'lg' => 2,
|
||||
]),
|
||||
Select::make('image')
|
||||
->label('Docker Image')
|
||||
->live()
|
||||
->visible(fn (Server $server) => in_array($server->image, $server->egg->docker_images))
|
||||
->disabled(!auth()->user()->can(Permission::ACTION_STARTUP_DOCKER_IMAGE, $server))
|
||||
->afterStateUpdated(function ($state, Server $server) {
|
||||
$original = $server->image;
|
||||
$server->forceFill(['image' => $state])->saveOrFail();
|
||||
|
||||
if ($original !== $server->image) {
|
||||
Activity::event('server:startup.image')
|
||||
->property(['old' => $original, 'new' => $state])
|
||||
->log();
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title('Docker image updated')
|
||||
->body('Restart the server to use the new image.')
|
||||
->success()
|
||||
->send();
|
||||
})
|
||||
->options(function (Server $server) {
|
||||
$images = $server->egg->docker_images;
|
||||
|
||||
return array_flip($images);
|
||||
})
|
||||
->selectablePlaceholder(false)
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 2,
|
||||
'lg' => 2,
|
||||
]),
|
||||
Section::make('Server Variables')
|
||||
->schema([
|
||||
Repeater::make('server_variables')
|
||||
->label('')
|
||||
->relationship('viewableServerVariables')
|
||||
->grid()
|
||||
->disabled(!auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server))
|
||||
->reorderable(false)->addable(false)->deletable(false)
|
||||
->schema(function () {
|
||||
$text = TextInput::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->disabled(fn (ServerVariable $serverVariable) => !$serverVariable->variable->user_editable)
|
||||
->required(fn (ServerVariable $serverVariable) => $serverVariable->variable->getRequiredAttribute())
|
||||
->rules([
|
||||
fn (ServerVariable $serverVariable): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) {
|
||||
$validator = Validator::make(['validatorkey' => $value], [
|
||||
'validatorkey' => $serverVariable->variable->rules,
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$message = str($validator->errors()->first())->replace('validatorkey', $serverVariable->variable->name);
|
||||
|
||||
$fail($message);
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
$select = Select::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->disabled(fn (ServerVariable $serverVariable) => !$serverVariable->variable->user_editable)
|
||||
->options($this->getSelectOptionsFromRules(...))
|
||||
->selectablePlaceholder(false);
|
||||
|
||||
$components = [$text, $select];
|
||||
|
||||
foreach ($components as &$component) {
|
||||
$component = $component
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(function ($state, ServerVariable $serverVariable) {
|
||||
$this->update($state, $serverVariable);
|
||||
})
|
||||
->hintIcon('tabler-code')
|
||||
->label(fn (ServerVariable $serverVariable) => $serverVariable->variable->name)
|
||||
->hintIconTooltip(fn (ServerVariable $serverVariable) => implode('|', $serverVariable->variable->rules))
|
||||
->prefix(fn (ServerVariable $serverVariable) => '{{' . $serverVariable->variable->env_variable . '}}')
|
||||
->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description);
|
||||
}
|
||||
|
||||
return $components;
|
||||
})
|
||||
->columnSpan(6),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function authorizeAccess(): void
|
||||
{
|
||||
abort_unless(auth()->user()->can(Permission::ACTION_STARTUP_READ, Filament::getTenant()), 403);
|
||||
}
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_STARTUP_READ, Filament::getTenant());
|
||||
}
|
||||
|
||||
private function shouldHideComponent(ServerVariable $serverVariable, Component $component): bool
|
||||
{
|
||||
$containsRuleIn = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'), false);
|
||||
|
||||
if ($component instanceof Select) {
|
||||
return !$containsRuleIn;
|
||||
}
|
||||
|
||||
if ($component instanceof TextInput) {
|
||||
return $containsRuleIn;
|
||||
}
|
||||
|
||||
throw new \Exception('Component type not supported: ' . $component::class);
|
||||
}
|
||||
|
||||
private function getSelectOptionsFromRules(ServerVariable $serverVariable): array
|
||||
{
|
||||
$inRule = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'));
|
||||
|
||||
return str($inRule)
|
||||
->after('in:')
|
||||
->explode(',')
|
||||
->each(fn ($value) => str($value)->trim())
|
||||
->mapWithKeys(fn ($value) => [$value => $value])
|
||||
->all();
|
||||
}
|
||||
|
||||
public function update(?string $state, ServerVariable $serverVariable): null
|
||||
{
|
||||
$original = $serverVariable->variable_value;
|
||||
|
||||
try {
|
||||
|
||||
$validator = Validator::make(
|
||||
['variable_value' => $state],
|
||||
['variable_value' => $serverVariable->variable->rules]
|
||||
);
|
||||
|
||||
if ($validator->fails()) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title('Validation Failed: ' . $serverVariable->variable->name)
|
||||
->body(implode(', ', $validator->errors()->all()))
|
||||
->send();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ServerVariable::query()->updateOrCreate([
|
||||
'server_id' => $this->getRecord()->id,
|
||||
'variable_id' => $serverVariable->variable->id,
|
||||
], [
|
||||
'variable_value' => $state ?? '',
|
||||
]);
|
||||
|
||||
if ($original !== $state) {
|
||||
Activity::event('server:startup.edit')
|
||||
->property([
|
||||
'variable' => $serverVariable->variable->env_variable,
|
||||
'old' => $original,
|
||||
'new' => $state,
|
||||
])
|
||||
->log();
|
||||
}
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Updated: ' . $serverVariable->variable->name)
|
||||
->body(fn () => $original . ' -> ' . $state)
|
||||
->send();
|
||||
} catch (\Exception $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title('Failed: ' . $serverVariable->variable->name)
|
||||
->body($e->getMessage())
|
||||
->send();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
79
app/Filament/Server/Resources/ActivityResource.php
Normal file
79
app/Filament/Server/Resources/ActivityResource.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources;
|
||||
|
||||
use App\Filament\Server\Resources\ActivityResource\Pages;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Role;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Resource;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
|
||||
class ActivityResource extends Resource
|
||||
{
|
||||
protected static ?string $model = ActivityLog::class;
|
||||
|
||||
protected static ?string $label = 'Activity';
|
||||
|
||||
protected static ?string $pluralLabel = 'Activity';
|
||||
|
||||
protected static ?int $navigationSort = 8;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-stack';
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $server->activity()
|
||||
->getQuery()
|
||||
->whereNotIn('activity_logs.event', ActivityLog::DISABLED_EVENTS)
|
||||
->when(config('activity.hide_admin_activity'), function (Builder $builder) use ($server) {
|
||||
// We could do this with a query and a lot of joins, but that gets pretty
|
||||
// painful so for now we'll execute a simpler query.
|
||||
$subusers = $server->subusers()->pluck('user_id')->merge([$server->owner_id]);
|
||||
$rootAdmins = Role::getRootAdmin()->users()->pluck('id');
|
||||
|
||||
$builder->select('activity_logs.*')
|
||||
->leftJoin('users', function (JoinClause $join) {
|
||||
$join->on('users.id', 'activity_logs.actor_id')
|
||||
->where('activity_logs.actor_type', (new User())->getMorphClass());
|
||||
})
|
||||
->where(function (Builder $builder) use ($subusers, $rootAdmins) {
|
||||
$builder->whereNull('users.id')
|
||||
->orWhereNotIn('users.id', $rootAdmins)
|
||||
->orWhereIn('users.id', $subusers);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: find better way handle server conflict state
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
if ($server->isInConflictState()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::canAccess();
|
||||
}
|
||||
|
||||
public static function canViewAny(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_ACTIVITY_READ, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListActivities::route('/'),
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\ActivityResource\Pages;
|
||||
|
||||
use App\Filament\Server\Resources\ActivityResource;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\User;
|
||||
use App\Tables\Columns\DateTimeColumn;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ListActivities extends ListRecords
|
||||
{
|
||||
protected static string $resource = ActivityResource::class;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('event')
|
||||
->html()
|
||||
->formatStateUsing(fn ($state, ActivityLog $activityLog) => __('activity.'.str($state)->replace(':', '.'))) // TODO: convert properties to a format that trans likes, see ActivityLogEntry.tsx - wrapProperties
|
||||
->description(fn ($state) => $state),
|
||||
TextColumn::make('user')
|
||||
->state(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User ? $activityLog->actor->username : 'System')
|
||||
->tooltip(fn (ActivityLog $activityLog) => auth()->user()->can('seeIps activityLog') ? $activityLog->ip : '')
|
||||
->url(fn (ActivityLog $activityLog): string => $activityLog->actor instanceof User ? route('filament.admin.resources.users.edit', ['record' => $activityLog->actor]) : ''),
|
||||
DateTimeColumn::make('timestamp')
|
||||
->since()
|
||||
->sortable(),
|
||||
])
|
||||
->defaultSort('timestamp', 'desc');
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
64
app/Filament/Server/Resources/AllocationResource.php
Normal file
64
app/Filament/Server/Resources/AllocationResource.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources;
|
||||
|
||||
use App\Filament\Server\Resources\AllocationResource\Pages;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Resource;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AllocationResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Allocation::class;
|
||||
|
||||
protected static ?string $label = 'Network';
|
||||
|
||||
protected static ?string $pluralLabel = 'Network';
|
||||
|
||||
protected static ?int $navigationSort = 7;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-network';
|
||||
|
||||
// TODO: find better way handle server conflict state
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
if ($server->isInConflictState()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::canAccess();
|
||||
}
|
||||
|
||||
public static function canViewAny(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_ALLOCATION_READ, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_ALLOCATION_CREATE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canEdit(Model $record): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_ALLOCATION_UPDATE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canDelete(Model $record): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListAllocations::route('/'),
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\AllocationResource\Pages;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Server\Resources\AllocationResource;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Services\Allocations\FindAssignableAllocationService;
|
||||
use Filament\Actions;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\DetachAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\TextInputColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ListAllocations extends ListRecords
|
||||
{
|
||||
protected static string $resource = AllocationResource::class;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('ip')
|
||||
->label('Address')
|
||||
->formatStateUsing(fn (Allocation $allocation) => $allocation->alias),
|
||||
TextColumn::make('alias')
|
||||
->hidden(),
|
||||
TextColumn::make('port'),
|
||||
TextInputColumn::make('notes')
|
||||
->disabled(fn () => !auth()->user()->can(Permission::ACTION_ALLOCATION_UPDATE, $server))
|
||||
->label('Notes'),
|
||||
IconColumn::make('primary')
|
||||
->icon(fn ($state) => match ($state) {
|
||||
true => 'tabler-star-filled',
|
||||
default => 'tabler-star',
|
||||
})
|
||||
->color(fn ($state) => match ($state) {
|
||||
true => 'warning',
|
||||
default => 'gray',
|
||||
})
|
||||
->action(function (Allocation $allocation) use ($server) {
|
||||
if (auth()->user()->can(PERMISSION::ACTION_ALLOCATION_UPDATE, $server)) {
|
||||
return $server->update(['allocation_id' => $allocation->id]);
|
||||
}
|
||||
})
|
||||
->default(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
|
||||
->label('Primary'),
|
||||
])
|
||||
->actions([
|
||||
DetachAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, $server))
|
||||
->label('Delete')
|
||||
->icon('tabler-trash')
|
||||
->hidden(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
|
||||
->action(function (Allocation $allocation) {
|
||||
Allocation::query()->where('id', $allocation->id)->update([
|
||||
'notes' => null,
|
||||
'server_id' => null,
|
||||
]);
|
||||
|
||||
Activity::event('server:allocation.delete')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->toString())
|
||||
->log();
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return [
|
||||
Actions\Action::make('addAllocation')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_CREATE, $server))
|
||||
->label(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'Allocation limit reached' : 'Add Allocation')
|
||||
->hidden(fn () => !config('panel.client_features.allocations.enabled'))
|
||||
->disabled(fn () => $server->allocations()->count() >= $server->allocation_limit)
|
||||
->color(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'danger' : 'primary')
|
||||
->action(function (FindAssignableAllocationService $service) use ($server) {
|
||||
$allocation = $service->handle($server);
|
||||
|
||||
Activity::event('server:allocation.create')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->toString())
|
||||
->log();
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
57
app/Filament/Server/Resources/BackupResource.php
Normal file
57
app/Filament/Server/Resources/BackupResource.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources;
|
||||
|
||||
use App\Filament\Server\Resources\BackupResource\Pages;
|
||||
use App\Models\Backup;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Resource;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class BackupResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Backup::class;
|
||||
|
||||
protected static ?int $navigationSort = 3;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-file-zip';
|
||||
|
||||
protected static bool $canCreateAnother = false;
|
||||
|
||||
// TODO: find better way handle server conflict state
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
if ($server->isInConflictState()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::canAccess();
|
||||
}
|
||||
|
||||
public static function canViewAny(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_BACKUP_READ, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_BACKUP_CREATE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canDelete(Model $record): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_BACKUP_DELETE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListBackups::route('/'),
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\BackupResource\Pages;
|
||||
|
||||
use App\Enums\ServerState;
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Server\Resources\BackupResource;
|
||||
use App\Http\Controllers\Api\Client\Servers\BackupController;
|
||||
use App\Models\Backup;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonBackupRepository;
|
||||
use App\Services\Backups\DownloadLinkService;
|
||||
use App\Services\Backups\InitiateBackupService;
|
||||
use App\Tables\Columns\BytesColumn;
|
||||
use App\Tables\Columns\DateTimeColumn;
|
||||
use Filament\Actions;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\ActionGroup;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ListBackups extends ListRecords
|
||||
{
|
||||
protected static string $resource = BackupResource::class;
|
||||
|
||||
protected static bool $canCreateAnother = false;
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Name')
|
||||
->columnSpanFull()
|
||||
->required(),
|
||||
TextArea::make('ignored')
|
||||
->columnSpanFull()
|
||||
->label('Ignored Files & Directories'),
|
||||
Toggle::make('is_locked')
|
||||
->label('Lock?')
|
||||
->helperText('Prevents this backup from being deleted until explicitly unlocked.'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
BytesColumn::make('bytes')
|
||||
->label('Size'),
|
||||
DateTimeColumn::make('created_at')
|
||||
->label('Created')
|
||||
->since()
|
||||
->sortable(),
|
||||
IconColumn::make('is_successful')
|
||||
->label('Successful')
|
||||
->boolean(),
|
||||
IconColumn::make('is_locked')
|
||||
->label('Lock Status')
|
||||
->icon(fn (Backup $backup) => !$backup->is_locked ? 'tabler-lock-open' : 'tabler-lock'),
|
||||
])
|
||||
->actions([
|
||||
ActionGroup::make([
|
||||
Action::make('lock')
|
||||
->icon(fn (Backup $backup) => !$backup->is_locked ? 'tabler-lock' : 'tabler-lock-open')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server))
|
||||
->label(fn (Backup $backup) => !$backup->is_locked ? 'Lock' : 'Unlock')
|
||||
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->toggleLock($request, $server, $backup)),
|
||||
Action::make('download')
|
||||
->color('primary')
|
||||
->icon('tabler-download')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DOWNLOAD, $server))
|
||||
->url(fn (DownloadLinkService $downloadLinkService, Backup $backup, Request $request) => $downloadLinkService->handle($backup, $request->user()), true),
|
||||
Action::make('restore')
|
||||
->color('success')
|
||||
->icon('tabler-folder-up')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_RESTORE, $server))
|
||||
->form([
|
||||
Placeholder::make('')
|
||||
->helperText('Your server will be stopped. You will not be able to control the power state, access the file manager, or create additional backups until this process is completed.'),
|
||||
Checkbox::make('truncate')
|
||||
->label('Delete all files before restoring backup?'),
|
||||
])
|
||||
->action(function (Backup $backup, $data, DaemonBackupRepository $daemonRepository, DownloadLinkService $downloadLinkService) use ($server) {
|
||||
if (!is_null($server->status)) {
|
||||
return Notification::make()
|
||||
->danger()
|
||||
->title('Backup Restore Failed')
|
||||
->body('This server is not currently in a state that allows for a backup to be restored.')
|
||||
->send();
|
||||
}
|
||||
|
||||
if (!$backup->is_successful && is_null($backup->completed_at)) { //TODO Change to Notifications
|
||||
return Notification::make()
|
||||
->danger()
|
||||
->title('Backup Restore Failed')
|
||||
->body('This backup cannot be restored at this time: not completed or failed.')
|
||||
->send();
|
||||
}
|
||||
|
||||
$log = Activity::event('server:backup.restore')
|
||||
->subject($backup)
|
||||
->property(['name' => $backup->name, 'truncate' => $data['truncate']]);
|
||||
|
||||
$log->transaction(function () use ($downloadLinkService, $daemonRepository, $backup, $server, $data) {
|
||||
// If the backup is for an S3 file we need to generate a unique Download link for
|
||||
// it that will allow daemon to actually access the file.
|
||||
if ($backup->disk === Backup::ADAPTER_AWS_S3) {
|
||||
$url = $downloadLinkService->handle($backup, auth()->user());
|
||||
}
|
||||
|
||||
// Update the status right away for the server so that we know not to allow certain
|
||||
// actions against it via the Panel API.
|
||||
$server->update(['status' => ServerState::RestoringBackup]);
|
||||
|
||||
$daemonRepository->setServer($server)->restore($backup, $url ?? null, $data['truncate']);
|
||||
});
|
||||
|
||||
return Notification::make()
|
||||
->title('Restoring Backup')
|
||||
->send();
|
||||
}),
|
||||
DeleteAction::make('delete')
|
||||
->disabled(fn (Backup $backup): bool => $backup->is_locked)
|
||||
->modalDescription(fn (Backup $backup) => 'Do you wish to delete, ' . $backup->name . '?')
|
||||
->modalSubmitActionLabel('Delete Backup')
|
||||
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->delete($request, $server, $backup)),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return [
|
||||
Actions\CreateAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_CREATE, $server))
|
||||
->label(fn () => $server->backups()->count() >= $server->backup_limit ? 'Backup limit reached' : 'Create Backup')
|
||||
->disabled(fn () => $server->backups()->count() >= $server->backup_limit)
|
||||
->color(fn () => $server->backups()->count() >= $server->backup_limit ? 'danger' : 'primary')
|
||||
->createAnother(false)
|
||||
->action(function (InitiateBackupService $initiateBackupService, $data) use ($server) {
|
||||
$action = $initiateBackupService->setIgnoredFiles(explode(PHP_EOL, $data['ignored'] ?? ''));
|
||||
|
||||
if (auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server)) {
|
||||
$action->setIsLocked((bool) $data['is_locked']);
|
||||
}
|
||||
|
||||
$backup = $action->handle($server, $data['name']);
|
||||
|
||||
Activity::event('server:backup.start')
|
||||
->subject($backup)
|
||||
->property(['name' => $backup->name, 'locked' => (bool) $data['is_locked']])
|
||||
->log();
|
||||
|
||||
return Notification::make()
|
||||
->title('Backup Created')
|
||||
->body($backup->name . ' created.')
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
65
app/Filament/Server/Resources/DatabaseResource.php
Normal file
65
app/Filament/Server/Resources/DatabaseResource.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources;
|
||||
|
||||
use App\Filament\Server\Resources\DatabaseResource\Pages;
|
||||
use App\Models\Database;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Resource;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class DatabaseResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Database::class;
|
||||
|
||||
protected static ?int $navigationSort = 6;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-database';
|
||||
|
||||
// TODO: find better way handle server conflict state
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
if ($server->isInConflictState()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::canAccess();
|
||||
}
|
||||
|
||||
public static function canViewAny(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_DATABASE_READ, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canView(Model $record): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_DATABASE_READ, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_DATABASE_CREATE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canEdit(Model $record): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_DATABASE_UPDATE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canDelete(Model $record): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_DATABASE_DELETE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListDatabases::route('/'),
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\DatabaseResource\Pages;
|
||||
|
||||
use App\Filament\Server\Resources\DatabaseResource;
|
||||
use App\Models\Database;
|
||||
use App\Models\DatabaseHost;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Services\Databases\DatabaseManagementService;
|
||||
use App\Services\Databases\DatabasePasswordService;
|
||||
use App\Tables\Columns\DateTimeColumn;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
class ListDatabases extends ListRecords
|
||||
{
|
||||
protected static string $resource = DatabaseResource::class;
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('database')
|
||||
->columnSpanFull()
|
||||
->suffixAction(CopyAction::make()),
|
||||
TextInput::make('username')
|
||||
->suffixAction(CopyAction::make()),
|
||||
TextInput::make('password')
|
||||
->password()->revealable()
|
||||
->hidden(fn () => !auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
|
||||
->hintAction(
|
||||
Action::make('rotate')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_DATABASE_UPDATE, $server))
|
||||
->icon('tabler-refresh')
|
||||
->requiresConfirmation()
|
||||
->action(function (DatabasePasswordService $service, Database $database, $set, $get) {
|
||||
$newPassword = $service->handle($database);
|
||||
|
||||
$set('password', $newPassword);
|
||||
$set('JDBC', 'jdbc:mysql://' . $get('username') . ':' . urlencode($newPassword) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database'));
|
||||
})
|
||||
)
|
||||
->suffixAction(CopyAction::make())
|
||||
->formatStateUsing(fn (Database $database) => $database->password),
|
||||
TextInput::make('remote')
|
||||
->label('Connections From'),
|
||||
TextInput::make('max_connections')
|
||||
->formatStateUsing(fn (Database $database) => $database->max_connections === 0 ? $database->max_connections : 'Unlimited'),
|
||||
TextInput::make('JDBC')
|
||||
->label('JDBC Connection String')
|
||||
->password()->revealable()
|
||||
->hidden(!auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
|
||||
->suffixAction(CopyAction::make())
|
||||
->columnSpanFull()
|
||||
->formatStateUsing(fn (Get $get, Database $database) => 'jdbc:mysql://' . $get('username') . ':' . urlencode($database->password) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database')),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('database'),
|
||||
TextColumn::make('username'),
|
||||
TextColumn::make('remote'),
|
||||
DateTimeColumn::make('created_at')
|
||||
->sortable(),
|
||||
])
|
||||
->actions([
|
||||
ViewAction::make()
|
||||
->modalHeading(fn (Database $database) => 'Viewing ' . $database->database),
|
||||
DeleteAction::make(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return [
|
||||
CreateAction::make('new')
|
||||
->label(fn () => $server->databases()->count() >= $server->database_limit ? 'Database limit reached' : 'Create Database')
|
||||
->disabled(fn () => $server->databases()->count() >= $server->database_limit)
|
||||
->color(fn () => $server->databases()->count() >= $server->database_limit ? 'danger' : 'primary')
|
||||
->createAnother(false)
|
||||
->form([
|
||||
Grid::make()
|
||||
->columns(3)
|
||||
->schema([
|
||||
TextInput::make('database')
|
||||
->columnSpan(2)
|
||||
->label('Database Name')
|
||||
->prefix('s'. $server->id . '_')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Leaving this blank will auto generate a random name'),
|
||||
TextInput::make('remote')
|
||||
->columnSpan(1)
|
||||
->label('Connections From')
|
||||
->default('%'),
|
||||
]),
|
||||
])
|
||||
->action(function ($data, DatabaseManagementService $service) use ($server) {
|
||||
if (empty($data['database'])) {
|
||||
$data['database'] = str_random(12);
|
||||
}
|
||||
|
||||
$data['database_host_id'] = DatabaseHost::where('node_id', $server->node_id)->first()->id;
|
||||
$data['database'] = 's'. $server->id . '_' . $data['database'];
|
||||
|
||||
$service->create($server, $data);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
62
app/Filament/Server/Resources/FileResource.php
Normal file
62
app/Filament/Server/Resources/FileResource.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources;
|
||||
|
||||
use App\Filament\Server\Resources\FileResource\Pages;
|
||||
use App\Models\File;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Resource;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class FileResource extends Resource
|
||||
{
|
||||
protected static ?string $model = File::class;
|
||||
|
||||
protected static ?int $navigationSort = 2;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-files';
|
||||
|
||||
// TODO: find better way handle server conflict state
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
if ($server->isInConflictState()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::canAccess();
|
||||
}
|
||||
|
||||
public static function canViewAny(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_FILE_READ, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_FILE_CREATE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canEdit(Model $record): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_FILE_UPDATE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canDelete(Model $record): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_FILE_DELETE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'edit' => Pages\EditFiles::route('/edit/{path}'),
|
||||
'search' => Pages\SearchFiles::route('/search/{searchTerm}'), // TODO: find better way?
|
||||
'index' => Pages\ListFiles::route('/{path?}'),
|
||||
];
|
||||
}
|
||||
}
|
178
app/Filament/Server/Resources/FileResource/Pages/EditFiles.php
Normal file
178
app/Filament/Server/Resources/FileResource/Pages/EditFiles.php
Normal file
@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\FileResource\Pages;
|
||||
|
||||
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
||||
use App\Enums\EditorLanguages;
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Server\Resources\FileResource;
|
||||
use App\Models\File;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonFileRepository;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
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;
|
||||
use Filament\Resources\Pages\PageRegistration;
|
||||
use Filament\Support\Enums\Alignment;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Facades\Route as RouteFacade;
|
||||
use Livewire\Attributes\Locked;
|
||||
|
||||
/**
|
||||
* @property Form $form
|
||||
*/
|
||||
class EditFiles extends Page
|
||||
{
|
||||
use HasUnsavedDataChangesAlert;
|
||||
use InteractsWithFormActions;
|
||||
use InteractsWithForms;
|
||||
|
||||
protected static string $resource = FileResource::class;
|
||||
|
||||
protected static string $view = 'filament.server.pages.edit-file';
|
||||
|
||||
#[Locked]
|
||||
public string $path;
|
||||
|
||||
public ?array $data = [];
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
/** @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 () {
|
||||
$split = explode('.', $this->path);
|
||||
|
||||
return end($split);
|
||||
}),
|
||||
Section::make('Editing: ' . $this->path)
|
||||
->footerActions([
|
||||
Action::make('save')
|
||||
->label('Save Changes')
|
||||
->authorize(auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->icon('tabler-device-floppy')
|
||||
->keyBindings('mod+s')
|
||||
->action(function () use ($server) {
|
||||
$data = $this->form->getState();
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->putContent($this->path, $data['editor'] ?? '');
|
||||
|
||||
Activity::event('server:file.write')
|
||||
->property('file', $this->path)
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->duration(5000) // 5 seconds
|
||||
->title('Saved File')
|
||||
->body(fn () => $this->path)
|
||||
->send();
|
||||
}),
|
||||
Action::make('cancel')
|
||||
->label('Cancel')
|
||||
->color('danger')
|
||||
->icon('tabler-x')
|
||||
->url(fn () => ListFiles::getUrl(['path' => dirname($this->path)])),
|
||||
])
|
||||
->footerActionsAlignment(Alignment::End)
|
||||
->schema([
|
||||
MonacoEditor::make('editor')
|
||||
->label('')
|
||||
->formatStateUsing(function () use ($server) {
|
||||
// @phpstan-ignore-next-line
|
||||
return app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->getContent($this->path, config('panel.files.max_edit_size'));
|
||||
})
|
||||
->language(fn (Get $get) => $get('lang') ?? 'plaintext')
|
||||
->view('filament.plugins.monaco-editor'),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function mount(string $path): void
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
|
||||
$this->path = $path;
|
||||
|
||||
$this->form->fill();
|
||||
}
|
||||
|
||||
protected function authorizeAccess(): void
|
||||
{
|
||||
abort_unless(auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, Filament::getTenant()), 403);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int | string, string | Form>
|
||||
*/
|
||||
protected function getForms(): array
|
||||
{
|
||||
return [
|
||||
'form' => $this->form(static::getResource()::form(
|
||||
$this->makeForm()
|
||||
->statePath($this->getFormStatePath())
|
||||
->columns($this->hasInlineLabels() ? 1 : 2)
|
||||
->inlineLabel($this->hasInlineLabels()),
|
||||
)),
|
||||
];
|
||||
}
|
||||
|
||||
public function getFormStatePath(): ?string
|
||||
{
|
||||
return 'data';
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
$resource = static::getResource();
|
||||
|
||||
$breadcrumbs = [
|
||||
$resource::getUrl() => $resource::getBreadcrumb(),
|
||||
];
|
||||
|
||||
$previousParts = '';
|
||||
foreach (explode('/', $this->path) as $part) {
|
||||
$previousParts = $previousParts . '/' . $part;
|
||||
$breadcrumbs[self::getUrl(['path' => ltrim($previousParts, '/')])] = $part;
|
||||
}
|
||||
|
||||
return $breadcrumbs;
|
||||
}
|
||||
|
||||
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', '.*'),
|
||||
);
|
||||
}
|
||||
}
|
588
app/Filament/Server/Resources/FileResource/Pages/ListFiles.php
Normal file
588
app/Filament/Server/Resources/FileResource/Pages/ListFiles.php
Normal file
@ -0,0 +1,588 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\FileResource\Pages;
|
||||
|
||||
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
||||
use App\Enums\EditorLanguages;
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Server\Resources\FileResource;
|
||||
use App\Models\File;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonFileRepository;
|
||||
use App\Services\Nodes\NodeJWTService;
|
||||
use App\Tables\Columns\BytesColumn;
|
||||
use App\Tables\Columns\DateTimeColumn;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Filament\Actions\Action as HeaderAction;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Panel;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Resources\Pages\PageRegistration;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\ActionGroup;
|
||||
use Filament\Tables\Actions\BulkAction;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Facades\Route as RouteFacade;
|
||||
use Livewire\Attributes\Locked;
|
||||
|
||||
class ListFiles extends ListRecords
|
||||
{
|
||||
protected static string $resource = FileResource::class;
|
||||
|
||||
#[Locked]
|
||||
public string $path;
|
||||
|
||||
public function mount(?string $path = null): void
|
||||
{
|
||||
parent::mount();
|
||||
$this->path = $path ?? '/';
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
$resource = static::getResource();
|
||||
|
||||
$breadcrumbs = [
|
||||
$resource::getUrl() => $resource::getBreadcrumb(),
|
||||
];
|
||||
|
||||
$previousParts = '';
|
||||
foreach (explode('/', $this->path) as $part) {
|
||||
$previousParts = $previousParts . '/' . $part;
|
||||
$breadcrumbs[self::getUrl(['path' => ltrim($previousParts, '/')])] = $part;
|
||||
}
|
||||
|
||||
return $breadcrumbs;
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $table
|
||||
->paginated([15, 25, 50, 100])
|
||||
->defaultPaginationPageOption(15)
|
||||
->query(fn () => File::get($server, $this->path)->orderByDesc('is_directory')->orderBy('name'))
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable()
|
||||
->icon(fn (File $file) => $file->getIcon()),
|
||||
BytesColumn::make('size'),
|
||||
DateTimeColumn::make('modified_at')
|
||||
->since()
|
||||
->sortable(),
|
||||
])
|
||||
->recordUrl(function (File $file) use ($server) {
|
||||
|
||||
if ($file->is_directory) {
|
||||
return self::getUrl(['path' => join_paths($this->path, $file->name)]);
|
||||
}
|
||||
|
||||
if (!auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $file->canEdit() ? EditFiles::getUrl(['path' => join_paths($this->path, $file->name)]) : null;
|
||||
})
|
||||
->actions([
|
||||
Action::make('view')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
|
||||
->label('Open')
|
||||
->icon('tabler-eye')
|
||||
->visible(fn (File $file) => $file->is_directory)
|
||||
->url(fn (File $file) => self::getUrl(['path' => join_paths($this->path, $file->name)])),
|
||||
EditAction::make('edit')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
|
||||
->label('Edit')
|
||||
->icon('tabler-edit')
|
||||
->visible(fn (File $file) => $file->canEdit())
|
||||
->url(fn (File $file) => EditFiles::getUrl(['path' => join_paths($this->path, $file->name)])),
|
||||
ActionGroup::make([
|
||||
Action::make('rename')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->label('Rename')
|
||||
->icon('tabler-forms')
|
||||
->form([
|
||||
TextInput::make('name')
|
||||
->label('File name')
|
||||
->default(fn (File $file) => $file->name)
|
||||
->required(),
|
||||
])
|
||||
->action(function ($data, File $file) use ($server) {
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->renameFiles($this->path, [['to' => $data['name'], 'from' => $file->name]]);
|
||||
|
||||
Activity::event('server:file.rename')
|
||||
->property('directory', $this->path)
|
||||
->property('files', [['to' => $data['name'], 'from' => $file->name]])
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title('File Renamed')
|
||||
->body(fn () => $file->name . ' -> ' . $data['name'])
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
Action::make('copy')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||
->label('Copy')
|
||||
->icon('tabler-copy')
|
||||
->visible(fn (File $file) => $file->is_file)
|
||||
->action(function (File $file) use ($server) {
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->copyFile(join_paths($this->path, $file->name));
|
||||
|
||||
Activity::event('server:file.copy')
|
||||
->property('file', join_paths($this->path, $file->name))
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title('File copied')
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return redirect(ListFiles::getUrl(['path' => $this->path]));
|
||||
}),
|
||||
Action::make('download')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
|
||||
->label('Download')
|
||||
->icon('tabler-download')
|
||||
->visible(fn (File $file) => $file->is_file)
|
||||
->action(function (File $file) use ($server) {
|
||||
// @phpstan-ignore-next-line
|
||||
$token = app(NodeJWTService::class)
|
||||
->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')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->label('Move')
|
||||
->icon('tabler-replace')
|
||||
->form([
|
||||
TextInput::make('location')
|
||||
->label('File name')
|
||||
->hint('Enter the new name and directory of this file or folder, relative to the current directory.')
|
||||
->default(fn (File $file) => $file->name)
|
||||
->required()
|
||||
->live(),
|
||||
Placeholder::make('new_location')
|
||||
->content(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location')))),
|
||||
])
|
||||
->action(function ($data, File $file) use ($server) {
|
||||
$location = resolve_path(join_paths($this->path, $data['location']));
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->renameFiles($this->path, [['to' => $location, 'from' => $file->name]]);
|
||||
|
||||
Activity::event('server:file.rename')
|
||||
->property('directory', $this->path)
|
||||
->property('files', [['to' => $location, 'from' => $file->name]])
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title(join_paths($this->path, $file->name) . ' was moved to ' . $location)
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
Action::make('permissions')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->label('Permissions')
|
||||
->icon('tabler-license')
|
||||
->form([
|
||||
CheckboxList::make('owner')
|
||||
->bulkToggleable()
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'write' => 'Write',
|
||||
'execute' => 'Execute',
|
||||
])
|
||||
->formatStateUsing(function ($state, File $file) {
|
||||
$mode = (int) substr((string) $file->mode_bits, 0, 1);
|
||||
|
||||
return $this->getPermissionsFromModeBit($mode);
|
||||
}),
|
||||
CheckboxList::make('group')
|
||||
->bulkToggleable()
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'write' => 'Write',
|
||||
'execute' => 'Execute',
|
||||
])
|
||||
->formatStateUsing(function ($state, File $file) {
|
||||
$mode = (int) substr((string) $file->mode_bits, 1, 1);
|
||||
|
||||
return $this->getPermissionsFromModeBit($mode);
|
||||
}),
|
||||
CheckboxList::make('public')
|
||||
->bulkToggleable()
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'write' => 'Write',
|
||||
'execute' => 'Execute',
|
||||
])
|
||||
->formatStateUsing(function ($state, File $file) {
|
||||
$mode = (int) substr((string) $file->mode_bits, 2, 1);
|
||||
|
||||
return $this->getPermissionsFromModeBit($mode);
|
||||
}),
|
||||
])
|
||||
->action(function ($data, File $file) use ($server) {
|
||||
$owner = (in_array('read', $data['owner']) ? 4 : 0) | (in_array('write', $data['owner']) ? 2 : 0) | (in_array('execute', $data['owner']) ? 1 : 0);
|
||||
$group = (in_array('read', $data['group']) ? 4 : 0) | (in_array('write', $data['group']) ? 2 : 0) | (in_array('execute', $data['group']) ? 1 : 0);
|
||||
$public = (in_array('read', $data['public']) ? 4 : 0) | (in_array('write', $data['public']) ? 2 : 0) | (in_array('execute', $data['public']) ? 1 : 0);
|
||||
|
||||
$mode = $owner . $group . $public;
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->chmodFiles($this->path, [['file' => $file->name, 'mode' => $mode]]);
|
||||
|
||||
Notification::make()
|
||||
->title('Permissions changed to ' . $mode)
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
Action::make('archive')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
|
||||
->label('Archive')
|
||||
->icon('tabler-archive')
|
||||
->action(function (File $file) use ($server) {
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->compressFiles($this->path, [$file->name]);
|
||||
|
||||
Activity::event('server:file.compress')
|
||||
->property('directory', $this->path)
|
||||
->property('files', [$file->name])
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title('Archive created')
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return redirect(ListFiles::getUrl(['path' => $this->path]));
|
||||
}),
|
||||
Action::make('unarchive')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
|
||||
->label('Unarchive')
|
||||
->icon('tabler-archive')
|
||||
->visible(fn (File $file) => $file->isArchive())
|
||||
->action(function (File $file) use ($server) {
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->decompressFile($this->path, $file->name);
|
||||
|
||||
Activity::event('server:file.decompress')
|
||||
->property('directory', $this->path)
|
||||
->property('files', $file->name)
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title('Unarchive completed')
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return redirect(ListFiles::getUrl(['path' => $this->path]));
|
||||
}),
|
||||
]),
|
||||
DeleteAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
|
||||
->label('')
|
||||
->icon('tabler-trash')
|
||||
->requiresConfirmation()
|
||||
->modalDescription(fn (File $file) => $file->name)
|
||||
->modalHeading('Delete file?')
|
||||
->action(function (File $file) use ($server) {
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->deleteFiles($this->path, [$file->name]);
|
||||
|
||||
Activity::event('server:file.delete')
|
||||
->property('directory', $this->path)
|
||||
->property('files', $file->name)
|
||||
->log();
|
||||
}),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
BulkAction::make('move')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->form([
|
||||
TextInput::make('location')
|
||||
->label('File name')
|
||||
->hint('Enter the new name and directory of this file or folder, relative to the current directory.')
|
||||
->default(fn (File $file) => $file->name)
|
||||
->required()
|
||||
->live(),
|
||||
Placeholder::make('new_location')
|
||||
->content(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location') ?? ''))),
|
||||
])
|
||||
->action(function (Collection $files, $data) use ($server) {
|
||||
$location = resolve_path(join_paths($this->path, $data['location']));
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
$files = $files->map(fn ($file) => ['to' => $location, 'from' => $file->name])->toArray();
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->renameFiles($this->path, $files);
|
||||
|
||||
Activity::event('server:file.rename')
|
||||
->property('directory', $this->path)
|
||||
->property('files', $files)
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title(count($files) . ' Files were moved from to ' . $location)
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
BulkAction::make('archive')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
|
||||
->action(function (Collection $files) use ($server) {
|
||||
// @phpstan-ignore-next-line
|
||||
$files = $files->map(fn ($file) => $file->name)->toArray();
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->compressFiles($this->path, $files);
|
||||
|
||||
Activity::event('server:file.compress')
|
||||
->property('directory', $this->path)
|
||||
->property('files', $files)
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title('Archive created')
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return redirect(ListFiles::getUrl(['path' => $this->path]));
|
||||
}),
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
|
||||
->action(function (Collection $files) use ($server) {
|
||||
// @phpstan-ignore-next-line
|
||||
$files = $files->map(fn ($file) => $file->name)->toArray();
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->deleteFiles($this->path, $files);
|
||||
|
||||
Activity::event('server:file.delete')
|
||||
->property('directory', $this->path)
|
||||
->property('files', $files)
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title(count($files) . ' Files deleted.')
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return [
|
||||
HeaderAction::make('new_file')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||
->label('New File')
|
||||
->color('gray')
|
||||
->keyBindings('')
|
||||
->modalSubmitActionLabel('Create')
|
||||
->action(function ($data) use ($server) {
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->putContent(join_paths($this->path, $data['name']), $data['editor'] ?? '');
|
||||
|
||||
Activity::event('server:file.write')
|
||||
->property('file', join_paths($this->path, $data['name']))
|
||||
->log();
|
||||
})
|
||||
->form([
|
||||
TextInput::make('name')
|
||||
->label('File Name')
|
||||
->required(),
|
||||
Select::make('lang')
|
||||
->live()
|
||||
->hidden() //TODO: Make file language selection work
|
||||
->label('Language')
|
||||
->placeholder('File Language')
|
||||
->options(EditorLanguages::class),
|
||||
MonacoEditor::make('editor')
|
||||
->label('')
|
||||
->view('filament.plugins.monaco-editor')
|
||||
->language(fn (Get $get) => $get('lang')),
|
||||
]),
|
||||
HeaderAction::make('new_folder')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||
->label('New Folder')
|
||||
->color('gray')
|
||||
->action(function ($data) use ($server) {
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->createDirectory($data['name'], $this->path);
|
||||
|
||||
Activity::event('server:file.write')
|
||||
->property('file', join_paths($this->path, $data['name']))
|
||||
->log();
|
||||
})
|
||||
->form([
|
||||
TextInput::make('name')
|
||||
->label('Folder Name')
|
||||
->required(),
|
||||
]),
|
||||
HeaderAction::make('upload')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||
->label('Upload')
|
||||
->action(function ($data) use ($server) {
|
||||
if (count($data['files']) > 0 && !isset($data['url'])) {
|
||||
/** @var UploadedFile $file */
|
||||
foreach ($data['files'] as $file) {
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->putContent(join_paths($this->path, $file->getClientOriginalName()), $file->getContent());
|
||||
|
||||
Activity::event('server:file.uploaded')
|
||||
->property('directory', $this->path)
|
||||
->property('file', $file->getFilename())
|
||||
->log();
|
||||
}
|
||||
} elseif ($data['url'] !== null) {
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->pull($data['url'], $this->path);
|
||||
|
||||
Activity::event('server:file.pull')
|
||||
->property('url', $data['url'])
|
||||
->property('directory', $this->path)
|
||||
->log();
|
||||
}
|
||||
|
||||
return redirect(ListFiles::getUrl(['path' => $this->path]));
|
||||
|
||||
})
|
||||
->form([
|
||||
Tabs::make()
|
||||
->contained(false)
|
||||
->schema([
|
||||
Tabs\Tab::make('Upload Files')
|
||||
->live()
|
||||
->schema([
|
||||
FileUpload::make('files')
|
||||
->label('File(s)')
|
||||
->storeFiles(false)
|
||||
->previewable(false)
|
||||
->preserveFilenames()
|
||||
->multiple(),
|
||||
]),
|
||||
Tabs\Tab::make('Upload From URL')
|
||||
->live()
|
||||
->disabled(fn (Get $get) => count($get('files')) > 0)
|
||||
->schema([
|
||||
TextInput::make('url')
|
||||
->label('URL')
|
||||
->url(),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
HeaderAction::make('search')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
|
||||
->label('Global Search')
|
||||
->modalSubmitActionLabel('Search')
|
||||
->form([
|
||||
TextInput::make('searchTerm')
|
||||
->placeholder('Enter a search term, e.g. *.txt')
|
||||
->minLength(3),
|
||||
])
|
||||
->action(fn ($data) => redirect(SearchFiles::getUrl([
|
||||
'searchTerm' => $data['searchTerm'],
|
||||
'path' => $this->path,
|
||||
]))),
|
||||
];
|
||||
}
|
||||
|
||||
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', '.*'),
|
||||
);
|
||||
}
|
||||
|
||||
private function getPermissionsFromModeBit(int $mode): array
|
||||
{
|
||||
if ($mode === 1) {
|
||||
return ['execute'];
|
||||
} elseif ($mode === 2) {
|
||||
return ['write'];
|
||||
} elseif ($mode === 3) {
|
||||
return ['write', 'execute'];
|
||||
} elseif ($mode === 4) {
|
||||
return ['read'];
|
||||
} elseif ($mode === 5) {
|
||||
return ['read', 'execute'];
|
||||
} elseif ($mode === 6) {
|
||||
return ['read', 'write'];
|
||||
} elseif ($mode === 7) {
|
||||
return ['read', 'write', 'execute'];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\FileResource\Pages;
|
||||
|
||||
use App\Filament\Server\Resources\FileResource;
|
||||
use App\Models\File;
|
||||
use App\Models\Server;
|
||||
use App\Tables\Columns\BytesColumn;
|
||||
use App\Tables\Columns\DateTimeColumn;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Livewire\Attributes\Locked;
|
||||
|
||||
class SearchFiles extends ListRecords
|
||||
{
|
||||
protected static string $resource = FileResource::class;
|
||||
|
||||
protected static ?string $title = 'Global Search';
|
||||
|
||||
#[Locked]
|
||||
public string $searchTerm;
|
||||
|
||||
#[Locked]
|
||||
public string $path;
|
||||
|
||||
public function mount(?string $searchTerm = null, ?string $path = null): void
|
||||
{
|
||||
parent::mount();
|
||||
$this->searchTerm = $searchTerm;
|
||||
$this->path = $path ?? '/';
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
$resource = static::getResource();
|
||||
|
||||
return [
|
||||
$resource::getUrl() => $resource::getBreadcrumb(),
|
||||
self::getUrl(['searchTerm' => $this->searchTerm]) => 'Search "' . $this->searchTerm . '"',
|
||||
];
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $table
|
||||
->paginated(false)
|
||||
->query(fn () => File::get($server, $this->path, $this->searchTerm)->orderByDesc('is_directory')->orderBy('name'))
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable()
|
||||
->icon(fn (File $file) => $file->getIcon()),
|
||||
BytesColumn::make('size'),
|
||||
DateTimeColumn::make('modified_at')
|
||||
->since()
|
||||
->sortable(),
|
||||
])
|
||||
->recordUrl(function (File $file) {
|
||||
if ($file->is_directory) {
|
||||
return ListFiles::getUrl(['path' => join_paths($this->path, $file->name)]);
|
||||
}
|
||||
|
||||
return $file->canEdit() ? EditFiles::getUrl(['path' => join_paths($this->path, $file->name)]) : null;
|
||||
});
|
||||
}
|
||||
}
|
268
app/Filament/Server/Resources/ScheduleResource.php
Normal file
268
app/Filament/Server/Resources/ScheduleResource.php
Normal file
@ -0,0 +1,268 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources;
|
||||
|
||||
use App\Filament\Server\Resources\ScheduleResource\Pages;
|
||||
use App\Filament\Server\Resources\ScheduleResource\RelationManagers\TasksRelationManager;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Schedule;
|
||||
use App\Models\Server;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\Resource;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ScheduleResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Schedule::class;
|
||||
|
||||
protected static ?int $navigationSort = 4;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-clock';
|
||||
|
||||
// TODO: find better way handle server conflict state
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
if ($server->isInConflictState()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::canAccess();
|
||||
}
|
||||
|
||||
public static function canViewAny(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_SCHEDULE_READ, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_SCHEDULE_CREATE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canEdit(Model $record): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_SCHEDULE_UPDATE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canDelete(Model $record): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_SCHEDULE_DELETE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->columns(10)
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->columnSpan(10)
|
||||
->label('Schedule Name')
|
||||
->placeholder('A human readable identifier for this schedule.')
|
||||
->autocomplete(false)
|
||||
->required(),
|
||||
Toggle::make('only_when_online')
|
||||
->label('Only when Server is Online?')
|
||||
->hintIconTooltip('Only execute this schedule when the server is in a running state.')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->columnSpan(5)
|
||||
->required()
|
||||
->default(1),
|
||||
Toggle::make('is_active')
|
||||
->label('Enable Schedule?')
|
||||
->hintIconTooltip('This schedule will be executed automatically if enabled.')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->columnSpan(5)
|
||||
->required()
|
||||
->default(1),
|
||||
TextInput::make('cron_minute')
|
||||
->columnSpan(2)
|
||||
->label('Minute')
|
||||
->default('*/5')
|
||||
->required(),
|
||||
TextInput::make('cron_hour')
|
||||
->columnSpan(2)
|
||||
->label('Hour')
|
||||
->default('*')
|
||||
->required(),
|
||||
TextInput::make('cron_day_of_month')
|
||||
->columnSpan(2)
|
||||
->label('Day of Month')
|
||||
->default('*')
|
||||
->required(),
|
||||
TextInput::make('cron_month')
|
||||
->columnSpan(2)
|
||||
->label('Month')
|
||||
->default('*')
|
||||
->required(),
|
||||
TextInput::make('cron_day_of_week')
|
||||
->columnSpan(2)
|
||||
->label('Day of Week')
|
||||
->default('*')
|
||||
->required(),
|
||||
Section::make('Presets')
|
||||
->schema([
|
||||
Actions::make([
|
||||
Action::make('hourly')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->action(function (Set $set) {
|
||||
$set('cron_minute', '0');
|
||||
$set('cron_hour', '*');
|
||||
$set('cron_day_of_month', '*');
|
||||
$set('cron_month', '*');
|
||||
$set('cron_day_of_week', '*');
|
||||
}),
|
||||
Action::make('daily')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->action(function (Set $set) {
|
||||
$set('cron_minute', '0');
|
||||
$set('cron_hour', '0');
|
||||
$set('cron_day_of_month', '*');
|
||||
$set('cron_month', '*');
|
||||
$set('cron_day_of_week', '*');
|
||||
}),
|
||||
Action::make('weekly')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->action(function (Set $set) {
|
||||
$set('cron_minute', '0');
|
||||
$set('cron_hour', '0');
|
||||
$set('cron_day_of_month', '*');
|
||||
$set('cron_month', '*');
|
||||
$set('cron_day_of_week', '0');
|
||||
}),
|
||||
Action::make('monthly')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->action(function (Set $set) {
|
||||
$set('cron_minute', '0');
|
||||
$set('cron_hour', '0');
|
||||
$set('cron_day_of_month', '1');
|
||||
$set('cron_month', '*');
|
||||
$set('cron_day_of_week', '0');
|
||||
}),
|
||||
Action::make('every_x_minutes')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->form([
|
||||
TextInput::make('x')
|
||||
->label('')
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(60)
|
||||
->prefix('Every')
|
||||
->suffix('Minutes'),
|
||||
])
|
||||
->action(function (Set $set, $data) {
|
||||
$set('cron_minute', '*/' . $data['x']);
|
||||
$set('cron_hour', '*');
|
||||
$set('cron_day_of_month', '*');
|
||||
$set('cron_month', '*');
|
||||
$set('cron_day_of_week', '*');
|
||||
}),
|
||||
Action::make('every_x_hours')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->form([
|
||||
TextInput::make('x')
|
||||
->label('')
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(24)
|
||||
->prefix('Every')
|
||||
->suffix('Hours'),
|
||||
])
|
||||
->action(function (Set $set, $data) {
|
||||
$set('cron_minute', '0');
|
||||
$set('cron_hour', '*/' . $data['x']);
|
||||
$set('cron_day_of_month', '*');
|
||||
$set('cron_month', '*');
|
||||
$set('cron_day_of_week', '*');
|
||||
}),
|
||||
Action::make('every_x_days')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->form([
|
||||
TextInput::make('x')
|
||||
->label('')
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(24)
|
||||
->prefix('Every')
|
||||
->suffix('Days'),
|
||||
])
|
||||
->action(function (Set $set, $data) {
|
||||
$set('cron_minute', '0');
|
||||
$set('cron_hour', '0');
|
||||
$set('cron_day_of_month', '*/' . $data['x']);
|
||||
$set('cron_month', '*');
|
||||
$set('cron_day_of_week', '*');
|
||||
}),
|
||||
Action::make('every_x_months')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->form([
|
||||
TextInput::make('x')
|
||||
->label('')
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(24)
|
||||
->prefix('Every')
|
||||
->suffix('Months'),
|
||||
])
|
||||
->action(function (Set $set, $data) {
|
||||
$set('cron_minute', '0');
|
||||
$set('cron_hour', '0');
|
||||
$set('cron_day_of_month', '0');
|
||||
$set('cron_month', '*/' . $data['x']);
|
||||
$set('cron_day_of_week', '*');
|
||||
}),
|
||||
Action::make('every_x_day_of_week')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->form([
|
||||
Select::make('x')
|
||||
->label('')
|
||||
->prefix('Every')
|
||||
->options([
|
||||
'0' => 'Sunday',
|
||||
'1' => 'Monday',
|
||||
'2' => 'Tuesday',
|
||||
'3' => 'Wednesday',
|
||||
'4' => 'Thursday',
|
||||
'5' => 'Friday',
|
||||
'6' => 'Saturday',
|
||||
]),
|
||||
])
|
||||
->action(function (Set $set, $data) {
|
||||
$set('cron_minute', '0');
|
||||
$set('cron_hour', '0');
|
||||
$set('cron_day_of_month', '*');
|
||||
$set('cron_month', '*');
|
||||
$set('cron_day_of_week', $data['x']);
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
TasksRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListSchedules::route('/'),
|
||||
'create' => Pages\CreateSchedule::route('/create'),
|
||||
'view' => Pages\ViewSchedule::route('/{record}'),
|
||||
'edit' => Pages\EditSchedule::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\ScheduleResource\Pages;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
use App\Filament\Server\Resources\ScheduleResource;
|
||||
use App\Helpers\Utilities;
|
||||
use App\Models\Server;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateSchedule extends CreateRecord
|
||||
{
|
||||
protected static string $resource = ScheduleResource::class;
|
||||
|
||||
protected static bool $canCreateAnother = false;
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
if (!isset($data['server_id'])) {
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
$data['server_id'] = $server->id;
|
||||
}
|
||||
|
||||
if (!isset($data['next_run_at'])) {
|
||||
$data['next_run_at'] = $this->getNextRunAt($data['cron_minute'], $data['cron_hour'], $data['cron_day_of_month'], $data['cron_month'], $data['cron_day_of_week']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getNextRunAt(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek): Carbon
|
||||
{
|
||||
try {
|
||||
return Utilities::getScheduleNextRunDate(
|
||||
$minute,
|
||||
$hour,
|
||||
$dayOfMonth,
|
||||
$month,
|
||||
$dayOfWeek
|
||||
);
|
||||
} catch (Exception) {
|
||||
throw new DisplayException('The cron data provided does not evaluate to a valid expression.');
|
||||
}
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\ScheduleResource\Pages;
|
||||
|
||||
use App\Filament\Server\Resources\ScheduleResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditSchedule extends EditRecord
|
||||
{
|
||||
protected static string $resource = ScheduleResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\ViewAction::make(),
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\ScheduleResource\Pages;
|
||||
|
||||
use App\Filament\Server\Resources\ScheduleResource;
|
||||
use App\Models\Schedule;
|
||||
use App\Tables\Columns\DateTimeColumn;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ListSchedules extends ListRecords
|
||||
{
|
||||
protected static string $resource = ScheduleResource::class;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
TextColumn::make('cron')
|
||||
->state(fn (Schedule $schedule) => $schedule->cron_minute . ' ' . $schedule->cron_hour . ' ' . $schedule->cron_day_of_month . ' ' . $schedule->cron_month . ' ' . $schedule->cron_day_of_week),
|
||||
TextColumn::make('status')
|
||||
->state(fn (Schedule $schedule) => !$schedule->is_active ? 'Inactive' : ($schedule->is_processing ? 'Processing' : 'Active')),
|
||||
IconColumn::make('only_when_online')
|
||||
->boolean()
|
||||
->sortable(),
|
||||
DateTimeColumn::make('last_run_at')
|
||||
->label('Last run')
|
||||
->since()
|
||||
->sortable(),
|
||||
DateTimeColumn::make('next_run_at')
|
||||
->label('Next run')
|
||||
->since()
|
||||
->sortable(),
|
||||
])
|
||||
->actions([
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\ScheduleResource\Pages;
|
||||
|
||||
use App\Filament\Server\Resources\ScheduleResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewSchedule extends ViewRecord
|
||||
{
|
||||
protected static string $resource = ScheduleResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\EditAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\ScheduleResource\RelationManagers;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Models\Schedule;
|
||||
use App\Models\Task;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
|
||||
class TasksRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'tasks';
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var Schedule $schedule */
|
||||
$schedule = $this->getOwnerRecord();
|
||||
|
||||
return $table
|
||||
->reorderable('sequence_id', true)
|
||||
->columns([
|
||||
TextColumn::make('action')
|
||||
->state(fn (Task $task) => match ($task->action) {
|
||||
Task::ACTION_POWER => 'Send power action',
|
||||
Task::ACTION_COMMAND => 'Send command',
|
||||
Task::ACTION_BACKUP => 'Create backup',
|
||||
Task::ACTION_DELETE_FILES => 'Delete files',
|
||||
default => $task->action
|
||||
}),
|
||||
TextColumn::make('time_offset')
|
||||
->hidden(fn () => config('queue.default') === 'sync')
|
||||
->suffix(' Seconds'),
|
||||
IconColumn::make('continue_on_failure')
|
||||
->boolean(),
|
||||
])
|
||||
->headerActions([
|
||||
CreateAction::make()
|
||||
->createAnother(false)
|
||||
->label(fn () => $schedule->tasks()->count() >= config('panel.client_features.schedules.per_schedule_task_limit', 10) ? 'Task Limit Reached' : 'Create Task')
|
||||
->disabled(fn () => $schedule->tasks()->count() >= config('panel.client_features.schedules.per_schedule_task_limit', 10))
|
||||
->form([
|
||||
Select::make('action')
|
||||
->required()
|
||||
->live()
|
||||
->disableOptionWhen(fn (string $value): bool => $value === Task::ACTION_BACKUP && $schedule->server->backup_limit === 0)
|
||||
->options([
|
||||
Task::ACTION_POWER => 'Send power action',
|
||||
Task::ACTION_COMMAND => 'Send command',
|
||||
Task::ACTION_BACKUP => 'Create backup',
|
||||
Task::ACTION_DELETE_FILES => 'Delete files',
|
||||
]),
|
||||
Textarea::make('payload')
|
||||
->hidden(fn (Get $get) => $get('action') === Task::ACTION_POWER)
|
||||
->label(fn (Get $get) => match ($get('action')) {
|
||||
Task::ACTION_POWER => 'Power action',
|
||||
Task::ACTION_COMMAND => 'Command',
|
||||
Task::ACTION_BACKUP => 'Files to ignore',
|
||||
Task::ACTION_DELETE_FILES => 'Files to delete',
|
||||
default => 'Payload'
|
||||
}),
|
||||
Select::make('payload')
|
||||
->visible(fn (Get $get) => $get('action') === Task::ACTION_POWER)
|
||||
->label('Power Action')
|
||||
->required()
|
||||
->options([
|
||||
'start' => 'Start',
|
||||
'restart' => 'Restart',
|
||||
'stop' => 'Stop',
|
||||
'Kill' => 'Kill',
|
||||
]),
|
||||
TextInput::make('time_offset')
|
||||
->hidden(fn (Get $get) => config('queue.default') === 'sync' || $get('sequence_id') === 1)
|
||||
->default(0)
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(900)
|
||||
->suffix('Seconds'),
|
||||
Toggle::make('continue_on_failure'),
|
||||
])
|
||||
->action(function ($data) use ($schedule) {
|
||||
$sequenceId = ($schedule->tasks()->orderByDesc('sequence_id')->first()->sequence_id ?? 0) + 1;
|
||||
|
||||
$task = Task::query()->create([
|
||||
'schedule_id' => $schedule->id,
|
||||
'sequence_id' => $sequenceId,
|
||||
'action' => $data['action'],
|
||||
'payload' => $data['payload'] ?? '',
|
||||
'time_offset' => $data['time_offset'] ?? 0,
|
||||
'continue_on_failure' => (bool) $data['continue_on_failure'],
|
||||
]);
|
||||
|
||||
Activity::event('server:task.create')
|
||||
->subject($schedule, $task)
|
||||
->property(['name' => $schedule->name, 'action' => $task->action, 'payload' => $task->payload])
|
||||
->log();
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
469
app/Filament/Server/Resources/UserResource.php
Normal file
469
app/Filament/Server/Resources/UserResource.php
Normal file
@ -0,0 +1,469 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources;
|
||||
|
||||
use App\Filament\Server\Resources\UserResource\Pages;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Models\Subuser;
|
||||
use App\Models\User;
|
||||
use App\Services\Subusers\SubuserDeletionService;
|
||||
use App\Services\Subusers\SubuserUpdateService;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UserResource extends Resource
|
||||
{
|
||||
protected static ?string $model = User::class;
|
||||
|
||||
protected static ?int $navigationSort = 5;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-users';
|
||||
|
||||
protected static ?string $tenantOwnershipRelationshipName = 'subServers';
|
||||
|
||||
// TODO: find better way handle server conflict state
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
if ($server->isInConflictState()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::canAccess();
|
||||
}
|
||||
|
||||
public static function canViewAny(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_USER_READ, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_USER_CREATE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canEdit(Model $record): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_USER_UPDATE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canDelete(Model $record): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_USER_DELETE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $table
|
||||
->paginated(false)
|
||||
->searchable(false)
|
||||
->columns([
|
||||
ImageColumn::make('picture')
|
||||
->visibleFrom('lg')
|
||||
->label('')
|
||||
->extraImgAttributes(['class' => 'rounded-full'])
|
||||
->defaultImageUrl(fn (User $user) => 'https://gravatar.com/avatar/' . md5(strtolower($user->email))),
|
||||
TextColumn::make('username')
|
||||
->searchable(),
|
||||
TextColumn::make('email')
|
||||
->searchable(),
|
||||
TextColumn::make('permissions')
|
||||
->state(fn (User $user) => count(Subuser::query()->where('user_id', $user->id)->where('server_id', $server->id)->first()->permissions)),
|
||||
])
|
||||
->actions([
|
||||
DeleteAction::make()
|
||||
->label('Remove User')
|
||||
->hidden(fn (User $user) => auth()->user()->id === $user->id)
|
||||
->action(function (User $user, SubuserDeletionService $subuserDeletionService) use ($server) {
|
||||
$subuser = Subuser::query()->where('user_id', $user->id)->where('server_id', $server->id)->first();
|
||||
$subuserDeletionService->handle($subuser, $server);
|
||||
|
||||
}),
|
||||
EditAction::make()
|
||||
->label('Edit User')
|
||||
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_USER_UPDATE, $server))
|
||||
->modalHeading(fn (User $user) => 'Editing ' . $user->email)
|
||||
->action(function (array $data, SubuserUpdateService $subuserUpdateService, User $user) use ($server) {
|
||||
$subuser = Subuser::query()->where('user_id', $user->id)->where('server_id', $server->id)->first();
|
||||
|
||||
if (in_array('console', $data['control'])) {
|
||||
$data['websocket'][0] = 'connect';
|
||||
}
|
||||
|
||||
$permissions = collect($data)->forget('email')->map(fn ($permissions, $key) => collect($permissions)->map(fn ($permission) => "$key.$permission"))->flatten()->all();
|
||||
$subuserUpdateService->handle($subuser, $server, $permissions);
|
||||
|
||||
Notification::make()
|
||||
->title('User Updated!')
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return redirect(self::getUrl(tenant: $server));
|
||||
})
|
||||
->form([
|
||||
Grid::make()
|
||||
->columnSpanFull()
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 5,
|
||||
'lg' => 6,
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('email')
|
||||
->inlineLabel()
|
||||
->disabled()
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 4,
|
||||
'lg' => 5,
|
||||
]),
|
||||
Actions::make([
|
||||
Action::make('assignAll')
|
||||
->label('Assign All')
|
||||
->action(function (Set $set) {
|
||||
$permissions = [
|
||||
'control' => [
|
||||
'console',
|
||||
'start',
|
||||
'stop',
|
||||
'restart',
|
||||
],
|
||||
'user' => [
|
||||
'read',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
],
|
||||
'file' => [
|
||||
'read',
|
||||
'read-content',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
'archive',
|
||||
'sftp',
|
||||
],
|
||||
'backup' => [
|
||||
'read',
|
||||
'create',
|
||||
'delete',
|
||||
'download',
|
||||
'restore',
|
||||
],
|
||||
'allocation' => [
|
||||
'read',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
],
|
||||
'startup' => [
|
||||
'read',
|
||||
'update',
|
||||
'docker-image',
|
||||
],
|
||||
'database' => [
|
||||
'read',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
'view_password',
|
||||
],
|
||||
'schedule' => [
|
||||
'read',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
],
|
||||
'settings' => [
|
||||
'rename',
|
||||
'reinstall',
|
||||
'activity',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($permissions as $key => $value) {
|
||||
$allValues = array_unique($value);
|
||||
$set($key, $allValues);
|
||||
}
|
||||
}),
|
||||
])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
]),
|
||||
Tabs::make()
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Tab::make('Console')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.control_desc'))
|
||||
->icon('tabler-terminal-2')
|
||||
->schema([
|
||||
CheckboxList::make('control')
|
||||
->formatStateUsing(function (User $user, Set $set) use ($server) {
|
||||
$permissionsArray = Subuser::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('server_id', $server->id)
|
||||
->first()
|
||||
->permissions;
|
||||
|
||||
$transformedPermissions = [];
|
||||
|
||||
foreach ($permissionsArray as $permission) {
|
||||
[$group, $action] = explode('.', $permission, 2);
|
||||
$transformedPermissions[$group][] = $action;
|
||||
}
|
||||
|
||||
foreach ($transformedPermissions as $key => $value) {
|
||||
$set($key, $value);
|
||||
}
|
||||
|
||||
return $transformedPermissions['control'] ?? [];
|
||||
})
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->options([
|
||||
'console' => 'Console',
|
||||
'start' => 'Start',
|
||||
'stop' => 'Stop',
|
||||
'restart' => 'Restart',
|
||||
])
|
||||
->descriptions([
|
||||
'console' => trans('server/users.permissions.control_console'),
|
||||
'start' => trans('server/users.permissions.control_start'),
|
||||
'stop' => trans('server/users.permissions.control_stop'),
|
||||
'restart' => trans('server/users.permissions.control_restart'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('User')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.user_desc'))
|
||||
->icon('tabler-users')
|
||||
->schema([
|
||||
CheckboxList::make('user')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
])
|
||||
->descriptions([
|
||||
'create' => trans('server/users.permissions.user_create'),
|
||||
'read' => trans('server/users.permissions.user_read'),
|
||||
'update' => trans('server/users.permissions.user_update'),
|
||||
'delete' => trans('server/users.permissions.user_delete'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('File')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.file_desc'))
|
||||
->icon('tabler-folders')
|
||||
->schema([
|
||||
CheckboxList::make('file')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'read-content' => 'Read Content',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
'archive' => 'Archive',
|
||||
'sftp' => 'SFTP',
|
||||
])
|
||||
->descriptions([
|
||||
'create' => trans('server/users.permissions.file_create'),
|
||||
'read' => trans('server/users.permissions.file_read'),
|
||||
'read-content' => trans('server/users.permissions.file_read_content'),
|
||||
'update' => trans('server/users.permissions.file_update'),
|
||||
'delete' => trans('server/users.permissions.file_delete'),
|
||||
'archive' => trans('server/users.permissions.file_archive'),
|
||||
'sftp' => trans('server/users.permissions.file_sftp'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Backup')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.backup_desc'))
|
||||
->icon('tabler-download')
|
||||
->schema([
|
||||
CheckboxList::make('backup')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'delete' => 'Delete',
|
||||
'download' => 'Download',
|
||||
'restore' => 'Restore',
|
||||
])
|
||||
->descriptions([
|
||||
'create' => trans('server/users.permissions.backup_create'),
|
||||
'read' => trans('server/users.permissions.backup_read'),
|
||||
'delete' => trans('server/users.permissions.backup_delete'),
|
||||
'download' => trans('server/users.permissions.backup_download'),
|
||||
'restore' => trans('server/users.permissions.backup_restore'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Allocation')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.allocation_desc'))
|
||||
->icon('tabler-network')
|
||||
->schema([
|
||||
CheckboxList::make('allocation')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
])
|
||||
->descriptions([
|
||||
'read' => trans('server/users.permissions.allocation_read'),
|
||||
'create' => trans('server/users.permissions.allocation_create'),
|
||||
'update' => trans('server/users.permissions.allocation_update'),
|
||||
'delete' => trans('server/users.permissions.allocation_delete'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Startup')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.startup_desc'))
|
||||
->icon('tabler-question-mark')
|
||||
->schema([
|
||||
CheckboxList::make('startup')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'update' => 'Update',
|
||||
'docker-image' => 'Docker Image',
|
||||
])
|
||||
->descriptions([
|
||||
'read' => trans('server/users.permissions.startup_read'),
|
||||
'update' => trans('server/users.permissions.startup_update'),
|
||||
'docker-image' => trans('server/users.permissions.startup_docker_image'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Database')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.database_desc'))
|
||||
->icon('tabler-database')
|
||||
->schema([
|
||||
CheckboxList::make('database')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
'view_password' => 'View Password',
|
||||
])
|
||||
->descriptions([
|
||||
'read' => trans('server/users.permissions.database_read'),
|
||||
'create' => trans('server/users.permissions.database_create'),
|
||||
'update' => trans('server/users.permissions.database_update'),
|
||||
'delete' => trans('server/users.permissions.database_delete'),
|
||||
'view_password' => trans('server/users.permissions.database_view_password'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Schedule')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.schedule_desc'))
|
||||
->icon('tabler-clock')
|
||||
->schema([
|
||||
CheckboxList::make('schedule')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
])
|
||||
->descriptions([
|
||||
'read' => trans('server/users.permissions.schedule_read'),
|
||||
'create' => trans('server/users.permissions.schedule_create'),
|
||||
'update' => trans('server/users.permissions.schedule_update'),
|
||||
'delete' => trans('server/users.permissions.schedule_delete'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Settings')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.settings_desc'))
|
||||
->icon('tabler-settings')
|
||||
->schema([
|
||||
CheckboxList::make('settings')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->options([
|
||||
'rename' => 'Rename',
|
||||
'reinstall' => 'Reinstall',
|
||||
'activity' => 'Activity',
|
||||
])
|
||||
->descriptions([
|
||||
'rename' => trans('server/users.permissions.setting_rename'),
|
||||
'reinstall' => trans('server/users.permissions.setting_reinstall'),
|
||||
'activity' => trans('server/users.permissions.setting_activity'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListUsers::route('/'),
|
||||
];
|
||||
}
|
||||
}
|
390
app/Filament/Server/Resources/UserResource/Pages/ListUsers.php
Normal file
390
app/Filament/Server/Resources/UserResource/Pages/ListUsers.php
Normal file
@ -0,0 +1,390 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\UserResource\Pages;
|
||||
|
||||
use App\Filament\Server\Resources\UserResource;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Services\Subusers\SubuserCreationService;
|
||||
use Filament\Actions;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions as assignAll;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListUsers extends ListRecords
|
||||
{
|
||||
protected static string $resource = UserResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return [
|
||||
Actions\CreateAction::make('invite')
|
||||
->label('Invite User')
|
||||
->createAnother(false)
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_USER_CREATE, $server))
|
||||
->form([
|
||||
Grid::make()
|
||||
->columnSpanFull()
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 5,
|
||||
'lg' => 6,
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('email')
|
||||
->email()
|
||||
->inlineLabel()
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 4,
|
||||
'lg' => 5,
|
||||
])
|
||||
->required()
|
||||
->unique(),
|
||||
assignAll::make([
|
||||
Action::make('assignAll')
|
||||
->label('Assign All')
|
||||
->action(function (Set $set, Get $get) {
|
||||
$permissions = [
|
||||
'control' => [
|
||||
'console',
|
||||
'start',
|
||||
'stop',
|
||||
'restart',
|
||||
'kill',
|
||||
],
|
||||
'user' => [
|
||||
'read',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
],
|
||||
'file' => [
|
||||
'read',
|
||||
'read-content',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
'archive',
|
||||
'sftp',
|
||||
],
|
||||
'backup' => [
|
||||
'read',
|
||||
'create',
|
||||
'delete',
|
||||
'download',
|
||||
'restore',
|
||||
],
|
||||
'allocation' => [
|
||||
'read',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
],
|
||||
'startup' => [
|
||||
'read',
|
||||
'update',
|
||||
'docker-image',
|
||||
],
|
||||
'database' => [
|
||||
'read',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
'view_password',
|
||||
],
|
||||
'schedule' => [
|
||||
'read',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
],
|
||||
'settings' => [
|
||||
'rename',
|
||||
'reinstall',
|
||||
'activity',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($permissions as $key => $value) {
|
||||
$currentValues = $get($key) ?? [];
|
||||
$allValues = array_unique(array_merge($currentValues, $value));
|
||||
$set($key, $allValues);
|
||||
}
|
||||
}),
|
||||
])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
]),
|
||||
Tabs::make()
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Tabs\Tab::make('Console')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.control_desc'))
|
||||
->icon('tabler-terminal-2')
|
||||
->schema([
|
||||
CheckboxList::make('control')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'console' => 'Console',
|
||||
'start' => 'Start',
|
||||
'stop' => 'Stop',
|
||||
'restart' => 'Restart',
|
||||
])
|
||||
->descriptions([
|
||||
'console' => trans('server/users.permissions.control_console'),
|
||||
'start' => trans('server/users.permissions.control_start'),
|
||||
'stop' => trans('server/users.permissions.control_stop'),
|
||||
'restart' => trans('server/users.permissions.control_restart'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tabs\Tab::make('User')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.user_desc'))
|
||||
->icon('tabler-users')
|
||||
->schema([
|
||||
CheckboxList::make('user')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
])
|
||||
->descriptions([
|
||||
'create' => trans('server/users.permissions.user_create'),
|
||||
'read' => trans('server/users.permissions.user_read'),
|
||||
'update' => trans('server/users.permissions.user_update'),
|
||||
'delete' => trans('server/users.permissions.user_delete'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tabs\Tab::make('File')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.file_desc'))
|
||||
->icon('tabler-folders')
|
||||
->schema([
|
||||
CheckboxList::make('file')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'read-content' => 'Read Content',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
'archive' => 'Archive',
|
||||
'sftp' => 'SFTP',
|
||||
])
|
||||
->descriptions([
|
||||
'create' => trans('server/users.permissions.file_create'),
|
||||
'read' => trans('server/users.permissions.file_read'),
|
||||
'read-content' => trans('server/users.permissions.file_read_content'),
|
||||
'update' => trans('server/users.permissions.file_update'),
|
||||
'delete' => trans('server/users.permissions.file_delete'),
|
||||
'archive' => trans('server/users.permissions.file_archive'),
|
||||
'sftp' => trans('server/users.permissions.file_sftp'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tabs\Tab::make('Backup')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.backup_desc'))
|
||||
->icon('tabler-download')
|
||||
->schema([
|
||||
CheckboxList::make('backup')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'delete' => 'Delete',
|
||||
'download' => 'Download',
|
||||
'restore' => 'Restore',
|
||||
])
|
||||
->descriptions([
|
||||
'create' => trans('server/users.permissions.backup_create'),
|
||||
'read' => trans('server/users.permissions.backup_read'),
|
||||
'delete' => trans('server/users.permissions.backup_delete'),
|
||||
'download' => trans('server/users.permissions.backup_download'),
|
||||
'restore' => trans('server/users.permissions.backup_restore'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tabs\Tab::make('Allocation')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.allocation_desc'))
|
||||
->icon('tabler-network')
|
||||
->schema([
|
||||
CheckboxList::make('allocation')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
])
|
||||
->descriptions([
|
||||
'read' => trans('server/users.permissions.allocation_read'),
|
||||
'create' => trans('server/users.permissions.allocation_create'),
|
||||
'update' => trans('server/users.permissions.allocation_update'),
|
||||
'delete' => trans('server/users.permissions.allocation_delete'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tabs\Tab::make('Startup')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.startup_desc'))
|
||||
->icon('tabler-question-mark')
|
||||
->schema([
|
||||
CheckboxList::make('startup')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'update' => 'Update',
|
||||
'docker-image' => 'Docker Image',
|
||||
])
|
||||
->descriptions([
|
||||
'read' => trans('server/users.permissions.startup_read'),
|
||||
'update' => trans('server/users.permissions.startup_update'),
|
||||
'docker-image' => trans('server/users.permissions.startup_docker_image'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tabs\Tab::make('Database')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.database_desc'))
|
||||
->icon('tabler-database')
|
||||
->schema([
|
||||
CheckboxList::make('database')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
'view_password' => 'View Password',
|
||||
])
|
||||
->descriptions([
|
||||
'read' => trans('server/users.permissions.database_read'),
|
||||
'create' => trans('server/users.permissions.database_create'),
|
||||
'update' => trans('server/users.permissions.database_update'),
|
||||
'delete' => trans('server/users.permissions.database_delete'),
|
||||
'view_password' => trans('server/users.permissions.database_view_password'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tabs\Tab::make('Schedule')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.schedule_desc'))
|
||||
->icon('tabler-clock')
|
||||
->schema([
|
||||
CheckboxList::make('schedule')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
])
|
||||
->descriptions([
|
||||
'read' => trans('server/users.permissions.schedule_read'),
|
||||
'create' => trans('server/users.permissions.schedule_create'),
|
||||
'update' => trans('server/users.permissions.schedule_update'),
|
||||
'delete' => trans('server/users.permissions.schedule_delete'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tabs\Tab::make('Settings')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.settings_desc'))
|
||||
->icon('tabler-settings')
|
||||
->schema([
|
||||
CheckboxList::make('settings')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'rename' => 'Rename',
|
||||
'reinstall' => 'Reinstall',
|
||||
'activity' => 'Activity',
|
||||
])
|
||||
->descriptions([
|
||||
'rename' => trans('server/users.permissions.setting_rename'),
|
||||
'reinstall' => trans('server/users.permissions.setting_reinstall'),
|
||||
'activity' => trans('server/users.permissions.setting_activity'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
|
||||
]),
|
||||
])
|
||||
->modalHeading('Invite User')
|
||||
->modalSubmitActionLabel('Invite')
|
||||
->action(function (array $data, SubuserCreationService $service) use ($server) {
|
||||
$email = $data['email'];
|
||||
|
||||
if (in_array('console', $data['control'])) {
|
||||
$data['websocket'][0] = 'connect';
|
||||
}
|
||||
$permissions = collect($data)->forget('email')->map(fn ($permissions, $key) => collect($permissions)->map(fn ($permission) => "$key.$permission"))->flatten()->all();
|
||||
$service->handle($server, $email, $permissions);
|
||||
|
||||
Notification::make()
|
||||
->title('User Invited!')
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return redirect(self::getUrl(tenant: $server));
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
71
app/Filament/Server/Widgets/ServerConsole.php
Normal file
71
app/Filament/Server/Widgets/ServerConsole.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Widgets;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use Filament\Widgets\Widget;
|
||||
use Illuminate\Support\Arr;
|
||||
use Livewire\Attributes\On;
|
||||
|
||||
class ServerConsole extends Widget
|
||||
{
|
||||
protected static string $view = 'filament.components.server-console';
|
||||
|
||||
protected int|string|array $columnSpan = 'full';
|
||||
|
||||
protected static bool $isLazy = false;
|
||||
|
||||
public ?Server $server = null;
|
||||
|
||||
public ?User $user = null;
|
||||
|
||||
public array $history = [];
|
||||
|
||||
public int $historyIndex = 0;
|
||||
|
||||
public string $input = '';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
$this->historyIndex = min($this->historyIndex + 1, count($this->history) - 1);
|
||||
|
||||
$this->input = $this->history[$this->historyIndex] ?? '';
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->historyIndex = max($this->historyIndex - 1, -1);
|
||||
|
||||
$this->input = $this->history[$this->historyIndex] ?? '';
|
||||
}
|
||||
|
||||
public function enter(): void
|
||||
{
|
||||
if (!empty($this->input)) {
|
||||
$this->dispatch('sendServerCommand', command: $this->input);
|
||||
|
||||
$this->history = Arr::prepend($this->history, $this->input);
|
||||
$this->historyIndex = -1;
|
||||
|
||||
$this->input = '';
|
||||
}
|
||||
}
|
||||
|
||||
#[On('storeStats')]
|
||||
public function storeStats(string $data): void
|
||||
{
|
||||
$data = json_decode($data);
|
||||
|
||||
$timestamp = now()->getTimestamp();
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
$cacheKey = "servers.{$this->server->id}.$key";
|
||||
$data = cache()->get($cacheKey, []);
|
||||
|
||||
$data[$timestamp] = $value;
|
||||
|
||||
cache()->put($cacheKey, $data, now()->addMinute());
|
||||
}
|
||||
}
|
||||
}
|
77
app/Filament/Server/Widgets/ServerCpuChart.php
Normal file
77
app/Filament/Server/Widgets/ServerCpuChart.php
Normal file
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Widgets;
|
||||
|
||||
use App\Models\Server;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Support\RawJs;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Support\Number;
|
||||
|
||||
class ServerCpuChart extends ChartWidget
|
||||
{
|
||||
protected static ?string $pollingInterval = '1s';
|
||||
|
||||
protected static ?string $maxHeight = '300px';
|
||||
|
||||
public ?Server $server = null;
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
$cpu = collect(cache()->get("servers.{$this->server->id}.cpu_absolute"))
|
||||
->slice(-10)
|
||||
->map(fn ($value, $key) => [
|
||||
'cpu' => Number::format($value, maxPrecision: 2, locale: auth()->user()->language),
|
||||
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
|
||||
])
|
||||
->all();
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'data' => array_column($cpu, 'cpu'),
|
||||
'backgroundColor' => [
|
||||
'rgba(96, 165, 250, 0.3)',
|
||||
],
|
||||
'tension' => '0.3',
|
||||
'fill' => true,
|
||||
],
|
||||
],
|
||||
'labels' => array_column($cpu, 'timestamp'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getType(): string
|
||||
{
|
||||
return 'line';
|
||||
}
|
||||
|
||||
protected function getOptions(): RawJs
|
||||
{
|
||||
return RawJs::make(<<<'JS'
|
||||
{
|
||||
scales: {
|
||||
y: {
|
||||
min: 0,
|
||||
},
|
||||
x: {
|
||||
display: false,
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
JS);
|
||||
}
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
$cpu = Number::format(collect(cache()->get("servers.{$this->server->id}.cpu_absolute"))->last() ?? 0, maxPrecision: 2, locale: auth()->user()->language) . '%';
|
||||
$max = Number::format($this->server->cpu, locale: auth()->user()->language) . '%';
|
||||
|
||||
return 'CPU - ' . $cpu . ($this->server->cpu > 0 ? ' Of ' . $max : '');
|
||||
}
|
||||
}
|
90
app/Filament/Server/Widgets/ServerMemoryChart.php
Normal file
90
app/Filament/Server/Widgets/ServerMemoryChart.php
Normal file
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Widgets;
|
||||
|
||||
use App\Models\Server;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Support\RawJs;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Support\Number;
|
||||
|
||||
class ServerMemoryChart extends ChartWidget
|
||||
{
|
||||
protected static ?string $pollingInterval = '1s';
|
||||
|
||||
protected static ?string $maxHeight = '300px';
|
||||
|
||||
public ?Server $server = null;
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
$memUsed = collect(cache()->get("servers.{$this->server->id}.memory_bytes"))->slice(-10)
|
||||
->map(fn ($value, $key) => [
|
||||
'memory' => Number::format(config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language),
|
||||
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
|
||||
])
|
||||
->all();
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'data' => array_column($memUsed, 'memory'),
|
||||
'backgroundColor' => [
|
||||
'rgba(96, 165, 250, 0.3)',
|
||||
],
|
||||
'tension' => '0.3',
|
||||
'fill' => true,
|
||||
],
|
||||
],
|
||||
'labels' => array_column($memUsed, 'timestamp'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getType(): string
|
||||
{
|
||||
return 'line';
|
||||
}
|
||||
|
||||
protected function getOptions(): RawJs
|
||||
{
|
||||
return RawJs::make(<<<'JS'
|
||||
{
|
||||
scales: {
|
||||
y: {
|
||||
min: 0,
|
||||
},
|
||||
x: {
|
||||
display: false,
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
JS);
|
||||
}
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
$latestMemoryUsed = collect(cache()->get("servers.{$this->server->id}.memory_bytes"))->last() ?? 0;
|
||||
$totalMemory = collect(cache()->get("servers.{$this->server->id}.memory_limit_bytes"))->last() ?? 0;
|
||||
|
||||
$used = config('panel.use_binary_prefix')
|
||||
? Number::format($latestMemoryUsed / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($latestMemoryUsed / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
|
||||
if ($totalMemory === 0) {
|
||||
$total = config('panel.use_binary_prefix')
|
||||
? Number::format($this->server->memory / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($this->server->memory / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
} else {
|
||||
$total = config('panel.use_binary_prefix')
|
||||
? Number::format($totalMemory / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($totalMemory / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
}
|
||||
|
||||
return 'Memory - ' . $used . ($this->server->memory > 0 ? ' Of ' . $total : '');
|
||||
}
|
||||
}
|
93
app/Filament/Server/Widgets/ServerNetworkChart.php
Normal file
93
app/Filament/Server/Widgets/ServerNetworkChart.php
Normal file
@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Widgets;
|
||||
|
||||
use App\Models\Server;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Support\RawJs;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
|
||||
class ServerNetworkChart extends ChartWidget
|
||||
{
|
||||
protected static ?string $heading = 'Network';
|
||||
|
||||
protected static ?string $pollingInterval = '1s';
|
||||
|
||||
protected static ?string $maxHeight = '300px';
|
||||
|
||||
public ?Server $server = null;
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
$data = cache()->get("servers.{$this->server->id}.network");
|
||||
|
||||
$rx = collect($data)
|
||||
->slice(-10)
|
||||
->map(fn ($value, $key) => [
|
||||
'rx' => $value->rx_bytes,
|
||||
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
|
||||
])
|
||||
->all();
|
||||
|
||||
$tx = collect($data)
|
||||
->slice(-10)
|
||||
->map(fn ($value, $key) => [
|
||||
'tx' => $value->rx_bytes,
|
||||
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
|
||||
])
|
||||
->all();
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => 'Inbound',
|
||||
'data' => array_column($rx, 'rx'),
|
||||
'backgroundColor' => [
|
||||
'rgba(96, 165, 250, 0.3)',
|
||||
],
|
||||
'tension' => '0.3',
|
||||
'fill' => true,
|
||||
],
|
||||
[
|
||||
'label' => 'Outbound',
|
||||
'data' => array_column($tx, 'tx'),
|
||||
'backgroundColor' => [
|
||||
'rgba(165, 96, 250, 0.3)',
|
||||
],
|
||||
'tension' => '0.3',
|
||||
'fill' => true,
|
||||
],
|
||||
],
|
||||
'labels' => array_column($rx, 'timestamp'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getType(): string
|
||||
{
|
||||
return 'line';
|
||||
}
|
||||
|
||||
protected function getOptions(): RawJs
|
||||
{
|
||||
return RawJs::make(<<<'JS'
|
||||
{
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
ticks: {
|
||||
display: true,
|
||||
},
|
||||
display: false, //debug
|
||||
},
|
||||
y: {
|
||||
ticks: {
|
||||
display: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
JS);
|
||||
}
|
||||
}
|
37
app/Filament/Server/Widgets/ServerOverview.php
Normal file
37
app/Filament/Server/Widgets/ServerOverview.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Widgets;
|
||||
|
||||
use App\Models\Server;
|
||||
use Carbon\CarbonInterface;
|
||||
use Filament\Widgets\StatsOverviewWidget;
|
||||
use Filament\Widgets\StatsOverviewWidget\Stat;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ServerOverview extends StatsOverviewWidget
|
||||
{
|
||||
protected static ?string $pollingInterval = '1s';
|
||||
|
||||
public ?Server $server = null;
|
||||
|
||||
protected function getStats(): array
|
||||
{
|
||||
return [
|
||||
Stat::make('Name', $this->server->name)
|
||||
->description($this->server->description),
|
||||
Stat::make('Status', Str::title($this->server->condition)),
|
||||
Stat::make('Uptime', $this->uptime()),
|
||||
];
|
||||
}
|
||||
|
||||
private function uptime(): string
|
||||
{
|
||||
$uptime = collect(cache()->get("servers.{$this->server->id}.uptime"))->last() ?? 0;
|
||||
|
||||
if ($uptime === 0) {
|
||||
return 'Offline';
|
||||
}
|
||||
|
||||
return now()->subMillis($uptime)->diffForHumans(syntax: CarbonInterface::DIFF_ABSOLUTE, short: true, parts: 2);
|
||||
}
|
||||
}
|
@ -3,17 +3,16 @@
|
||||
namespace App\Http\Controllers\Api\Client\Servers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Notifications\RemovedFromServer;
|
||||
use App\Services\Subusers\SubuserDeletionService;
|
||||
use App\Services\Subusers\SubuserUpdateService;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Facades\Activity;
|
||||
use App\Models\Permission;
|
||||
use App\Services\Subusers\SubuserCreationService;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
use App\Transformers\Api\Client\SubuserTransformer;
|
||||
use App\Http\Controllers\Api\Client\ClientApiController;
|
||||
use App\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
use App\Http\Requests\Api\Client\Servers\Subusers\GetSubuserRequest;
|
||||
use App\Http\Requests\Api\Client\Servers\Subusers\StoreSubuserRequest;
|
||||
use App\Http\Requests\Api\Client\Servers\Subusers\DeleteSubuserRequest;
|
||||
@ -26,7 +25,8 @@ class SubuserController extends ClientApiController
|
||||
*/
|
||||
public function __construct(
|
||||
private SubuserCreationService $creationService,
|
||||
private DaemonServerRepository $serverRepository
|
||||
private SubuserUpdateService $updateService,
|
||||
private SubuserDeletionService $deletionService
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
@ -89,40 +89,7 @@ class SubuserController extends ClientApiController
|
||||
/** @var \App\Models\Subuser $subuser */
|
||||
$subuser = $request->attributes->get('subuser');
|
||||
|
||||
$permissions = $this->getDefaultPermissions($request);
|
||||
$current = $subuser->permissions;
|
||||
|
||||
sort($permissions);
|
||||
sort($current);
|
||||
|
||||
$log = Activity::event('server:subuser.update')
|
||||
->subject($subuser->user)
|
||||
->property([
|
||||
'email' => $subuser->user->email,
|
||||
'old' => $current,
|
||||
'new' => $permissions,
|
||||
'revoked' => true,
|
||||
]);
|
||||
|
||||
// Only update the database and hit up the daemon instance to invalidate JTI's if the permissions
|
||||
// have actually changed for the user.
|
||||
if ($permissions !== $current) {
|
||||
$log->transaction(function ($instance) use ($request, $subuser, $server) {
|
||||
$subuser->update(['permissions' => $this->getDefaultPermissions($request)]);
|
||||
|
||||
try {
|
||||
$this->serverRepository->setServer($server)->revokeUserJTI($subuser->user_id);
|
||||
} catch (DaemonConnectionException $exception) {
|
||||
// Don't block this request if we can't connect to the daemon instance. Chances are it is
|
||||
// offline and the token will be invalid once daemon boots back.
|
||||
logger()->warning($exception, ['user_id' => $subuser->user_id, 'server_id' => $server->id]);
|
||||
|
||||
$instance->property('revoked', false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$log->reset();
|
||||
$this->updateService->handle($subuser, $server, $this->getDefaultPermissions($request));
|
||||
|
||||
return $this->fractal->item($subuser->refresh())
|
||||
->transformWith($this->getTransformer(SubuserTransformer::class))
|
||||
@ -137,28 +104,7 @@ class SubuserController extends ClientApiController
|
||||
/** @var \App\Models\Subuser $subuser */
|
||||
$subuser = $request->attributes->get('subuser');
|
||||
|
||||
$log = Activity::event('server:subuser.delete')
|
||||
->subject($subuser->user)
|
||||
->property('email', $subuser->user->email)
|
||||
->property('revoked', true);
|
||||
|
||||
$log->transaction(function ($instance) use ($server, $subuser) {
|
||||
$subuser->delete();
|
||||
|
||||
$subuser->user->notify(new RemovedFromServer([
|
||||
'user' => $subuser->user->name_first,
|
||||
'name' => $subuser->server->name,
|
||||
]));
|
||||
|
||||
try {
|
||||
$this->serverRepository->setServer($server)->revokeUserJTI($subuser->user_id);
|
||||
} catch (DaemonConnectionException $exception) {
|
||||
// Don't block this request if we can't connect to the daemon instance.
|
||||
logger()->warning($exception, ['user_id' => $subuser->user_id, 'server_id' => $server->id]);
|
||||
|
||||
$instance->property('revoked', false);
|
||||
}
|
||||
});
|
||||
$this->deletionService->handle($subuser, $server);
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
@ -20,6 +20,11 @@ class ServerSubject
|
||||
public function handle(Request $request, \Closure $next): Response
|
||||
{
|
||||
$server = $request->route()->parameter('server');
|
||||
|
||||
if ($request->route()->hasParameter('tenant')) {
|
||||
$server = Server::find($request->route()->parameter('tenant'));
|
||||
}
|
||||
|
||||
if ($server instanceof Server) {
|
||||
LogTarget::setActor($request->user());
|
||||
LogTarget::setSubject($server);
|
||||
|
@ -105,7 +105,7 @@ class Allocation extends Model
|
||||
protected function address(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => "$this->ip:$this->port",
|
||||
get: fn () => "$this->alias:$this->port",
|
||||
);
|
||||
}
|
||||
|
||||
|
177
app/Models/File.php
Normal file
177
app/Models/File.php
Normal file
@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Repositories\Daemon\DaemonFileRepository;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Sushi\Sushi;
|
||||
|
||||
/**
|
||||
* @property string $name
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $modified_at
|
||||
* @property string $mode
|
||||
* @property int $mode_bits
|
||||
* @property int $size
|
||||
* @property bool $is_directory
|
||||
* @property bool $is_file
|
||||
* @property bool $is_symlink
|
||||
* @property string $mime_type
|
||||
*/
|
||||
class File extends Model
|
||||
{
|
||||
use Sushi;
|
||||
|
||||
protected $primaryKey = 'name';
|
||||
|
||||
public $incrementing = false;
|
||||
|
||||
protected $keyType = 'string';
|
||||
|
||||
public const ARCHIVE_MIMES = [
|
||||
'application/vnd.rar', // .rar
|
||||
'application/x-rar-compressed', // .rar (2)
|
||||
'application/x-tar', // .tar
|
||||
'application/x-br', // .tar.br
|
||||
'application/x-bzip2', // .tar.bz2, .bz2
|
||||
'application/gzip', // .tar.gz, .gz
|
||||
'application/x-gzip',
|
||||
'application/x-lzip', // .tar.lz4, .lz4 (not sure if this mime type is correct)
|
||||
'application/x-sz', // .tar.sz, .sz (not sure if this mime type is correct)
|
||||
'application/x-xz', // .tar.xz, .xz
|
||||
'application/x-7z-compressed', // .7z
|
||||
'application/zstd', // .tar.zst, .zst
|
||||
'application/zip', // .zip
|
||||
];
|
||||
|
||||
protected static Server $server;
|
||||
|
||||
protected static string $path;
|
||||
|
||||
protected static ?string $searchTerm;
|
||||
|
||||
public static function get(Server $server, string $path = '/', ?string $searchTerm = null): Builder
|
||||
{
|
||||
self::$server = $server;
|
||||
self::$path = $path;
|
||||
self::$searchTerm = $searchTerm;
|
||||
|
||||
return self::query();
|
||||
}
|
||||
|
||||
public function isArchive(): bool
|
||||
{
|
||||
return $this->is_file && in_array($this->mime_type, self::ARCHIVE_MIMES);
|
||||
}
|
||||
|
||||
public function isImage(): bool
|
||||
{
|
||||
return preg_match('/^image\/(?!svg\+xml)/', $this->mime_type);
|
||||
}
|
||||
|
||||
public function getIcon(): string
|
||||
{
|
||||
if ($this->is_directory) {
|
||||
return 'tabler-folder';
|
||||
}
|
||||
|
||||
if ($this->isArchive()) {
|
||||
return 'tabler-file-zip';
|
||||
}
|
||||
|
||||
if ($this->isImage()) {
|
||||
return 'tabler-photo';
|
||||
}
|
||||
|
||||
return $this->is_symlink ? 'tabler-file-symlink' : 'tabler-file';
|
||||
}
|
||||
|
||||
public function canEdit(): bool
|
||||
{
|
||||
if ($this->is_directory || $this->isArchive() || $this->is_symlink || $this->isImage()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->is_file && !in_array($this->mime_type, ['application/jar', 'application/octet-stream', 'inode/directory']);
|
||||
}
|
||||
|
||||
public function server(): Server
|
||||
{
|
||||
return self::$server;
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'created_at' => 'datetime',
|
||||
'modified_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function getSchema(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'string',
|
||||
'created_at' => 'string',
|
||||
'modified_at' => 'string',
|
||||
'mode' => 'string',
|
||||
'mode_bits' => 'integer',
|
||||
'size' => 'integer',
|
||||
'is_directory' => 'boolean',
|
||||
'is_file' => 'boolean',
|
||||
'is_symlink' => 'boolean',
|
||||
'mime_type' => 'string',
|
||||
];
|
||||
}
|
||||
|
||||
public function getRows(): array
|
||||
{
|
||||
try {
|
||||
/** @var DaemonFileRepository $fileRepository */
|
||||
$fileRepository = app(DaemonFileRepository::class)->setServer(self::$server); // @phpstan-ignore-line
|
||||
|
||||
if (!is_null(self::$searchTerm)) {
|
||||
$contents = cache()->remember('file_search_' . self::$path . '_' . self::$searchTerm, now()->addMinute(), fn () => $fileRepository->search(self::$searchTerm, self::$path));
|
||||
} else {
|
||||
$contents = $fileRepository->getDirectory(self::$path ?? '/');
|
||||
}
|
||||
|
||||
if (isset($contents['error'])) {
|
||||
throw new Exception($contents['error']);
|
||||
}
|
||||
|
||||
return array_map(function ($file) {
|
||||
return [
|
||||
'name' => $file['name'],
|
||||
'created_at' => Carbon::parse($file['created']),
|
||||
'modified_at' => Carbon::parse($file['modified']),
|
||||
'mode' => $file['mode'],
|
||||
'mode_bits' => (int) $file['mode_bits'],
|
||||
'size' => (int) $file['size'],
|
||||
'is_directory' => $file['directory'],
|
||||
'is_file' => $file['file'],
|
||||
'is_symlink' => $file['symlink'],
|
||||
'mime_type' => $file['mime'],
|
||||
];
|
||||
}, $contents);
|
||||
} catch (Exception $exception) {
|
||||
report($exception);
|
||||
|
||||
Notification::make()
|
||||
->title('Error loading files')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
protected function sushiShouldCache(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
@ -93,7 +93,7 @@ class Permission extends Model
|
||||
|
||||
public const ACTION_SETTINGS_REINSTALL = 'settings.reinstall';
|
||||
|
||||
public const ACTION_ACTIVITY_READ = 'activity.read';
|
||||
public const ACTION_ACTIVITY_READ = 'settings.activity';
|
||||
|
||||
/**
|
||||
* Should timestamps be used on this model.
|
||||
|
@ -33,6 +33,9 @@ class Role extends BaseRole
|
||||
'view',
|
||||
'update',
|
||||
],
|
||||
'activity' => [
|
||||
'seeIps',
|
||||
],
|
||||
];
|
||||
|
||||
public function isRootAdmin(): bool
|
||||
|
@ -44,7 +44,7 @@ use App\Exceptions\Http\Server\ServerStateConflictException;
|
||||
* @property string $image
|
||||
* @property int|null $allocation_limit
|
||||
* @property int|null $database_limit
|
||||
* @property int $backup_limit
|
||||
* @property int|null $backup_limit
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property \Illuminate\Support\Carbon|null $installed_at
|
||||
@ -287,6 +287,14 @@ class Server extends Model
|
||||
return $this->hasMany(ServerVariable::class);
|
||||
}
|
||||
|
||||
public function viewableServerVariables(): HasMany
|
||||
{
|
||||
return $this->hasMany(ServerVariable::class)->rightJoin('egg_variables', function (JoinClause $join) {
|
||||
$join->on('egg_variables.id', 'server_variables.variable_id')
|
||||
->where('egg_variables.user_viewable', true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information for the node associated with this server.
|
||||
*/
|
||||
@ -358,6 +366,11 @@ class Server extends Model
|
||||
};
|
||||
}
|
||||
|
||||
public function isInConflictState(): bool
|
||||
{
|
||||
return $this->isSuspended() || $this->node->isUnderMaintenance() || !$this->isInstalled() || $this->status === ServerState::RestoringBackup || !is_null($this->transfer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the server is currently in a user-accessible state. If not, an
|
||||
* exception is raised. This should be called whenever something needs to make
|
||||
@ -367,13 +380,7 @@ class Server extends Model
|
||||
*/
|
||||
public function validateCurrentState(): void
|
||||
{
|
||||
if (
|
||||
$this->isSuspended() ||
|
||||
$this->node->isUnderMaintenance() ||
|
||||
!$this->isInstalled() ||
|
||||
$this->status === ServerState::RestoringBackup ||
|
||||
!is_null($this->transfer)
|
||||
) {
|
||||
if ($this->isInConflictState()) {
|
||||
throw new ServerStateConflictException($this);
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,10 @@ use DateTimeZone;
|
||||
use Filament\Models\Contracts\FilamentUser;
|
||||
use Filament\Models\Contracts\HasAvatar;
|
||||
use Filament\Models\Contracts\HasName;
|
||||
use Filament\Models\Contracts\HasTenants;
|
||||
use Filament\Panel;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules\In;
|
||||
use Illuminate\Auth\Authenticatable;
|
||||
@ -86,7 +89,7 @@ use Spatie\Permission\Traits\HasRoles;
|
||||
* @method static Builder|User whereUsername($value)
|
||||
* @method static Builder|User whereUuid($value)
|
||||
*/
|
||||
class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, FilamentUser, HasAvatar, HasName
|
||||
class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, FilamentUser, HasAvatar, HasName, HasTenants
|
||||
{
|
||||
use Authenticatable;
|
||||
use Authorizable {can as protected canned; }
|
||||
@ -323,6 +326,11 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
return $this->hasMany(Subuser::class);
|
||||
}
|
||||
|
||||
public function subServers(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Server::class, 'subusers');
|
||||
}
|
||||
|
||||
protected function checkPermission(Server $server, string $permission = ''): bool
|
||||
{
|
||||
if ($this->isRootAdmin() || $server->owner_id === $this->id) {
|
||||
@ -377,9 +385,13 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($panel->getId() === 'admin') {
|
||||
return $this->roles()->count() >= 1 && $this->getAllPermissions()->count() >= 1;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getFilamentName(): string
|
||||
{
|
||||
return $this->name_first ?: $this->username;
|
||||
@ -398,4 +410,24 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
|
||||
return $user instanceof User && !$user->isRootAdmin();
|
||||
}
|
||||
|
||||
public function getTenants(Panel $panel): array|Collection
|
||||
{
|
||||
return $this->accessibleServers()->get();
|
||||
}
|
||||
|
||||
public function canAccessTenant(IlluminateModel $tenant): bool
|
||||
{
|
||||
if ($tenant instanceof Server) {
|
||||
if ($this->isRootAdmin() || $tenant->owner_id === $this->id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$subuser = $tenant->subusers->where('user_id', $this->id)->first();
|
||||
|
||||
return $subuser !== null;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Filament\Server\Pages\Console;
|
||||
use App\Models;
|
||||
use App\Models\ApiKey;
|
||||
use App\Models\Node;
|
||||
@ -11,6 +12,8 @@ use Dedoc\Scramble\Support\Generator\OpenApi;
|
||||
use Dedoc\Scramble\Support\Generator\SecurityScheme;
|
||||
use Filament\Support\Colors\Color;
|
||||
use Filament\Support\Facades\FilamentColor;
|
||||
use Filament\Support\Facades\FilamentView;
|
||||
use Filament\View\PanelsRenderHook;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
@ -92,6 +95,12 @@ class AppServiceProvider extends ServiceProvider
|
||||
'warning' => Color::Amber,
|
||||
]);
|
||||
|
||||
FilamentView::registerRenderHook(
|
||||
PanelsRenderHook::CONTENT_START,
|
||||
fn () => view('filament.server-conflict-banner'),
|
||||
scopes: Console::class,
|
||||
);
|
||||
|
||||
Gate::before(function (User $user, $ability) {
|
||||
return $user->isRootAdmin() ? true : null;
|
||||
});
|
||||
|
64
app/Providers/Filament/AppPanelProvider.php
Normal file
64
app/Providers/Filament/AppPanelProvider.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers\Filament;
|
||||
|
||||
use App\Filament\Pages\Auth\Login;
|
||||
use App\Filament\Resources\UserResource\Pages\EditProfile;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Http\Middleware\Authenticate;
|
||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
||||
use Filament\Navigation\MenuItem;
|
||||
use Filament\Panel;
|
||||
use Filament\PanelProvider;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||
use Illuminate\Session\Middleware\AuthenticateSession;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||
|
||||
class AppPanelProvider extends PanelProvider
|
||||
{
|
||||
public function panel(Panel $panel): Panel
|
||||
{
|
||||
return $panel
|
||||
->id('app')
|
||||
->path('app')
|
||||
->spa()
|
||||
->breadcrumbs(false)
|
||||
->brandName(config('app.name', 'Pelican'))
|
||||
->brandLogo(config('app.logo'))
|
||||
->brandLogoHeight('2rem')
|
||||
->favicon(config('app.favicon', '/pelican.ico'))
|
||||
->topNavigation(config('panel.filament.top-navigation', true))
|
||||
->maxContentWidth(MaxWidth::ScreenTwoExtraLarge)
|
||||
->profile(EditProfile::class, false)
|
||||
->login(Login::class)
|
||||
->userMenuItems([
|
||||
MenuItem::make()
|
||||
->label('Admin')
|
||||
->url('/admin')
|
||||
->icon('tabler-arrow-forward')
|
||||
->sort(5)
|
||||
->visible(fn (): bool => auth()->user()->canAccessPanel(Filament::getPanel('admin'))),
|
||||
])
|
||||
->discoverResources(in: app_path('Filament/App/Resources'), for: 'App\\Filament\\App\\Resources')
|
||||
->middleware([
|
||||
EncryptCookies::class,
|
||||
AddQueuedCookiesToResponse::class,
|
||||
StartSession::class,
|
||||
AuthenticateSession::class,
|
||||
ShareErrorsFromSession::class,
|
||||
VerifyCsrfToken::class,
|
||||
SubstituteBindings::class,
|
||||
DisableBladeIconComponents::class,
|
||||
DispatchServingFilamentEvent::class,
|
||||
])
|
||||
->authMiddleware([
|
||||
Authenticate::class,
|
||||
]);
|
||||
}
|
||||
}
|
76
app/Providers/Filament/ServerPanelProvider.php
Normal file
76
app/Providers/Filament/ServerPanelProvider.php
Normal file
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers\Filament;
|
||||
|
||||
use App\Filament\App\Resources\ServerResource\Pages\ListServers;
|
||||
use App\Filament\Pages\Auth\Login;
|
||||
use App\Filament\Resources\UserResource\Pages\EditProfile;
|
||||
use App\Http\Middleware\Activity\ServerSubject;
|
||||
use App\Models\Server;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Http\Middleware\Authenticate;
|
||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
||||
use Filament\Navigation\MenuItem;
|
||||
use Filament\Panel;
|
||||
use Filament\PanelProvider;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||
use Illuminate\Session\Middleware\AuthenticateSession;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||
|
||||
class ServerPanelProvider extends PanelProvider
|
||||
{
|
||||
public function panel(Panel $panel): Panel
|
||||
{
|
||||
return $panel
|
||||
->id('server')
|
||||
->path('app/server')
|
||||
->homeUrl('/app')
|
||||
->spa()
|
||||
->tenant(Server::class)
|
||||
->brandName(config('app.name', 'Pelican'))
|
||||
->brandLogo(config('app.logo'))
|
||||
->brandLogoHeight('2rem')
|
||||
->favicon(config('app.favicon', '/pelican.ico'))
|
||||
->topNavigation(config('panel.filament.top-navigation', true))
|
||||
->maxContentWidth(MaxWidth::ScreenTwoExtraLarge)
|
||||
->login(Login::class)
|
||||
->userMenuItems([
|
||||
'profile' => MenuItem::make()->label('Profile')->url(fn () => EditProfile::getUrl(panel: 'app')),
|
||||
MenuItem::make()
|
||||
->label('Server List')
|
||||
->icon('tabler-brand-docker')
|
||||
->url(fn () => ListServers::getUrl(panel: 'app'))
|
||||
->sort(6),
|
||||
MenuItem::make()
|
||||
->label('Admin')
|
||||
->icon('tabler-arrow-forward')
|
||||
->url(fn () => Filament::getPanel('admin')->getUrl())
|
||||
->sort(5)
|
||||
->visible(fn (): bool => auth()->user()->canAccessPanel(Filament::getPanel('admin'))),
|
||||
])
|
||||
->discoverResources(in: app_path('Filament/Server/Resources'), for: 'App\\Filament\\Server\\Resources')
|
||||
->discoverPages(in: app_path('Filament/Server/Pages'), for: 'App\\Filament\\Server\\Pages')
|
||||
->discoverWidgets(in: app_path('Filament/Server/Widgets'), for: 'App\\Filament\\Server\\Widgets')
|
||||
->middleware([
|
||||
EncryptCookies::class,
|
||||
AddQueuedCookiesToResponse::class,
|
||||
StartSession::class,
|
||||
AuthenticateSession::class,
|
||||
ShareErrorsFromSession::class,
|
||||
VerifyCsrfToken::class,
|
||||
SubstituteBindings::class,
|
||||
DisableBladeIconComponents::class,
|
||||
DispatchServingFilamentEvent::class,
|
||||
ServerSubject::class,
|
||||
])
|
||||
->authMiddleware([
|
||||
Authenticate::class,
|
||||
]);
|
||||
}
|
||||
}
|
@ -273,4 +273,30 @@ class DaemonFileRepository extends DaemonRepository
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches all files in the directory (and its subdirectories) for the given search term.
|
||||
*
|
||||
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function search(string $searchTerm, ?string $directory): array
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
$response = $this->getHttpClient()
|
||||
->timeout(120)
|
||||
->get(
|
||||
sprintf('/api/servers/%s/files/search', $this->server->uuid),
|
||||
[
|
||||
'pattern' => $searchTerm,
|
||||
'directory' => $directory ?? '/',
|
||||
]
|
||||
);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
|
||||
return $response->json();
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ use App\Models\ActivityLog;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Request;
|
||||
use App\Models\ActivityLogSubject;
|
||||
use App\Models\Server;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Illuminate\Contracts\Auth\Factory as AuthFactory;
|
||||
|
||||
@ -205,6 +207,10 @@ class ActivityLogService
|
||||
|
||||
if ($subject = $this->targetable->subject()) {
|
||||
$this->subject($subject);
|
||||
} elseif ($tenant = Filament::getTenant()) {
|
||||
if ($tenant instanceof Server) {
|
||||
$this->subject($tenant);
|
||||
}
|
||||
}
|
||||
|
||||
if ($actor = $this->targetable->actor()) {
|
||||
|
@ -44,8 +44,6 @@ class SubuserCreationService
|
||||
$user = $this->userCreationService->handle([
|
||||
'email' => $email,
|
||||
'username' => $username,
|
||||
'name_first' => 'Server',
|
||||
'name_last' => 'Subuser',
|
||||
'root_admin' => false,
|
||||
]);
|
||||
}
|
||||
|
43
app/Services/Subusers/SubuserDeletionService.php
Normal file
43
app/Services/Subusers/SubuserDeletionService.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Subusers;
|
||||
|
||||
use App\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
use App\Facades\Activity;
|
||||
use App\Models\Server;
|
||||
use App\Models\Subuser;
|
||||
use App\Notifications\RemovedFromServer;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
|
||||
class SubuserDeletionService
|
||||
{
|
||||
public function __construct(
|
||||
private DaemonServerRepository $serverRepository,
|
||||
) {}
|
||||
|
||||
public function handle(Subuser $subuser, Server $server): void
|
||||
{
|
||||
$log = Activity::event('server:subuser.delete')
|
||||
->subject($subuser->user)
|
||||
->property('email', $subuser->user->email)
|
||||
->property('revoked', true);
|
||||
|
||||
$log->transaction(function ($instance) use ($server, $subuser) {
|
||||
$subuser->delete();
|
||||
|
||||
$subuser->user->notify(new RemovedFromServer([
|
||||
'user' => $subuser->user->name_first,
|
||||
'name' => $subuser->server->name,
|
||||
]));
|
||||
|
||||
try {
|
||||
$this->serverRepository->setServer($server)->revokeUserJTI($subuser->user_id);
|
||||
} catch (DaemonConnectionException $exception) {
|
||||
// Don't block this request if we can't connect to the daemon instance.
|
||||
logger()->warning($exception, ['user_id' => $subuser->user_id, 'server_id' => $server->id]);
|
||||
|
||||
$instance->property('revoked', false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
53
app/Services/Subusers/SubuserUpdateService.php
Normal file
53
app/Services/Subusers/SubuserUpdateService.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Subusers;
|
||||
|
||||
use App\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
use App\Facades\Activity;
|
||||
use App\Models\Server;
|
||||
use App\Models\Subuser;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
|
||||
class SubuserUpdateService
|
||||
{
|
||||
public function __construct(
|
||||
private DaemonServerRepository $serverRepository,
|
||||
) {}
|
||||
|
||||
public function handle(Subuser $subuser, Server $server, array $permissions): void
|
||||
{
|
||||
$current = $subuser->permissions;
|
||||
|
||||
sort($permissions);
|
||||
sort($current);
|
||||
|
||||
$log = Activity::event('server:subuser.update')
|
||||
->subject($subuser->user)
|
||||
->property([
|
||||
'email' => $subuser->user->email,
|
||||
'old' => $current,
|
||||
'new' => $permissions,
|
||||
'revoked' => true,
|
||||
]);
|
||||
|
||||
// Only update the database and hit up the daemon instance to invalidate JTI's if the permissions
|
||||
// have actually changed for the user.
|
||||
if ($permissions !== $current) {
|
||||
$log->transaction(function ($instance) use ($subuser, $permissions, $server) {
|
||||
$subuser->update(['permissions' => $permissions]);
|
||||
|
||||
try {
|
||||
$this->serverRepository->setServer($server)->revokeUserJTI($subuser->user_id);
|
||||
} catch (DaemonConnectionException $exception) {
|
||||
// Don't block this request if we can't connect to the daemon instance. Chances are it is
|
||||
// offline and the token will be invalid once daemon boots back.
|
||||
logger()->warning($exception, ['user_id' => $subuser->user_id, 'server_id' => $server->id]);
|
||||
|
||||
$instance->property('revoked', false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$log->reset();
|
||||
}
|
||||
}
|
15
app/Tables/Columns/BytesColumn.php
Normal file
15
app/Tables/Columns/BytesColumn.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tables\Columns;
|
||||
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
|
||||
class BytesColumn extends TextColumn
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->formatStateUsing(fn ($state) => $state ? convert_bytes_to_readable($state) : '');
|
||||
}
|
||||
}
|
@ -10,6 +10,19 @@ class DateTimeColumn extends TextColumn
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->dateTime(timezone: auth()->user()?->timezone ?? config('app.timezone', 'UTC'));
|
||||
$this->dateTime();
|
||||
}
|
||||
|
||||
public function since(?string $timezone = null): static
|
||||
{
|
||||
$this->formatStateUsing(fn ($state) => $state->diffForHumans());
|
||||
$this->tooltip(fn ($state) => $state?->timezone($this->getTimezone()));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTimezone(): string
|
||||
{
|
||||
return auth()->user()?->timezone ?? config('app.timezone', 'UTC');
|
||||
}
|
||||
}
|
||||
|
10
app/Tables/Columns/ServerEntryColumn.php
Normal file
10
app/Tables/Columns/ServerEntryColumn.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tables\Columns;
|
||||
|
||||
use Filament\Tables\Columns\Column;
|
||||
|
||||
class ServerEntryColumn extends Column
|
||||
{
|
||||
protected string $view = 'tables.columns.server-entry-column';
|
||||
}
|
@ -17,3 +17,54 @@ if (!function_exists('is_ip')) {
|
||||
return $address !== null && filter_var($address, FILTER_VALIDATE_IP) !== false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('convert_bytes_to_readable')) {
|
||||
function convert_bytes_to_readable(int $bytes, int $decimals = 2): string
|
||||
{
|
||||
$conversionUnit = config('panel.use_binary_prefix') ? 1024 : 1000;
|
||||
$suffix = config('panel.use_binary_prefix') ? ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB'] : ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
|
||||
if ($bytes <= 0) {
|
||||
return '0 ' . $suffix[0];
|
||||
}
|
||||
|
||||
$base = log($bytes) / log($conversionUnit);
|
||||
$f_base = floor($base);
|
||||
|
||||
return round(pow($conversionUnit, $base - $f_base), $decimals) . ' ' . $suffix[$f_base];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('join_paths')) {
|
||||
function join_paths(string $base, string ...$paths): string
|
||||
{
|
||||
if ($base === '/') {
|
||||
return str_replace('//', '', implode('/', $paths));
|
||||
}
|
||||
|
||||
return str_replace('//', '', $base . '/' . implode('/', $paths));
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('resolve_path')) {
|
||||
function resolve_path(string $path): string
|
||||
{
|
||||
// @phpstan-ignore-next-line
|
||||
$parts = array_filter(explode('/', $path), 'strlen');
|
||||
|
||||
$absolutes = [];
|
||||
foreach ($parts as $part) {
|
||||
if ($part == '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($part == '..') {
|
||||
array_pop($absolutes);
|
||||
} else {
|
||||
$absolutes[] = $part;
|
||||
}
|
||||
}
|
||||
|
||||
return implode('/', $absolutes);
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ return [
|
||||
App\Providers\BackupsServiceProvider::class,
|
||||
App\Providers\EventServiceProvider::class,
|
||||
App\Providers\Filament\AdminPanelProvider::class,
|
||||
App\Providers\Filament\AppPanelProvider::class,
|
||||
App\Providers\Filament\ServerPanelProvider::class,
|
||||
App\Providers\RouteServiceProvider::class,
|
||||
App\Providers\ViewComposerServiceProvider::class,
|
||||
SocialiteProviders\Manager\ServiceProvider::class,
|
||||
|
@ -8,8 +8,9 @@
|
||||
"ext-mbstring": "*",
|
||||
"ext-pdo": "*",
|
||||
"ext-zip": "*",
|
||||
"abdelhamiderrahmouni/filament-monaco-editor": "0.2.1",
|
||||
"abdelhamiderrahmouni/filament-monaco-editor": "^0.2.5",
|
||||
"aws/aws-sdk-php": "~3.288.1",
|
||||
"calebporzio/sushi": "^2.5",
|
||||
"chillerlan/php-qrcode": "^5.0.2",
|
||||
"coderflex/filament-turnstile": "^2.2",
|
||||
"dedoc/scramble": "^0.10.0",
|
||||
|
66
composer.lock
generated
66
composer.lock
generated
@ -8,16 +8,16 @@
|
||||
"packages": [
|
||||
{
|
||||
"name": "abdelhamiderrahmouni/filament-monaco-editor",
|
||||
"version": "v0.2.1",
|
||||
"version": "v0.2.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/abdelhamiderrahmouni/filament-monaco-editor.git",
|
||||
"reference": "3a74de11f74bee5b782df74c998c071c64a17ac0"
|
||||
"reference": "19ee073a593fe02865d8cfcad18db996f21b5fc5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/abdelhamiderrahmouni/filament-monaco-editor/zipball/3a74de11f74bee5b782df74c998c071c64a17ac0",
|
||||
"reference": "3a74de11f74bee5b782df74c998c071c64a17ac0",
|
||||
"url": "https://api.github.com/repos/abdelhamiderrahmouni/filament-monaco-editor/zipball/19ee073a593fe02865d8cfcad18db996f21b5fc5",
|
||||
"reference": "19ee073a593fe02865d8cfcad18db996f21b5fc5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -88,7 +88,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-04-20T01:01:49+00:00"
|
||||
"time": "2024-05-16T10:37:32+00:00"
|
||||
},
|
||||
{
|
||||
"name": "anourvalar/eloquent-serialize",
|
||||
@ -515,6 +515,60 @@
|
||||
],
|
||||
"time": "2023-11-29T23:19:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "calebporzio/sushi",
|
||||
"version": "v2.5.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/calebporzio/sushi.git",
|
||||
"reference": "01dd34fe3374f5fb7ce63756c0419385e31cd532"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/calebporzio/sushi/zipball/01dd34fe3374f5fb7ce63756c0419385e31cd532",
|
||||
"reference": "01dd34fe3374f5fb7ce63756c0419385e31cd532",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-pdo_sqlite": "*",
|
||||
"ext-sqlite3": "*",
|
||||
"illuminate/database": "^5.8 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0",
|
||||
"illuminate/support": "^5.8 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0",
|
||||
"php": "^7.1.3|^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/dbal": "^2.9 || ^3.1.4",
|
||||
"orchestra/testbench": "3.8.* || 3.9.* || ^4.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0",
|
||||
"phpunit/phpunit": "^7.5 || ^8.4 || ^9.0 || ^10.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Sushi\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Caleb Porzio",
|
||||
"email": "calebporzio@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Eloquent's missing \"array\" driver.",
|
||||
"support": {
|
||||
"source": "https://github.com/calebporzio/sushi/tree/v2.5.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/calebporzio",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-04-24T15:23:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "carbonphp/carbon-doctrine-types",
|
||||
"version": "1.0.0",
|
||||
@ -13702,5 +13756,5 @@
|
||||
"ext-zip": "*"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.6.0"
|
||||
"plugin-api-version": "2.3.0"
|
||||
}
|
||||
|
@ -2,15 +2,15 @@
|
||||
|
||||
return [
|
||||
'general' => [
|
||||
'enable-preview' => true,
|
||||
'enable-preview' => false,
|
||||
'show-full-screen-toggle' => true,
|
||||
'show-placeholder' => true,
|
||||
'placeholder-text' => 'Your code here...',
|
||||
'show-loader' => true,
|
||||
'font-size' => '15px',
|
||||
'font-size' => '16px',
|
||||
'line-numbers-min-chars' => true,
|
||||
'automatic-layout' => true,
|
||||
'default-theme' => 'iPlastic',
|
||||
'default-theme' => 'blackboard',
|
||||
],
|
||||
'themes' => [
|
||||
'blackboard' => [
|
||||
|
7
config/livewire.php
Normal file
7
config/livewire.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'temporary_file_upload' => [
|
||||
'rules' => 'file',
|
||||
],
|
||||
];
|
@ -22,7 +22,7 @@ return [
|
||||
],
|
||||
'user' => [
|
||||
'account' => [
|
||||
'email-changed' => 'Changed email from :old to :new',
|
||||
'email-changed' => 'Changed email from <b>:old</b> to <b>:new</b>',
|
||||
'password-changed' => 'Changed password',
|
||||
],
|
||||
'api-key' => [
|
||||
@ -50,28 +50,28 @@ return [
|
||||
'kill' => 'Killed the server process',
|
||||
],
|
||||
'backup' => [
|
||||
'download' => 'Downloaded the :name backup',
|
||||
'delete' => 'Deleted the :name backup',
|
||||
'restore' => 'Restored the :name backup (deleted files: :truncate)',
|
||||
'restore-complete' => 'Completed restoration of the :name backup',
|
||||
'restore-failed' => 'Failed to complete restoration of the :name backup',
|
||||
'start' => 'Started a new backup :name',
|
||||
'complete' => 'Marked the :name backup as complete',
|
||||
'fail' => 'Marked the :name backup as failed',
|
||||
'lock' => 'Locked the :name backup',
|
||||
'unlock' => 'Unlocked the :name backup',
|
||||
'download' => 'Downloaded the <b>:name</b> backup',
|
||||
'delete' => 'Deleted the <b>:name</b> backup',
|
||||
'restore' => 'Restored the <b>:name</b> backup (deleted files: :truncate)',
|
||||
'restore-complete' => 'Completed restoration of the <b>:name</b> backup',
|
||||
'restore-failed' => 'Failed to complete restoration of the <b>:name</b> backup',
|
||||
'start' => 'Started a new backup <b>:name</b>',
|
||||
'complete' => 'Marked the <b>:name</b> backup as complete',
|
||||
'fail' => 'Marked the <b>:name</b> backup as failed',
|
||||
'lock' => 'Locked the <b>:name</b> backup',
|
||||
'unlock' => 'Unlocked the <b>:name</b> backup',
|
||||
],
|
||||
'database' => [
|
||||
'create' => 'Created new database :name',
|
||||
'rotate-password' => 'Password rotated for database :name',
|
||||
'delete' => 'Deleted database :name',
|
||||
'create' => 'Created new database <b>:name</b>',
|
||||
'rotate-password' => 'Password rotated for database <b>:name</b>',
|
||||
'delete' => 'Deleted database <b>:name</b>',
|
||||
],
|
||||
'file' => [
|
||||
'compress_one' => 'Compressed :directory:file',
|
||||
'compress_other' => 'Compressed :count files in :directory',
|
||||
'read' => 'Viewed the contents of :file',
|
||||
'copy' => 'Created a copy of :file',
|
||||
'create-directory' => 'Created directory :directory:name',
|
||||
'create-directory' => 'Created directory :directory<b>:name</b>',
|
||||
'decompress' => 'Decompressed :files in :directory',
|
||||
'delete_one' => 'Deleted :directory:files.0',
|
||||
'delete_other' => 'Deleted :count files in :directory',
|
||||
@ -98,33 +98,33 @@ return [
|
||||
],
|
||||
'allocation' => [
|
||||
'create' => 'Added :allocation to the server',
|
||||
'notes' => 'Updated the notes for :allocation from ":old" to ":new"',
|
||||
'notes' => 'Updated the notes for :allocation from "<b>:old</b>" to "<b>:new</b>"',
|
||||
'primary' => 'Set :allocation as the primary server allocation',
|
||||
'delete' => 'Deleted the :allocation allocation',
|
||||
],
|
||||
'schedule' => [
|
||||
'create' => 'Created the :name schedule',
|
||||
'update' => 'Updated the :name schedule',
|
||||
'execute' => 'Manually executed the :name schedule',
|
||||
'delete' => 'Deleted the :name schedule',
|
||||
'create' => 'Created the <b>:name</b> schedule',
|
||||
'update' => 'Updated the <b>:name</b> schedule',
|
||||
'execute' => 'Manually executed the <b>:name</b> schedule',
|
||||
'delete' => 'Deleted the <b>:name</b> schedule',
|
||||
],
|
||||
'task' => [
|
||||
'create' => 'Created a new ":action" task for the :name schedule',
|
||||
'update' => 'Updated the ":action" task for the :name schedule',
|
||||
'delete' => 'Deleted a task for the :name schedule',
|
||||
'create' => 'Created a new "<b>:action</b>" task for the <b>:name</b> schedule',
|
||||
'update' => 'Updated the "<b>:action</b>" task for the <b>:name</b> schedule',
|
||||
'delete' => 'Deleted a task for the <b>:name</b> schedule',
|
||||
],
|
||||
'settings' => [
|
||||
'rename' => 'Renamed the server from :old to :new',
|
||||
'description' => 'Changed the server description from :old to :new',
|
||||
'rename' => 'Renamed the server from <b>:old</b> to <b>:new</b>',
|
||||
'description' => 'Changed the server description from <b>:old</b> to <b>:new</b>',
|
||||
],
|
||||
'startup' => [
|
||||
'edit' => 'Changed the :variable variable from ":old" to ":new"',
|
||||
'image' => 'Updated the Docker Image for the server from :old to :new',
|
||||
'edit' => 'Changed the <b>:variable</b> variable from "<b>:old</b>" to "<b>:new</b>"',
|
||||
'image' => 'Updated the Docker Image for the server from <b>:old</b> to <b>:new</b>',
|
||||
],
|
||||
'subuser' => [
|
||||
'create' => 'Added :email as a subuser',
|
||||
'update' => 'Updated the subuser permissions for :email',
|
||||
'delete' => 'Removed :email as a subuser',
|
||||
'create' => 'Added <b>:email</b> as a subuser',
|
||||
'update' => 'Updated the subuser permissions for <b>:email</b>',
|
||||
'delete' => 'Removed <b>:email</b> as a subuser',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
@ -2,26 +2,44 @@
|
||||
|
||||
return [
|
||||
'permissions' => [
|
||||
'websocket_*' => 'Allows access to the websocket for this server.',
|
||||
'control_console' => 'Allows the user to send data to the server console.',
|
||||
'control_start' => 'Allows the user to start the server instance.',
|
||||
'control_stop' => 'Allows the user to stop the server instance.',
|
||||
'control_restart' => 'Allows the user to restart the server instance.',
|
||||
'control_kill' => 'Allows the user to kill the server instance.',
|
||||
'user_create' => 'Allows the user to create new user accounts for the server.',
|
||||
'user_read' => 'Allows the user permission to view users associated with this server.',
|
||||
'user_update' => 'Allows the user to modify other users associated with this server.',
|
||||
'user_delete' => 'Allows the user to delete other users associated with this server.',
|
||||
'file_create' => 'Allows the user permission to create new files and directories.',
|
||||
'file_read' => 'Allows the user to see files and folders associated with this server instance, as well as view their contents.',
|
||||
'file_update' => 'Allows the user to update files and folders associated with the server.',
|
||||
'file_delete' => 'Allows the user to delete files and directories.',
|
||||
'file_archive' => 'Allows the user to create file archives and decompress existing archives.',
|
||||
'file_sftp' => 'Allows the user to perform the above file actions using a SFTP client.',
|
||||
'allocation_read' => 'Allows access to the server allocation management pages.',
|
||||
'allocation_update' => 'Allows user permission to make modifications to the server\'s allocations.',
|
||||
'database_create' => 'Allows user permission to create a new database for the server.',
|
||||
'database_read' => 'Allows user permission to view the server databases.',
|
||||
'startup_desc' => 'Permissions that control a user\'s ability to view this server\'s startup parameters.',
|
||||
'settings_desc' => 'Permissions that control a user\'s access to the schedule management for this server.',
|
||||
'control_desc' => 'Permissions that control a user\'s ability to control the power state of a server, or send commands.',
|
||||
'user_desc' => 'Permissions that allow a user to manage other subusers on a server. They will never be able to edit their own account, or assign permissions they do not have themselves.',
|
||||
'file_desc' => 'Permissions that control a user\'s ability to modify the filesystem for this server.',
|
||||
'allocation_desc' => 'Permissions that control a user\'s ability to modify the port allocations for this server.',
|
||||
'database_desc' => 'Permissions that control a user\'s access to the database management for this server.',
|
||||
'backup_desc' => 'Permissions that control a user\'s ability to generate and manage server backups.',
|
||||
'schedule_desc' => 'Permissions that control a user\'s access to the schedule management for this server.',
|
||||
'startup_read' => 'Allows a user to view the startup variables for a server.',
|
||||
'startup_update' => 'Allows a user to modify the startup variables for the server.',
|
||||
'startup_docker_image' => 'Allows a user to modify the Docker image used when running the server.',
|
||||
'setting_reinstall' => 'Allows a user to trigger a reinstall of this server.',
|
||||
'setting_rename' => 'Allows a user to rename this server and change the description of it.',
|
||||
'setting_activity' => 'Allows a user to view the activity logs for the server.',
|
||||
'websocket_*' => 'Allows a user access to the websocket for this server.',
|
||||
'control_console' => 'Allows a user to send data to the server console.',
|
||||
'control_start' => 'Allows a user to start the server instance.',
|
||||
'control_stop' => 'Allows a user to stop the server instance.',
|
||||
'control_restart' => 'Allows a user to restart the server instance.',
|
||||
'control_kill' => 'Allows a user to kill the server instance.',
|
||||
'user_create' => 'Allows a user to create new user accounts for the server.',
|
||||
'user_read' => 'Allows a user permission to view users associated with this server.',
|
||||
'user_update' => 'Allows a user to modify other users associated with this server.',
|
||||
'user_delete' => 'Allows a user to delete other users associated with this server.',
|
||||
'file_create' => 'Allows a user permission to create new files and directories.',
|
||||
'file_read' => 'Allows a user to view the contents of a directory, but not view the contents of or download files.',
|
||||
'file_read_content' => 'Allows a user to view the contents of a given file. This will also allow the user to download files.',
|
||||
'file_update' => 'Allows a user to update files and folders associated with the server.',
|
||||
'file_delete' => 'Allows a user to delete files and directories.',
|
||||
'file_archive' => 'Allows a user to create file archives and decompress existing archives.',
|
||||
'file_sftp' => 'Allows a user to perform the above file actions using a SFTP client.',
|
||||
'allocation_read' => 'Allows a user to view all allocations currently assigned to this server. Users with any level of access to this server can always view the primary allocation.',
|
||||
'allocation_update' => 'Allows a user to change the primary server allocation and attach notes to each allocation.',
|
||||
'allocation_delete' => 'Allows a user to delete an allocation from the server.',
|
||||
'allocation_create' => 'Allows a user to assign additional allocations to the server.',
|
||||
'database_create' => 'Allows a user permission to create a new database for the server.',
|
||||
'database_read' => 'Allows a user permission to view the server databases.',
|
||||
'database_update' => 'Allows a user permission to make modifications to a database. If the user does not have the "View Password" permission as well they will not be able to modify the password.',
|
||||
'database_delete' => 'Allows a user permission to delete a database instance.',
|
||||
'database_view_password' => 'Allows a user permission to view a database password in the system.',
|
||||
@ -29,5 +47,10 @@ return [
|
||||
'schedule_read' => 'Allows a user permission to view schedules for a server.',
|
||||
'schedule_update' => 'Allows a user permission to make modifications to an existing server schedule.',
|
||||
'schedule_delete' => 'Allows a user to delete a schedule for the server.',
|
||||
'backup_create' => 'Allows a user to create new backups for this server.',
|
||||
'backup_read' => 'Allows a user to view all backups that exist for this server.',
|
||||
'backup_delete' => 'Allows a user to remove backups from the system.',
|
||||
'backup_download' => 'Allows a user to download a backup for the server. Danger: this allows a user to access all files for the server in the backup.',
|
||||
'backup_restore' => 'Allows a user to restore a backup for the server. Danger: this allows the user to delete all of the server files in the process.',
|
||||
],
|
||||
];
|
||||
|
File diff suppressed because one or more lines are too long
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Link, NavLink } from 'react-router-dom';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCogs, faLayerGroup, faSignOutAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faCogs, faHandSparkles, faLayerGroup, faSignOutAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { useStoreState } from 'easy-peasy';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ApplicationStore } from '@/state';
|
||||
@ -64,6 +64,11 @@ export default () => {
|
||||
</div>
|
||||
<RightNavigation className={'flex h-full items-center justify-center'}>
|
||||
<SearchContainer />
|
||||
<Tooltip placement={'bottom'} content={'New Client Area'}>
|
||||
<NavLink to={'/app/'} target={'_blank'} rel={'noreferrer'}>
|
||||
<FontAwesomeIcon icon={faHandSparkles} />
|
||||
</NavLink>
|
||||
</Tooltip>
|
||||
<Tooltip placement={'bottom'} content={t<string>('dashboard')}>
|
||||
<NavLink to={'/'} exact>
|
||||
<FontAwesomeIcon icon={faLayerGroup} />
|
||||
|
134
resources/views/filament/components/server-console.blade.php
Normal file
134
resources/views/filament/components/server-console.blade.php
Normal file
@ -0,0 +1,134 @@
|
||||
<x-filament::widget>
|
||||
@assets
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/xterm/5.5.0/xterm.js" integrity="sha512-Gujw5GajF5is3nMoGv9X+tCMqePLL/60qvAv1LofUZTV9jK8ENbM9L+maGmOsNzuZaiuyc/fpph1KT9uR5w3CQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/xterm/5.5.0/xterm.css" integrity="sha512-AbNrj/oSHJaILgcdnkYm+DQ08SqVbZ8jlkJbFyyS1WDcAaXAcAfxJnCH69el7oVgTwVwyA5u5T+RdFyUykrV3Q==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
@endassets
|
||||
|
||||
<div id="terminal" wire:ignore></div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
class="w-full bg-transparent"
|
||||
type="text"
|
||||
placeholder="Type a command..."
|
||||
wire:model="input"
|
||||
wire:keydown.enter="enter"
|
||||
wire:keydown.up.prevent="up"
|
||||
wire:keydown.down="down"
|
||||
>
|
||||
</div>
|
||||
|
||||
@script
|
||||
<script>
|
||||
let options = {
|
||||
fontSize: 18,
|
||||
// fontFamily: th('fontFamily.mono'),
|
||||
disableStdin: true,
|
||||
cursorStyle: 'underline',
|
||||
allowTransparency: true,
|
||||
rows: 35,
|
||||
cols: 110,
|
||||
// theme: theme,
|
||||
};
|
||||
|
||||
const terminal = new Terminal(options);
|
||||
// TODO: load addons
|
||||
terminal.open(document.getElementById('terminal'));
|
||||
|
||||
const TERMINAL_PRELUDE = '\u001b[1m\u001b[33mpelican@' + '{{ \Filament\Facades\Filament::getTenant()->name }}' + ' ~ \u001b[0m';
|
||||
|
||||
const handleConsoleOutput = (line, prelude = false) =>
|
||||
terminal.writeln((prelude ? TERMINAL_PRELUDE : '') + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m');
|
||||
|
||||
const handleTransferStatus = (status) => {
|
||||
switch (status) {
|
||||
// Sent by either the source or target node if a failure occurs.
|
||||
case 'failure':
|
||||
terminal.writeln(TERMINAL_PRELUDE + 'Transfer has failed.\u001b[0m');
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDaemonErrorOutput = (line) =>
|
||||
terminal.writeln(
|
||||
TERMINAL_PRELUDE + '\u001b[1m\u001b[41m' + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m'
|
||||
);
|
||||
|
||||
const handlePowerChangeEvent = (state) =>
|
||||
terminal.writeln(TERMINAL_PRELUDE + 'Server marked as ' + state + '...\u001b[0m');
|
||||
|
||||
@php
|
||||
if ($user->cannot(\App\Models\Permission::ACTION_WEBSOCKET_CONNECT, $server)) {
|
||||
throw new \App\Exceptions\Http\HttpForbiddenException('You do not have permission to connect to this server\'s websocket.');
|
||||
}
|
||||
|
||||
$permissions = app(\App\Services\Servers\GetUserPermissionsService::class)->handle($server, $user);
|
||||
|
||||
$socket = str_replace(['https://', 'http://'], ['wss://', 'ws://'], $server->node->getConnectionAddress());
|
||||
$socket .= sprintf('/api/servers/%s/ws', $server->uuid);
|
||||
|
||||
$token = app(\App\Services\Nodes\NodeJWTService::class)
|
||||
->setExpiresAt(now()->addMinutes(10)->toImmutable())
|
||||
->setUser($user)
|
||||
->setClaims([
|
||||
'server_uuid' => $server->uuid,
|
||||
'permissions' => $permissions,
|
||||
])
|
||||
->handle($server->node, $user->id . $server->uuid);
|
||||
@endphp
|
||||
|
||||
const socket = new WebSocket("{{ $socket }}");
|
||||
const token = '{{ $token->toString() }}';
|
||||
|
||||
socket.onmessage = function(websocketMessageEvent) {
|
||||
let eventData = JSON.parse(websocketMessageEvent.data);
|
||||
|
||||
if (eventData.event === 'console output' || eventData.event === 'install output') {
|
||||
handleConsoleOutput(eventData.args[0]);
|
||||
}
|
||||
|
||||
if (eventData.event === 'status') {
|
||||
handlePowerChangeEvent(eventData.args[0]);
|
||||
}
|
||||
|
||||
if (eventData.event === 'daemon error') {
|
||||
handleDaemonErrorOutput(eventData.args[0]);
|
||||
}
|
||||
|
||||
if (eventData.event === 'stats') {
|
||||
$wire.dispatchSelf('storeStats', { data: eventData.args[0] });
|
||||
}
|
||||
|
||||
if (eventData.event === 'auth success') {
|
||||
socket.send(JSON.stringify({
|
||||
'event': 'send logs',
|
||||
'args': [null]
|
||||
}));
|
||||
}
|
||||
|
||||
// TODO: handle "token expiring" and "token expired"
|
||||
};
|
||||
|
||||
socket.onopen = (event) => {
|
||||
socket.send(JSON.stringify({
|
||||
'event': 'auth',
|
||||
'args': [token]
|
||||
}));
|
||||
};
|
||||
|
||||
Livewire.on('setServerState', ({ state }) => {
|
||||
socket.send(JSON.stringify({
|
||||
'event': 'set state',
|
||||
'args': [state]
|
||||
}));
|
||||
});
|
||||
|
||||
Livewire.on('sendServerCommand', ({ command }) => {
|
||||
socket.send(JSON.stringify({
|
||||
'event': 'send command',
|
||||
'args': [command]
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
@endscript
|
||||
</x-filament::widget>
|
@ -14,6 +14,14 @@
|
||||
automaticLayout: {{ (int) $getAutomaticLayout() }},
|
||||
monacoId: $id('monaco-editor'),
|
||||
|
||||
toggleFullScreenMode() {
|
||||
this.fullScreenModeEnabled = !this.fullScreenModeEnabled;
|
||||
this.fullScreenModeEnabled ? document.body.classList.add('overflow-hidden')
|
||||
: document.body.classList.remove('overflow-hidden');
|
||||
$el.style.width = this.fullScreenModeEnabled ? '100vw'
|
||||
: $el.parentElement.clientWidth + 'px';
|
||||
},
|
||||
|
||||
monacoEditor(editor){
|
||||
editor.onDidChangeModelContent((e) => {
|
||||
this.monacoContent = editor.getValue();
|
||||
@ -45,11 +53,21 @@
|
||||
|
||||
monacoEditorAddLoaderScriptToHead() {
|
||||
script = document.createElement('script');
|
||||
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.39.0/min/vs/loader.min.js';
|
||||
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.49.0/min/vs/loader.min.js';
|
||||
document.head.appendChild(script);
|
||||
},
|
||||
|
||||
wrapPreview(value){
|
||||
return `<head>{{ $getPreviewHeadEndContent() }}</head>` +
|
||||
`<body {{ $getPreviewBodyAttributes() }}>` +
|
||||
`{{ $getPreviewBodyStartContent() }}` +
|
||||
`${value}` +
|
||||
`{{ $getPreviewBodyEndContent() }}` +
|
||||
`</body>`;
|
||||
},
|
||||
|
||||
}" x-init="
|
||||
previewContent = wrapPreview(monacoContent);
|
||||
$el.style.height = '500px';
|
||||
$watch('fullScreenModeEnabled', value => {
|
||||
if (value) {
|
||||
@ -67,8 +85,8 @@
|
||||
if(typeof _amdLoaderGlobal !== 'undefined'){
|
||||
|
||||
// Based on https://jsfiddle.net/developit/bwgkr6uq/ which works without needing service worker. Provided by loader.min.js.
|
||||
require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.39.0/min/vs' }});
|
||||
let proxy = URL.createObjectURL(new Blob([` self.MonacoEnvironment = { baseUrl: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.39.0/min' }; importScripts('https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.39.0/min/vs/base/worker/workerMain.min.js');`], { type: 'text/javascript' }));
|
||||
require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.49.0/min/vs' }});
|
||||
let proxy = URL.createObjectURL(new Blob([` self.MonacoEnvironment = { baseUrl: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.49.0/min' }; importScripts('https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.49.0/min/vs/base/worker/workerMain.min.js');`], { type: 'text/javascript' }));
|
||||
window.MonacoEnvironment = { getWorkerUrl: () => proxy };
|
||||
|
||||
require(['vs/editor/editor.main'], () => {
|
||||
@ -83,7 +101,9 @@
|
||||
language: monacoLanguage,
|
||||
scrollbar: {
|
||||
horizontal: 'auto',
|
||||
horizontalScrollbarSize: 15
|
||||
horizontalScrollbarSize: 15,
|
||||
vertical: 'auto',
|
||||
verticalScrollbarSize: 15
|
||||
},
|
||||
|
||||
});
|
||||
@ -100,15 +120,21 @@
|
||||
}, 5); " :id="monacoId"
|
||||
class="fme-wrapper"
|
||||
:class="{ 'fme-full-screen': fullScreenModeEnabled }" x-cloak>
|
||||
<div class="h-full w-full">
|
||||
<div class="fme-container" style="padding-top: 0">
|
||||
<div class="flex items-center ml-auto">
|
||||
@if($getShowFullScreenToggle())
|
||||
<button type="button" aria-label="{{ __("full_screen_btn_label") }}" class="fme-full-screen-btn" @click="toggleFullScreenMode()">
|
||||
<svg class="fme-full-screen-btn-icon" x-show="!fullScreenModeEnabled" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M16 4l4 0l0 4" /><path d="M14 10l6 -6" /><path d="M8 20l-4 0l0 -4" /><path d="M4 20l6 -6" /><path d="M16 20l4 0l0 -4" /><path d="M14 14l6 6" /><path d="M8 4l-4 0l0 4" /><path d="M4 4l6 6" /></svg>
|
||||
<svg class="fme-full-screen-btn-icon" x-show="fullScreenModeEnabled" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 9l4 0l0 -4" /><path d="M3 3l6 6" /><path d="M5 15l4 0l0 4" /><path d="M3 21l6 -6" /><path d="M19 9l-4 0l0 -4" /><path d="M15 9l6 -6" /><path d="M19 15l-4 0l0 4" /><path d="M15 15l6 6" /></svg>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
<div class="fme-container" x-show="!showPreview">
|
||||
<!-- Editor -->
|
||||
<div x-show="!monacoLoader" class="fme-element-wrapper">
|
||||
<div x-ref="monacoEditorElement" class="fme-element"></div>
|
||||
<div x-ref="monacoEditorElement" class="fme-element" wire:ignore style="height: 100%"></div>
|
||||
<div x-ref="monacoPlaceholderElement" x-show="monacoPlaceholder" @click="monacoEditorFocus()" :style="'font-size: ' + monacoFontSize" class="fme-placeholder" x-text="monacoPlaceholderText"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</x-dynamic-component>
|
||||
|
19
resources/views/filament/server-conflict-banner.blade.php
Normal file
19
resources/views/filament/server-conflict-banner.blade.php
Normal file
@ -0,0 +1,19 @@
|
||||
@php
|
||||
$shouldShow = false;
|
||||
|
||||
try {
|
||||
\Filament\Facades\Filament::getTenant()->validateCurrentState();
|
||||
} catch (\App\Exceptions\Http\Server\ServerStateConflictException $exception) {
|
||||
$shouldShow = true;
|
||||
$message = $exception->getMessage();
|
||||
}
|
||||
@endphp
|
||||
|
||||
@if ($shouldShow)
|
||||
<div class="mt-2 p-2 rounded-lg text-white" style="background-color: #D97706;">
|
||||
<div class="flex items-center">
|
||||
<x-filament::icon icon="tabler-alert-triangle" class="h-6 w-6 mr-2 text-gray-500 dark:text-gray-400 text-white" />
|
||||
<p>{!! $message !!}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
7
resources/views/filament/server/pages/console.blade.php
Normal file
7
resources/views/filament/server/pages/console.blade.php
Normal file
@ -0,0 +1,7 @@
|
||||
<x-filament-panels::page class="fi-console-page">
|
||||
<x-filament-widgets::widgets
|
||||
:columns="$this->getColumns()"
|
||||
:data="$this->getWidgetData()"
|
||||
:widgets="$this->getVisibleWidgets()"
|
||||
/>
|
||||
</x-filament-panels::page>
|
11
resources/views/filament/server/pages/edit-file.blade.php
Normal file
11
resources/views/filament/server/pages/edit-file.blade.php
Normal file
@ -0,0 +1,11 @@
|
||||
<x-filament-panels::page class="fi-resource-edit-file-page">
|
||||
<x-filament-panels::form
|
||||
id="form"
|
||||
:wire:key="$this->getId() . '.forms.' . $this->getFormStatePath()"
|
||||
wire:submit="save"
|
||||
>
|
||||
{{ $this->form }}
|
||||
</x-filament-panels::form>
|
||||
|
||||
<x-filament-panels::page.unsaved-data-changes-alert />
|
||||
</x-filament-panels::page>
|
@ -0,0 +1,9 @@
|
||||
<x-filament-panels::page>
|
||||
<x-filament-panels::form
|
||||
id="form"
|
||||
:wire:key="$this->getId() . '.forms.' . $this->getFormStatePath()"
|
||||
wire:submit="save"
|
||||
>
|
||||
{{ $this->form }}
|
||||
</x-filament-panels::form>
|
||||
</x-filament-panels::page>
|
37
resources/views/tables/columns/server-entry-column.blade.php
Normal file
37
resources/views/tables/columns/server-entry-column.blade.php
Normal file
@ -0,0 +1,37 @@
|
||||
<div class="w-full grid gap-y-2">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<x-filament::icon-button
|
||||
:icon="$getRecord()->conditionIcon()"
|
||||
:color="$getRecord()->conditionColor()"
|
||||
:tooltip="\Illuminate\Support\Str::title($getRecord()->condition)" size="xl"
|
||||
/>
|
||||
|
||||
<span class="text-2xl font-semibold text-gray-500 dark:text-gray-400">
|
||||
{{ $getRecord()->name }} ({{ $this->uptime($getRecord()) }})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<div class="flex-1 text-sm font-medium text-gray-850 dark:text-white">
|
||||
<strong>Allocation:</strong> {{ $getRecord()->allocation->address }}
|
||||
</div>
|
||||
<div class="flex-1 text-sm font-medium text-gray-850 dark:text-white">
|
||||
<strong>Egg:</strong> {{ $getRecord()->egg->name }}
|
||||
</div>
|
||||
<div class="flex-1 text-sm font-medium text-gray-850 dark:text-white">
|
||||
<strong>Owner:</strong> {{ $getRecord()->user->username }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<div class="flex-1 text-sm font-medium text-gray-850 dark:text-white">
|
||||
<strong>CPU:</strong> {{ $this->cpu($getRecord()) }}
|
||||
</div>
|
||||
<div class="flex-1 text-sm font-medium text-gray-850 dark:text-white">
|
||||
<strong>RAM:</strong> {{ $this->memory($getRecord()) }}
|
||||
</div>
|
||||
<div class="flex-1 text-sm font-medium text-gray-850 dark:text-white">
|
||||
<strong>Disk:</strong> {{ $this->disk($getRecord()) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
Loading…
x
Reference in New Issue
Block a user