275 lines
8.9 KiB
TypeScript
275 lines
8.9 KiB
TypeScript
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'
|
|
| 'Provisioning'
|
|
| 'Security'
|
|
| 'Observability'
|
|
|
|
export const SERVICE_GROUP_ORDER: ServiceGroup[] = [
|
|
'Compute',
|
|
'Developer Tools',
|
|
'Storage',
|
|
'Databases',
|
|
'Networking',
|
|
'Integration',
|
|
'Provisioning',
|
|
'Security',
|
|
'Observability',
|
|
]
|
|
|
|
export interface ServiceCatalogMetadata {
|
|
displayName: string
|
|
/** Per-cloud display override, e.g. 'EKS' vs 'AKS' vs 'GKE'. */
|
|
displayNameByCloud?: Partial<Record<CloudProvider, string>>
|
|
/** 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<Record<CloudProvider, string>>
|
|
/**
|
|
* 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<Record<CloudProvider, CloudAvailability>>
|
|
}
|
|
|
|
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,
|
|
},
|
|
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,
|
|
},
|
|
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,
|
|
},
|
|
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'},
|
|
},
|
|
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<string, ServiceCatalogMetadata>
|
|
|
|
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
|
|
}
|