mirror of
https://github.com/tiennm99/coolify.git
synced 2026-04-17 17:21:04 +00:00
feat(logs): Add loading indicator to download all logs buttons (#7847)
This commit is contained in:
@@ -121,6 +121,25 @@ class Show extends Component
|
|||||||
return sanitizeLogsForExport($logs);
|
return sanitizeLogsForExport($logs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function downloadAllLogs(): string
|
||||||
|
{
|
||||||
|
$logs = decode_remote_command_output($this->application_deployment_queue, includeAll: true)
|
||||||
|
->map(function ($line) {
|
||||||
|
$prefix = '';
|
||||||
|
if ($line['hidden']) {
|
||||||
|
$prefix = '[DEBUG] ';
|
||||||
|
}
|
||||||
|
if (isset($line['command']) && $line['command']) {
|
||||||
|
$prefix .= '[CMD]: ';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $line['timestamp'].' '.$prefix.trim($line['line']);
|
||||||
|
})
|
||||||
|
->join("\n");
|
||||||
|
|
||||||
|
return sanitizeLogsForExport($logs);
|
||||||
|
}
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
{
|
{
|
||||||
return view('livewire.project.application.deployment.show');
|
return view('livewire.project.application.deployment.show');
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ class GetLogs extends Component
|
|||||||
{
|
{
|
||||||
public const MAX_LOG_LINES = 50000;
|
public const MAX_LOG_LINES = 50000;
|
||||||
|
|
||||||
|
public const MAX_DOWNLOAD_SIZE_BYTES = 50 * 1024 * 1024; // 50MB
|
||||||
|
|
||||||
public string $outputs = '';
|
public string $outputs = '';
|
||||||
|
|
||||||
public string $errors = '';
|
public string $errors = '';
|
||||||
@@ -164,10 +166,12 @@ class GetLogs extends Component
|
|||||||
}
|
}
|
||||||
// Collect new logs into temporary variable first to prevent flickering
|
// Collect new logs into temporary variable first to prevent flickering
|
||||||
// (avoids clearing output before new data is ready)
|
// (avoids clearing output before new data is ready)
|
||||||
$newOutputs = '';
|
// Use array accumulation + implode for O(n) instead of O(n²) string concatenation
|
||||||
Process::run($sshCommand, function (string $type, string $output) use (&$newOutputs) {
|
$logChunks = [];
|
||||||
$newOutputs .= removeAnsiColors($output);
|
Process::timeout(config('constants.ssh.command_timeout'))->run($sshCommand, function (string $type, string $output) use (&$logChunks) {
|
||||||
|
$logChunks[] = removeAnsiColors($output);
|
||||||
});
|
});
|
||||||
|
$newOutputs = implode('', $logChunks);
|
||||||
|
|
||||||
if ($this->showTimeStamps) {
|
if ($this->showTimeStamps) {
|
||||||
$newOutputs = str($newOutputs)->split('/\n/')->sort(function ($a, $b) {
|
$newOutputs = str($newOutputs)->split('/\n/')->sort(function ($a, $b) {
|
||||||
@@ -215,11 +219,40 @@ class GetLogs extends Component
|
|||||||
|
|
||||||
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
|
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
|
||||||
|
|
||||||
$allLogs = '';
|
// Use array accumulation + implode for O(n) instead of O(n²) string concatenation
|
||||||
Process::run($sshCommand, function (string $type, string $output) use (&$allLogs) {
|
// Enforce 50MB size limit to prevent memory exhaustion from large logs
|
||||||
$allLogs .= removeAnsiColors($output);
|
$logChunks = [];
|
||||||
|
$accumulatedBytes = 0;
|
||||||
|
$truncated = false;
|
||||||
|
|
||||||
|
Process::timeout(config('constants.ssh.command_timeout'))->run($sshCommand, function (string $type, string $output) use (&$logChunks, &$accumulatedBytes, &$truncated) {
|
||||||
|
if ($truncated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$output = removeAnsiColors($output);
|
||||||
|
$outputBytes = strlen($output);
|
||||||
|
|
||||||
|
if ($accumulatedBytes + $outputBytes > self::MAX_DOWNLOAD_SIZE_BYTES) {
|
||||||
|
$remaining = self::MAX_DOWNLOAD_SIZE_BYTES - $accumulatedBytes;
|
||||||
|
if ($remaining > 0) {
|
||||||
|
$logChunks[] = substr($output, 0, $remaining);
|
||||||
|
}
|
||||||
|
$truncated = true;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$logChunks[] = $output;
|
||||||
|
$accumulatedBytes += $outputBytes;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$allLogs = implode('', $logChunks);
|
||||||
|
|
||||||
|
if ($truncated) {
|
||||||
|
$allLogs .= "\n\n[... Output truncated at 50MB limit ...]";
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->showTimeStamps) {
|
if ($this->showTimeStamps) {
|
||||||
$allLogs = str($allLogs)->split('/\n/')->sort(function ($a, $b) {
|
$allLogs = str($allLogs)->split('/\n/')->sort(function ($a, $b) {
|
||||||
$a = explode(' ', $a);
|
$a = explode(' ', $a);
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ function excludeCertainErrors(string $errorOutput, ?int $exitCode = null)
|
|||||||
throw new \RuntimeException($errorMessage, $exitCode);
|
throw new \RuntimeException($errorMessage, $exitCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null): Collection
|
function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null, bool $includeAll = false): Collection
|
||||||
{
|
{
|
||||||
if (is_null($application_deployment_queue)) {
|
if (is_null($application_deployment_queue)) {
|
||||||
return collect([]);
|
return collect([]);
|
||||||
@@ -216,7 +216,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
|
|||||||
|
|
||||||
$seenCommands = collect();
|
$seenCommands = collect();
|
||||||
$formatted = collect($decoded);
|
$formatted = collect($decoded);
|
||||||
if (! $is_debug_enabled) {
|
if (! $is_debug_enabled && ! $includeAll) {
|
||||||
$formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false);
|
$formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -173,7 +173,7 @@
|
|||||||
class="flex flex-col w-full bg-white dark:text-white dark:bg-coolgray-100 dark:border-coolgray-300"
|
class="flex flex-col w-full bg-white dark:text-white dark:bg-coolgray-100 dark:border-coolgray-300"
|
||||||
:class="fullscreen ? 'h-full' : 'border border-dotted rounded-sm'">
|
:class="fullscreen ? 'h-full' : 'border border-dotted rounded-sm'">
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between gap-2 px-4 py-2 border-b dark:border-coolgray-300 border-neutral-200 shrink-0">
|
class="flex flex-wrap items-center justify-between gap-2 px-4 py-2 border-b dark:border-coolgray-300 border-neutral-200 shrink-0">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
@if (data_get($application_deployment_queue, 'status') === 'in_progress')
|
@if (data_get($application_deployment_queue, 'status') === 'in_progress')
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
@@ -190,7 +190,7 @@
|
|||||||
<span x-show="searchQuery.trim()" x-text="matchCount + ' matches'"
|
<span x-show="searchQuery.trim()" x-text="matchCount + ' matches'"
|
||||||
class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"></span>
|
class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex flex-wrap items-center justify-end gap-2 flex-1">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<svg class="absolute left-2 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400"
|
<svg class="absolute left-2 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400"
|
||||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||||
@@ -208,8 +208,9 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div class="flex flex-wrap items-center gap-1">
|
||||||
x-on:click="
|
<button
|
||||||
|
x-on:click="
|
||||||
$wire.copyLogs().then(logs => {
|
$wire.copyLogs().then(logs => {
|
||||||
navigator.clipboard.writeText(logs);
|
navigator.clipboard.writeText(logs);
|
||||||
Livewire.dispatch('success', ['Logs copied to clipboard.']);
|
Livewire.dispatch('success', ['Logs copied to clipboard.']);
|
||||||
@@ -223,14 +224,61 @@
|
|||||||
d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" />
|
d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button x-on:click="downloadLogs()" title="Download Logs"
|
<div x-data="{ downloadMenuOpen: false, downloadingAllLogs: false }" class="relative">
|
||||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
<button x-on:click="downloadMenuOpen = !downloadMenuOpen" title="Download Logs"
|
||||||
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||||
stroke-width="1.5" stroke="currentColor">
|
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||||
<path stroke-linecap="round" stroke-linejoin="round"
|
stroke-width="1.5" stroke="currentColor">
|
||||||
d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
</svg>
|
d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||||
</button>
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div x-show="downloadMenuOpen" x-on:click.away="downloadMenuOpen = false"
|
||||||
|
x-transition:enter="transition ease-out duration-100"
|
||||||
|
x-transition:enter-start="transform opacity-0 scale-95"
|
||||||
|
x-transition:enter-end="transform opacity-100 scale-100"
|
||||||
|
x-transition:leave="transition ease-in duration-75"
|
||||||
|
x-transition:leave-start="transform opacity-100 scale-100"
|
||||||
|
x-transition:leave-end="transform opacity-0 scale-95"
|
||||||
|
class="absolute right-0 z-50 mt-2 w-max origin-top-right rounded-md bg-white dark:bg-coolgray-200 shadow-lg ring-1 ring-neutral-200 dark:ring-coolgray-300 focus:outline-none">
|
||||||
|
<div class="py-1">
|
||||||
|
<button x-on:click="downloadLogs(); downloadMenuOpen = false"
|
||||||
|
class="block w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-coolgray-300">
|
||||||
|
Download displayed logs
|
||||||
|
</button>
|
||||||
|
<button x-on:click="
|
||||||
|
downloadingAllLogs = true;
|
||||||
|
$wire.downloadAllLogs().then(logs => {
|
||||||
|
if (!logs) return;
|
||||||
|
const blob = new Blob([logs], { type: 'text/plain' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
const timestamp = new Date().toISOString().slice(0,19).replace(/[T:]/g, '-');
|
||||||
|
a.download = 'deployment-' + deploymentId + '-all-logs-' + timestamp + '.txt';
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
Livewire.dispatch('success', ['All logs downloaded.']);
|
||||||
|
}).finally(() => {
|
||||||
|
downloadingAllLogs = false;
|
||||||
|
downloadMenuOpen = false;
|
||||||
|
});
|
||||||
|
"
|
||||||
|
:disabled="downloadingAllLogs"
|
||||||
|
:class="{ 'opacity-50 cursor-not-allowed': downloadingAllLogs }"
|
||||||
|
class="block w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-coolgray-300">
|
||||||
|
<span x-show="!downloadingAllLogs">Download all logs</span>
|
||||||
|
<span x-show="downloadingAllLogs" class="flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
Downloading...
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button title="Toggle Timestamps" x-on:click="showTimestamps = !showTimestamps"
|
<button title="Toggle Timestamps" x-on:click="showTimestamps = !showTimestamps"
|
||||||
:class="showTimestamps ? '!text-warning' : ''"
|
:class="showTimestamps ? '!text-warning' : ''"
|
||||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||||
@@ -276,6 +324,7 @@
|
|||||||
d="M6 14h4m0 0v4m0-4l-6 6m14-10h-4m0 0V6m0 4l6-6" />
|
d="M6 14h4m0 0v4m0-4l-6 6m14-10h-4m0 0V6m0 4l6-6" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="logsContainer"
|
<div id="logsContainer"
|
||||||
|
|||||||
@@ -246,19 +246,19 @@
|
|||||||
<div class="flex flex-col dark:text-white dark:border-coolgray-300 border-neutral-200"
|
<div class="flex flex-col dark:text-white dark:border-coolgray-300 border-neutral-200"
|
||||||
:class="fullscreen ? 'h-full w-full bg-white dark:bg-coolgray-100' : 'bg-white dark:bg-coolgray-100 border border-solid rounded-sm'">
|
:class="fullscreen ? 'h-full w-full bg-white dark:bg-coolgray-100' : 'bg-white dark:bg-coolgray-100 border border-solid rounded-sm'">
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between gap-2 px-4 py-2 border-b dark:border-coolgray-300 border-neutral-200 shrink-0">
|
class="flex flex-wrap items-center justify-between gap-2 px-4 py-2 border-b dark:border-coolgray-300 border-neutral-200 shrink-0">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<form wire:submit="getLogs(true)" class="relative flex items-center">
|
<form wire:submit="getLogs(true)" class="relative flex items-center">
|
||||||
<span
|
<span
|
||||||
class="absolute left-2 top-1/2 -translate-y-1/2 text-xs text-gray-400 pointer-events-none">Lines:</span>
|
class="absolute left-2 top-1/2 -translate-y-1/2 text-xs text-gray-400 pointer-events-none">Lines:</span>
|
||||||
<input type="number" wire:model="numberOfLines" placeholder="100" min="1" max="50000"
|
<input type="number" wire:model="numberOfLines" placeholder="100" min="1" max="50000"
|
||||||
title="Number of Lines (max 50,000)" {{ $streamLogs ? 'readonly' : '' }}
|
title="Number of Lines (max 50,000)" {{ $streamLogs ? 'readonly' : '' }}
|
||||||
class="input input-sm w-32 pl-11 text-center dark:bg-coolgray-300" />
|
class="input input-sm w-32 pl-11 dark:bg-coolgray-300" />
|
||||||
</form>
|
</form>
|
||||||
<span x-show="searchQuery.trim()" x-text="matchCount + ' matches'"
|
<span x-show="searchQuery.trim()" x-text="matchCount + ' matches'"
|
||||||
class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"></span>
|
class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex flex-wrap items-center justify-end gap-2 flex-1">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<svg class="absolute left-2 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400"
|
<svg class="absolute left-2 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400"
|
||||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||||
@@ -276,7 +276,8 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button wire:click="getLogs(true)" title="Refresh Logs" {{ $streamLogs ? 'disabled' : '' }}
|
<div class="flex flex-wrap items-center gap-1">
|
||||||
|
<button wire:click="getLogs(true)" title="Refresh Logs" {{ $streamLogs ? 'disabled' : '' }}
|
||||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 disabled:opacity-50">
|
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 disabled:opacity-50">
|
||||||
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||||
stroke-width="1.5" stroke="currentColor">
|
stroke-width="1.5" stroke="currentColor">
|
||||||
@@ -316,7 +317,7 @@
|
|||||||
d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" />
|
d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div x-data="{ downloadMenuOpen: false }" class="relative">
|
<div x-data="{ downloadMenuOpen: false, downloadingAllLogs: false }" class="relative">
|
||||||
<button x-on:click="downloadMenuOpen = !downloadMenuOpen" title="Download Logs"
|
<button x-on:click="downloadMenuOpen = !downloadMenuOpen" title="Download Logs"
|
||||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||||
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||||
@@ -332,13 +333,14 @@
|
|||||||
x-transition:leave="transition ease-in duration-75"
|
x-transition:leave="transition ease-in duration-75"
|
||||||
x-transition:leave-start="transform opacity-100 scale-100"
|
x-transition:leave-start="transform opacity-100 scale-100"
|
||||||
x-transition:leave-end="transform opacity-0 scale-95"
|
x-transition:leave-end="transform opacity-0 scale-95"
|
||||||
class="absolute right-0 z-50 mt-2 w-48 origin-top-right rounded-md bg-white dark:bg-coolgray-200 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
class="absolute right-0 z-50 mt-2 w-max origin-top-right rounded-md bg-white dark:bg-coolgray-200 shadow-lg ring-1 ring-neutral-200 dark:ring-coolgray-300 focus:outline-none">
|
||||||
<div class="py-1">
|
<div class="py-1">
|
||||||
<button x-on:click="downloadLogs(); downloadMenuOpen = false"
|
<button x-on:click="downloadLogs(); downloadMenuOpen = false"
|
||||||
class="block w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-coolgray-300">
|
class="block w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-coolgray-300">
|
||||||
Download displayed logs
|
Download displayed logs
|
||||||
</button>
|
</button>
|
||||||
<button x-on:click="
|
<button x-on:click="
|
||||||
|
downloadingAllLogs = true;
|
||||||
$wire.downloadAllLogs().then(logs => {
|
$wire.downloadAllLogs().then(logs => {
|
||||||
if (!logs) return;
|
if (!logs) return;
|
||||||
const blob = new Blob([logs], { type: 'text/plain' });
|
const blob = new Blob([logs], { type: 'text/plain' });
|
||||||
@@ -350,11 +352,22 @@
|
|||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
Livewire.dispatch('success', ['All logs downloaded.']);
|
Livewire.dispatch('success', ['All logs downloaded.']);
|
||||||
|
}).finally(() => {
|
||||||
|
downloadingAllLogs = false;
|
||||||
|
downloadMenuOpen = false;
|
||||||
});
|
});
|
||||||
downloadMenuOpen = false;
|
|
||||||
"
|
"
|
||||||
|
:disabled="downloadingAllLogs"
|
||||||
|
:class="{ 'opacity-50 cursor-not-allowed': downloadingAllLogs }"
|
||||||
class="block w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-coolgray-300">
|
class="block w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-coolgray-300">
|
||||||
Download all logs
|
<span x-show="!downloadingAllLogs">Download all logs</span>
|
||||||
|
<span x-show="downloadingAllLogs" class="flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
Downloading...
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -402,6 +415,7 @@
|
|||||||
stroke-width="2" d="M6 14h4m0 0v4m0-4l-6 6m14-10h-4m0 0V6m0 4l6-6" />
|
stroke-width="2" d="M6 14h4m0 0v4m0-4l-6 6m14-10h-4m0 0V6m0 4l6-6" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="logsContainer" @scroll="handleScroll"
|
<div id="logsContainer" @scroll="handleScroll"
|
||||||
|
|||||||
Reference in New Issue
Block a user