import type {CloudAvailability, CloudProvider} from './types' /** * The single source of truth for which services the console knows about. * * Availability is NOT declared here — it is derived per cloud from whether an * adapter is registered (see `CloudProxyService.services`). This file only * carries the presentation metadata the nav needs, which is why adding a * service is one row here plus one adapter, with no frontend edit. * * `CloudServiceType` is derived from these keys, so a new row is also a new * route-addressable service type. Keeping the union closed means an unknown * slug is a 404 instead of a service that silently 501s later. */ /** Sidebar grouping. A flat list stops being usable past ~12 services. */ export type ServiceGroup = | 'Compute' | 'Developer Tools' | 'Storage' | 'Databases' | 'Networking' | 'Integration' | 'Analytics' | 'Provisioning' | 'Security' | 'Observability' export const SERVICE_GROUP_ORDER: ServiceGroup[] = [ 'Compute', 'Developer Tools', 'Storage', 'Databases', 'Networking', 'Integration', 'Analytics', 'Provisioning', 'Security', 'Observability', ] export interface ServiceCatalogMetadata { displayName: string /** Per-cloud display override, e.g. 'EKS' vs 'AKS' vs 'GKE'. */ displayNameByCloud?: Partial> /** Resolved to a component client-side; an unknown key degrades to a default. */ iconKey: string group: ServiceGroup /** Sort order within the group. */ order: number /** Defaults to the catalog key. A leading '/' marks a page outside Cloud Explorer. */ route?: string /** * Per-cloud route override. Needed when one category is served by a legacy * standalone page on one cloud and by Cloud Explorer on another — AWS secrets * still live at /secretsmanager while Azure Key Vault is a normal explorer route. */ routeByCloud?: Partial> /** * Escape hatch for a service whose UI predates the SPI, so availability * cannot come from the registry. Every entry here is migration debt — delete * the field once a real adapter exists. */ legacyAvailability?: Partial> } export const SERVICE_CATALOG = { compute: {displayName: 'Compute', iconKey: 'compute', group: 'Compute', order: 10}, k8s: { displayName: 'k8s Engine', displayNameByCloud: {aws: 'EKS', azure: 'AKS', gcp: 'GKE'}, iconKey: 'k8s', group: 'Compute', order: 20, }, serverless: {displayName: 'Serverless', iconKey: 'serverless', group: 'Compute', order: 30}, containers: { displayName: 'Containers', displayNameByCloud: {gcp: 'Cloud Run'}, iconKey: 'containers', group: 'Compute', order: 40, }, ecs: { displayName: 'ECS', iconKey: 'containers', group: 'Compute', order: 50, }, fargate: { displayName: 'Fargate', iconKey: 'containers', group: 'Compute', order: 51, }, lightsail: { displayName: 'Lightsail', iconKey: 'lightsail', group: 'Compute', order: 60, }, codebuild: { displayName: 'CodeBuild', iconKey: 'codebuild', group: 'Developer Tools', order: 10, }, codepipeline: { displayName: 'CodePipeline', iconKey: 'codepipeline', group: 'Developer Tools', order: 20, }, codedeploy: { displayName: 'CodeDeploy', iconKey: 'codedeploy', group: 'Developer Tools', order: 30, }, storage: {displayName: 'Storage', iconKey: 'storage', group: 'Storage', order: 10}, ecr: { displayName: 'Elastic Container Registry', displayNameByCloud: {aws: 'ECR'}, iconKey: 'ecr', group: 'Storage', order: 20, }, database: {displayName: 'Database', iconKey: 'database', group: 'Databases', order: 10}, nosql: { displayName: 'NoSQL', // Both labels are declared even though each arrives with its own adapter, // so this row reads the same whichever of the two lands first. A label for // a cloud with no adapter is inert: availability comes from the registry. displayNameByCloud: {aws: 'DynamoDB', azure: 'Cosmos DB NoSQL'}, iconKey: 'nosql', group: 'Databases', order: 20, }, elasticache: { displayName: 'ElastiCache', iconKey: 'database', group: 'Databases', order: 30, }, networking: {displayName: 'Networking', iconKey: 'networking', group: 'Networking', order: 10}, workflows: { displayName: 'Workflows', displayNameByCloud: {aws: 'Step Functions'}, iconKey: 'workflows', group: 'Integration', order: 30, }, loadbalancing: { displayName: 'Load Balancing', displayNameByCloud: {aws: 'ELB'}, iconKey: 'loadbalancing', group: 'Networking', order: 20, }, targetgroups: { displayName: 'Target Groups', displayNameByCloud: {aws: 'ELB Target Groups'}, iconKey: 'loadbalancing', group: 'Networking', order: 30, }, messaging: { displayName: 'Messaging', displayNameByCloud: {aws: 'SQS', gcp: 'Pub/Sub'}, iconKey: 'messaging', group: 'Integration', order: 10, }, notifications: { displayName: 'Notifications', displayNameByCloud: {aws: 'SNS'}, iconKey: 'notifications', group: 'Integration', order: 11, }, events: { displayName: 'Events', displayNameByCloud: {aws: 'EventBridge'}, iconKey: 'events', group: 'Integration', order: 15, }, msk: { displayName: 'Managed Streaming for Kafka', displayNameByCloud: {aws: 'MSK'}, iconKey: 'msk', group: 'Analytics', order: 16, legacyAvailability: {aws: 'available'}, }, 'msk-connect': { displayName: 'Managed Streaming for Kafka Connect', displayNameByCloud: {aws: 'MSK Connect'}, iconKey: 'msk-connect', group: 'Analytics', order: 17, legacyAvailability: {aws: 'available'}, }, kinesis: { displayName: 'Kinesis Data Streams', displayNameByCloud: {aws: 'Kinesis'}, iconKey: 'kinesis', group: 'Analytics', order: 18, }, identity: {displayName: 'Identity', iconKey: 'iam', group: 'Security', order: 5}, apigateway: {displayName: 'API Gateway', iconKey: 'apigateway', group: 'Integration', order: 10}, email: { displayName: 'Email', displayNameByCloud: {aws: 'SES Mailbox'}, iconKey: 'email', group: 'Integration', order: 20, }, kms: { displayName: 'Key Management', displayNameByCloud: {aws: 'KMS'}, iconKey: 'kms', group: 'Security', order: 20, }, parameters: { displayName: 'Parameter Store', iconKey: 'parameters', group: 'Security', order: 30, }, secrets: { displayName: 'Secrets Manager', displayNameByCloud: {azure: 'Key Vault', gcp: 'Secret Manager'}, iconKey: 'secrets', group: 'Security', order: 10, // AWS retains its dedicated page while Azure and GCP use Cloud Explorer. routeByCloud: {aws: '/secretsmanager'}, }, userpools: { displayName: 'User Directories', displayNameByCloud: {aws: 'Cognito'}, iconKey: 'userpools', group: 'Security', order: 7, }, iac: { displayName: 'Infrastructure as Code', displayNameByCloud: {aws: 'CloudFormation'}, iconKey: 'iac', group: 'Provisioning', order: 10, }, configuration: { displayName: 'Configuration', displayNameByCloud: {aws: 'AppConfig'}, iconKey: 'configuration', group: 'Provisioning', order: 20, }, scheduler: { displayName: 'Cloud Scheduler', displayNameByCloud: {gcp: 'Cloud Scheduler'}, iconKey: 'scheduler', group: 'Integration', order: 20, }, logs: { displayName: 'Logs', displayNameByCloud: {aws: 'CloudWatch Logs'}, iconKey: 'logs', group: 'Observability', order: 10, }, } as const satisfies Record export type CloudServiceType = keyof typeof SERVICE_CATALOG export interface ServiceCatalogEntry extends ServiceCatalogMetadata { service: CloudServiceType } /** Every known service, sorted by group then in-group order. */ export const SERVICE_CATALOG_ENTRIES: ServiceCatalogEntry[] = ( Object.keys(SERVICE_CATALOG) as CloudServiceType[] ) .map((service) => ({service, ...SERVICE_CATALOG[service]})) .sort(compareCatalogEntries) export const SERVICE_TYPES: CloudServiceType[] = SERVICE_CATALOG_ENTRIES.map((entry) => entry.service) export function catalogEntry(service: string): ServiceCatalogEntry | undefined { return isServiceType(service) ? {service, ...SERVICE_CATALOG[service]} : undefined } /** * Route guard. A slug missing from the catalog is a 404 rather than a service * that renders and then fails on every call. */ export function isServiceType(value: string): value is CloudServiceType { return Object.hasOwn(SERVICE_CATALOG, value) } export function displayNameFor(entry: ServiceCatalogEntry, cloud: CloudProvider): string { return entry.displayNameByCloud?.[cloud] ?? entry.displayName } export function routeFor(entry: ServiceCatalogEntry, cloud?: CloudProvider): string { const perCloud = cloud ? entry.routeByCloud?.[cloud] : undefined return perCloud ?? entry.route ?? entry.service } export function compareCatalogEntries(a: ServiceCatalogEntry, b: ServiceCatalogEntry): number { const groupDelta = SERVICE_GROUP_ORDER.indexOf(a.group) - SERVICE_GROUP_ORDER.indexOf(b.group) return groupDelta !== 0 ? groupDelta : a.order - b.order }