118118 <span class =" label" >Type</span >
119119 <span class =" value type-badge" >{{ deployment.metadata.type }}</span >
120120 </div >
121+ <div class =" info-row" >
122+ <span class =" label" >Registry</span >
123+ <span class =" value" >
124+ <template v-if =" registryCredential " >
125+ <span class =" credential-badge" >
126+ <i class =" pi pi-lock" />
127+ {{ registryCredential.name }}
128+ </span >
129+ <button
130+ v-if =" canWrite"
131+ class =" btn btn-sm btn-icon"
132+ title =" Change credential"
133+ @click =" openCredentialModal"
134+ >
135+ <i class =" pi pi-pencil" />
136+ </button >
137+ </template >
138+ <template v-else >
139+ <span class =" public-badge" >Public</span >
140+ <button v-if =" canWrite" class =" btn btn-sm btn-link" @click =" openCredentialModal" >
141+ Set credential
142+ </button >
143+ </template >
144+ </span >
145+ </div >
121146 <div v-if =" !isInfrastructure" class =" info-row action-row" >
122147 <button class =" btn btn-sm btn-secondary" @click =" migrateToInfrastructure" >
123148 <i class =" pi pi-server" />
11101135 </div >
11111136 </Teleport >
11121137
1138+ <Teleport to =" body" >
1139+ <div v-if =" showCredentialModal" class =" modal-overlay" >
1140+ <div class =" credential-modal modal-container" >
1141+ <div class =" modal-header" >
1142+ <h3 >
1143+ <i class =" pi pi-lock" />
1144+ Registry Credential
1145+ </h3 >
1146+ <button class =" close-btn" @click =" showCredentialModal = false" >
1147+ <i class =" pi pi-times" />
1148+ </button >
1149+ </div >
1150+ <div class =" modal-body" >
1151+ <p class =" modal-description" >
1152+ Select a saved registry credential to use when pulling images for this deployment.
1153+ </p >
1154+ <div class =" form-group" >
1155+ <label >Credential</label >
1156+ <select v-model =" selectedCredentialId" class =" form-input" >
1157+ <option :value =" null" >None (Public Registry)</option >
1158+ <option v-for =" cred in allCredentials" :key =" cred.id" :value =" cred.id" >
1159+ {{ cred.name }} ({{ cred.registry_type_slug }})
1160+ </option >
1161+ </select >
1162+ <span class =" hint" >
1163+ This credential will be used when pulling images on restart or update.
1164+ <router-link to =" /settings?tab=credentials" >Manage credentials</router-link >
1165+ </span >
1166+ </div >
1167+ </div >
1168+ <div class =" modal-footer" >
1169+ <button class =" btn btn-secondary" @click =" showCredentialModal = false" >Cancel</button >
1170+ <button class =" btn btn-primary" :disabled =" savingCredential" @click =" saveCredential" >
1171+ <i v-if =" savingCredential" class =" pi pi-spin pi-spinner" />
1172+ {{ savingCredential ? "Saving..." : "Save" }}
1173+ </button >
1174+ </div >
1175+ </div >
1176+ </div >
1177+ </Teleport >
1178+
11131179 <Teleport to =" body" >
11141180 <div v-if =" showAddEnvModal" class =" modal-overlay" >
11151181 <div class =" env-modal modal-container" >
@@ -1248,11 +1314,12 @@ import {
12481314 filesApi ,
12491315 infrastructureApi ,
12501316 securityApi ,
1317+ credentialsApi ,
12511318 type EnvVar ,
12521319} from " @/services/api" ;
12531320import { useNotificationsStore } from " @/stores/notifications" ;
12541321import { useAuthStore } from " @/stores/auth" ;
1255- import type { ProxyStatus , QuickAction , SecurityEvent , DeploymentSecurityConfig } from " @/types" ;
1322+ import type { ProxyStatus , QuickAction , SecurityEvent , DeploymentSecurityConfig , RegistryCredential } from " @/types" ;
12561323import FileBrowser from " @/components/FileBrowser.vue" ;
12571324import LogViewer from " @/components/LogViewer.vue" ;
12581325import ConfirmModal from " @/components/ConfirmModal.vue" ;
@@ -1322,6 +1389,12 @@ const resourceUsage = ref({
13221389 network: 0 ,
13231390});
13241391
1392+ const registryCredential = ref <RegistryCredential | null >(null );
1393+ const allCredentials = ref <RegistryCredential []>([]);
1394+ const showCredentialModal = ref (false );
1395+ const selectedCredentialId = ref <string | null >(null );
1396+ const savingCredential = ref (false );
1397+
13251398const showDeleteEnvModal = ref (false );
13261399const envKeyToDelete = ref (" " );
13271400
@@ -1527,6 +1600,17 @@ const fetchDeployment = async () => {
15271600
15281601 services .value = deployment .value ?.services || [];
15291602
1603+ if (deployment .value ?.metadata ?.credential_id ) {
1604+ try {
1605+ const credResponse = await credentialsApi .get (deployment .value .metadata .credential_id );
1606+ registryCredential .value = credResponse .data .credential ;
1607+ } catch {
1608+ registryCredential .value = null ;
1609+ }
1610+ } else {
1611+ registryCredential .value = null ;
1612+ }
1613+
15301614 fetchStats ();
15311615
15321616 try {
@@ -2014,9 +2098,11 @@ const openDomainSettings = () => {
20142098const saveDomainSettings = async () => {
20152099 savingDomainSettings .value = true ;
20162100 try {
2101+ const currentMetadata = deployment .value ?.metadata || {};
20172102 await deploymentsApi .updateMetadata (route .params .name as string , {
2103+ ... currentMetadata ,
20182104 name: deployment .value ?.name || " " ,
2019- type: deployment . value ?. metadata ? .type || " custom" ,
2105+ type: currentMetadata .type || " custom" ,
20202106 networking: {
20212107 expose: domainSettings .value .expose ,
20222108 domain: domainSettings .value .domain ,
@@ -2028,10 +2114,11 @@ const saveDomainSettings = async () => {
20282114 enabled: domainSettings .value .sslEnabled ,
20292115 auto_cert: domainSettings .value .autoCert ,
20302116 },
2031- healthcheck: {
2117+ healthcheck: currentMetadata . healthcheck || {
20322118 path: " /" ,
20332119 interval: " 30s" ,
20342120 },
2121+ credential_id: registryCredential .value ?.id ,
20352122 });
20362123 showDomainSettingsModal .value = false ;
20372124 notifications .success (" Saved" , " Domain settings updated successfully" );
@@ -2044,6 +2131,38 @@ const saveDomainSettings = async () => {
20442131 }
20452132};
20462133
2134+ const openCredentialModal = async () => {
2135+ selectedCredentialId .value = deployment .value ?.metadata ?.credential_id || null ;
2136+ try {
2137+ const response = await credentialsApi .list ();
2138+ allCredentials .value = response .data .credentials || [];
2139+ } catch {
2140+ allCredentials .value = [];
2141+ }
2142+ showCredentialModal .value = true ;
2143+ };
2144+
2145+ const saveCredential = async () => {
2146+ savingCredential .value = true ;
2147+ try {
2148+ const currentMetadata = deployment .value ?.metadata || {};
2149+ await deploymentsApi .updateMetadata (route .params .name as string , {
2150+ ... currentMetadata ,
2151+ name: currentMetadata .name || deployment .value ?.name || " " ,
2152+ type: currentMetadata .type || " custom" ,
2153+ credential_id: selectedCredentialId .value || undefined ,
2154+ });
2155+ showCredentialModal .value = false ;
2156+ notifications .success (" Saved" , " Registry credential updated" );
2157+ await fetchDeployment ();
2158+ } catch (err : any ) {
2159+ const msg = err .response ?.data ?.error || err .message ;
2160+ notifications .error (" Save Failed" , msg );
2161+ } finally {
2162+ savingCredential .value = false ;
2163+ }
2164+ };
2165+
20472166const copyConfig = () => {
20482167 navigator .clipboard .writeText (composeConfig .value );
20492168 notifications .success (" Copied" , " Configuration copied to clipboard" );
@@ -2219,6 +2338,36 @@ onUnmounted(() => {
22192338 color : var (--color-warning-700 );
22202339}
22212340
2341+ .credential-badge {
2342+ display : inline-flex ;
2343+ align-items : center ;
2344+ gap : var (--space-1 );
2345+ font-size : var (--text-sm );
2346+ padding : var (--space-1 ) var (--space-2 );
2347+ border-radius : var (--radius-sm );
2348+ background : var (--color-primary-50 );
2349+ color : var (--color-primary-700 );
2350+ }
2351+
2352+ .credential-badge i {
2353+ font-size : var (--text-xs );
2354+ }
2355+
2356+ .public-badge {
2357+ font-size : var (--text-sm );
2358+ color : var (--color-neutral-500 );
2359+ margin-right : var (--space-2 );
2360+ }
2361+
2362+ .credential-modal {
2363+ max-width : 480px ;
2364+ }
2365+
2366+ .credential-modal .modal-description {
2367+ color : var (--color-neutral-600 );
2368+ margin-bottom : var (--space-4 );
2369+ }
2370+
22222371.header-actions {
22232372 display : flex ;
22242373 gap : var (--space-2 );
0 commit comments