chore: images page in progress

This commit is contained in:
David Buday 2024-12-09 17:56:49 +01:00
parent d529f8f8aa
commit 07c545a8da
2 changed files with 270 additions and 32 deletions

View file

@ -2,66 +2,161 @@
namespace App\Livewire\Images\Images;
use App\Actions\Docker\DeleteAllDanglingServerDockerImages;
use App\Actions\Docker\GetServerDockerImageDetails;
use App\Actions\Docker\ListServerDockerImages;
use App\Models\Server;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Collection as SupportCollection;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class Index extends Component
{
public string $selected_uuid = 'default';
public array $serverImages = [];
public SupportCollection $serverImages;
public Collection $servers;
public bool $isLoadingImages = false;
public array $selectedImages = [];
public ?array $imageDetails = null;
public string $searchQuery = '';
public bool $showOnlyDangling = false;
public bool $selectAll = false;
public function mount()
{
if (!auth()->user()->isAdmin()) {
abort(403);
}
$this->servers = Server::isReachable()->get();
$this->serverImages = collect([]);
}
//Zove se automatski kad se na fronti selecta server
public function updatedSelectedUuid()
{
$this->loadServerImages();
$this->selectedImages = [];
}
public function loadServerImages()
{
$this->isLoadingImages = true;
$this->imageDetails = null;
try {
if ($this->selected_uuid === 'default') {
dd('Please select a server.');
return;
}
$server = $this->servers->firstWhere('uuid', $this->selected_uuid);
if (!$server) {
dd('Server not found');
return;
}
// 1. Koristi instant_remote_process "docker images"
// 2. Parse output u $serverImages array
// 3. repository, tag, id, size, created_at
$this->serverImages = collect(ListServerDockerImages::run($server));
} catch (\Exception $e) {
dd("Error loading docker images: " . $e->getMessage());
$this->addError('images', "Error loading docker images: " . $e->getMessage());
} finally {
$this->isLoadingImages = false;
}
}
public function getImageDetails($imageId) {}
public function deleteImage($imageId) {}
public function getImageDetails($imageId)
{
try {
$server = $this->servers->firstWhere('uuid', $this->selected_uuid);
if (!$server) {
return;
}
$this->imageDetails = GetServerDockerImageDetails::run($server, $imageId);
public function pruneUnused() {}
// Add formatted size
if (isset($this->imageDetails[0]['Size'])) {
$size = $this->imageDetails[0]['Size'];
$this->imageDetails[0]['FormattedSize'] = $this->formatBytes($size);
}
// Add formatted creation date
if (isset($this->imageDetails[0]['Created'])) {
$this->imageDetails[0]['FormattedCreated'] = \Carbon\Carbon::parse($this->imageDetails[0]['Created'])->diffForHumans();
}
} catch (\Exception $e) {
$this->addError('details', "Error loading image details: " . $e->getMessage());
}
}
public function deleteImage($imageId)
{
try {
$server = $this->servers->firstWhere('uuid', $this->selected_uuid);
if (!$server) {
return;
}
instant_remote_process(["docker rmi -f {$imageId}"], $server);
$this->imageDetails = null;
$this->loadServerImages();
} catch (\Exception $e) {
$this->addError('delete', "Error deleting image: " . $e->getMessage());
}
}
public function pruneUnused()
{
try {
$server = $this->servers->firstWhere('uuid', $this->selected_uuid);
if (!$server) {
return;
}
DeleteAllDanglingServerDockerImages::run($server);
$this->loadServerImages();
} catch (\Exception $e) {
$this->addError('prune', "Error pruning images: " . $e->getMessage());
}
}
public function getFilteredImagesProperty()
{
return $this->serverImages
->when($this->searchQuery, function ($collection) {
return $collection->filter(function ($image) {
return str_contains(strtolower($image['Repository'] ?? ''), strtolower($this->searchQuery)) ||
str_contains(strtolower($image['Tag'] ?? ''), strtolower($this->searchQuery)) ||
str_contains(strtolower($image['ID'] ?? ''), strtolower($this->searchQuery));
});
})
->when($this->showOnlyDangling, function ($collection) {
return $collection->filter(function ($image) {
return ($image['Repository'] ?? '') === '<none>' || ($image['Tag'] ?? '') === '<none>';
});
})
->values();
}
public function updatedSelectAll($value)
{
if ($value) {
$this->selectedImages = $this->filteredImages->pluck('ID')->toArray();
} else {
$this->selectedImages = [];
}
}
protected function formatBytes($bytes, $precision = 2)
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, $precision) . ' ' . $units[$pow];
}
public function render()
{
return view('livewire.images.images.index');
return view('livewire.images.images.index', [
'filteredImages' => $this->getFilteredImagesProperty()
]);
}
}

View file

@ -7,10 +7,8 @@
<h2>Docker Images</h2>
<form class="flex items-center gap-2" wire:submit="loadServerImages">
<x-forms.select id="server" required wire:model.live="selected_uuid">
<option value="default" disabled>Select a server</option>
@foreach ($servers as $server)
@if ($loop->first)
<option disabled value="default">Select a server</option>
@endif
<option value="{{ $server->uuid }}">{{ $server->name }}</option>
@endforeach
</x-forms.select>
@ -20,8 +18,35 @@
</x-forms.button>
</form>
</div>
@if ($selected_uuid !== 'default')
<div class="flex items-center gap-2">
<x-forms.button wire:click="pruneUnused"
wire:confirm="Are you sure you want to prune unused images?">
Prune Unused
</x-forms.button>
<x-forms.button wire:click="deleteSelectedImages"
wire:confirm="Are you sure you want to delete selected images?" :disabled="empty($selectedImages)"
class="bg-red-600 hover:bg-red-700">
Delete Selected ({{ count($selectedImages) }})
</x-forms.button>
</div>
@endif
</div>
@if ($selected_uuid !== 'default')
<div class="flex items-center gap-4 mb-4">
<div class="flex-1">
<x-forms.input type="search" wire:model.live.debounce.300ms="searchQuery"
placeholder="Search images..." />
</div>
<label class="flex items-center gap-2">
<input type="checkbox" wire:model.live="showOnlyDangling">
<span>Show only dangling images</span>
</label>
</div>
@endif
<div class="space-y-4">
<div wire:loading.block wire:target="loadServerImages" class="text-center py-4">
<x-loading text="Loading images..." />
@ -33,6 +58,9 @@
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead>
<tr>
<th class="px-6 py-3 text-left">
<input type="checkbox" wire:model.live="selectAll">
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Repository</th>
@ -42,25 +70,46 @@
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Image ID</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Size</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
{{-- TODO: Implement image listing logic --}}
{{-- Example row structure:
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800">
<td class="px-6 py-4 whitespace-nowrap">repository_name</td>
<td class="px-6 py-4 whitespace-nowrap">tag</td>
<td class="px-6 py-4 whitespace-nowrap font-mono text-sm">image_id</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex gap-2">
<x-forms.button>Details</x-forms.button>
</div>
</td>
</tr>
--}}
@forelse ($filteredImages as $image)
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800">
<td class="px-6 py-4">
<input type="checkbox" wire:model.live="selectedImages"
value="{{ $image['ID'] }}">
</td>
<td class="px-6 py-4 whitespace-nowrap">{{ $image['Repository'] }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ $image['Tag'] }}</td>
<td class="px-6 py-4 whitespace-nowrap font-mono text-sm">
{{ substr($image['ID'], 7, 12) }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ $image['Size'] }}</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex gap-2">
<x-forms.button wire:click="getImageDetails('{{ $image['ID'] }}')">
Details
</x-forms.button>
<x-forms.button wire:click="deleteImage('{{ $image['ID'] }}')"
wire:confirm="Are you sure you want to delete this image?"
class="bg-red-600 hover:bg-red-700">
Delete
</x-forms.button>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="px-6 py-4 text-center text-gray-500">
No images found
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@ -71,5 +120,99 @@
@endif
</div>
</div>
@if ($imageDetails)
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-4xl w-full max-h-[80vh] overflow-y-auto">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">Image Details</h3>
<button wire:click="$set('imageDetails', null)"
class="text-gray-500 hover:text-gray-700">×</button>
</div>
<div class="space-y-6">
<div class="grid grid-cols-2 gap-4">
<div>
<h4 class="font-semibold">ID:</h4>
<p class="font-mono text-sm">{{ $imageDetails[0]['Id'] ?? 'N/A' }}</p>
</div>
<div>
<h4 class="font-semibold">Created:</h4>
<p>{{ $imageDetails[0]['FormattedCreated'] ?? 'N/A' }}</p>
</div>
<div>
<h4 class="font-semibold">Size:</h4>
<p>{{ $imageDetails[0]['FormattedSize'] ?? 'N/A' }}</p>
</div>
<div>
<h4 class="font-semibold">Container Count:</h4>
<p>{{ $imageDetails[0]['ContainerCount'] ?? 'N/A' }}</p>
</div>
</div>
@if (isset($imageDetails[0]['Config']))
<div class="border-t pt-4">
<h4 class="font-semibold mb-4">Configuration</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
@if (isset($imageDetails[0]['Config']['Env']) && !empty($imageDetails[0]['Config']['Env']))
<div class="bg-gray-50 dark:bg-gray-700 p-4 rounded">
<h5 class="font-semibold mb-2">Environment Variables</h5>
<div class="font-mono text-sm space-y-1 max-h-40 overflow-y-auto">
@foreach ($imageDetails[0]['Config']['Env'] as $env)
<div class="truncate">{{ $env }}</div>
@endforeach
</div>
</div>
@endif
@if (isset($imageDetails[0]['Config']['ExposedPorts']) && !empty($imageDetails[0]['Config']['ExposedPorts']))
<div class="bg-gray-50 dark:bg-gray-700 p-4 rounded">
<h5 class="font-semibold mb-2">Exposed Ports</h5>
<div class="font-mono text-sm space-y-1">
@foreach (array_keys($imageDetails[0]['Config']['ExposedPorts']) as $port)
<div>{{ $port }}</div>
@endforeach
</div>
</div>
@endif
@if (isset($imageDetails[0]['Config']['Volumes']) && !empty($imageDetails[0]['Config']['Volumes']))
<div class="bg-gray-50 dark:bg-gray-700 p-4 rounded">
<h5 class="font-semibold mb-2">Volumes</h5>
<div class="font-mono text-sm space-y-1">
@foreach (array_keys($imageDetails[0]['Config']['Volumes']) as $volume)
<div>{{ $volume }}</div>
@endforeach
</div>
</div>
@endif
@if (isset($imageDetails[0]['Config']['Cmd']) && !empty($imageDetails[0]['Config']['Cmd']))
<div class="bg-gray-50 dark:bg-gray-700 p-4 rounded">
<h5 class="font-semibold mb-2">Command</h5>
<div class="font-mono text-sm">
{{ implode(' ', $imageDetails[0]['Config']['Cmd']) }}
</div>
</div>
@endif
</div>
</div>
@endif
<div class="flex justify-end gap-2 border-t pt-4">
<x-forms.button wire:click="pruneUnused"
wire:confirm="Are you sure you want to prune unused images?">
Prune Unused
</x-forms.button>
<x-forms.button wire:click="deleteImage('{{ $imageDetails[0]['Id'] }}')"
wire:confirm="Are you sure you want to delete this image?"
class="bg-red-600 hover:bg-red-700">
Delete Image
</x-forms.button>
</div>
</div>
</div>
</div>
@endif
</div>
</div>