mirror of
https://github.com/vitodeploy/vito.git
synced 2025-04-19 18:01: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 App\Models\Server;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
use Spatie\RouteAttributes\Attributes\Get;
|
||||||
use Spatie\RouteAttributes\Attributes\Middleware;
|
use Spatie\RouteAttributes\Attributes\Middleware;
|
||||||
use Spatie\RouteAttributes\Attributes\Post;
|
use Spatie\RouteAttributes\Attributes\Post;
|
||||||
|
|
||||||
#[Middleware('auth')]
|
#[Middleware('auth')]
|
||||||
class ConsoleController extends Controller
|
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)
|
public function run(Server $server, Request $request)
|
||||||
{
|
{
|
||||||
$this->authorize('update', $server);
|
$this->authorize('update', $server);
|
||||||
@ -24,15 +26,29 @@ public function run(Server $server, Request $request)
|
|||||||
'command' => 'required|string',
|
'command' => 'required|string',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return response()->stream(
|
|
||||||
function () use ($server, $request) {
|
|
||||||
$ssh = $server->ssh($request->user);
|
$ssh = $server->ssh($request->user);
|
||||||
$log = 'console-'.time();
|
$log = 'console-'.time();
|
||||||
$ssh->exec(command: $request->command, log: $log, stream: true, streamCallback: function ($output) {
|
|
||||||
echo $output;
|
$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, $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();
|
ob_flush();
|
||||||
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,
|
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;
|
namespace App\Web\Pages\Servers\Console;
|
||||||
|
|
||||||
use App\Web\Pages\Servers\Page;
|
use App\Web\Pages\Servers\Page;
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
|
||||||
class Index extends Page
|
class Index extends Page
|
||||||
{
|
{
|
||||||
@ -27,4 +28,16 @@ public function getWidgets(): array
|
|||||||
[Widgets\Console::class, ['server' => $this->server]],
|
[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": {
|
"resources/css/filament/app/theme.css": {
|
||||||
"file": "assets/theme-d1a47f2d.css",
|
"file": "assets/theme-ab462327.css",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
"src": "resources/css/filament/app/theme.css"
|
"src": "resources/css/filament/app/theme.css"
|
||||||
},
|
},
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<div
|
<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 }}
|
{{ $slot }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,13 +2,48 @@
|
|||||||
x-data="{
|
x-data="{
|
||||||
user: '{{ $server->ssh_user }}',
|
user: '{{ $server->ssh_user }}',
|
||||||
running: false,
|
running: false,
|
||||||
|
dir: '{{ cache()->get("console.$server->id.dir", "~") }}',
|
||||||
command: '',
|
command: '',
|
||||||
output: '',
|
output: '',
|
||||||
|
serverName: '{{ $server->name }}',
|
||||||
|
shellPrefix: '',
|
||||||
|
clearAfterCommand: false,
|
||||||
runUrl: '{{ route("servers.console.run", ["server" => $server]) }}',
|
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() {
|
async run() {
|
||||||
if (! this.command) return
|
if (! this.command) return
|
||||||
this.running = true
|
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 = {
|
const fetchOptions = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -25,6 +60,7 @@
|
|||||||
const response = await fetch(this.runUrl, fetchOptions)
|
const response = await fetch(this.runUrl, fetchOptions)
|
||||||
const reader = response.body.getReader()
|
const reader = response.body.getReader()
|
||||||
const decoder = new TextDecoder('utf-8')
|
const decoder = new TextDecoder('utf-8')
|
||||||
|
this.setShellPrefix()
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (! this.running) {
|
if (! this.running) {
|
||||||
@ -39,65 +75,91 @@
|
|||||||
|
|
||||||
this.output += textChunk
|
this.output += textChunk
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
document.getElementById('console-output').scrollTop =
|
document.getElementById('console-output').scrollTop =
|
||||||
document.getElementById('console-output').scrollHeight
|
document.getElementById('console-output').scrollHeight
|
||||||
|
}, 100)
|
||||||
}
|
}
|
||||||
this.output += '\nDone!'
|
this.output += '\n'
|
||||||
|
await this.getWorkingDir()
|
||||||
this.running = false
|
this.running = false
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('command').focus()
|
||||||
|
}, 100)
|
||||||
},
|
},
|
||||||
stop() {
|
stop() {
|
||||||
this.running = false
|
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>
|
<div class="relative">
|
||||||
<x-console-view id="console-output">
|
<form class="flex items-center justify-between">
|
||||||
<div class="w-full" x-text="output"></div>
|
<x-filament::input.wrapper>
|
||||||
</x-console-view>
|
<x-filament::input.select id="user" name="user" x-model="user" class="w-full" x-bind:disabled="running">
|
||||||
<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="root">root</option>
|
||||||
<option value="{{ $server->ssh_user }}">{{ $server->ssh_user }}</option>
|
<option value="{{ $server->ssh_user }}">{{ $server->ssh_user }}</option>
|
||||||
</x-filament::input.select>
|
</x-filament::input.select>
|
||||||
</x-filament::input.wrapper>
|
</x-filament::input.wrapper>
|
||||||
<x-filament::input.wrapper class="col-span-6 w-full">
|
<div class="flex items-center">
|
||||||
<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>
|
|
||||||
<x-filament::button
|
<x-filament::button
|
||||||
color="gray"
|
color="gray"
|
||||||
icon="heroicon-o-play"
|
type="button"
|
||||||
type="submit"
|
x-on:click="output = ''"
|
||||||
id="btn-run"
|
icon="heroicon-o-trash"
|
||||||
x-on:click="run"
|
tooltip="Clear"
|
||||||
class="col-span-1"
|
|
||||||
x-show="!running"
|
x-show="!running"
|
||||||
/>
|
>
|
||||||
|
Clear
|
||||||
|
</x-filament::button>
|
||||||
<x-filament::button
|
<x-filament::button
|
||||||
color="gray"
|
color="gray"
|
||||||
type="button"
|
type="button"
|
||||||
id="btn-stop"
|
id="btn-stop"
|
||||||
x-on:click="stop"
|
x-on:click="stop"
|
||||||
class="col-span-1"
|
|
||||||
x-show="running"
|
x-show="running"
|
||||||
icon="heroicon-o-stop"
|
icon="heroicon-o-stop"
|
||||||
/>
|
tooltip="Stop"
|
||||||
|
class="ml-2"
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
</x-filament::button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user