add directory state to console (#416)

This commit is contained in:
Saeed Vaziry 2025-01-01 14:52:27 -08:00 committed by GitHub
parent ba069a2db0
commit f5c9d6701b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 149 additions and 50 deletions

View File

@ -4,14 +4,16 @@
use App\Models\Server;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Validation\Rule;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post;
#[Middleware('auth')]
class ConsoleController extends Controller
{
#[Post('/{server}/console', name: 'servers.console.run')]
#[Post('servers/{server}/console/run', name: 'servers.console.run')]
public function run(Server $server, Request $request)
{
$this->authorize('update', $server);
@ -24,15 +26,29 @@ public function run(Server $server, Request $request)
'command' => 'required|string',
]);
$ssh = $server->ssh($request->user);
$log = 'console-'.time();
$user = $request->input('user');
$currentDir = $user == 'root' ? '/root' : '/home/'.$user;
if (Cache::has('console.'.$server->id.'.dir')) {
$currentDir = Cache::get('console.'.$server->id.'.dir');
}
return response()->stream(
function () use ($server, $request) {
$ssh = $server->ssh($request->user);
$log = 'console-'.time();
$ssh->exec(command: $request->command, log: $log, stream: true, streamCallback: function ($output) {
echo $output;
function () use ($server, $request, $ssh, $log, $currentDir) {
$command = 'cd '.$currentDir.' && '.$request->command.' && echo "VITO_WORKING_DIR: $(pwd)"';
$output = '';
$ssh->exec(command: $command, log: $log, stream: true, streamCallback: function ($out) use (&$output) {
echo preg_replace('/^VITO_WORKING_DIR:.*(\r?\n)?/m', '', $out);
$output .= $out;
ob_flush();
flush();
});
// extract the working dir and put it in the session
if (preg_match('/VITO_WORKING_DIR: (.*)/', $output, $matches)) {
Cache::put('console.'.$server->id.'.dir', $matches[1]);
}
},
200,
[
@ -42,4 +58,12 @@ function () use ($server, $request) {
]
);
}
#[Get('servers/{server}/console/working-dir', name: 'servers.console.working-dir')]
public function workingDir(Server $server)
{
return response()->json([
'dir' => Cache::get('console.'.$server->id.'.dir'),
]);
}
}

View File

@ -3,6 +3,7 @@
namespace App\Web\Pages\Servers\Console;
use App\Web\Pages\Servers\Page;
use Filament\Actions\Action;
class Index extends Page
{
@ -27,4 +28,16 @@ public function getWidgets(): array
[Widgets\Console::class, ['server' => $this->server]],
];
}
protected function getHeaderActions(): array
{
return [
Action::make('read-the-docs')
->label('Read the Docs')
->icon('heroicon-o-document-text')
->color('gray')
->url('https://vitodeploy.com/servers/console.html')
->openUrlInNewTab(),
];
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"resources/css/filament/app/theme.css": {
"file": "assets/theme-d1a47f2d.css",
"file": "assets/theme-ab462327.css",
"isEntry": true,
"src": "resources/css/filament/app/theme.css"
},

View File

@ -1,5 +1,5 @@
<div
{{ $attributes->merge(["class" => "font-mono whitespace-pre relative h-[500px] w-full overflow-auto whitespace-pre-line rounded-xl border border-gray-200 bg-black p-5 text-gray-50 dark:border-gray-800"]) }}
{{ $attributes->merge(["class" => "font-mono relative h-[500px] w-full overflow-auto whitespace-pre-line rounded-xl border border-gray-200 bg-black p-5 text-gray-50 dark:border-gray-800"]) }}
>
{{ $slot }}
</div>

View File

@ -2,13 +2,48 @@
x-data="{
user: '{{ $server->ssh_user }}',
running: false,
dir: '{{ cache()->get("console.$server->id.dir", "~") }}',
command: '',
output: '',
serverName: '{{ $server->name }}',
shellPrefix: '',
clearAfterCommand: false,
runUrl: '{{ route("servers.console.run", ["server" => $server]) }}',
init() {
this.setShellPrefix()
$watch('user', async (value) => {
await this.getWorkingDir()
})
const consoleOutput = document.getElementById('console-output')
consoleOutput.addEventListener('mouseup', (event) => {
if (window.getSelection()?.toString()) {
return
}
this.focusCommand()
})
this.focusCommand()
document.addEventListener('keydown', (event) => {
if (event.ctrlKey && event.key === 'l') {
event.preventDefault()
if (this.running) return
this.output = ''
}
})
},
async run() {
if (! this.command) return
this.running = true
this.output = this.command + '\n'
let output = this.shellPrefix + ' ' + this.command + '\n'
if (this.clearAfterCommand) {
this.output = output
} else {
this.output += output
}
setTimeout(() => {
document.getElementById('console-output').scrollTop =
document.getElementById('console-output').scrollHeight
}, 100)
const fetchOptions = {
method: 'POST',
headers: {
@ -25,6 +60,7 @@
const response = await fetch(this.runUrl, fetchOptions)
const reader = response.body.getReader()
const decoder = new TextDecoder('utf-8')
this.setShellPrefix()
while (true) {
if (! this.running) {
@ -39,65 +75,91 @@
this.output += textChunk
document.getElementById('console-output').scrollTop =
document.getElementById('console-output').scrollHeight
setTimeout(() => {
document.getElementById('console-output').scrollTop =
document.getElementById('console-output').scrollHeight
}, 100)
}
this.output += '\nDone!'
this.output += '\n'
await this.getWorkingDir()
this.running = false
setTimeout(() => {
document.getElementById('command').focus()
}, 100)
},
stop() {
this.running = false
},
setShellPrefix() {
this.shellPrefix = `${this.user}@${this.serverName}:${this.dir}$`
},
focusCommand() {
document.getElementById('command').focus()
},
async getWorkingDir() {
const response = await fetch(
'{{ route("servers.console.working-dir", ["server" => $server]) }}',
)
if (response.ok) {
const data = await response.json()
this.dir = data.dir
this.setShellPrefix()
}
},
}"
>
<div>
<x-console-view id="console-output">
<div class="w-full" x-text="output"></div>
</x-console-view>
<form onsubmit="return false" id="console-form" class="mt-5 flex items-center justify-between">
<div class="grid w-full grid-cols-8 gap-2">
<x-filament::input.wrapper class="col-span-1 w-full">
<x-filament::input.select
id="user"
name="user"
x-model="user"
class="w-full"
x-bind:disabled="running"
>
<option value="root">root</option>
<option value="{{ $server->ssh_user }}">{{ $server->ssh_user }}</option>
</x-filament::input.select>
</x-filament::input.wrapper>
<x-filament::input.wrapper class="col-span-6 w-full">
<x-filament::input
id="command"
name="command"
x-model="command"
type="text"
placeholder="Type your command here..."
class="mx-1 flex-grow"
autocomplete="off"
/>
</x-filament::input.wrapper>
<div class="relative">
<form class="flex items-center justify-between">
<x-filament::input.wrapper>
<x-filament::input.select id="user" name="user" x-model="user" class="w-full" x-bind:disabled="running">
<option value="root">root</option>
<option value="{{ $server->ssh_user }}">{{ $server->ssh_user }}</option>
</x-filament::input.select>
</x-filament::input.wrapper>
<div class="flex items-center">
<x-filament::button
color="gray"
icon="heroicon-o-play"
type="submit"
id="btn-run"
x-on:click="run"
class="col-span-1"
type="button"
x-on:click="output = ''"
icon="heroicon-o-trash"
tooltip="Clear"
x-show="!running"
/>
>
Clear
</x-filament::button>
<x-filament::button
color="gray"
type="button"
id="btn-stop"
x-on:click="stop"
class="col-span-1"
x-show="running"
icon="heroicon-o-stop"
/>
tooltip="Stop"
class="ml-2"
>
Stop
</x-filament::button>
</div>
</form>
<x-console-view id="console-output" class="mt-5">
<div class="w-full" x-text="output"></div>
</x-console-view>
<div
class="relative -mt-5 flex h-[50px] w-full items-center rounded-b-xl border-b border-l border-r border-gray-200 bg-black px-5 font-mono text-gray-50 dark:border-gray-800"
>
<form class="flex w-full items-center" x-show="!running" onsubmit="return false" id="console-form">
<div x-text="shellPrefix"></div>
<input
type="text"
class="h-5 flex-grow border-0 bg-transparent p-0 px-1 outline-none ring-0 focus:outline-none focus:ring-0"
autofocus
id="command"
name="command"
x-model="command"
autocomplete="off"
/>
<button type="submit" id="btn-run" x-on:click="run" class="hidden"></button>
</form>
</div>
</div>
</div>