Skip to content

Commit b2a9c1f

Browse files
committed
feat(ui): Display and manage deployment registry credentials
Add UI for viewing and changing registry credentials associated with deployments. Changes include: - Add credential_id to ServiceMetadata in types and API service - Display credential name in deployment detail General Information card - Add modal to select/change registry credential for a deployment - Preserve credential_id when saving domain settings Signed-off-by: nfebe <fenn25.fn@gmail.com>
1 parent d7b13cb commit b2a9c1f

3 files changed

Lines changed: 147 additions & 8 deletions

File tree

src/services/api.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export interface ServiceMetadata {
5959
path: string;
6060
interval: string;
6161
};
62+
credential_id?: string;
6263
}
6364

6465
export interface EnvVar {
@@ -71,7 +72,8 @@ export const deploymentsApi = {
7172
get: (name: string) => apiClient.get<Deployment>(`/deployments/${name}`),
7273
create: (data: any) => apiClient.post("/deployments", data),
7374
update: (name: string, data: any) => apiClient.put(`/deployments/${name}`, data),
74-
updateMetadata: (name: string, metadata: ServiceMetadata) => apiClient.put(`/deployments/${name}/metadata`, metadata),
75+
updateMetadata: (name: string, metadata: Partial<ServiceMetadata>) =>
76+
apiClient.put(`/deployments/${name}/metadata`, metadata),
7577
delete: (name: string, options?: { deleteSSL?: boolean; deleteDatabase?: boolean; deleteVhost?: boolean }) => {
7678
const params = new URLSearchParams();
7779
if (options?.deleteSSL !== undefined) params.set("delete_ssl", String(options.deleteSSL));

src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface ServiceMetadata {
2727
healthcheck: HealthCheckConfig;
2828
quick_actions?: QuickAction[];
2929
security?: DeploymentSecurityConfig;
30+
credential_id?: string;
3031
}
3132

3233
export interface QuickAction {

src/views/DeploymentDetailView.vue

Lines changed: 143 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,31 @@
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" />
@@ -1110,6 +1135,47 @@
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";
12531320
import { useNotificationsStore } from "@/stores/notifications";
12541321
import { useAuthStore } from "@/stores/auth";
1255-
import type { ProxyStatus, QuickAction, SecurityEvent, DeploymentSecurityConfig } from "@/types";
1322+
import type { ProxyStatus, QuickAction, SecurityEvent, DeploymentSecurityConfig, RegistryCredential } from "@/types";
12561323
import FileBrowser from "@/components/FileBrowser.vue";
12571324
import LogViewer from "@/components/LogViewer.vue";
12581325
import 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+
13251398
const showDeleteEnvModal = ref(false);
13261399
const 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+
20472153
const 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

Comments
 (0)