mirror of
https://github.com/coollabsio/coolify.git
synced 2026-03-11 08:55:47 +00:00
chore: images page in progress
This commit is contained in:
parent
d529f8f8aa
commit
07c545a8da
2 changed files with 270 additions and 32 deletions
|
|
@ -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()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue