Files
floci-ui/packages/api/src/cloud-spi/serviceCatalog.ts
T
Zakaria ca3e67a803
CI / checks (pull_request) Successful in 28m38s
feat(api): add AppSync GraphQL APIs to the cloud explorer
Create GraphQL APIs with API-key (the first key is made for you), IAM or
Cognito user pool authorization, then edit the schema (a starter schema is
offered, and a rejected one is explained), create and delete API keys, create
data sources for a DynamoDB table, a Lambda function or none, and attach
JavaScript resolvers to fields of the schema from a template for the data
source.

TheCloud's endpoint checks queries against the schema and the API key but does
not run resolvers yet, so the page says so and offers no query runner.
2026-10-25 18:21:46 -04:00

389 lines
12 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'
| '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<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},
autoscaling: {
displayName: 'Auto Scaling',
displayNameByCloud: {aws: 'EC2 Auto Scaling'},
iconKey: 'autoscaling',
group: 'Compute',
order: 15,
},
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,
},
filesystems: {
displayName: 'File Systems',
displayNameByCloud: {aws: 'EFS'},
iconKey: 'filesystems',
group: 'Storage',
order: 30,
},
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,
},
datacatalog: {
displayName: 'Data Catalog',
displayNameByCloud: {aws: 'Glue Data Catalog'},
iconKey: 'datacatalog',
group: 'Analytics',
order: 20,
},
query: {
displayName: 'Query',
displayNameByCloud: {aws: 'Athena'},
iconKey: 'query',
group: 'Analytics',
order: 21,
},
identity: {displayName: 'Identity', iconKey: 'iam', group: 'Security', order: 5},
apigateway: {displayName: 'API Gateway', iconKey: 'apigateway', group: 'Integration', order: 10},
graphql: {
displayName: 'GraphQL APIs',
displayNameByCloud: {aws: 'AppSync'},
iconKey: 'graphql',
group: 'Integration',
order: 12,
},
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'},
},
certificates: {
displayName: 'Certificates',
displayNameByCloud: {aws: 'Certificate Manager'},
iconKey: 'certificates',
group: 'Security',
order: 35,
},
cdn: {
displayName: 'CDN',
displayNameByCloud: {aws: 'CloudFront'},
iconKey: 'cdn',
group: 'Networking',
order: 50,
},
dns: {
displayName: 'DNS',
displayNameByCloud: {aws: 'Route 53'},
iconKey: 'dns',
group: 'Networking',
order: 40,
},
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,
},
pipes: {
displayName: 'Pipes',
displayNameByCloud: {aws: 'EventBridge Pipes'},
iconKey: 'pipes',
group: 'Integration',
order: 25,
},
scheduler: {
displayName: 'Cloud Scheduler',
displayNameByCloud: {gcp: 'Cloud Scheduler'},
iconKey: 'scheduler',
group: 'Integration',
order: 20,
},
monitoring: {
displayName: 'Monitoring',
displayNameByCloud: {aws: 'CloudWatch'},
iconKey: 'monitoring',
group: 'Observability',
order: 5,
},
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
}