mirror of
https://github.com/vitodeploy/vito.git
synced 2025-04-19 09:51:37 +00:00
add directory state to console (#416)
This commit is contained in:
parent
ba069a2db0
commit
f5c9d6701b
@ -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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
1
public/build/assets/theme-ab462327.css
Normal file
1
public/build/assets/theme-ab462327.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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"
|
||||
},
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user