Loading portal-gui/src/app/config/api.ts +4 −0 Changes for portal-gui/src/app/config/api.ts: 4 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -4,6 +4,10 @@ export const oeg = { jsonUrl: '/api/oeg/openapi.json', }; export const federation = { baseUrl: '/api/oeg/platform/v1/federation', }; export const OEGpaths = { application: '/apps', edgeCloudZones: '/edge-cloud-zones', Loading portal-gui/src/services/federation/federation-request.ts 0 → 100644 +63 −0 Changes for portal-gui/src/services/federation/federation-request.ts: 63 added lines, 0 removed lines. Original line number Diff line number Diff line import { federation } from '@/app/config/api'; import { apiFetch } from '@/lib/api-fetch'; import type { ProblemDetails } from '@/types/federation'; export class FederationRequestError extends Error { readonly status: number; readonly problem: ProblemDetails | null; constructor(message: string, status: number, problem: ProblemDetails | null) { super(message); this.status = status; this.problem = problem; } get code(): string | null { return this.problem?.type?.split(':').at(-1) ?? null; } } export function isProblem(error: unknown, code: string): boolean { return error instanceof FederationRequestError && error.code === code; } function parseBody(text: string): unknown { if (!text) return undefined; try { return JSON.parse(text); } catch { return text; } } function errorMessage(status: number, problem: ProblemDetails | null): string { const message = problem?.detail ?? problem?.title ?? `Request failed (${status})`; const params = problem?.invalidParams ?.map(({ param, reason }) => (reason ? `${param}: ${reason}` : param)) .join('; '); return params ? `${message} (${params})` : message; } export async function federationRequest<T>( path: string, options?: RequestInit, ): Promise<T> { const response = await apiFetch(`${federation.baseUrl}${path}`, { ...options, headers: { 'Content-Type': 'application/json', ...options?.headers }, }); const body = parseBody(await response.text()); if (!response.ok) { const problem = body && typeof body === 'object' ? (body as ProblemDetails) : null; throw new FederationRequestError( errorMessage(response.status, problem), response.status, problem, ); } return body as T; } portal-gui/src/services/federation/federations.ts 0 → 100644 +103 −0 Changes for portal-gui/src/services/federation/federations.ts: 103 added lines, 0 removed lines. Original line number Diff line number Diff line import type { FederationContextIdResponse, FederationDetails, FederationRequestData, FederationResponseData, ZoneRegisteredData, ZoneRegistrationResponseData, } from '@/types/federation'; import { federationRequest, isProblem } from './federation-request'; const federations = (partnerOpId: string) => `/partners/${encodeURIComponent(partnerOpId)}/federations`; const federation = (partnerOpId: string, contextId: string) => `${federations(partnerOpId)}/${encodeURIComponent(contextId)}`; const zones = (partnerOpId: string, contextId: string) => `${federation(partnerOpId, contextId)}/zones`; const zone = (partnerOpId: string, contextId: string, zoneId: string) => `${zones(partnerOpId, contextId)}/${encodeURIComponent(zoneId)}`; // null when there is no federation with this partner yet. export async function findFederationContextId( partnerOpId: string, ): Promise<string | null> { try { const response = await federationRequest<FederationContextIdResponse>( federations(partnerOpId), ); return response.FederationContextId; } catch (err) { if (isProblem(err, 'federation-context-unknown')) return null; throw err; } } export function createFederation( partnerOpId: string, request: FederationRequestData, ): Promise<FederationResponseData> { return federationRequest<FederationResponseData>(federations(partnerOpId), { method: 'POST', body: JSON.stringify(request), }); } export function getFederationDetails( partnerOpId: string, contextId: string, ): Promise<FederationDetails> { return federationRequest<FederationDetails>( federation(partnerOpId, contextId), ); } export async function deleteFederation( partnerOpId: string, contextId: string, ): Promise<void> { await federationRequest(federation(partnerOpId, contextId), { method: 'DELETE', }); } export function subscribeZones( partnerOpId: string, contextId: string, zoneIds: string[], ): Promise<ZoneRegistrationResponseData> { return federationRequest<ZoneRegistrationResponseData>( zones(partnerOpId, contextId), { method: 'POST', body: JSON.stringify({ acceptedAvailabilityZones: zoneIds }), }, ); } export async function findZoneSubscription( partnerOpId: string, contextId: string, zoneId: string, ): Promise<ZoneRegisteredData | null> { try { return await federationRequest<ZoneRegisteredData>( zone(partnerOpId, contextId, zoneId), ); } catch (err) { if (isProblem(err, 'zone-not-subscribed')) return null; throw err; } } export async function unsubscribeZone( partnerOpId: string, contextId: string, zoneId: string, ): Promise<void> { await federationRequest(zone(partnerOpId, contextId, zoneId), { method: 'DELETE', }); } portal-gui/src/services/federation/partners.ts 0 → 100644 +30 −0 Changes for portal-gui/src/services/federation/partners.ts: 30 added lines, 0 removed lines. Original line number Diff line number Diff line import type { Partner, PartnerCreate, PartnerUpdate } from '@/types/federation'; import { federationRequest } from './federation-request'; const partner = (partnerOpId: string) => `/partners/${encodeURIComponent(partnerOpId)}`; export function listPartners(): Promise<Partner[]> { return federationRequest<Partner[]>('/partners'); } export function getPartner(partnerOpId: string): Promise<Partner> { return federationRequest<Partner>(partner(partnerOpId)); } export function registerPartner(request: PartnerCreate): Promise<Partner> { return federationRequest<Partner>('/partners', { method: 'POST', body: JSON.stringify(request), }); } export function updatePartner( partnerOpId: string, changes: PartnerUpdate, ): Promise<Partner> { return federationRequest<Partner>(partner(partnerOpId), { method: 'PATCH', body: JSON.stringify(changes), }); } portal-gui/src/types/federation.ts 0 → 100644 +172 −0 Changes for portal-gui/src/types/federation.ts: 172 added lines, 0 removed lines. Original line number Diff line number Diff line // Partner registration uses the Federation Manager's own schema (snake_case). export type PartnerStatus = | 'pending' | 'active' | 'suspended' | 'decommissioned'; export type Partner = { id: string; name: string; mcc_mnc: string; oauth2_client_id: string; base_url: string | null; our_client_id: string | null; our_client_secret_ref: string | null; token_endpoint: string | null; status: PartnerStatus; }; export type PartnerCreate = { name: string; mcc_mnc: string; oauth2_client_id: string; base_url: string; our_client_id?: string; our_client_secret_ref?: string; token_endpoint?: string; status?: 'pending' | 'active'; }; export type PartnerUpdate = Partial<{ name: string; oauth2_client_id: string; base_url: string; our_client_id: string | null; our_client_secret_ref: string | null; token_endpoint: string | null; status: PartnerStatus; }>; // Federation lifecycle bodies follow GSMA OPG.04 (camelCase). export type PlatformCapability = | 'homeRouting' | 'Anchoring' | 'serviceAPIs' | 'faultMgmt' | 'eventMgmt' | 'resourceMonitor'; export type MobileNetworkIds = { mcc?: string; mncs?: string[]; }; export type ServiceEndpoint = { port: number; fqdn?: string; ipv4Addresses?: string[]; ipv6Addresses?: string[]; }; export type ZoneDetails = { zoneId: string; geolocation: string; geographyDetails: string; }; export type FederationRequestData = { initialDate: string; partnerStatusLink: string; origOPFederationId?: string; origOPCountryCode?: string; origOPMobileNetworkCodes?: MobileNetworkIds; origOPFixedNetworkCodes?: string[]; }; export type FederationResponseData = { federationContextId: string; platformCaps: PlatformCapability[]; partnerOPFederationId?: string; partnerOPCountryCode?: string; edgeDiscoveryServiceEndPoint?: ServiceEndpoint; lcmServiceEndPoint?: ServiceEndpoint; partnerOPMobileNetworkCodes?: MobileNetworkIds; partnerOPFixedNetworkCodes?: string[]; offeredAvailabilityZones?: ZoneDetails[]; federationExpiryDate?: string; federationRenewalDate?: string; }; export type FederationDetails = { edgeDiscoveryServiceEndPoint: ServiceEndpoint; lcmServiceEndPoint: ServiceEndpoint; allowedMobileNetworkIds?: MobileNetworkIds; allowedFixedNetworkIds?: string[]; offeredAvailabilityZones?: ZoneDetails[]; }; export type FederationContextIdResponse = { FederationContextId: string; }; export type GpuInfo = { gpuVendorType: 'GPU_PROVIDER_NVIDIA' | 'GPU_PROVIDER_AMD'; gpuModeName: string; gpuMemory: number; numGPU: number; }; export type ComputeResourceInfo = { cpuArchType: 'ISA_X86_64' | 'ISA_ARM_64'; numCPU: string; memory: number; diskStorage?: number; gpu?: GpuInfo[]; vpu?: number; fpga?: number; cpuExclusivity?: boolean; }; export type Flavour = { flavourId: string; cpuArchType: 'ISA_X86' | 'ISA_X86_64' | 'ISA_ARM_64'; supportedOSTypes: { architecture: string; distribution: string; version: string; license: string; }[]; numCPU: number; memorySize: number; storageSize: number; gpu?: GpuInfo[]; }; export type ZoneRegisteredData = { zoneId: string; reservedComputeResources: ComputeResourceInfo[]; computeResourceQuotaLimits: ComputeResourceInfo[]; flavoursSupported: Flavour[]; networkResources?: { egressBandWidth: number; dedicatedNIC: number; supportSriov: boolean; supportDPDK: boolean; }; zoneServiceLevelObjsInfo?: { latencyRanges: { minLatency?: number; maxLatency?: number }; jitterRanges: { minJitter?: number; maxJitter?: number }; throughputRanges: { minThroughput?: number; maxThroughput?: number }; }; }; export type ZoneRegistrationRequestData = { acceptedAvailabilityZones: string[]; availZoneNotifLink?: string; }; export type ZoneRegistrationResponseData = { acceptedZoneResourceInfo: ZoneRegisteredData[]; }; export type ProblemDetails = { type?: string; title?: string; status?: number; detail?: string; cause?: string; invalidParams?: { param: string; reason?: string }[]; }; Loading
portal-gui/src/app/config/api.ts +4 −0 Changes for portal-gui/src/app/config/api.ts: 4 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -4,6 +4,10 @@ export const oeg = { jsonUrl: '/api/oeg/openapi.json', }; export const federation = { baseUrl: '/api/oeg/platform/v1/federation', }; export const OEGpaths = { application: '/apps', edgeCloudZones: '/edge-cloud-zones', Loading
portal-gui/src/services/federation/federation-request.ts 0 → 100644 +63 −0 Changes for portal-gui/src/services/federation/federation-request.ts: 63 added lines, 0 removed lines. Original line number Diff line number Diff line import { federation } from '@/app/config/api'; import { apiFetch } from '@/lib/api-fetch'; import type { ProblemDetails } from '@/types/federation'; export class FederationRequestError extends Error { readonly status: number; readonly problem: ProblemDetails | null; constructor(message: string, status: number, problem: ProblemDetails | null) { super(message); this.status = status; this.problem = problem; } get code(): string | null { return this.problem?.type?.split(':').at(-1) ?? null; } } export function isProblem(error: unknown, code: string): boolean { return error instanceof FederationRequestError && error.code === code; } function parseBody(text: string): unknown { if (!text) return undefined; try { return JSON.parse(text); } catch { return text; } } function errorMessage(status: number, problem: ProblemDetails | null): string { const message = problem?.detail ?? problem?.title ?? `Request failed (${status})`; const params = problem?.invalidParams ?.map(({ param, reason }) => (reason ? `${param}: ${reason}` : param)) .join('; '); return params ? `${message} (${params})` : message; } export async function federationRequest<T>( path: string, options?: RequestInit, ): Promise<T> { const response = await apiFetch(`${federation.baseUrl}${path}`, { ...options, headers: { 'Content-Type': 'application/json', ...options?.headers }, }); const body = parseBody(await response.text()); if (!response.ok) { const problem = body && typeof body === 'object' ? (body as ProblemDetails) : null; throw new FederationRequestError( errorMessage(response.status, problem), response.status, problem, ); } return body as T; }
portal-gui/src/services/federation/federations.ts 0 → 100644 +103 −0 Changes for portal-gui/src/services/federation/federations.ts: 103 added lines, 0 removed lines. Original line number Diff line number Diff line import type { FederationContextIdResponse, FederationDetails, FederationRequestData, FederationResponseData, ZoneRegisteredData, ZoneRegistrationResponseData, } from '@/types/federation'; import { federationRequest, isProblem } from './federation-request'; const federations = (partnerOpId: string) => `/partners/${encodeURIComponent(partnerOpId)}/federations`; const federation = (partnerOpId: string, contextId: string) => `${federations(partnerOpId)}/${encodeURIComponent(contextId)}`; const zones = (partnerOpId: string, contextId: string) => `${federation(partnerOpId, contextId)}/zones`; const zone = (partnerOpId: string, contextId: string, zoneId: string) => `${zones(partnerOpId, contextId)}/${encodeURIComponent(zoneId)}`; // null when there is no federation with this partner yet. export async function findFederationContextId( partnerOpId: string, ): Promise<string | null> { try { const response = await federationRequest<FederationContextIdResponse>( federations(partnerOpId), ); return response.FederationContextId; } catch (err) { if (isProblem(err, 'federation-context-unknown')) return null; throw err; } } export function createFederation( partnerOpId: string, request: FederationRequestData, ): Promise<FederationResponseData> { return federationRequest<FederationResponseData>(federations(partnerOpId), { method: 'POST', body: JSON.stringify(request), }); } export function getFederationDetails( partnerOpId: string, contextId: string, ): Promise<FederationDetails> { return federationRequest<FederationDetails>( federation(partnerOpId, contextId), ); } export async function deleteFederation( partnerOpId: string, contextId: string, ): Promise<void> { await federationRequest(federation(partnerOpId, contextId), { method: 'DELETE', }); } export function subscribeZones( partnerOpId: string, contextId: string, zoneIds: string[], ): Promise<ZoneRegistrationResponseData> { return federationRequest<ZoneRegistrationResponseData>( zones(partnerOpId, contextId), { method: 'POST', body: JSON.stringify({ acceptedAvailabilityZones: zoneIds }), }, ); } export async function findZoneSubscription( partnerOpId: string, contextId: string, zoneId: string, ): Promise<ZoneRegisteredData | null> { try { return await federationRequest<ZoneRegisteredData>( zone(partnerOpId, contextId, zoneId), ); } catch (err) { if (isProblem(err, 'zone-not-subscribed')) return null; throw err; } } export async function unsubscribeZone( partnerOpId: string, contextId: string, zoneId: string, ): Promise<void> { await federationRequest(zone(partnerOpId, contextId, zoneId), { method: 'DELETE', }); }
portal-gui/src/services/federation/partners.ts 0 → 100644 +30 −0 Changes for portal-gui/src/services/federation/partners.ts: 30 added lines, 0 removed lines. Original line number Diff line number Diff line import type { Partner, PartnerCreate, PartnerUpdate } from '@/types/federation'; import { federationRequest } from './federation-request'; const partner = (partnerOpId: string) => `/partners/${encodeURIComponent(partnerOpId)}`; export function listPartners(): Promise<Partner[]> { return federationRequest<Partner[]>('/partners'); } export function getPartner(partnerOpId: string): Promise<Partner> { return federationRequest<Partner>(partner(partnerOpId)); } export function registerPartner(request: PartnerCreate): Promise<Partner> { return federationRequest<Partner>('/partners', { method: 'POST', body: JSON.stringify(request), }); } export function updatePartner( partnerOpId: string, changes: PartnerUpdate, ): Promise<Partner> { return federationRequest<Partner>(partner(partnerOpId), { method: 'PATCH', body: JSON.stringify(changes), }); }
portal-gui/src/types/federation.ts 0 → 100644 +172 −0 Changes for portal-gui/src/types/federation.ts: 172 added lines, 0 removed lines. Original line number Diff line number Diff line // Partner registration uses the Federation Manager's own schema (snake_case). export type PartnerStatus = | 'pending' | 'active' | 'suspended' | 'decommissioned'; export type Partner = { id: string; name: string; mcc_mnc: string; oauth2_client_id: string; base_url: string | null; our_client_id: string | null; our_client_secret_ref: string | null; token_endpoint: string | null; status: PartnerStatus; }; export type PartnerCreate = { name: string; mcc_mnc: string; oauth2_client_id: string; base_url: string; our_client_id?: string; our_client_secret_ref?: string; token_endpoint?: string; status?: 'pending' | 'active'; }; export type PartnerUpdate = Partial<{ name: string; oauth2_client_id: string; base_url: string; our_client_id: string | null; our_client_secret_ref: string | null; token_endpoint: string | null; status: PartnerStatus; }>; // Federation lifecycle bodies follow GSMA OPG.04 (camelCase). export type PlatformCapability = | 'homeRouting' | 'Anchoring' | 'serviceAPIs' | 'faultMgmt' | 'eventMgmt' | 'resourceMonitor'; export type MobileNetworkIds = { mcc?: string; mncs?: string[]; }; export type ServiceEndpoint = { port: number; fqdn?: string; ipv4Addresses?: string[]; ipv6Addresses?: string[]; }; export type ZoneDetails = { zoneId: string; geolocation: string; geographyDetails: string; }; export type FederationRequestData = { initialDate: string; partnerStatusLink: string; origOPFederationId?: string; origOPCountryCode?: string; origOPMobileNetworkCodes?: MobileNetworkIds; origOPFixedNetworkCodes?: string[]; }; export type FederationResponseData = { federationContextId: string; platformCaps: PlatformCapability[]; partnerOPFederationId?: string; partnerOPCountryCode?: string; edgeDiscoveryServiceEndPoint?: ServiceEndpoint; lcmServiceEndPoint?: ServiceEndpoint; partnerOPMobileNetworkCodes?: MobileNetworkIds; partnerOPFixedNetworkCodes?: string[]; offeredAvailabilityZones?: ZoneDetails[]; federationExpiryDate?: string; federationRenewalDate?: string; }; export type FederationDetails = { edgeDiscoveryServiceEndPoint: ServiceEndpoint; lcmServiceEndPoint: ServiceEndpoint; allowedMobileNetworkIds?: MobileNetworkIds; allowedFixedNetworkIds?: string[]; offeredAvailabilityZones?: ZoneDetails[]; }; export type FederationContextIdResponse = { FederationContextId: string; }; export type GpuInfo = { gpuVendorType: 'GPU_PROVIDER_NVIDIA' | 'GPU_PROVIDER_AMD'; gpuModeName: string; gpuMemory: number; numGPU: number; }; export type ComputeResourceInfo = { cpuArchType: 'ISA_X86_64' | 'ISA_ARM_64'; numCPU: string; memory: number; diskStorage?: number; gpu?: GpuInfo[]; vpu?: number; fpga?: number; cpuExclusivity?: boolean; }; export type Flavour = { flavourId: string; cpuArchType: 'ISA_X86' | 'ISA_X86_64' | 'ISA_ARM_64'; supportedOSTypes: { architecture: string; distribution: string; version: string; license: string; }[]; numCPU: number; memorySize: number; storageSize: number; gpu?: GpuInfo[]; }; export type ZoneRegisteredData = { zoneId: string; reservedComputeResources: ComputeResourceInfo[]; computeResourceQuotaLimits: ComputeResourceInfo[]; flavoursSupported: Flavour[]; networkResources?: { egressBandWidth: number; dedicatedNIC: number; supportSriov: boolean; supportDPDK: boolean; }; zoneServiceLevelObjsInfo?: { latencyRanges: { minLatency?: number; maxLatency?: number }; jitterRanges: { minJitter?: number; maxJitter?: number }; throughputRanges: { minThroughput?: number; maxThroughput?: number }; }; }; export type ZoneRegistrationRequestData = { acceptedAvailabilityZones: string[]; availZoneNotifLink?: string; }; export type ZoneRegistrationResponseData = { acceptedZoneResourceInfo: ZoneRegisteredData[]; }; export type ProblemDetails = { type?: string; title?: string; status?: number; detail?: string; cause?: string; invalidParams?: { param: string; reason?: string }[]; };