feat(api): add Athena workgroups and a query editor to the cloud explorer
CI / checks (pull_request) Successful in 28m25s

Create and delete Athena workgroups (with a result folder, optionally
enforced), and run SQL in one: pick a database from the Glue Data Catalog,
preview a table, run with Ctrl+Enter, cancel a running query, read up to 1,000
rows and download the full result CSV from S3. Recent queries in the workgroup
can be reopened. TheCloud runs the SQL for real over the files a Glue table
points at. The Glue page links to the editor.
This commit is contained in:
Zakaria
2026-10-25 18:10:33 -04:00
parent 69d80c60a3
commit 5eee0b7e9b
25 changed files with 1291 additions and 121 deletions
+80
View File
@@ -0,0 +1,80 @@
import type {CloudProvider, FieldSchema, ServiceSchema, TableColumnSchema} from './types'
/** CreateWorkGroup's name rule. */
export const WORKGROUP_NAME_PATTERN = '^[a-zA-Z0-9._-]{1,128}$'
/** The workgroup every account has, which Athena does not let you delete. */
export const PRIMARY_WORKGROUP = 'primary'
/** Rows the editor shows for one query; the full result stays in the CSV file in S3. */
export const QUERY_RESULT_ROW_LIMIT = 1000
/** Recent queries the history shows. */
export const QUERY_HISTORY_LIMIT = 25
const workGroupColumns: TableColumnSchema[] = [
{name: 'name', label: 'Name'},
{name: 'status', label: 'State', format: 'badge'},
{name: 'description', label: 'Description', path: 'metadata.description', emptyText: '—'},
{name: 'createdAt', label: 'Created', format: 'datetime', emptyText: '—'},
]
const workGroupFilters: FieldSchema[] = [{name: 'search', label: 'Search', type: 'text', required: false}]
export function awsQuerySchema(): ServiceSchema {
return {
cloud: 'aws',
service: 'query',
displayName: 'Athena workgroups',
fields: [
{
name: 'name',
label: 'Name',
type: 'text',
required: true,
validation: {pattern: WORKGROUP_NAME_PATTERN, message: 'Use up to 128 letters, numbers, dots, dashes and underscores'},
},
{name: 'description', label: 'Description', type: 'text', required: false, validation: {maxLength: 1024}},
{
name: 'outputLocation',
label: 'Query result location',
type: 'text',
required: false,
span: true,
description: 'Where each query writes its results as a CSV file, such as s3://my-bucket/athena-results/. Leave empty to use TheCloud\'s default results bucket.',
validation: {pattern: '^s3://[a-z0-9.-]{3,63}(/.*)?$', message: 'Use an S3 URI such as s3://my-bucket/athena-results/'},
},
{
name: 'enforce',
label: 'Override client-side settings',
type: 'select',
required: false,
defaultValue: 'false',
description: 'When on, queries always use this workgroup\'s result location, even if a client asks for another.',
options: [
{label: 'Off', value: 'false'},
{label: 'On', value: 'true'},
],
},
],
actions: ['list', 'create', 'delete', 'inspect'],
capabilities: {
resourceActions: [
{name: 'list', label: 'List workgroups', enabled: true, status: 'available', runtimeRequired: true},
{name: 'create', label: 'Create workgroup', enabled: true, status: 'available', runtimeRequired: true},
{name: 'delete', label: 'Delete workgroup', enabled: true, status: 'available', runtimeRequired: true},
{name: 'inspect', label: 'Inspect workgroup', enabled: true, status: 'available', runtimeRequired: true},
],
queryActions: [
{name: 'run', label: 'Run query', enabled: true, status: 'available', runtimeRequired: true},
{name: 'results', label: 'Get query results', enabled: true, status: 'available', runtimeRequired: true},
{name: 'stop', label: 'Cancel query', enabled: true, status: 'available', runtimeRequired: true},
{name: 'history', label: 'Recent queries', enabled: true, status: 'available', runtimeRequired: true},
{name: 'catalog', label: 'Browse databases and tables', enabled: true, status: 'available', runtimeRequired: true},
],
},
filters: workGroupFilters,
columns: workGroupColumns,
}
}
export function querySchemaFor(cloud: CloudProvider): ServiceSchema | null {
return cloud === 'aws' ? awsQuerySchema() : null
}
@@ -229,6 +229,13 @@ export const SERVICE_CATALOG = {
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},
email: {
+46
View File
@@ -188,6 +188,7 @@ export type UserPoolActionName =
export type DistributionActionName = 'setEnabled' | 'createInvalidation'
export type AutoScalingActionName = 'setCapacity' | 'terminateInstance'
export type CertificateActionName = 'download' | 'export' | 'revoke'
export type QueryActionName = 'run' | 'results' | 'stop' | 'history' | 'catalog'
export type DataCatalogActionName = 'createTable' | 'deleteTable'
export type FileSystemActionName = 'createMountTarget' | 'deleteMountTarget' | 'createAccessPoint' | 'deleteAccessPoint' | 'attachFunction' | 'detachFunction'
export type MonitoringActionName = 'listMetrics' | 'getMetricData' | 'alarmHistory' | 'setAlarmState' | 'setAlarmActions'
@@ -253,6 +254,7 @@ export interface ServiceSchema {
certificateActions?: CapabilitySchema<CertificateActionName>[]
fileSystemActions?: CapabilitySchema<FileSystemActionName>[]
dataCatalogActions?: CapabilitySchema<DataCatalogActionName>[]
queryActions?: CapabilitySchema<QueryActionName>[]
monitoringActions?: CapabilitySchema<MonitoringActionName>[]
/** API Gateway v2 (HTTP and WebSocket APIs): what the routes, stages and security panel can do. */
httpApiActions?: CapabilitySchema<HttpApiActionName>[]
@@ -560,6 +562,43 @@ export interface AccessPointInput {
permissions: string
}
export interface QueryInput {
sql: string
/** The Glue database unqualified table names resolve in. */
database?: string
}
export interface QueryExecutionSummary {
id: string
sql: string
database: string | null
workGroup: string | null
/** QUEUED, RUNNING, SUCCEEDED, FAILED or CANCELLED. */
state: string
stateReason: string | null
statementType: string | null
submittedAt: string | null
completedAt: string | null
runtimeMs: number | null
dataScannedBytes: number | null
/** The S3 object holding the full result as CSV. */
outputLocation: string | null
}
export interface QueryResult {
execution: QueryExecutionSummary
columns: Array<{name: string; type: string | null}>
rows: Array<Array<string | null>>
/** More rows exist than the console returns; the full result is in the output file. */
truncated: boolean
}
export interface QueryCatalog {
databases: string[]
database: string | null
tables: Array<{name: string; columns: Array<{name: string; type: string}>}>
}
export interface CatalogColumnInput {
name: string
type: string
@@ -1255,6 +1294,13 @@ export interface CloudServiceAdapter {
/** With `replace`, the group launches a new instance in its place; otherwise desired capacity drops by one. */
terminateAutoScalingInstance?(groupName: string, instanceId: string, replace: boolean): Promise<void>
getCertificatePem?(certificateId: string): Promise<CertificatePem>
startQuery?(workGroup: string, input: QueryInput): Promise<QueryExecutionSummary>
/** The query's state, and its first rows once it has succeeded. */
getQueryResult?(workGroup: string, queryId: string): Promise<QueryResult>
stopQuery?(workGroup: string, queryId: string): Promise<void>
listQueries?(workGroup: string): Promise<QueryExecutionSummary[]>
/** The databases a query can use and, for the one named, its tables. */
describeQueryCatalog?(database?: string): Promise<QueryCatalog>
createCatalogTable?(databaseName: string, input: CatalogTableInput): Promise<CloudResource>
deleteCatalogTable?(databaseName: string, tableName: string): Promise<CloudResource>
createMountTarget?(fileSystemId: string, subnetId: string): Promise<CloudResource>