feat(external-registry): Implement CRUD operations and connection verification for external registries
This commit is contained in:
		| @@ -137,6 +137,7 @@ export class Cloudly { | |||||||
|  |  | ||||||
|     // start the managers |     // start the managers | ||||||
|     this.imageManager.start(); |     this.imageManager.start(); | ||||||
|  |     this.externalRegistryManager.start(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
| @@ -149,5 +150,6 @@ export class Cloudly { | |||||||
|     await this.secretManager.stop(); |     await this.secretManager.stop(); | ||||||
|     await this.serviceManager.stop(); |     await this.serviceManager.stop(); | ||||||
|     await this.deploymentManager.stop(); |     await this.deploymentManager.stop(); | ||||||
|  |     await this.externalRegistryManager.stop(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -17,14 +17,92 @@ export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalR | |||||||
|     return externalRegistries; |     return externalRegistries; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public static async getDefaultRegistry(type: 'docker' | 'npm' = 'docker') { | ||||||
|  |     const defaultRegistry = await this.getInstance({ | ||||||
|  |       'data.type': type, | ||||||
|  |       'data.isDefault': true, | ||||||
|  |     }); | ||||||
|  |     return defaultRegistry; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public static async createExternalRegistry(registryDataArg: Partial<plugins.servezoneInterfaces.data.IExternalRegistry['data']>) { |   public static async createExternalRegistry(registryDataArg: Partial<plugins.servezoneInterfaces.data.IExternalRegistry['data']>) { | ||||||
|     const externalRegistry = new ExternalRegistry(); |     const externalRegistry = new ExternalRegistry(); | ||||||
|     externalRegistry.id = await ExternalRegistry.getNewId(); |     externalRegistry.id = await ExternalRegistry.getNewId(); | ||||||
|     Object.assign(externalRegistry, registryDataArg); |     externalRegistry.data = { | ||||||
|  |       type: registryDataArg.type || 'docker', | ||||||
|  |       name: registryDataArg.name || '', | ||||||
|  |       url: registryDataArg.url || '', | ||||||
|  |       username: registryDataArg.username || '', | ||||||
|  |       password: registryDataArg.password || '', | ||||||
|  |       description: registryDataArg.description, | ||||||
|  |       isDefault: registryDataArg.isDefault || false, | ||||||
|  |       authType: registryDataArg.authType || 'basic', | ||||||
|  |       insecure: registryDataArg.insecure || false, | ||||||
|  |       namespace: registryDataArg.namespace, | ||||||
|  |       proxy: registryDataArg.proxy, | ||||||
|  |       config: registryDataArg.config, | ||||||
|  |       status: 'unverified', | ||||||
|  |       createdAt: Date.now(), | ||||||
|  |       updatedAt: Date.now(), | ||||||
|  |     }; | ||||||
|  |      | ||||||
|  |     // If this is set as default, unset other defaults of the same type | ||||||
|  |     if (externalRegistry.data.isDefault) { | ||||||
|  |       const existingDefaults = await ExternalRegistry.getInstances({ | ||||||
|  |         'data.type': externalRegistry.data.type, | ||||||
|  |         'data.isDefault': true, | ||||||
|  |       }); | ||||||
|  |       for (const existingDefault of existingDefaults) { | ||||||
|  |         existingDefault.data.isDefault = false; | ||||||
|  |         await existingDefault.save(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |      | ||||||
|     await externalRegistry.save(); |     await externalRegistry.save(); | ||||||
|     return externalRegistry; |     return externalRegistry; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public static async updateExternalRegistry( | ||||||
|  |     registryIdArg: string, | ||||||
|  |     registryDataArg: Partial<plugins.servezoneInterfaces.data.IExternalRegistry['data']> | ||||||
|  |   ) { | ||||||
|  |     const externalRegistry = await this.getRegistryById(registryIdArg); | ||||||
|  |     if (!externalRegistry) { | ||||||
|  |       throw new Error(`Registry with id ${registryIdArg} not found`); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // If setting as default, unset other defaults of the same type | ||||||
|  |     if (registryDataArg.isDefault && !externalRegistry.data.isDefault) { | ||||||
|  |       const existingDefaults = await ExternalRegistry.getInstances({ | ||||||
|  |         'data.type': externalRegistry.data.type, | ||||||
|  |         'data.isDefault': true, | ||||||
|  |       }); | ||||||
|  |       for (const existingDefault of existingDefaults) { | ||||||
|  |         if (existingDefault.id !== registryIdArg) { | ||||||
|  |           existingDefault.data.isDefault = false; | ||||||
|  |           await existingDefault.save(); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Update fields | ||||||
|  |     Object.assign(externalRegistry.data, registryDataArg, { | ||||||
|  |       updatedAt: Date.now(), | ||||||
|  |     }); | ||||||
|  |      | ||||||
|  |     await externalRegistry.save(); | ||||||
|  |     return externalRegistry; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public static async deleteExternalRegistry(registryIdArg: string) { | ||||||
|  |     const externalRegistry = await this.getRegistryById(registryIdArg); | ||||||
|  |     if (!externalRegistry) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     await externalRegistry.delete(); | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   // INSTANCE |   // INSTANCE | ||||||
|  |  | ||||||
|   @plugins.smartdata.svDb() |   @plugins.smartdata.svDb() | ||||||
| @@ -37,4 +115,79 @@ export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalR | |||||||
|     super(); |     super(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Verify the registry connection | ||||||
|  |    */ | ||||||
|  |   public async verifyConnection(): Promise<{ success: boolean; message?: string }> { | ||||||
|  |     try { | ||||||
|  |       // For Docker registries, try to access the v2 API | ||||||
|  |       if (this.data.type === 'docker') { | ||||||
|  |         const registryUrl = this.data.url.replace(/\/$/, ''); // Remove trailing slash | ||||||
|  |         const authHeader = 'Basic ' + Buffer.from(`${this.data.username}:${this.data.password}`).toString('base64'); | ||||||
|  |          | ||||||
|  |         // Try to access the Docker Registry v2 API | ||||||
|  |         const response = await fetch(`${registryUrl}/v2/`, { | ||||||
|  |           headers: { | ||||||
|  |             'Authorization': authHeader, | ||||||
|  |           }, | ||||||
|  |           // Allow insecure if configured | ||||||
|  |           ...(this.data.insecure ? { rejectUnauthorized: false } : {}), | ||||||
|  |         }).catch(err => { | ||||||
|  |           throw new Error(`Failed to connect: ${err.message}`); | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         if (response.status === 200 || response.status === 401) { | ||||||
|  |           // 200 means successful auth, 401 means registry exists but needs auth | ||||||
|  |           this.data.status = 'active'; | ||||||
|  |           this.data.lastVerified = Date.now(); | ||||||
|  |           this.data.lastError = undefined; | ||||||
|  |           await this.save(); | ||||||
|  |           return { success: true, message: 'Registry connection successful' }; | ||||||
|  |         } else { | ||||||
|  |           throw new Error(`Registry returned status ${response.status}`); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       // For npm registries, implement npm-specific verification | ||||||
|  |       if (this.data.type === 'npm') { | ||||||
|  |         // TODO: Implement npm registry verification | ||||||
|  |         this.data.status = 'unverified'; | ||||||
|  |         return { success: false, message: 'NPM registry verification not yet implemented' }; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       return { success: false, message: 'Unknown registry type' }; | ||||||
|  |     } catch (error) { | ||||||
|  |       this.data.status = 'error'; | ||||||
|  |       this.data.lastError = error.message; | ||||||
|  |       await this.save(); | ||||||
|  |       return { success: false, message: error.message }; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Get the full registry URL with namespace if applicable | ||||||
|  |    */ | ||||||
|  |   public getFullRegistryUrl(): string { | ||||||
|  |     let url = this.data.url.replace(/\/$/, ''); // Remove trailing slash | ||||||
|  |     if (this.data.namespace) { | ||||||
|  |       url = `${url}/${this.data.namespace}`; | ||||||
|  |     } | ||||||
|  |     return url; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Get Docker auth config for this registry | ||||||
|  |    */ | ||||||
|  |   public getDockerAuthConfig() { | ||||||
|  |     if (this.data.type !== 'docker') { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     return { | ||||||
|  |       username: this.data.username, | ||||||
|  |       password: this.data.password, | ||||||
|  |       email: this.data.config?.dockerConfig?.email, | ||||||
|  |       serveraddress: this.data.config?.dockerConfig?.serverAddress || this.data.url, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -13,23 +13,34 @@ export class ExternalRegistryManager { | |||||||
|  |  | ||||||
|   constructor(cloudlyRef: Cloudly) { |   constructor(cloudlyRef: Cloudly) { | ||||||
|     this.cloudlyRef = cloudlyRef; |     this.cloudlyRef = cloudlyRef; | ||||||
|   } |      | ||||||
|  |     // Add typedrouter to cloudly's main router | ||||||
|   public async start() { |     this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter); | ||||||
|     // lets set up a typedrouter |  | ||||||
|     this.typedrouter.addTypedRouter(this.typedrouter); |  | ||||||
|  |  | ||||||
|  |     // Get registry by ID | ||||||
|     this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_GetRegistryById>( |     this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_GetRegistryById>( | ||||||
|       new plugins.typedrequest.TypedHandler('getExternalRegistryById', async (dataArg) => { |       new plugins.typedrequest.TypedHandler('getExternalRegistryById', async (dataArg) => { | ||||||
|  |         await plugins.smartguard.passGuardsOrReject(dataArg, [ | ||||||
|  |           this.cloudlyRef.authManager.validIdentityGuard, | ||||||
|  |         ]); | ||||||
|  |          | ||||||
|         const registry = await ExternalRegistry.getRegistryById(dataArg.id); |         const registry = await ExternalRegistry.getRegistryById(dataArg.id); | ||||||
|  |         if (!registry) { | ||||||
|  |           throw new Error(`Registry with id ${dataArg.id} not found`); | ||||||
|  |         } | ||||||
|         return { |         return { | ||||||
|           registry: await registry.createSavableObject(), |           registry: await registry.createSavableObject(), | ||||||
|         }; |         }; | ||||||
|       }) |       }) | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  |     // Get all registries | ||||||
|     this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_GetRegistries>( |     this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_GetRegistries>( | ||||||
|       new plugins.typedrequest.TypedHandler('getExternalRegistries', async (dataArg) => { |       new plugins.typedrequest.TypedHandler('getExternalRegistries', async (dataArg) => { | ||||||
|  |         await plugins.smartguard.passGuardsOrReject(dataArg, [ | ||||||
|  |           this.cloudlyRef.authManager.validIdentityGuard, | ||||||
|  |         ]); | ||||||
|  |          | ||||||
|         const registries = await ExternalRegistry.getRegistries(); |         const registries = await ExternalRegistry.getRegistries(); | ||||||
|         return { |         return { | ||||||
|           registries: await Promise.all( |           registries: await Promise.all( | ||||||
| @@ -39,13 +50,81 @@ export class ExternalRegistryManager { | |||||||
|       }) |       }) | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  |     // Create registry | ||||||
|     this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_CreateRegistry>( |     this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_CreateRegistry>( | ||||||
|       new plugins.typedrequest.TypedHandler('createExternalRegistry', async (dataArg) => { |       new plugins.typedrequest.TypedHandler('createExternalRegistry', async (dataArg) => { | ||||||
|  |         await plugins.smartguard.passGuardsOrReject(dataArg, [ | ||||||
|  |           this.cloudlyRef.authManager.validIdentityGuard, | ||||||
|  |         ]); | ||||||
|  |          | ||||||
|         const registry = await ExternalRegistry.createExternalRegistry(dataArg.registryData); |         const registry = await ExternalRegistry.createExternalRegistry(dataArg.registryData); | ||||||
|         return { |         return { | ||||||
|           registry: await registry.createSavableObject(), |           registry: await registry.createSavableObject(), | ||||||
|         }; |         }; | ||||||
|       }) |       }) | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  |     // Update registry | ||||||
|  |     this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_UpdateRegistry>( | ||||||
|  |       new plugins.typedrequest.TypedHandler('updateExternalRegistry', async (dataArg) => { | ||||||
|  |         await plugins.smartguard.passGuardsOrReject(dataArg, [ | ||||||
|  |           this.cloudlyRef.authManager.validIdentityGuard, | ||||||
|  |         ]); | ||||||
|  |          | ||||||
|  |         const registry = await ExternalRegistry.updateExternalRegistry( | ||||||
|  |           dataArg.registryId, | ||||||
|  |           dataArg.registryData | ||||||
|  |         ); | ||||||
|  |         return { | ||||||
|  |           resultRegistry: await registry.createSavableObject(), | ||||||
|  |         }; | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     // Delete registry | ||||||
|  |     this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_DeleteRegistryById>( | ||||||
|  |       new plugins.typedrequest.TypedHandler('deleteExternalRegistryById', async (dataArg) => { | ||||||
|  |         await plugins.smartguard.passGuardsOrReject(dataArg, [ | ||||||
|  |           this.cloudlyRef.authManager.validIdentityGuard, | ||||||
|  |         ]); | ||||||
|  |          | ||||||
|  |         const success = await ExternalRegistry.deleteExternalRegistry(dataArg.registryId); | ||||||
|  |         return { | ||||||
|  |           ok: success, | ||||||
|  |         }; | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     // Verify registry connection | ||||||
|  |     this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_VerifyRegistry>( | ||||||
|  |       new plugins.typedrequest.TypedHandler('verifyExternalRegistry', async (dataArg) => { | ||||||
|  |         await plugins.smartguard.passGuardsOrReject(dataArg, [ | ||||||
|  |           this.cloudlyRef.authManager.validIdentityGuard, | ||||||
|  |         ]); | ||||||
|  |          | ||||||
|  |         const registry = await ExternalRegistry.getRegistryById(dataArg.registryId); | ||||||
|  |         if (!registry) { | ||||||
|  |           return { | ||||||
|  |             success: false, | ||||||
|  |             message: `Registry with id ${dataArg.registryId} not found`, | ||||||
|  |           }; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         const result = await registry.verifyConnection(); | ||||||
|  |         return { | ||||||
|  |           success: result.success, | ||||||
|  |           message: result.message, | ||||||
|  |           registry: await registry.createSavableObject(), | ||||||
|  |         }; | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async start() { | ||||||
|  |     console.log('External Registry Manager started'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async stop() { | ||||||
|  |     console.log('External Registry Manager stopped'); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,10 +3,108 @@ import * as plugins from '../plugins.js'; | |||||||
| export interface IExternalRegistry { | export interface IExternalRegistry { | ||||||
|   id: string; |   id: string; | ||||||
|   data: { |   data: { | ||||||
|  |     /** | ||||||
|  |      * Registry type | ||||||
|  |      */ | ||||||
|     type: 'docker' | 'npm'; |     type: 'docker' | 'npm'; | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * Human-readable name for the registry | ||||||
|  |      */ | ||||||
|     name: string; |     name: string; | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * Registry URL (e.g., https://registry.gitlab.com, docker.io) | ||||||
|  |      */ | ||||||
|     url: string; |     url: string; | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * Username for authentication | ||||||
|  |      */ | ||||||
|     username: string; |     username: string; | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * Password or access token for authentication | ||||||
|  |      */ | ||||||
|     password: string; |     password: string; | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * Optional description | ||||||
|  |      */ | ||||||
|  |     description?: string; | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * Whether this is the default registry for its type | ||||||
|  |      */ | ||||||
|  |     isDefault?: boolean; | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * Authentication type | ||||||
|  |      */ | ||||||
|  |     authType?: 'basic' | 'token' | 'oauth2'; | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * Allow insecure registry connections (HTTP or self-signed certs) | ||||||
|  |      */ | ||||||
|  |     insecure?: boolean; | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * Optional namespace/organization for the registry | ||||||
|  |      */ | ||||||
|  |     namespace?: string; | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * Proxy configuration | ||||||
|  |      */ | ||||||
|  |     proxy?: { | ||||||
|  |       http?: string; | ||||||
|  |       https?: string; | ||||||
|  |       noProxy?: string; | ||||||
|  |     }; | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * Registry-specific configuration | ||||||
|  |      */ | ||||||
|  |     config?: { | ||||||
|  |       /** | ||||||
|  |        * For Docker registries | ||||||
|  |        */ | ||||||
|  |       dockerConfig?: { | ||||||
|  |         email?: string; | ||||||
|  |         serverAddress?: string; | ||||||
|  |       }; | ||||||
|  |       /** | ||||||
|  |        * For npm registries | ||||||
|  |        */ | ||||||
|  |       npmConfig?: { | ||||||
|  |         scope?: string; | ||||||
|  |         alwaysAuth?: boolean; | ||||||
|  |       }; | ||||||
|  |     }; | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * Status of the registry connection | ||||||
|  |      */ | ||||||
|  |     status?: 'active' | 'inactive' | 'error' | 'unverified'; | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * Last error message if status is 'error' | ||||||
|  |      */ | ||||||
|  |     lastError?: string; | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * Timestamp when the registry was last successfully verified | ||||||
|  |      */ | ||||||
|  |     lastVerified?: number; | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * Timestamp when the registry was created | ||||||
|  |      */ | ||||||
|  |     createdAt?: number; | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * Timestamp when the registry was last updated | ||||||
|  |      */ | ||||||
|  |     updatedAt?: number; | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
| @@ -50,6 +50,7 @@ export interface IReq_UpdateRegistry extends plugins.typedrequestInterfaces.impl | |||||||
|   method: 'updateExternalRegistry'; |   method: 'updateExternalRegistry'; | ||||||
|   request: { |   request: { | ||||||
|     identity: userInterfaces.IIdentity; |     identity: userInterfaces.IIdentity; | ||||||
|  |     registryId: string; | ||||||
|     registryData: data.IExternalRegistry['data']; |     registryData: data.IExternalRegistry['data']; | ||||||
|   }; |   }; | ||||||
|   response: { |   response: { | ||||||
| @@ -69,4 +70,20 @@ export interface IReq_DeleteRegistryById extends plugins.typedrequestInterfaces. | |||||||
|   response: { |   response: { | ||||||
|     ok: boolean; |     ok: boolean; | ||||||
|   }; |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface IReq_VerifyRegistry extends plugins.typedrequestInterfaces.implementsTR< | ||||||
|  |   plugins.typedrequestInterfaces.ITypedRequest, | ||||||
|  |   IReq_VerifyRegistry | ||||||
|  | > { | ||||||
|  |   method: 'verifyExternalRegistry'; | ||||||
|  |   request: { | ||||||
|  |     identity: userInterfaces.IIdentity; | ||||||
|  |     registryId: string; | ||||||
|  |   }; | ||||||
|  |   response: { | ||||||
|  |     success: boolean; | ||||||
|  |     message?: string; | ||||||
|  |     registry?: data.IExternalRegistry; | ||||||
|  |   }; | ||||||
| } | } | ||||||
| @@ -47,6 +47,7 @@ export interface IDataState { | |||||||
|   secretGroups?: plugins.interfaces.data.ISecretGroup[]; |   secretGroups?: plugins.interfaces.data.ISecretGroup[]; | ||||||
|   secretBundles?: plugins.interfaces.data.ISecretBundle[]; |   secretBundles?: plugins.interfaces.data.ISecretBundle[]; | ||||||
|   clusters?: plugins.interfaces.data.ICluster[]; |   clusters?: plugins.interfaces.data.ICluster[]; | ||||||
|  |   externalRegistries?: plugins.interfaces.data.IExternalRegistry[]; | ||||||
|   images?: any[]; |   images?: any[]; | ||||||
|   services?: plugins.interfaces.data.IService[]; |   services?: plugins.interfaces.data.IService[]; | ||||||
|   deployments?: plugins.interfaces.data.IDeployment[]; |   deployments?: plugins.interfaces.data.IDeployment[]; | ||||||
| @@ -64,6 +65,7 @@ export const dataState = await appstate.getStatePart<IDataState>( | |||||||
|     secretGroups: [], |     secretGroups: [], | ||||||
|     secretBundles: [], |     secretBundles: [], | ||||||
|     clusters: [], |     clusters: [], | ||||||
|  |     externalRegistries: [], | ||||||
|     images: [], |     images: [], | ||||||
|     services: [], |     services: [], | ||||||
|     deployments: [], |     deployments: [], | ||||||
| @@ -138,6 +140,28 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => { | |||||||
|     clusters: responseClusters.clusters, |     clusters: responseClusters.clusters, | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   // External Registries | ||||||
|  |   const trGetExternalRegistries = | ||||||
|  |     new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.externalRegistry.IReq_GetRegistries>( | ||||||
|  |       '/typedrequest', | ||||||
|  |       'getExternalRegistries' | ||||||
|  |     ); | ||||||
|  |   try { | ||||||
|  |     const responseExternalRegistries = await trGetExternalRegistries.fire({ | ||||||
|  |       identity: loginStatePart.getState().identity, | ||||||
|  |     }); | ||||||
|  |     currentState = { | ||||||
|  |       ...currentState, | ||||||
|  |       externalRegistries: responseExternalRegistries?.registries || [], | ||||||
|  |     }; | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('Failed to fetch external registries:', error); | ||||||
|  |     currentState = { | ||||||
|  |       ...currentState, | ||||||
|  |       externalRegistries: [], | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   // Services |   // Services | ||||||
|   const trGetServices = |   const trGetServices = | ||||||
|     new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.service.IRequest_Any_Cloudly_GetServices>( |     new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.service.IRequest_Any_Cloudly_GetServices>( | ||||||
| @@ -559,6 +583,86 @@ export const verifyDomainAction = dataState.createAction( | |||||||
|   } |   } | ||||||
| ); | ); | ||||||
|  |  | ||||||
|  | // External Registry Actions | ||||||
|  | export const createExternalRegistryAction = dataState.createAction( | ||||||
|  |   async (statePartArg, payloadArg: { registryData: plugins.interfaces.data.IExternalRegistry['data'] }) => { | ||||||
|  |     let currentState = statePartArg.getState(); | ||||||
|  |     const trCreateRegistry = | ||||||
|  |       new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.externalRegistry.IReq_CreateRegistry>( | ||||||
|  |         '/typedrequest', | ||||||
|  |         'createExternalRegistry' | ||||||
|  |       ); | ||||||
|  |     const response = await trCreateRegistry.fire({ | ||||||
|  |       identity: loginStatePart.getState().identity, | ||||||
|  |       registryData: payloadArg.registryData, | ||||||
|  |     }); | ||||||
|  |     currentState = await dataState.dispatchAction(getAllDataAction, null); | ||||||
|  |     return currentState; | ||||||
|  |   } | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | export const updateExternalRegistryAction = dataState.createAction( | ||||||
|  |   async (statePartArg, payloadArg: { registryId: string; registryData: plugins.interfaces.data.IExternalRegistry['data'] }) => { | ||||||
|  |     let currentState = statePartArg.getState(); | ||||||
|  |     const trUpdateRegistry = | ||||||
|  |       new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.externalRegistry.IReq_UpdateRegistry>( | ||||||
|  |         '/typedrequest', | ||||||
|  |         'updateExternalRegistry' | ||||||
|  |       ); | ||||||
|  |     const response = await trUpdateRegistry.fire({ | ||||||
|  |       identity: loginStatePart.getState().identity, | ||||||
|  |       registryId: payloadArg.registryId, | ||||||
|  |       registryData: payloadArg.registryData, | ||||||
|  |     }); | ||||||
|  |     currentState = await dataState.dispatchAction(getAllDataAction, null); | ||||||
|  |     return currentState; | ||||||
|  |   } | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | export const deleteExternalRegistryAction = dataState.createAction( | ||||||
|  |   async (statePartArg, payloadArg: { registryId: string }) => { | ||||||
|  |     let currentState = statePartArg.getState(); | ||||||
|  |     const trDeleteRegistry = | ||||||
|  |       new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.externalRegistry.IReq_DeleteRegistryById>( | ||||||
|  |         '/typedrequest', | ||||||
|  |         'deleteExternalRegistryById' | ||||||
|  |       ); | ||||||
|  |     const response = await trDeleteRegistry.fire({ | ||||||
|  |       identity: loginStatePart.getState().identity, | ||||||
|  |       registryId: payloadArg.registryId, | ||||||
|  |     }); | ||||||
|  |     currentState = await dataState.dispatchAction(getAllDataAction, null); | ||||||
|  |     return currentState; | ||||||
|  |   } | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | export const verifyExternalRegistryAction = dataState.createAction( | ||||||
|  |   async (statePartArg, payloadArg: { registryId: string }) => { | ||||||
|  |     let currentState = statePartArg.getState(); | ||||||
|  |     const trVerifyRegistry = | ||||||
|  |       new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.externalRegistry.IReq_VerifyRegistry>( | ||||||
|  |         '/typedrequest', | ||||||
|  |         'verifyExternalRegistry' | ||||||
|  |       ); | ||||||
|  |     const response = await trVerifyRegistry.fire({ | ||||||
|  |       identity: loginStatePart.getState().identity, | ||||||
|  |       registryId: payloadArg.registryId, | ||||||
|  |     }); | ||||||
|  |      | ||||||
|  |     if (response.success && response.registry) { | ||||||
|  |       // Update the registry in the state with the verified status | ||||||
|  |       currentState = { | ||||||
|  |         ...currentState, | ||||||
|  |         externalRegistries: currentState.externalRegistries?.map(reg =>  | ||||||
|  |           reg.id === payloadArg.registryId ? response.registry : reg | ||||||
|  |         ) || [], | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     return currentState; | ||||||
|  |   } | ||||||
|  | ); | ||||||
|  |  | ||||||
| // cluster | // cluster | ||||||
| export const addClusterAction = dataState.createAction( | export const addClusterAction = dataState.createAction( | ||||||
|   async ( |   async ( | ||||||
|   | |||||||
| @@ -18,22 +18,63 @@ export class CloudlyViewExternalRegistries extends DeesElement { | |||||||
|   private data: appstate.IDataState = { |   private data: appstate.IDataState = { | ||||||
|     secretGroups: [], |     secretGroups: [], | ||||||
|     secretBundles: [], |     secretBundles: [], | ||||||
|  |     externalRegistries: [], | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   constructor() { |   constructor() { | ||||||
|     super(); |     super(); | ||||||
|     const subecription = appstate.dataState |     const subscription = appstate.dataState | ||||||
|       .select((stateArg) => stateArg) |       .select((stateArg) => stateArg) | ||||||
|       .subscribe((dataArg) => { |       .subscribe((dataArg) => { | ||||||
|         this.data = dataArg; |         this.data = dataArg; | ||||||
|       }); |       }); | ||||||
|     this.rxSubscriptions.push(subecription); |     this.rxSubscriptions.push(subscription); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async connectedCallback() { | ||||||
|  |     super.connectedCallback(); | ||||||
|  |     // Load external registries | ||||||
|  |     await appstate.dataState.dispatchAction(appstate.getAllDataAction, {}); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public static styles = [ |   public static styles = [ | ||||||
|     cssManager.defaultStyles, |     cssManager.defaultStyles, | ||||||
|     shared.viewHostCss, |     shared.viewHostCss, | ||||||
|     css`  |     css` | ||||||
|  |       .status-badge { | ||||||
|  |         display: inline-block; | ||||||
|  |         padding: 2px 8px; | ||||||
|  |         border-radius: 4px; | ||||||
|  |         font-size: 0.85em; | ||||||
|  |         font-weight: 500; | ||||||
|  |         color: white; | ||||||
|  |       } | ||||||
|  |       .status-active { background: #4CAF50; } | ||||||
|  |       .status-inactive { background: #9E9E9E; } | ||||||
|  |       .status-error { background: #f44336; } | ||||||
|  |       .status-unverified { background: #FF9800; } | ||||||
|  |        | ||||||
|  |       .type-badge { | ||||||
|  |         display: inline-block; | ||||||
|  |         padding: 2px 8px; | ||||||
|  |         border-radius: 4px; | ||||||
|  |         font-size: 0.85em; | ||||||
|  |         font-weight: 500; | ||||||
|  |         color: white; | ||||||
|  |       } | ||||||
|  |       .type-docker { background: #2196F3; } | ||||||
|  |       .type-npm { background: #CB3837; } | ||||||
|  |        | ||||||
|  |       .default-badge { | ||||||
|  |         display: inline-block; | ||||||
|  |         padding: 2px 8px; | ||||||
|  |         border-radius: 4px; | ||||||
|  |         font-size: 0.85em; | ||||||
|  |         font-weight: 500; | ||||||
|  |         background: #673AB7; | ||||||
|  |         color: white; | ||||||
|  |         margin-left: 8px; | ||||||
|  |       } | ||||||
|     `, |     `, | ||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
| @@ -42,43 +83,125 @@ export class CloudlyViewExternalRegistries extends DeesElement { | |||||||
|       <cloudly-sectionheading>External Registries</cloudly-sectionheading> |       <cloudly-sectionheading>External Registries</cloudly-sectionheading> | ||||||
|       <dees-table |       <dees-table | ||||||
|         .heading1=${'External Registries'} |         .heading1=${'External Registries'} | ||||||
|         .heading2=${'decoded in client'} |         .heading2=${'Configure external Docker and NPM registries'} | ||||||
|         .data=${this.data.deployments} |         .data=${this.data.externalRegistries || []} | ||||||
|         .displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => { |         .displayFunction=${(registry: plugins.interfaces.data.IExternalRegistry) => { | ||||||
|           return { |           return { | ||||||
|             id: itemArg.id, |             Name: html`${registry.data.name}${registry.data.isDefault ? html`<span class="default-badge">DEFAULT</span>` : ''}`, | ||||||
|             serverAmount: itemArg.data.servers.length, |             Type: html`<span class="type-badge type-${registry.data.type}">${registry.data.type.toUpperCase()}</span>`, | ||||||
|  |             URL: registry.data.url, | ||||||
|  |             Username: registry.data.username, | ||||||
|  |             Namespace: registry.data.namespace || '-', | ||||||
|  |             Status: html`<span class="status-badge status-${registry.data.status || 'unverified'}">${(registry.data.status || 'unverified').toUpperCase()}</span>`, | ||||||
|  |             'Last Verified': registry.data.lastVerified ? new Date(registry.data.lastVerified).toLocaleString() : 'Never', | ||||||
|           }; |           }; | ||||||
|         }} |         }} | ||||||
|         .dataActions=${[ |         .dataActions=${[ | ||||||
|           { |           { | ||||||
|             name: 'add configBundle', |             name: 'Add Registry', | ||||||
|             iconName: 'plus', |             iconName: 'plus', | ||||||
|             type: ['header', 'footer'], |             type: ['header', 'footer'], | ||||||
|             actionFunc: async (dataActionArg) => { |             actionFunc: async (dataActionArg) => { | ||||||
|               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ |               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||||
|                 heading: 'Add ConfigBundle', |                 heading: 'Add External Registry', | ||||||
|                 content: html` |                 content: html` | ||||||
|                   <dees-form> |                   <dees-form> | ||||||
|                     <dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text> |                     <dees-input-dropdown  | ||||||
|                     <dees-input-text |                       .key=${'type'}  | ||||||
|                       .key=${'data.secretGroupIds'} |                       .label=${'Registry Type'}  | ||||||
|                       .label=${'secretGroupIds'} |                       .options=${[ | ||||||
|                       .value=${''} |                         {key: 'docker', option: 'Docker'}, | ||||||
|                     ></dees-input-text> |                         {key: 'npm', option: 'NPM'} | ||||||
|                     <dees-input-text |                       ]} | ||||||
|                       .key=${'data.includedTags'} |                       .value=${'docker'} | ||||||
|                       .label=${'includedTags'} |                       .required=${true}> | ||||||
|                       .value=${''} |                     </dees-input-dropdown> | ||||||
|                     ></dees-input-text> |                     <dees-input-text  | ||||||
|  |                       .key=${'name'}  | ||||||
|  |                       .label=${'Registry Name'}  | ||||||
|  |                       .placeholder=${'My Docker Hub'} | ||||||
|  |                       .required=${true}> | ||||||
|  |                     </dees-input-text> | ||||||
|  |                     <dees-input-text  | ||||||
|  |                       .key=${'url'}  | ||||||
|  |                       .label=${'Registry URL'}  | ||||||
|  |                       .placeholder=${'https://index.docker.io/v2/ or registry.gitlab.com'} | ||||||
|  |                       .required=${true}> | ||||||
|  |                     </dees-input-text> | ||||||
|  |                     <dees-input-text  | ||||||
|  |                       .key=${'username'}  | ||||||
|  |                       .label=${'Username'}  | ||||||
|  |                       .placeholder=${'username'} | ||||||
|  |                       .required=${true}> | ||||||
|  |                     </dees-input-text> | ||||||
|  |                     <dees-input-text  | ||||||
|  |                       .key=${'password'}  | ||||||
|  |                       .label=${'Password / Access Token'}  | ||||||
|  |                       .placeholder=${'••••••••'} | ||||||
|  |                       .isPasswordBool=${true} | ||||||
|  |                       .required=${true}> | ||||||
|  |                     </dees-input-text> | ||||||
|  |                     <dees-input-text  | ||||||
|  |                       .key=${'namespace'}  | ||||||
|  |                       .label=${'Namespace/Organization (optional)'}  | ||||||
|  |                       .placeholder=${'myorg'}> | ||||||
|  |                     </dees-input-text> | ||||||
|  |                     <dees-input-text  | ||||||
|  |                       .key=${'description'}  | ||||||
|  |                       .label=${'Description (optional)'}  | ||||||
|  |                       .placeholder=${'Production Docker registry'}> | ||||||
|  |                     </dees-input-text> | ||||||
|  |                     <dees-input-dropdown  | ||||||
|  |                       .key=${'authType'}  | ||||||
|  |                       .label=${'Authentication Type'}  | ||||||
|  |                       .options=${[ | ||||||
|  |                         {key: 'basic', option: 'Basic Auth'}, | ||||||
|  |                         {key: 'token', option: 'Token'}, | ||||||
|  |                         {key: 'oauth2', option: 'OAuth2'} | ||||||
|  |                       ]} | ||||||
|  |                       .value=${'basic'}> | ||||||
|  |                     </dees-input-dropdown> | ||||||
|  |                     <dees-input-checkbox  | ||||||
|  |                       .key=${'isDefault'}  | ||||||
|  |                       .label=${'Set as default registry for this type'}  | ||||||
|  |                       .value=${false}> | ||||||
|  |                     </dees-input-checkbox> | ||||||
|  |                     <dees-input-checkbox  | ||||||
|  |                       .key=${'insecure'}  | ||||||
|  |                       .label=${'Allow insecure connections (HTTP/self-signed certs)'}  | ||||||
|  |                       .value=${false}> | ||||||
|  |                     </dees-input-checkbox> | ||||||
|                   </dees-form> |                   </dees-form> | ||||||
|                 `, |                 `, | ||||||
|                 menuOptions: [ |                 menuOptions: [ | ||||||
|                   { name: 'create', action: async (modalArg) => {} }, |  | ||||||
|                   { |                   { | ||||||
|                     name: 'cancel', |                     name: 'Create Registry', | ||||||
|                     action: async (modalArg) => { |                     action: async (modalArg) => { | ||||||
|                       modalArg.destroy(); |                       const form = modalArg.shadowRoot.querySelector('dees-form') as any; | ||||||
|  |                       const formData = await form.gatherData(); | ||||||
|  |                        | ||||||
|  |                       await appstate.dataState.dispatchAction(appstate.createExternalRegistryAction, { | ||||||
|  |                         registryData: { | ||||||
|  |                           type: formData.type, | ||||||
|  |                           name: formData.name, | ||||||
|  |                           url: formData.url, | ||||||
|  |                           username: formData.username, | ||||||
|  |                           password: formData.password, | ||||||
|  |                           namespace: formData.namespace || undefined, | ||||||
|  |                           description: formData.description || undefined, | ||||||
|  |                           authType: formData.authType, | ||||||
|  |                           isDefault: formData.isDefault, | ||||||
|  |                           insecure: formData.insecure, | ||||||
|  |                         }, | ||||||
|  |                       }); | ||||||
|  |                        | ||||||
|  |                       await modalArg.destroy(); | ||||||
|  |                     }, | ||||||
|  |                   }, | ||||||
|  |                   { | ||||||
|  |                     name: 'Cancel', | ||||||
|  |                     action: async (modalArg) => { | ||||||
|  |                       await modalArg.destroy(); | ||||||
|                     }, |                     }, | ||||||
|                   }, |                   }, | ||||||
|                 ], |                 ], | ||||||
| @@ -86,34 +209,218 @@ export class CloudlyViewExternalRegistries extends DeesElement { | |||||||
|             }, |             }, | ||||||
|           }, |           }, | ||||||
|           { |           { | ||||||
|             name: 'delete', |             name: 'Edit', | ||||||
|             iconName: 'trash', |             iconName: 'edit', | ||||||
|             type: ['contextmenu', 'inRow'], |             type: ['contextmenu', 'inRow'], | ||||||
|             actionFunc: async (actionDataArg) => { |             actionFunc: async (actionDataArg) => { | ||||||
|               plugins.deesCatalog.DeesModal.createAndShow({ |               const registry = actionDataArg.item as plugins.interfaces.data.IExternalRegistry; | ||||||
|                 heading: `Delete ConfigBundle ${actionDataArg.item.id}`, |               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||||
|  |                 heading: `Edit Registry: ${registry.data.name}`, | ||||||
|                 content: html` |                 content: html` | ||||||
|                   <div style="text-align:center"> |                   <dees-form> | ||||||
|                     Do you really want to delete the ConfigBundle? |                     <dees-input-dropdown  | ||||||
|  |                       .key=${'type'}  | ||||||
|  |                       .label=${'Registry Type'}  | ||||||
|  |                       .options=${[ | ||||||
|  |                         {key: 'docker', option: 'Docker'}, | ||||||
|  |                         {key: 'npm', option: 'NPM'} | ||||||
|  |                       ]} | ||||||
|  |                       .value=${registry.data.type} | ||||||
|  |                       .required=${true}> | ||||||
|  |                     </dees-input-dropdown> | ||||||
|  |                     <dees-input-text  | ||||||
|  |                       .key=${'name'}  | ||||||
|  |                       .label=${'Registry Name'}  | ||||||
|  |                       .value=${registry.data.name} | ||||||
|  |                       .required=${true}> | ||||||
|  |                     </dees-input-text> | ||||||
|  |                     <dees-input-text  | ||||||
|  |                       .key=${'url'}  | ||||||
|  |                       .label=${'Registry URL'}  | ||||||
|  |                       .value=${registry.data.url} | ||||||
|  |                       .required=${true}> | ||||||
|  |                     </dees-input-text> | ||||||
|  |                     <dees-input-text  | ||||||
|  |                       .key=${'username'}  | ||||||
|  |                       .label=${'Username'}  | ||||||
|  |                       .value=${registry.data.username} | ||||||
|  |                       .required=${true}> | ||||||
|  |                     </dees-input-text> | ||||||
|  |                     <dees-input-text  | ||||||
|  |                       .key=${'password'}  | ||||||
|  |                       .label=${'Password / Access Token (leave empty to keep current)'}  | ||||||
|  |                       .placeholder=${'••••••••'} | ||||||
|  |                       .isPasswordBool=${true}> | ||||||
|  |                     </dees-input-text> | ||||||
|  |                     <dees-input-text  | ||||||
|  |                       .key=${'namespace'}  | ||||||
|  |                       .label=${'Namespace/Organization (optional)'}  | ||||||
|  |                       .value=${registry.data.namespace || ''}> | ||||||
|  |                     </dees-input-text> | ||||||
|  |                     <dees-input-text  | ||||||
|  |                       .key=${'description'}  | ||||||
|  |                       .label=${'Description (optional)'}  | ||||||
|  |                       .value=${registry.data.description || ''}> | ||||||
|  |                     </dees-input-text> | ||||||
|  |                     <dees-input-dropdown  | ||||||
|  |                       .key=${'authType'}  | ||||||
|  |                       .label=${'Authentication Type'}  | ||||||
|  |                       .options=${[ | ||||||
|  |                         {key: 'basic', option: 'Basic Auth'}, | ||||||
|  |                         {key: 'token', option: 'Token'}, | ||||||
|  |                         {key: 'oauth2', option: 'OAuth2'} | ||||||
|  |                       ]} | ||||||
|  |                       .value=${registry.data.authType || 'basic'}> | ||||||
|  |                     </dees-input-dropdown> | ||||||
|  |                     <dees-input-checkbox  | ||||||
|  |                       .key=${'isDefault'}  | ||||||
|  |                       .label=${'Set as default registry for this type'}  | ||||||
|  |                       .value=${registry.data.isDefault || false}> | ||||||
|  |                     </dees-input-checkbox> | ||||||
|  |                     <dees-input-checkbox  | ||||||
|  |                       .key=${'insecure'}  | ||||||
|  |                       .label=${'Allow insecure connections (HTTP/self-signed certs)'}  | ||||||
|  |                       .value=${registry.data.insecure || false}> | ||||||
|  |                     </dees-input-checkbox> | ||||||
|  |                   </dees-form> | ||||||
|  |                 `, | ||||||
|  |                 menuOptions: [ | ||||||
|  |                   { | ||||||
|  |                     name: 'Update Registry', | ||||||
|  |                     action: async (modalArg) => { | ||||||
|  |                       const form = modalArg.shadowRoot.querySelector('dees-form') as any; | ||||||
|  |                       const formData = await form.gatherData(); | ||||||
|  |                        | ||||||
|  |                       const updateData: any = { | ||||||
|  |                         type: formData.type, | ||||||
|  |                         name: formData.name, | ||||||
|  |                         url: formData.url, | ||||||
|  |                         username: formData.username, | ||||||
|  |                         namespace: formData.namespace || undefined, | ||||||
|  |                         description: formData.description || undefined, | ||||||
|  |                         authType: formData.authType, | ||||||
|  |                         isDefault: formData.isDefault, | ||||||
|  |                         insecure: formData.insecure, | ||||||
|  |                       }; | ||||||
|  |                        | ||||||
|  |                       // Only include password if it was changed | ||||||
|  |                       if (formData.password) { | ||||||
|  |                         updateData.password = formData.password; | ||||||
|  |                       } else { | ||||||
|  |                         updateData.password = registry.data.password; | ||||||
|  |                       } | ||||||
|  |                        | ||||||
|  |                       await appstate.dataState.dispatchAction(appstate.updateExternalRegistryAction, { | ||||||
|  |                         registryId: registry.id, | ||||||
|  |                         registryData: updateData, | ||||||
|  |                       }); | ||||||
|  |                        | ||||||
|  |                       await modalArg.destroy(); | ||||||
|  |                     }, | ||||||
|  |                   }, | ||||||
|  |                   { | ||||||
|  |                     name: 'Cancel', | ||||||
|  |                     action: async (modalArg) => { | ||||||
|  |                       await modalArg.destroy(); | ||||||
|  |                     }, | ||||||
|  |                   }, | ||||||
|  |                 ], | ||||||
|  |               }); | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             name: 'Test Connection', | ||||||
|  |             iconName: 'check-circle', | ||||||
|  |             type: ['contextmenu'], | ||||||
|  |             actionFunc: async (actionDataArg) => { | ||||||
|  |               const registry = actionDataArg.item as plugins.interfaces.data.IExternalRegistry; | ||||||
|  |                | ||||||
|  |               // Show loading modal | ||||||
|  |               const loadingModal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||||
|  |                 heading: 'Testing Registry Connection', | ||||||
|  |                 content: html` | ||||||
|  |                   <div style="text-align: center; padding: 20px;"> | ||||||
|  |                     <dees-spinner></dees-spinner> | ||||||
|  |                     <p style="margin-top: 20px;">Testing connection to ${registry.data.name}...</p> | ||||||
|                   </div> |                   </div> | ||||||
|                   <div |                 `, | ||||||
|                     style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;" |                 menuOptions: [], | ||||||
|                   > |               }); | ||||||
|                     ${actionDataArg.item.id} |                | ||||||
|  |               // Test the connection | ||||||
|  |               await appstate.dataState.dispatchAction(appstate.verifyExternalRegistryAction, { | ||||||
|  |                 registryId: registry.id, | ||||||
|  |               }); | ||||||
|  |                | ||||||
|  |               // Close loading modal | ||||||
|  |               await loadingModal.destroy(); | ||||||
|  |                | ||||||
|  |               // Get updated registry | ||||||
|  |               const updatedRegistry = this.data.externalRegistries?.find(r => r.id === registry.id); | ||||||
|  |                | ||||||
|  |               // Show result modal | ||||||
|  |               const resultModal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||||
|  |                 heading: 'Connection Test Result', | ||||||
|  |                 content: html` | ||||||
|  |                   <div style="text-align: center; padding: 20px;"> | ||||||
|  |                     ${updatedRegistry?.data.status === 'active' ? html` | ||||||
|  |                       <div style="color: #4CAF50; font-size: 48px;">✓</div> | ||||||
|  |                       <p style="margin-top: 20px; color: #4CAF50;">Connection successful!</p> | ||||||
|  |                     ` : html` | ||||||
|  |                       <div style="color: #f44336; font-size: 48px;">✗</div> | ||||||
|  |                       <p style="margin-top: 20px; color: #f44336;">Connection failed!</p> | ||||||
|  |                       ${updatedRegistry?.data.lastError ? html` | ||||||
|  |                         <p style="margin-top: 10px; font-size: 0.9em; color: #999;"> | ||||||
|  |                           Error: ${updatedRegistry.data.lastError} | ||||||
|  |                         </p> | ||||||
|  |                       ` : ''} | ||||||
|  |                     `} | ||||||
|                   </div> |                   </div> | ||||||
|                 `, |                 `, | ||||||
|                 menuOptions: [ |                 menuOptions: [ | ||||||
|                   { |                   { | ||||||
|                     name: 'cancel', |                     name: 'OK', | ||||||
|  |                     action: async (modalArg) => { | ||||||
|  |                       await modalArg.destroy(); | ||||||
|  |                     }, | ||||||
|  |                   }, | ||||||
|  |                 ], | ||||||
|  |               }); | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             name: 'Delete', | ||||||
|  |             iconName: 'trash', | ||||||
|  |             type: ['contextmenu'], | ||||||
|  |             actionFunc: async (actionDataArg) => { | ||||||
|  |               const registry = actionDataArg.item as plugins.interfaces.data.IExternalRegistry; | ||||||
|  |               plugins.deesCatalog.DeesModal.createAndShow({ | ||||||
|  |                 heading: `Delete Registry: ${registry.data.name}`, | ||||||
|  |                 content: html` | ||||||
|  |                   <div style="text-align:center"> | ||||||
|  |                     <p>Do you really want to delete this external registry?</p> | ||||||
|  |                     <p style="color: #999; font-size: 0.9em; margin-top: 10px;"> | ||||||
|  |                       This will remove all stored credentials and configuration. | ||||||
|  |                     </p> | ||||||
|  |                   </div> | ||||||
|  |                   <div | ||||||
|  |                     style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;" | ||||||
|  |                   > | ||||||
|  |                     ${registry.data.name} (${registry.data.url}) | ||||||
|  |                   </div> | ||||||
|  |                 `, | ||||||
|  |                 menuOptions: [ | ||||||
|  |                   { | ||||||
|  |                     name: 'Cancel', | ||||||
|                     action: async (modalArg) => { |                     action: async (modalArg) => { | ||||||
|                       await modalArg.destroy(); |                       await modalArg.destroy(); | ||||||
|                     }, |                     }, | ||||||
|                   }, |                   }, | ||||||
|                   { |                   { | ||||||
|                     name: 'delete', |                     name: 'Delete', | ||||||
|                     action: async (modalArg) => { |                     action: async (modalArg) => { | ||||||
|                       appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { |                       await appstate.dataState.dispatchAction(appstate.deleteExternalRegistryAction, { | ||||||
|                         configBundleId: actionDataArg.item.id, |                         registryId: registry.id, | ||||||
|                       }); |                       }); | ||||||
|                       await modalArg.destroy(); |                       await modalArg.destroy(); | ||||||
|                     }, |                     }, | ||||||
| @@ -126,4 +433,4 @@ export class CloudlyViewExternalRegistries extends DeesElement { | |||||||
|       ></dees-table> |       ></dees-table> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
		Reference in New Issue
	
	Block a user