feat: auto-discover container images from registries

- Add ListImages() to registry interface, implement for Gitea
- Add owner field to registry config (needed for Gitea packages API)
- GET /api/registries/:id/images endpoint
- "Browse Images" button on Projects and Quick Deploy pages
- Image dropdown with registry grouping and search
- i18n support (EN/RU) for all new UI strings
This commit is contained in:
2026-03-28 14:04:11 +03:00
parent 77251c540b
commit 37e251da85
12 changed files with 355 additions and 18 deletions
@@ -17,6 +17,7 @@
let formUrl = $state('');
let formType = $state('gitea');
let formToken = $state('');
let formOwner = $state('');
let formSaving = $state(false);
let testingId = $state<string | null>(null);
@@ -31,8 +32,8 @@
return Object.keys(newErrors).length === 0;
}
function resetForm() { showForm = false; editingId = null; formName = ''; formUrl = ''; formType = 'gitea'; formToken = ''; errors = {}; }
function startEdit(registry: Registry) { editingId = registry.id; formName = registry.name; formUrl = registry.url; formType = registry.type; formToken = ''; showForm = true; errors = {}; }
function resetForm() { showForm = false; editingId = null; formName = ''; formUrl = ''; formType = 'gitea'; formToken = ''; formOwner = ''; errors = {}; }
function startEdit(registry: Registry) { editingId = registry.id; formName = registry.name; formUrl = registry.url; formType = registry.type; formToken = ''; formOwner = registry.owner ?? ''; showForm = true; errors = {}; }
async function loadRegistryList() {
loading = true;
@@ -43,7 +44,7 @@
if (!validateForm()) return;
formSaving = true;
try {
const payload: Partial<Registry> = { name: formName.trim(), url: formUrl.trim(), type: formType };
const payload: Partial<Registry> = { name: formName.trim(), url: formUrl.trim(), type: formType, owner: formOwner.trim() };
if (formToken.trim()) payload.token = formToken.trim();
if (editingId) { await updateRegistry(editingId, payload); toasts.success($t('settingsRegistries.registryUpdated')); }
else { await createRegistry(payload); toasts.success($t('settingsRegistries.registryAdded')); }
@@ -105,6 +106,7 @@
<p class="text-xs text-[var(--text-tertiary)]">{$t('settingsRegistries.typeHelp')}</p>
</div>
<FormField label={$t('settingsRegistries.token')} name="registryToken" type="password" bind:value={formToken} placeholder={editingId ? '(leave empty to keep current)' : 'registry-access-token'} required={!editingId} error={errors.token ?? ''} helpText={editingId ? $t('settingsRegistries.tokenHelpEdit') : $t('settingsRegistries.tokenHelpNew')} />
<FormField label={$t('settingsRegistries.owner')} name="registryOwner" bind:value={formOwner} placeholder="alexei" helpText={$t('settingsRegistries.ownerHelp')} />
</div>
<div class="mt-4 flex gap-3">
<button onclick={handleSave} disabled={formSaving} class="inline-flex items-center gap-2 rounded-lg bg-[var(--color-brand-600)] px-4 py-2.5 text-sm font-medium text-white shadow-sm hover:bg-[var(--color-brand-700)] disabled:opacity-50 transition-colors active:animate-press">
@@ -135,7 +137,7 @@
<h3 class="text-sm font-semibold text-[var(--text-primary)]">{registry.name}</h3>
<span class="rounded-full bg-[var(--surface-card-hover)] px-2 py-0.5 text-xs font-medium text-[var(--text-tertiary)]">{registry.type}</span>
</div>
<p class="mt-1 text-sm text-[var(--text-secondary)]">{registry.url}</p>
<p class="mt-1 text-sm text-[var(--text-secondary)]">{registry.url}{registry.owner ? `/${registry.owner}` : ''}</p>
</div>
<div class="flex items-center gap-2">
<button onclick={() => handleTestConnection(registry)} disabled={testingId === registry.id} class="inline-flex items-center gap-1.5 rounded-lg border border-[var(--border-primary)] px-3 py-1.5 text-xs font-medium text-[var(--text-secondary)] hover:bg-[var(--surface-card-hover)] disabled:opacity-50 transition-colors">