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 {
@@ -2015,8 +2099,6 @@ const saveDomainSettings = async () => {
20152099 savingDomainSettings .value = true ;
20162100 try {
20172101 await deploymentsApi .updateMetadata (route .params .name as string , {
2018- name: deployment .value ?.name || " " ,
2019- type: deployment .value ?.metadata ?.type || " custom" ,
20202102 networking: {
20212103 expose: domainSettings .value .expose ,
20222104 domain: domainSettings .value .domain ,
@@ -2028,10 +2110,6 @@ const saveDomainSettings = async () => {
20282110 enabled: domainSettings .value .sslEnabled ,
20292111 auto_cert: domainSettings .value .autoCert ,
20302112 },
2031- healthcheck: {
2032- path: " /" ,
2033- interval: " 30s" ,
2034- },
20352113 });
20362114 showDomainSettingsModal .value = false ;
20372115 notifications .success (" Saved" , " Domain settings updated successfully" );
@@ -2044,6 +2122,34 @@ const saveDomainSettings = async () => {
20442122 }
20452123};
20462124
2125+ const openCredentialModal = async () => {
2126+ selectedCredentialId .value = deployment .value ?.metadata ?.credential_id || null ;
2127+ try {
2128+ const response = await credentialsApi .list ();
2129+ allCredentials .value = response .data .credentials || [];
2130+ } catch {
2131+ allCredentials .value = [];
2132+ }
2133+ showCredentialModal .value = true ;
2134+ };
2135+
2136+ const saveCredential = async () => {
2137+ savingCredential .value = true ;
2138+ try {
2139+ await deploymentsApi .updateMetadata (route .params .name as string , {
2140+ credential_id: selectedCredentialId .value || " " ,
2141+ });
2142+ showCredentialModal .value = false ;
2143+ notifications .success (" Saved" , " Registry credential updated" );
2144+ await fetchDeployment ();
2145+ } catch (err : any ) {
2146+ const msg = err .response ?.data ?.error || err .message ;
2147+ notifications .error (" Save Failed" , msg );
2148+ } finally {
2149+ savingCredential .value = false ;
2150+ }
2151+ };
2152+
20472153const copyConfig = () => {
20482154 navigator .clipboard .writeText (composeConfig .value );
20492155 notifications .success (" Copied" , " Configuration copied to clipboard" );
@@ -2219,6 +2325,36 @@ onUnmounted(() => {
22192325 color : var (--color-warning-700 );
22202326}
22212327
2328+ .credential-badge {
2329+ display : inline-flex ;
2330+ align-items : center ;
2331+ gap : var (--space-1 );
2332+ font-size : var (--text-sm );
2333+ padding : var (--space-1 ) var (--space-2 );
2334+ border-radius : var (--radius-sm );
2335+ background : var (--color-primary-50 );
2336+ color : var (--color-primary-700 );
2337+ }
2338+
2339+ .credential-badge i {
2340+ font-size : var (--text-xs );
2341+ }
2342+
2343+ .public-badge {
2344+ font-size : var (--text-sm );
2345+ color : var (--color-neutral-500 );
2346+ margin-right : var (--space-2 );
2347+ }
2348+
2349+ .credential-modal {
2350+ max-width : 480px ;
2351+ }
2352+
2353+ .credential-modal .modal-description {
2354+ color : var (--color-neutral-600 );
2355+ margin-bottom : var (--space-4 );
2356+ }
2357+
22222358.header-actions {
22232359 display : flex ;
22242360 gap : var (--space-2 );
0 commit comments