Merge branch 'next' into fix-#2546-deletion-issues
This commit is contained in:
@@ -19,9 +19,6 @@
|
||||
<div class="box-description">
|
||||
Network: {{ data_get($resource, 'destination.network') }}
|
||||
</div>
|
||||
@if ($resource->server_status == false)
|
||||
<div class="text-xs font-bold text-error"> This server has connection problems. </div>
|
||||
@endif
|
||||
</div>
|
||||
@if ($resource?->additional_networks?->count() > 0)
|
||||
<div class="flex gap-2">
|
||||
|
||||
@@ -2,17 +2,19 @@
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>Environment Variables</h2>
|
||||
<x-modal-input buttonTitle="+ Add" title="New Environment Variable">
|
||||
<livewire:project.shared.environment-variable.add />
|
||||
</x-modal-input>
|
||||
<div class="flex flex-col items-center">
|
||||
<x-modal-input buttonTitle="+ Add" title="New Environment Variable">
|
||||
<livewire:project.shared.environment-variable.add />
|
||||
</x-modal-input>
|
||||
</div>
|
||||
<x-forms.button
|
||||
wire:click='switch'>{{ $view === 'normal' ? 'Developer view' : 'Normal view' }}</x-forms.button>
|
||||
wire:click='switch'>{{ $view === 'normal' ? 'Developer view' : 'Normal view (required to set variables at build time)' }}</x-forms.button>
|
||||
</div>
|
||||
<div>Environment variables (secrets) for this resource.</div>
|
||||
<div>Environment variables (secrets) for this resource. </div>
|
||||
@if ($this->resourceClass === 'App\Models\Application' && data_get($this->resource, 'build_pack') !== 'dockercompose')
|
||||
<div class="w-64 pt-2">
|
||||
<x-forms.checkbox id="resource.settings.is_env_sorting_enabled" label="Sort alphabetically"
|
||||
helper="Turn this off if one environment is dependent on an other. It will be sorted by creation order."
|
||||
helper="Turn this off if one environment is dependent on an other. It will be sorted by creation order (like you pasted them or in the order you created them)."
|
||||
instantSave></x-forms.checkbox>
|
||||
</div>
|
||||
@endif
|
||||
@@ -31,6 +33,10 @@
|
||||
@endif
|
||||
</div>
|
||||
@if ($view === 'normal')
|
||||
<div>
|
||||
<h3>Production Environment Variables</h3>
|
||||
<div>Environment (secrets) variables for Production.</div>
|
||||
</div>
|
||||
@forelse ($resource->environment_variables as $env)
|
||||
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
|
||||
:env="$env" :type="$resource->type()" />
|
||||
@@ -39,7 +45,7 @@
|
||||
@endforelse
|
||||
@if ($resource->type() === 'application' && $resource->environment_variables_preview->count() > 0 && $showPreview)
|
||||
<div>
|
||||
<h3>Preview Deployments</h3>
|
||||
<h3>Preview Deployments Environment Variables</h3>
|
||||
<div>Environment (secrets) variables for Preview Deployments.</div>
|
||||
</div>
|
||||
@foreach ($resource->environment_variables_preview as $env)
|
||||
@@ -48,16 +54,15 @@
|
||||
@endforeach
|
||||
@endif
|
||||
@else
|
||||
<form wire:submit='saveVariables(false)' class="flex flex-col gap-2">
|
||||
<x-forms.textarea rows="10" class="whitespace-pre-wrap" id="variables"></x-forms.textarea>
|
||||
<x-forms.button type="submit" class="btn btn-primary">Save</x-forms.button>
|
||||
<form wire:submit.prevent='submit' class="flex flex-col gap-2">
|
||||
<x-forms.textarea rows="10" class="whitespace-pre-wrap" id="variables" wire:model="variables" label="Production Environment Variables"></x-forms.textarea>
|
||||
|
||||
@if ($showPreview)
|
||||
<x-forms.textarea rows="10" class="whitespace-pre-wrap" label="Preview Deployments Environment Variables"
|
||||
id="variablesPreview" wire:model="variablesPreview"></x-forms.textarea>
|
||||
@endif
|
||||
|
||||
<x-forms.button type="submit" class="btn btn-primary">Save All Environment Variables</x-forms.button>
|
||||
</form>
|
||||
@if ($showPreview)
|
||||
<form wire:submit='saveVariables(true)' class="flex flex-col gap-2">
|
||||
<x-forms.textarea rows="10" class="whitespace-pre-wrap" label="Preview Environment Variables"
|
||||
id="variablesPreview"></x-forms.textarea>
|
||||
<x-forms.button type="submit" class="btn btn-primary">Save</x-forms.button>
|
||||
</form>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,15 +1,6 @@
|
||||
<div>
|
||||
<form wire:submit='submit'
|
||||
class="flex flex-col items-center gap-4 p-4 bg-white border lg:items-start dark:bg-base dark:border-coolgray-300">
|
||||
{{-- @if (!$env->isFoundInCompose && !$isSharedVariable)
|
||||
<div class="flex items-center justify-center gap-2 dark:text-warning text-coollabs"> <svg
|
||||
class="hidden w-4 h-4 dark:text-warning lg:block" viewBox="0 0 256 256"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor"
|
||||
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16">
|
||||
</path>
|
||||
</svg>This variable is not found in the compose file, so it won't be used.</div>
|
||||
@endif --}}
|
||||
@if ($isLocked)
|
||||
<div class="flex flex-1 w-full gap-2">
|
||||
<x-forms.input disabled id="env.key" />
|
||||
|
||||
@@ -4,57 +4,39 @@
|
||||
</x-slot>
|
||||
<livewire:project.shared.configuration-checker :resource="$resource" />
|
||||
@if ($type === 'application')
|
||||
<h1>Execute Command</h1>
|
||||
<h1>Terminal</h1>
|
||||
<livewire:project.application.heading :application="$resource" />
|
||||
<h2 class="pt-4">Command</h2>
|
||||
<div class="pb-2">Run any one-shot command inside a container.</div>
|
||||
@elseif ($type === 'database')
|
||||
<h1>Execute Command</h1>
|
||||
<h1>Terminal</h1>
|
||||
<livewire:project.database.heading :database="$resource" />
|
||||
<h2 class="pt-4">Command</h2>
|
||||
<div class="pb-2">Run any one-shot command inside a container.</div>
|
||||
@elseif ($type === 'service')
|
||||
<h2>Execute Command</h2>
|
||||
<livewire:project.service.navbar :service="$resource" :parameters="$parameters" title="Terminal" />
|
||||
@endif
|
||||
<div x-init="$wire.loadContainers">
|
||||
<div class="pt-4" wire:loading wire:target='loadContainers'>
|
||||
Loading containers...
|
||||
Loading resources...
|
||||
</div>
|
||||
<div wire:loading.remove wire:target='loadContainers'>
|
||||
@if (count($containers) > 0)
|
||||
<form class="flex flex-col gap-2 pt-4" wire:submit='runCommand'>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input placeholder="ls -l" autofocus id="command" label="Command" required />
|
||||
<x-forms.input id="workDir" label="Working directory" />
|
||||
</div>
|
||||
<form class="flex flex-col gap-2 justify-center pt-4 xl:items-end xl:flex-row"
|
||||
wire:submit="$dispatchSelf('connectToContainer')">
|
||||
<x-forms.select label="Container" id="container" required>
|
||||
<option disabled selected>Select container</option>
|
||||
@if (data_get($this->parameters, 'application_uuid'))
|
||||
@foreach ($containers as $container)
|
||||
<option value="{{ data_get($container, 'container.Names') }}">
|
||||
{{ data_get($container, 'container.Names') }}
|
||||
</option>
|
||||
@endforeach
|
||||
@elseif(data_get($this->parameters, 'service_uuid'))
|
||||
@foreach ($containers as $container)
|
||||
<option value="{{ $container }}">
|
||||
{{ $container }}
|
||||
</option>
|
||||
@endforeach
|
||||
@else
|
||||
<option value="{{ $container }}">
|
||||
{{ $container }}
|
||||
@foreach ($containers as $container)
|
||||
<option value="{{ data_get($container, 'container.Names') }}">
|
||||
{{ data_get($container, 'container.Names') }}
|
||||
({{ data_get($container, 'server.name') }})
|
||||
</option>
|
||||
@endif
|
||||
@endforeach
|
||||
</x-forms.select>
|
||||
<x-forms.button type="submit">Run</x-forms.button>
|
||||
<x-forms.button type="submit">Connect</x-forms.button>
|
||||
</form>
|
||||
@else
|
||||
<div class="pt-4">No containers are not running.</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full pt-10 mx-auto">
|
||||
<livewire:activity-monitor header="Command output" />
|
||||
<div class="mx-auto w-full">
|
||||
<livewire:project.shared.terminal />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
screen.scrollTop = 0;
|
||||
}
|
||||
}">
|
||||
<div class="flex items-center gap-2 ">
|
||||
<div class="flex gap-2 items-center">
|
||||
@if ($resource?->type() === 'application' || str($resource?->type())->startsWith('standalone'))
|
||||
<h4>{{ $container }}</h4>
|
||||
@else
|
||||
@@ -46,9 +46,9 @@
|
||||
<x-loading wire:poll.2000ms='getLogs(true)' />
|
||||
@endif
|
||||
</div>
|
||||
<form wire:submit='getLogs(true)' class="flex items-end gap-2 ">
|
||||
<form wire:submit='getLogs(true)' class="flex gap-2 items-end">
|
||||
<div class="w-96">
|
||||
<x-forms.input label="Only Show Number of Lines" placeholder="1000" required
|
||||
<x-forms.input label="Only Show Number of Lines" placeholder="1000" type="number" required
|
||||
id="numberOfLines"></x-forms.input>
|
||||
</div>
|
||||
<x-forms.button type="submit">Refresh</x-forms.button>
|
||||
@@ -56,7 +56,7 @@
|
||||
<x-forms.checkbox instantSave label="Include Timestamps" id="showTimeStamps"></x-forms.checkbox>
|
||||
</form>
|
||||
<div :class="fullscreen ? 'fullscreen' : 'relative w-full py-4 mx-auto'">
|
||||
<div class="flex flex-col-reverse w-full px-4 py-2 overflow-y-auto bg-white dark:text-white dark:bg-coolgray-100 scrollbar dark:border-coolgray-300"
|
||||
<div class="flex overflow-y-auto flex-col-reverse px-4 py-2 w-full bg-white dark:text-white dark:bg-coolgray-100 scrollbar dark:border-coolgray-300"
|
||||
:class="fullscreen ? '' : 'max-h-96 border border-solid rounded'">
|
||||
<button title="Minimize" x-show="fullscreen" class="fixed top-4 right-4"
|
||||
x-on:click="makeFullscreen"><svg class="icon" viewBox="0 0 24 24"
|
||||
@@ -76,7 +76,7 @@
|
||||
stroke-width="2" d="M12 5v14m4-4l-4 4m-4-4l4 4" />
|
||||
</svg></button>
|
||||
|
||||
<button title="Fullscreen" x-show="!fullscreen" class="absolute top-5 right-1"
|
||||
<button title="Fullscreen" x-show="!fullscreen" class="absolute right-1 top-5"
|
||||
x-on:click="makeFullscreen"><svg class="icon" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none">
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</x-forms.select>
|
||||
@else
|
||||
<x-forms.input placeholder="php" id="container"
|
||||
helper="You can leave it empty if your resource only have one container." label="Container name" />
|
||||
helper="You can leave this empty if your resource only has one container." label="Container name" />
|
||||
@endif
|
||||
@elseif ($type === 'service')
|
||||
<x-forms.select id="container" label="Container name">
|
||||
|
||||
@@ -1,32 +1,38 @@
|
||||
<div class="flex flex-col-reverse gap-2">
|
||||
<div class="flex flex-col gap-4">
|
||||
@forelse($executions as $execution)
|
||||
@if (data_get($execution, 'id') == $selectedKey)
|
||||
<div class="p-2">
|
||||
@if (data_get($execution, 'message'))
|
||||
<div>
|
||||
<pre>{{ data_get($execution, 'message') }}</pre>
|
||||
</div>
|
||||
@else
|
||||
<div>No output was recorded for this execution.</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
<a wire:click="selectTask({{ data_get($execution, 'id') }})" @class([
|
||||
'flex flex-col border-l transition-colors box-without-bg bg-coolgray-100 hover:bg-coolgray-200 cursor-pointer',
|
||||
'bg-coolgray-200 dark:text-white hover:bg-coolgray-200' =>
|
||||
'flex flex-col border-l-2 transition-colors p-4 cursor-pointer',
|
||||
'bg-white hover:bg-gray-100 dark:bg-coolgray-100 dark:hover:bg-coolgray-200',
|
||||
'text-black dark:text-white',
|
||||
'bg-gray-200 dark:bg-coolgray-200' =>
|
||||
data_get($execution, 'id') == $selectedKey,
|
||||
'border-green-500' => data_get($execution, 'status') === 'success',
|
||||
'border-red-500' => data_get($execution, 'status') === 'failed',
|
||||
'border-yellow-500' => data_get($execution, 'status') === 'running',
|
||||
])>
|
||||
@if (data_get($execution, 'status') === 'running')
|
||||
<div class="absolute top-2 right-2">
|
||||
<x-loading />
|
||||
</div>
|
||||
@endif
|
||||
<div>Status: {{ data_get($execution, 'status') }}</div>
|
||||
<div>Started At: {{ data_get($execution, 'created_at') }}</div>
|
||||
<div class="text-gray-700 dark:text-gray-300 font-semibold mb-1">Status: {{ data_get($execution, 'status') }}
|
||||
</div>
|
||||
<div class="text-gray-600 dark:text-gray-400 text-sm">
|
||||
Started At: {{ $this->formatDateInServerTimezone(data_get($execution, 'created_at', now())) }}
|
||||
</div>
|
||||
</a>
|
||||
@if (data_get($execution, 'id') == $selectedKey)
|
||||
<div class="p-4 mb-2 bg-gray-100 dark:bg-coolgray-200 rounded">
|
||||
@if (data_get($execution, 'message'))
|
||||
<div>
|
||||
<pre class="whitespace-pre-wrap">{{ data_get($execution, 'message') }}</pre>
|
||||
</div>
|
||||
@else
|
||||
<div>No output was recorded for this execution.</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@empty
|
||||
<div>No executions found.</div>
|
||||
<div class="p-4 bg-gray-100 dark:bg-coolgray-100 rounded">No executions found.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<div>
|
||||
<x-slot:title>
|
||||
{{ data_get_str($resource, 'name')->limit(10) }} > Scheduled Tasks | Coolify
|
||||
</x-slot>
|
||||
@if ($type === 'application')
|
||||
</x-slot>
|
||||
@if ($type === 'application')
|
||||
<h1>Scheduled Task</h1>
|
||||
<livewire:project.application.heading :application="$resource" />
|
||||
@elseif ($type === 'service')
|
||||
@elseif ($type === 'service')
|
||||
<livewire:project.service.navbar :service="$resource" :parameters="$parameters" />
|
||||
@endif
|
||||
@endif
|
||||
|
||||
<form wire:submit="submit" class="w-full">
|
||||
<div class="flex flex-col gap-2 pb-2">
|
||||
@@ -28,29 +28,26 @@
|
||||
:confirmWithPassword="false"
|
||||
step2ButtonText="Permanently Delete Scheduled Task"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-48">
|
||||
<x-forms.checkbox instantSave id="task.enabled" label="Enabled" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full gap-2">
|
||||
<x-forms.input placeholder="Name" id="task.name" label="Name" required />
|
||||
<x-forms.input placeholder="php artisan schedule:run" id="task.command" label="Command" required />
|
||||
<x-forms.input placeholder="0 0 * * * or daily" id="task.frequency" label="Frequency" required />
|
||||
@if ($type === 'application')
|
||||
<x-forms.input placeholder="php"
|
||||
helper="You can leave it empty if your resource only have one container." id="task.container"
|
||||
label="Container name" />
|
||||
@elseif ($type === 'service')
|
||||
<x-forms.input placeholder="php"
|
||||
helper="You can leave it empty if your resource only have one service in your stack. Otherwise use the stack name, without the random generated id. So if you have a mysql service in your stack, use mysql."
|
||||
id="task.container" label="Service name" />
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="pt-4">
|
||||
<h3 class="py-4">Recent executions <span class="text-xs text-neutral-500">(click to check output)</span></h3>
|
||||
<livewire:project.shared.scheduled-task.executions key="{{ $task->id }}" selectedKey="" :executions="$task->executions->take(-20)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full gap-2">
|
||||
<x-forms.input placeholder="Name" id="task.name" label="Name" required />
|
||||
<x-forms.input placeholder="php artisan schedule:run" id="task.command" label="Command" required />
|
||||
<x-forms.input placeholder="0 0 * * * or daily" id="task.frequency" label="Frequency" required />
|
||||
@if ($type === 'application')
|
||||
<x-forms.input placeholder="php"
|
||||
helper="You can leave this empty if your resource only has one container." id="task.container"
|
||||
label="Container name" />
|
||||
@elseif ($type === 'service')
|
||||
<x-forms.input placeholder="php"
|
||||
helper="You can leave this empty if your resource only has one service in your stack. Otherwise use the stack name, without the random generated ID. So if you have a mysql service in your stack, use mysql."
|
||||
id="task.container" label="Service name" />
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="pt-4">
|
||||
<h3 class="py-4">Recent executions <span class="text-xs text-neutral-500">(click to check output)</span></h3>
|
||||
<livewire:project.shared.scheduled-task.executions :task="$task" key="{{ $task->id }}" selectedKey="" :executions="$task->executions->take(20)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,43 +1,47 @@
|
||||
<div class="flex flex-col w-full gap-2 rounded">
|
||||
You can add Volumes, Files and Directories to your resources here.
|
||||
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submitPersistentVolume'>
|
||||
<h3>Volume Mount</h3>
|
||||
@if ($isSwarm)
|
||||
<h5>Swarm Mode detected: You need to set a shared volume (EFS/NFS/etc) on all the worker nodes if you would
|
||||
like to use a persistent volumes.</h5>
|
||||
@endif
|
||||
<x-forms.input placeholder="pv-name" id="name" label="Name" required helper="Volume name." />
|
||||
@if ($isSwarm)
|
||||
<x-forms.input placeholder="/root" id="host_path" label="Source Path" required
|
||||
<div class="flex flex-col w-full gap-2 rounded max-h-[80vh] overflow-y-auto scrollbar">
|
||||
<div class="p-4">
|
||||
You can add Volumes, Files and Directories to your resources here.
|
||||
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submitPersistentVolume'>
|
||||
<h3>Volume Mount</h3>
|
||||
@if ($isSwarm)
|
||||
<h5>Swarm Mode detected: You need to set a shared volume (EFS/NFS/etc) on all the worker nodes if you
|
||||
would
|
||||
like to use a persistent volumes.</h5>
|
||||
@endif
|
||||
<x-forms.input placeholder="pv-name" id="name" label="Name" required helper="Volume name." />
|
||||
@if ($isSwarm)
|
||||
<x-forms.input placeholder="/root" id="host_path" label="Source Path" required
|
||||
helper="Directory on the host system." />
|
||||
@else
|
||||
<x-forms.input placeholder="/root" id="host_path" label="Source Path"
|
||||
helper="Directory on the host system." />
|
||||
@endif
|
||||
<x-forms.input placeholder="/tmp/root" id="mount_path" label="Destination Path" required
|
||||
helper="Directory inside the container." />
|
||||
<x-forms.button type="submit" @click="modalOpen=false">
|
||||
Save
|
||||
</x-forms.button>
|
||||
|
||||
</form>
|
||||
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submitFileStorage'>
|
||||
<h3>File Mount</h3>
|
||||
<x-forms.input placeholder="/etc/nginx/nginx.conf" id="file_storage_path" label="Destination Path" required
|
||||
helper="File inside the container" />
|
||||
<x-forms.textarea label="Content" id="file_storage_content"></x-forms.textarea>
|
||||
<x-forms.button type="submit" @click="modalOpen=false">
|
||||
Save
|
||||
</x-forms.button>
|
||||
</form>
|
||||
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submitFileStorageDirectory'>
|
||||
<h3>Directory Mount</h3>
|
||||
<x-forms.input placeholder="{{ application_configuration_dir() }}/{{ $resource->uuid }}/etc/nginx"
|
||||
id="file_storage_directory_source" label="Source Directory" required
|
||||
helper="Directory on the host system." />
|
||||
@else
|
||||
<x-forms.input placeholder="/root" id="host_path" label="Source Path"
|
||||
helper="Directory on the host system." />
|
||||
@endif
|
||||
<x-forms.input placeholder="/tmp/root" id="mount_path" label="Destination Path" required
|
||||
helper="Directory inside the container." />
|
||||
<x-forms.button type="submit" @click="modalOpen=false">
|
||||
Save
|
||||
</x-forms.button>
|
||||
</form>
|
||||
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submitFileStorage'>
|
||||
<h3>File Mount</h3>
|
||||
<x-forms.input placeholder="/etc/nginx/nginx.conf" id="file_storage_path" label="Destination Path" required
|
||||
helper="File inside the container" />
|
||||
<x-forms.textarea label="Content" id="file_storage_content"></x-forms.textarea>
|
||||
<x-forms.button type="submit" @click="modalOpen=false">
|
||||
Save
|
||||
</x-forms.button>
|
||||
</form>
|
||||
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submitFileStorageDirectory'>
|
||||
<h3>Directory Mount</h3>
|
||||
<x-forms.input placeholder="{{ application_configuration_dir() }}/{{ $resource->uuid }}/etc/nginx"
|
||||
id="file_storage_directory_source" label="Source Directory" required
|
||||
helper="Directory on the host system." />
|
||||
<x-forms.input placeholder="/etc/nginx" id="file_storage_directory_destination" label="Destination Directory"
|
||||
required helper="Directory inside the container." />
|
||||
<x-forms.button type="submit" @click="modalOpen=false">
|
||||
Save
|
||||
</x-forms.button>
|
||||
</form>
|
||||
<x-forms.input placeholder="/etc/nginx" id="file_storage_directory_destination"
|
||||
label="Destination Directory" required helper="Directory inside the container." />
|
||||
<x-forms.button type="submit" @click="modalOpen=false">
|
||||
Save
|
||||
</x-forms.button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
240
resources/views/livewire/project/shared/terminal.blade.php
Normal file
240
resources/views/livewire/project/shared/terminal.blade.php
Normal file
@@ -0,0 +1,240 @@
|
||||
<div x-data="data()">
|
||||
{{-- <div x-show="!terminalActive" class="flex items-center justify-center w-full py-4 mx-auto h-[510px]">
|
||||
<div class="p-1 w-full h-full rounded border dark:bg-coolgray-100 dark:border-coolgray-300">
|
||||
<span class="font-mono text-sm text-gray-500" x-text="message"></span>
|
||||
</div>
|
||||
</div> --}}
|
||||
<div x-ref="terminalWrapper"
|
||||
:class="fullscreen ? 'fullscreen' : 'relative w-full h-full py-4 mx-auto max-h-[510px]'">
|
||||
<div id="terminal" wire:ignore></div>
|
||||
<button title="Minimize" x-show="fullscreen" class="fixed top-4 right-4 text-white" x-on:click="makeFullscreen"><svg
|
||||
class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M6 14h4m0 0v4m0-4l-6 6m14-10h-4m0 0V6m0 4l6-6" />
|
||||
</svg></button>
|
||||
<button title="Fullscreen" x-show="!fullscreen && terminalActive" class="absolute right-4 top-6 text-white"
|
||||
x-on:click="makeFullscreen"><svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none">
|
||||
<path
|
||||
d="M24 0v24H0V0h24ZM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018Zm.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022Zm-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01l-.184-.092Z" />
|
||||
<path fill="currentColor"
|
||||
d="M9.793 12.793a1 1 0 0 1 1.497 1.32l-.083.094L6.414 19H9a1 1 0 0 1 .117 1.993L9 21H4a1 1 0 0 1-.993-.883L3 20v-5a1 1 0 0 1 1.993-.117L5 15v2.586l4.793-4.793ZM20 3a1 1 0 0 1 .993.883L21 4v5a1 1 0 0 1-1.993.117L19 9V6.414l-4.793 4.793a1 1 0 0 1-1.497-1.32l.083-.094L17.586 5H15a1 1 0 0 1-.117-1.993L15 3h5Z" />
|
||||
</g>
|
||||
</svg></button>
|
||||
</div>
|
||||
|
||||
@script
|
||||
<script>
|
||||
const MAX_PENDING_WRITES = 5;
|
||||
let pendingWrites = 0;
|
||||
let paused = false;
|
||||
|
||||
let socket;
|
||||
let commandBuffer = '';
|
||||
|
||||
|
||||
function keepAlive() {
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(JSON.stringify({
|
||||
ping: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
const keepAliveInterval = setInterval(keepAlive, 30000);
|
||||
|
||||
// Clear the interval when the component is destroyed
|
||||
document.addEventListener('livewire:navigating', () => {
|
||||
clearInterval(keepAliveInterval);
|
||||
});
|
||||
|
||||
function initializeWebSocket() {
|
||||
if (!socket || socket.readyState === WebSocket.CLOSED) {
|
||||
const predefined = {
|
||||
protocol: "{{ env('TERMINAL_PROTOCOL') }}",
|
||||
host: "{{ env('TERMINAL_HOST') }}",
|
||||
port: "{{ env('TERMINAL_PORT') }}"
|
||||
}
|
||||
const connectionString = {
|
||||
protocol: window.location.protocol === 'https:' ? 'wss' : 'ws',
|
||||
host: window.location.hostname,
|
||||
port: ":6002",
|
||||
path: '/terminal/ws'
|
||||
}
|
||||
if (!window.location.port) {
|
||||
connectionString.port = ''
|
||||
}
|
||||
if (predefined.host) {
|
||||
connectionString.host = predefined.host
|
||||
}
|
||||
if (predefined.port) {
|
||||
connectionString.port = `:${predefined.port}`
|
||||
}
|
||||
if (predefined.protocol) {
|
||||
connectionString.protocol = predefined.protocol
|
||||
}
|
||||
|
||||
const url =
|
||||
`${connectionString.protocol}://${connectionString.host}${connectionString.port}${connectionString.path}`
|
||||
socket = new WebSocket(url);
|
||||
|
||||
socket.onmessage = handleSocketMessage;
|
||||
socket.onerror = (e) => {
|
||||
console.error('WebSocket error:', e);
|
||||
};
|
||||
socket.onclose = () => {
|
||||
console.log('WebSocket connection closed');
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function handleSocketMessage(event) {
|
||||
$data.message = '(connection closed)';
|
||||
// Initialize Terminal
|
||||
if (event.data === 'pty-ready') {
|
||||
term.open(document.getElementById('terminal'));
|
||||
$data.terminalActive = true;
|
||||
term.reset();
|
||||
term.focus();
|
||||
document.querySelector('.xterm-viewport').classList.add('scrollbar', 'rounded')
|
||||
$data.resizeTerminal()
|
||||
} else if (event.data === 'unprocessable') {
|
||||
term.reset();
|
||||
$data.terminalActive = false;
|
||||
$data.message = '(sorry, something went wrong, please try again)';
|
||||
} else {
|
||||
pendingWrites++;
|
||||
term.write(event.data, flowControlCallback);
|
||||
}
|
||||
}
|
||||
|
||||
function flowControlCallback() {
|
||||
pendingWrites--;
|
||||
if (pendingWrites > MAX_PENDING_WRITES && !paused) {
|
||||
paused = true;
|
||||
socket.send(JSON.stringify({
|
||||
pause: true
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (pendingWrites <= MAX_PENDING_WRITES && paused) {
|
||||
paused = false;
|
||||
socket.send(JSON.stringify({
|
||||
resume: true
|
||||
}));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
term.onData((data) => {
|
||||
socket.send(JSON.stringify({
|
||||
message: data
|
||||
}));
|
||||
// Type CTRL + D or exit in the terminal
|
||||
if (data === '\x04' || (data === '\r' && stripAnsiCommands(commandBuffer).trim().includes('exit'))) {
|
||||
checkIfProcessIsRunningAndKillIt();
|
||||
setTimeout(() => {
|
||||
$data.terminalActive = false;
|
||||
term.reset();
|
||||
}, 500);
|
||||
commandBuffer = '';
|
||||
} else if (data === '\r') {
|
||||
commandBuffer = '';
|
||||
} else {
|
||||
commandBuffer += data;
|
||||
}
|
||||
});
|
||||
|
||||
function stripAnsiCommands(input) {
|
||||
return input.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
||||
}
|
||||
|
||||
// Copy and paste
|
||||
// Enables ctrl + c and ctrl + v
|
||||
// defaults otherwise to ctrl + insert, shift + insert
|
||||
term.attachCustomKeyEventHandler((arg) => {
|
||||
if (arg.ctrlKey && arg.code === "KeyV" && arg.type === "keydown") {
|
||||
navigator.clipboard.readText()
|
||||
.then(text => {
|
||||
socket.send(JSON.stringify({
|
||||
message: text
|
||||
}));
|
||||
})
|
||||
};
|
||||
|
||||
if (arg.ctrlKey && arg.code === "KeyC" && arg.type === "keydown") {
|
||||
const selection = term.getSelection();
|
||||
if (selection) {
|
||||
navigator.clipboard.writeText(selection);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
$wire.on('send-back-command', function(command) {
|
||||
socket.send(JSON.stringify({
|
||||
command: command
|
||||
}));
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', function(e) {
|
||||
checkIfProcessIsRunningAndKillIt();
|
||||
});
|
||||
|
||||
function checkIfProcessIsRunningAndKillIt() {
|
||||
socket.send(JSON.stringify({
|
||||
checkActive: 'force'
|
||||
}));
|
||||
}
|
||||
|
||||
window.onresize = function() {
|
||||
$data.resizeTerminal()
|
||||
};
|
||||
|
||||
Alpine.data('data', () => ({
|
||||
fullscreen: false,
|
||||
terminalActive: false,
|
||||
message: '(connection closed)',
|
||||
init() {
|
||||
this.$watch('terminalActive', (value) => {
|
||||
this.$nextTick(() => {
|
||||
if (value) {
|
||||
$refs.terminalWrapper.style.display = 'block';
|
||||
this.resizeTerminal();
|
||||
} else {
|
||||
$refs.terminalWrapper.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
makeFullscreen() {
|
||||
this.fullscreen = !this.fullscreen;
|
||||
$nextTick(() => {
|
||||
this.resizeTerminal()
|
||||
})
|
||||
},
|
||||
|
||||
resizeTerminal() {
|
||||
if (!this.terminalActive) return;
|
||||
|
||||
fitAddon.fit();
|
||||
const height = $refs.terminalWrapper.clientHeight;
|
||||
const rows = height / term._core._renderService._charSizeService.height - 1;
|
||||
var termWidth = term.cols;
|
||||
var termHeight = parseInt(rows.toString(), 10);
|
||||
term.resize(termWidth, termHeight);
|
||||
socket.send(JSON.stringify({
|
||||
resize: {
|
||||
cols: termWidth,
|
||||
rows: termHeight
|
||||
}
|
||||
}));
|
||||
}
|
||||
}));
|
||||
|
||||
initializeWebSocket();
|
||||
</script>
|
||||
@endscript
|
||||
</div>
|
||||
Reference in New Issue
Block a user