first commit

This commit is contained in:
Zakaria
2026-09-22 16:13:21 -04:00
commit 7e49be27f1
297 changed files with 63162 additions and 0 deletions
+442
View File
@@ -0,0 +1,442 @@
import { emitApiRequest, type CloudProvider } from "@/telemetry";
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
export interface HttpClientConfig {
baseUrl?: string;
defaultHeaders?: Record<string, string>;
cache?: RequestCache;
timeout?: number;
}
export interface RequestTelemetry {
provider?: CloudProvider;
service?: string;
}
export type EndpointRegistry = Map<string, EndpointDefinition>;
export interface RequestOptions<TBody = unknown>
extends Omit<RequestInit, "body" | "method"> {
params?: Record<string, string | number | boolean | undefined>;
body?: TBody;
rawBody?: BodyInit;
cache?: RequestCache;
timeout?: number;
baseUrlOverride?: string;
telemetry?: RequestTelemetry;
}
export interface HttpResponse<TData> {
data: TData;
status: number;
headers: Headers;
ok: boolean;
}
export class HttpError extends Error {
constructor(
public readonly status: number,
public readonly statusText: string,
public readonly body: unknown,
public readonly url: string,
) {
super(httpErrorMessage(status, statusText, body, url));
this.name = "HttpError";
}
}
export class NetworkError extends Error {
constructor(
message: string,
public readonly url: string,
public readonly cause?: unknown,
) {
super(message);
this.name = "NetworkError";
}
}
function httpErrorMessage(
status: number,
statusText: string,
body: unknown,
url: string,
): string {
const apiMessage = apiErrorMessage(body);
if (!apiMessage) return `HTTP ${status} - ${statusText} [${url}]`;
const code = apiErrorCode(body);
const suffix = code ? ` (${code})` : "";
return `${apiMessage}${suffix} [${url}]`;
}
function apiErrorMessage(body: unknown): string | null {
if (!body || typeof body !== "object") return null;
const value = (body as { message?: unknown; error?: unknown }).message ?? (body as { error?: unknown }).error;
if (typeof value !== "string" || !value.trim()) return null;
const detail = (body as { detail?: unknown }).detail;
if (typeof detail === "string" && detail.trim() && detail.trim() !== value.trim()) {
return `${value.trim()}: ${detail.trim()}`;
}
return value.trim();
}
function apiErrorCode(body: unknown): string | null {
if (!body || typeof body !== "object") return null;
const value = (body as { code?: unknown }).code;
return typeof value === "string" && value.trim() ? value : null;
}
export class TimeoutError extends Error {
constructor(
public readonly url: string,
public readonly timeoutMs: number,
) {
super(`Request timed out after ${timeoutMs}ms [${url}]`);
this.name = "TimeoutError";
}
}
export class ResponseParseError extends Error {
constructor(
public readonly url: string,
public readonly cause?: unknown,
) {
super(`Failed to parse response [${url}]`);
this.name = "ResponseParseError";
}
}
export type RequestInterceptor = (
url: string,
init: RequestInit,
) =>
| Promise<{ url: string; init: RequestInit }>
| { url: string; init: RequestInit };
export type ResponseInterceptor = (
response: Response,
) => Promise<Response> | Response;
export type EndpointDefaults = Omit<RequestOptions<never>, "body" | "params">;
export type EndpointDefinition = {
path: string;
method: HttpMethod;
defaults?: Partial<EndpointDefaults>;
baseUrlOverride?: string;
telemetry?: RequestTelemetry;
};
export class HttpClient {
private readonly config: HttpClientConfig;
private readonly requestInterceptors: RequestInterceptor[] = [];
private readonly responseInterceptors: ResponseInterceptor[] = [];
private readonly endpointRegistry: EndpointRegistry;
constructor(
config: HttpClientConfig = {},
registry: EndpointRegistry = new Map(),
) {
this.config = config;
this.endpointRegistry = registry;
}
addRequestInterceptor(interceptor: RequestInterceptor): this {
this.requestInterceptors.push(interceptor);
return this;
}
addResponseInterceptor(interceptor: ResponseInterceptor): this {
this.responseInterceptors.push(interceptor);
return this;
}
async request<TResponse, TBody = unknown>(
method: HttpMethod,
endpoint: string,
options: RequestOptions<TBody> = {},
): Promise<HttpResponse<TResponse>> {
const started = performance.now();
const {
params,
body,
rawBody,
timeout,
cache,
baseUrlOverride,
telemetry,
...restOptions
} = options;
let finalUrl = endpoint;
let statusCode = 0;
try {
const base = baseUrlOverride ?? this.config.baseUrl ?? "";
const rawUrl = endpoint.startsWith("http")
? endpoint
: `${base}${endpoint}`;
finalUrl = this.buildUrl(rawUrl, params);
const headers: Record<string, string> = {
Accept: "application/json",
...this.config.defaultHeaders,
...(body !== undefined && rawBody === undefined
? { "Content-Type": "application/json" }
: {}),
...(restOptions.headers as Record<string, string> | undefined),
};
let init: RequestInit = {
...restOptions,
method,
headers,
cache: cache ?? this.config.cache,
...(rawBody !== undefined
? { body: rawBody }
: body !== undefined
? { body: JSON.stringify(body) }
: {}),
};
for (const interceptor of this.requestInterceptors) {
({ url: finalUrl, init } = await interceptor(finalUrl, init));
}
let response = await this.fetchWithTimeout(
finalUrl,
init,
timeout ?? this.config.timeout,
);
statusCode = response.status;
for (const interceptor of this.responseInterceptors) {
response = await interceptor(response);
}
if (!response.ok) {
throw await this.createHttpError(response, finalUrl);
}
return {
data: await this.parseResponse<TResponse>(response, finalUrl),
status: response.status,
headers: response.headers,
ok: response.ok,
};
} catch (error) {
throw this.normalizeError(error, finalUrl, timeout ?? this.config.timeout);
} finally {
if (telemetry?.service && statusCode > 0) {
emitApiRequest({
provider: telemetry.provider ?? "aws",
service: telemetry.service,
method,
path: endpoint,
statusCode,
latencyMs: Math.round(performance.now() - started),
timestamp: Date.now(),
});
}
}
}
async call<TResponse, TBody = unknown>(
key: string,
options: RequestOptions<TBody> = {},
pathParams?: Record<string, string>,
): Promise<HttpResponse<TResponse>> {
const endpoint = this.endpointRegistry.get(key);
if (!endpoint) throw new Error(`Endpoint "${key}" is not registered.`);
let path = endpoint.path;
if (pathParams) {
for (const [param, value] of Object.entries(pathParams)) {
path = path.replace(`:${param}`, encodeURIComponent(value));
}
}
return this.request<TResponse, TBody>(endpoint.method, path, {
...endpoint.defaults,
...options,
baseUrlOverride: endpoint.baseUrlOverride,
telemetry: {
...endpoint.telemetry,
...options.telemetry,
},
});
}
get<TResponse>(endpoint: string, options?: RequestOptions<never>) {
return this.request<TResponse, never>("GET", endpoint, options);
}
post<TResponse, TBody = unknown>(
endpoint: string,
body?: TBody,
options?: RequestOptions<TBody>,
) {
return this.request<TResponse, TBody>("POST", endpoint, {
...options,
body,
});
}
put<TResponse, TBody = unknown>(
endpoint: string,
body?: TBody,
options?: RequestOptions<TBody>,
) {
return this.request<TResponse, TBody>("PUT", endpoint, {
...options,
body,
});
}
patch<TResponse, TBody = unknown>(
endpoint: string,
body?: TBody,
options?: RequestOptions<TBody>,
) {
return this.request<TResponse, TBody>("PATCH", endpoint, {
...options,
body,
});
}
delete<TResponse>(endpoint: string, options?: RequestOptions<never>) {
return this.request<TResponse, never>("DELETE", endpoint, options);
}
private async fetchWithTimeout(
url: string,
init: RequestInit,
timeoutMs?: number,
): Promise<Response> {
if (!timeoutMs) return fetch(url, init);
const timeoutController = new AbortController();
let timedOut = false;
const timeoutId = window.setTimeout(() => {
timedOut = true;
timeoutController.abort();
}, timeoutMs);
const signal = this.mergeSignals(init.signal, timeoutController.signal);
try {
return await fetch(url, {
...init,
signal,
});
} catch (error) {
if (timedOut) throw new TimeoutError(url, timeoutMs);
throw error;
} finally {
window.clearTimeout(timeoutId);
}
}
private mergeSignals(
requestSignal: AbortSignal | null | undefined,
timeoutSignal: AbortSignal,
): AbortSignal {
if (!requestSignal) return timeoutSignal;
if (requestSignal.aborted) return requestSignal;
if (timeoutSignal.aborted) return timeoutSignal;
const controller = new AbortController();
const abort = () => controller.abort();
requestSignal.addEventListener("abort", abort, { once: true });
timeoutSignal.addEventListener("abort", abort, { once: true });
return controller.signal;
}
private async createHttpError(
response: Response,
url: string,
): Promise<HttpError> {
const text = await response.text();
let body: unknown;
if (text.trim()) {
try {
body = JSON.parse(text);
} catch {
body = text;
}
} else {
body = undefined;
}
return new HttpError(response.status, response.statusText, body, url);
}
private async parseResponse<TResponse>(
response: Response,
url: string,
): Promise<TResponse> {
if (response.status === 204) return undefined as TResponse;
const contentLength = response.headers.get("content-length");
if (contentLength === "0") return undefined as TResponse;
const text = await response.text();
if (!text.trim()) return undefined as TResponse;
const contentType = response.headers.get("content-type") ?? "";
if (!contentType.includes("application/json")) {
return text as TResponse;
}
try {
return JSON.parse(text) as TResponse;
} catch (error) {
throw new ResponseParseError(url, error);
}
}
private normalizeError(
error: unknown,
url: string,
timeoutMs?: number,
): Error {
if (
error instanceof HttpError ||
error instanceof NetworkError ||
error instanceof TimeoutError ||
error instanceof ResponseParseError
) {
return error;
}
if (error instanceof DOMException && error.name === "AbortError") {
if (timeoutMs) return new TimeoutError(url, timeoutMs);
return new NetworkError("Request aborted", url, error);
}
return new NetworkError(
error instanceof Error ? error.message : "Network error",
url,
error,
);
}
private buildUrl(
base: string,
params?: Record<string, string | number | boolean | undefined>,
): string {
if (!params) return base;
const query = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value !== undefined) query.append(key, String(value));
}
const qs = query.toString();
return qs ? `${base}?${qs}` : base;
}
}
+965
View File
@@ -0,0 +1,965 @@
import { EndpointRegistry, HttpClient } from "./HttpClient";
import { ACCOUNT_HEADER, getAccountId } from "@/lib/accountStore";
export class AuthenticationRequiredError extends Error {
constructor() {
super("Authentication is required before calling the API.");
this.name = "AuthenticationRequiredError";
}
}
export const apiEndpointKeys = {
clouds: {
list: "clouds.list",
services: "clouds.services.list",
status: "clouds.status.get",
serviceStatus: "clouds.services.status.get",
schema: "clouds.services.schema.get",
resources: {
list: "clouds.services.resources.list",
get: "clouds.services.resources.get",
create: "clouds.services.resources.create",
update: "clouds.services.resources.update",
delete: "clouds.services.resources.delete",
invoke: "clouds.services.resources.invoke",
encrypt: "clouds.services.resources.encrypt",
decrypt: "clouds.services.resources.decrypt",
createApiGatewayRoute: "clouds.services.resources.apigateway.route.create",
},
storage: {
objects: {
list: "clouds.services.storage.objects.list",
upload: "clouds.services.storage.objects.upload",
download: "clouds.services.storage.objects.download",
delete: "clouds.services.storage.objects.delete",
copy: "clouds.services.storage.objects.copy",
},
},
nosql: {
cosmos: {
containers: {
list: "clouds.services.nosql.cosmos.containers.list",
create: "clouds.services.nosql.cosmos.containers.create",
delete: "clouds.services.nosql.cosmos.containers.delete",
},
items: {
list: "clouds.services.nosql.cosmos.items.list",
upsert: "clouds.services.nosql.cosmos.items.upsert",
delete: "clouds.services.nosql.cosmos.items.delete",
query: "clouds.services.nosql.cosmos.items.query",
},
},
items: {
list: "clouds.services.nosql.items.list",
put: "clouds.services.nosql.items.put",
},
},
database: {
sql: {
databases: "clouds.services.database.sql.databases.list",
tables: "clouds.services.database.sql.tables.list",
query: "clouds.services.database.sql.query",
},
snapshots: {
list: "clouds.services.database.snapshots.list",
create: "clouds.services.database.snapshots.create",
},
orderableClasses: {
list: "clouds.services.database.orderable-classes.list",
},
},
logs: {
query: "clouds.services.logs.query",
},
k8s: {
nodegroups: {
list: "clouds.services.k8s.nodegroups.list",
create: "clouds.services.k8s.nodegroups.create",
delete: "clouds.services.k8s.nodegroups.delete",
},
fargateProfiles: {
list: "clouds.services.k8s.fargate-profiles.list",
create: "clouds.services.k8s.fargate-profiles.create",
delete: "clouds.services.k8s.fargate-profiles.delete",
},
},
email: {
inbox: {
clear: "clouds.services.email.inbox.clear",
},
},
childCollections: {
list: "clouds.services.childCollections.list",
items: {
list: "clouds.services.childCollections.items.list",
},
},
configuration: {
environments: {
list: "clouds.services.configuration.environments.list",
create: "clouds.services.configuration.environments.create",
delete: "clouds.services.configuration.environments.delete",
deployments: {
start: "clouds.services.configuration.environments.deployments.start",
get: "clouds.services.configuration.environments.deployments.get",
},
},
configurationProfiles: {
list: "clouds.services.configuration.profiles.list",
create: "clouds.services.configuration.profiles.create",
delete: "clouds.services.configuration.profiles.delete",
hostedVersions: {
list: "clouds.services.configuration.profiles.hosted-versions.list",
get: "clouds.services.configuration.profiles.hosted-versions.get",
create: "clouds.services.configuration.profiles.hosted-versions.create",
delete: "clouds.services.configuration.profiles.hosted-versions.delete",
},
},
deploymentStrategies: {
list: "clouds.services.configuration.deployment-strategies.list",
create: "clouds.services.configuration.deployment-strategies.create",
delete: "clouds.services.configuration.deployment-strategies.delete",
},
},
},
aws: {
eks: {
clusters: {
list: "aws.eks.clusters.list",
describe: "aws.eks.clusters.describe",
},
nodegroups: {
list: "aws.eks.nodegroups.list",
describe: "aws.eks.nodegroups.describe",
create: "aws.eks.nodegroups.create",
delete: "aws.eks.nodegroups.delete",
},
fargateProfiles: {
list: "aws.eks.fargate-profiles.list",
describe: "aws.eks.fargate-profiles.describe",
create: "aws.eks.fargate-profiles.create",
delete: "aws.eks.fargate-profiles.delete",
},
},
secretsmanager: {
secrets: {
list: "aws.secretsmanager.secrets.list",
create: "aws.secretsmanager.secrets.create",
describe: "aws.secretsmanager.secrets.describe",
delete: "aws.secretsmanager.secrets.delete",
value: {
get: "aws.secretsmanager.secrets.value.get",
put: "aws.secretsmanager.secrets.value.put",
},
},
},
ec2: {
instances: {
list: "aws.ec2.instances.list",
describe: "aws.ec2.instances.describe",
create: "aws.ec2.instances.create",
start: "aws.ec2.instances.start",
stop: "aws.ec2.instances.stop",
reboot: "aws.ec2.instances.reboot",
terminate: "aws.ec2.instances.terminate",
tags: "aws.ec2.instances.tags",
createImage: "aws.ec2.instances.create-image",
console: "aws.ec2.instances.console",
},
amis: {
list: "aws.ec2.amis.list",
deregister: "aws.ec2.amis.deregister",
},
keyPairs: {
list: "aws.ec2.key-pairs.list",
create: "aws.ec2.key-pairs.create",
delete: "aws.ec2.key-pairs.delete",
},
securityGroups: {
list: "aws.ec2.security-groups.list",
create: "aws.ec2.security-groups.create",
delete: "aws.ec2.security-groups.delete",
authorize: "aws.ec2.security-groups.authorize",
revoke: "aws.ec2.security-groups.revoke",
authorizeEgress: "aws.ec2.security-groups.authorize-egress",
revokeEgress: "aws.ec2.security-groups.revoke-egress",
},
vpcs: {
list: "aws.ec2.vpcs.list",
create: "aws.ec2.vpcs.create",
delete: "aws.ec2.vpcs.delete",
getAttributes: "aws.ec2.vpcs.get-attributes",
modifyAttribute: "aws.ec2.vpcs.modify-attribute",
},
subnets: {
list: "aws.ec2.subnets.list",
create: "aws.ec2.subnets.create",
delete: "aws.ec2.subnets.delete",
modifyAttribute: "aws.ec2.subnets.modify-attribute",
},
internetGateways: {
list: "aws.ec2.igw.list",
create: "aws.ec2.igw.create",
attach: "aws.ec2.igw.attach",
detach: "aws.ec2.igw.detach",
delete: "aws.ec2.igw.delete",
},
natGateways: {
list: "aws.ec2.nat.list",
create: "aws.ec2.nat.create",
delete: "aws.ec2.nat.delete",
},
routeTables: {
list: "aws.ec2.rtb.list",
create: "aws.ec2.rtb.create",
delete: "aws.ec2.rtb.delete",
createRoute: "aws.ec2.rtb.route.create",
deleteRoute: "aws.ec2.rtb.route.delete",
associate: "aws.ec2.rtb.associate",
disassociate: "aws.ec2.rtb.disassociate",
},
elasticIps: {
list: "aws.ec2.eip.list",
create: "aws.ec2.eip.create",
release: "aws.ec2.eip.release",
associate: "aws.ec2.eip.associate",
disassociate: "aws.ec2.eip.disassociate",
},
availabilityZones: "aws.ec2.availability-zones",
instanceTypes: "aws.ec2.instance-types",
vpcWizard: "aws.ec2.vpc-wizard",
},
},
} as const;
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "/api";
export const endpointRegistry: EndpointRegistry = new Map([
// Cloud Proxy
[
apiEndpointKeys.clouds.list,
{
path: "/clouds",
method: "GET",
telemetry: { provider: "system", service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.services,
{
path: "/clouds/:cloud/services",
method: "GET",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.status,
{
path: "/clouds/:cloud/status",
method: "GET",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.serviceStatus,
{
path: "/clouds/:cloud/services/:service/status",
method: "GET",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.schema,
{
path: "/clouds/:cloud/services/:service/schema",
method: "GET",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.resources.list,
{
path: "/clouds/:cloud/services/:service/resources",
method: "GET",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.resources.invoke,
{
path: "/clouds/:cloud/services/:service/resources/:id/invoke",
method: "POST",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.resources.createApiGatewayRoute,
{
path: "/clouds/:cloud/services/apigateway/resources/:id/routes",
method: "POST",
telemetry: { service: "apigateway" },
},
],
[
apiEndpointKeys.clouds.resources.get,
{
path: "/clouds/:cloud/services/:service/resources/:id",
method: "GET",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.resources.encrypt,
{
path: "/clouds/:cloud/services/kms/resources/:id/encrypt",
method: "POST",
telemetry: { service: "kms" },
},
],
[
apiEndpointKeys.clouds.resources.decrypt,
{
path: "/clouds/:cloud/services/kms/resources/:id/decrypt",
method: "POST",
telemetry: { service: "kms" },
},
],
[
apiEndpointKeys.clouds.resources.create,
{
path: "/clouds/:cloud/services/:service/resources",
method: "POST",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.resources.update,
{
path: "/clouds/:cloud/services/:service/resources/:id",
method: "PATCH",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.resources.delete,
{
path: "/clouds/:cloud/services/:service/resources/:id",
method: "DELETE",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.resources.invoke,
{
path: "/clouds/:cloud/services/:service/resources/:id/invoke",
method: "POST",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.storage.objects.list,
{
path: "/clouds/:cloud/services/storage/resources/:id/objects",
method: "GET",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.storage.objects.upload,
{
path: "/clouds/:cloud/services/storage/resources/:id/object",
method: "PUT",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.storage.objects.download,
{
path: "/clouds/:cloud/services/storage/resources/:id/object",
method: "GET",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.storage.objects.delete,
{
path: "/clouds/:cloud/services/storage/resources/:id/object",
method: "DELETE",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.storage.objects.copy,
{
path: "/clouds/:cloud/services/storage/resources/:id/object/copy",
method: "POST",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.nosql.cosmos.containers.list,
{
path: "/clouds/:cloud/services/nosql/resources/:id/containers",
method: "GET",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.nosql.cosmos.containers.create,
{
path: "/clouds/:cloud/services/nosql/resources/:id/containers",
method: "POST",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.nosql.cosmos.containers.delete,
{
path: "/clouds/:cloud/services/nosql/resources/:id/containers/:containerId",
method: "DELETE",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.nosql.cosmos.items.list,
{
path: "/clouds/:cloud/services/nosql/resources/:id/containers/:containerId/items",
method: "GET",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.nosql.cosmos.items.upsert,
{
path: "/clouds/:cloud/services/nosql/resources/:id/containers/:containerId/items",
method: "POST",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.nosql.cosmos.items.delete,
{
path: "/clouds/:cloud/services/nosql/resources/:id/containers/:containerId/items/:itemId",
method: "DELETE",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.nosql.cosmos.items.query,
{
path: "/clouds/:cloud/services/nosql/resources/:id/containers/:containerId/query",
method: "POST",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.database.sql.databases,
{
path: "/clouds/:cloud/services/database/resources/:id/sql/databases",
method: "POST",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.database.sql.tables,
{
path: "/clouds/:cloud/services/database/resources/:id/sql/tables",
method: "POST",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.database.sql.query,
{
path: "/clouds/:cloud/services/database/resources/:id/sql/query",
method: "POST",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.logs.query,
{
path: "/clouds/:cloud/services/logs/resources/:id/query",
method: "POST",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.database.snapshots.list,
{
path: "/clouds/:cloud/services/database/snapshots",
method: "GET",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.database.snapshots.create,
{
path: "/clouds/:cloud/services/database/snapshots",
method: "POST",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.database.orderableClasses.list,
{
path: "/clouds/:cloud/services/database/orderable-classes",
method: "GET",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.nosql.items.list,
{
path: "/clouds/:cloud/services/nosql/resources/:id/items",
method: "GET",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.nosql.items.put,
{
path: "/clouds/:cloud/services/nosql/resources/:id/items",
method: "POST",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.k8s.nodegroups.list,
{
path: "/clouds/:cloud/services/k8s/resources/:id/nodegroups",
method: "GET",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.k8s.nodegroups.create,
{
path: "/clouds/:cloud/services/k8s/resources/:id/nodegroups",
method: "POST",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.k8s.nodegroups.delete,
{
path: "/clouds/:cloud/services/k8s/resources/:id/nodegroups/:nodegroupId",
method: "DELETE",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.k8s.fargateProfiles.list,
{
path: "/clouds/:cloud/services/k8s/resources/:id/fargate-profiles",
method: "GET",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.k8s.fargateProfiles.create,
{
path: "/clouds/:cloud/services/k8s/resources/:id/fargate-profiles",
method: "POST",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.k8s.fargateProfiles.delete,
{
path: "/clouds/:cloud/services/k8s/resources/:id/fargate-profiles/:profileId",
method: "DELETE",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.email.inbox.clear,
{
path: "/clouds/:cloud/services/email/inbox",
method: "DELETE",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.configuration.environments.list,
{
path: "/clouds/:cloud/services/configuration/resources/:id/environments",
method: "GET",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.configuration.environments.create,
{
path: "/clouds/:cloud/services/configuration/resources/:id/environments",
method: "POST",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.configuration.environments.delete,
{
path: "/clouds/:cloud/services/configuration/resources/:id/environments/:environmentId",
method: "DELETE",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.configuration.environments.deployments.start,
{
path: "/clouds/:cloud/services/configuration/resources/:id/environments/:environmentId/deployments",
method: "POST",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.configuration.environments.deployments.get,
{
path: "/clouds/:cloud/services/configuration/resources/:id/environments/:environmentId/deployments/:deploymentNumber",
method: "GET",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.configuration.configurationProfiles.list,
{
path: "/clouds/:cloud/services/configuration/resources/:id/configuration-profiles",
method: "GET",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.configuration.configurationProfiles.create,
{
path: "/clouds/:cloud/services/configuration/resources/:id/configuration-profiles",
method: "POST",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.configuration.configurationProfiles.delete,
{
path: "/clouds/:cloud/services/configuration/resources/:id/configuration-profiles/:profileId",
method: "DELETE",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.configuration.configurationProfiles.hostedVersions.list,
{
path: "/clouds/:cloud/services/configuration/resources/:id/configuration-profiles/:profileId/hosted-configuration-versions",
method: "GET",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.configuration.configurationProfiles.hostedVersions.get,
{
path: "/clouds/:cloud/services/configuration/resources/:id/configuration-profiles/:profileId/hosted-configuration-versions/:versionNumber",
method: "GET",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.configuration.configurationProfiles.hostedVersions.create,
{
path: "/clouds/:cloud/services/configuration/resources/:id/configuration-profiles/:profileId/hosted-configuration-versions",
method: "POST",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.configuration.configurationProfiles.hostedVersions.delete,
{
path: "/clouds/:cloud/services/configuration/resources/:id/configuration-profiles/:profileId/hosted-configuration-versions/:versionNumber",
method: "DELETE",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.configuration.deploymentStrategies.list,
{
path: "/clouds/:cloud/services/configuration/deployment-strategies",
method: "GET",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.configuration.deploymentStrategies.create,
{
path: "/clouds/:cloud/services/configuration/deployment-strategies",
method: "POST",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.configuration.deploymentStrategies.delete,
{
path: "/clouds/:cloud/services/configuration/deployment-strategies/:strategyId",
method: "DELETE",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.childCollections.list,
{
path: "/clouds/:cloud/services/:service/resources/:id/collections",
method: "GET",
telemetry: { service: "cloud-proxy" },
},
],
[
apiEndpointKeys.clouds.childCollections.items.list,
{
path: "/clouds/:cloud/services/:service/resources/:id/collections/:cid/items",
method: "GET",
telemetry: { service: "cloud-proxy" },
},
],
// AWS EKS
[
apiEndpointKeys.aws.eks.clusters.list,
{
path: "/eks/clusters",
method: "GET",
telemetry: { provider: "aws", service: "eks" },
},
],
[
apiEndpointKeys.aws.eks.clusters.describe,
{
path: "/eks/clusters/:name",
method: "GET",
telemetry: { provider: "aws", service: "eks" },
},
],
[
apiEndpointKeys.aws.eks.nodegroups.list,
{
path: "/eks/clusters/:name/nodegroups",
method: "GET",
telemetry: { provider: "aws", service: "eks" },
},
],
[
apiEndpointKeys.aws.eks.nodegroups.describe,
{
path: "/eks/clusters/:name/nodegroups/:nodegroup",
method: "GET",
telemetry: { provider: "aws", service: "eks" },
},
],
[
apiEndpointKeys.aws.eks.nodegroups.create,
{
path: "/eks/clusters/:name/nodegroups",
method: "POST",
telemetry: { provider: "aws", service: "eks" },
},
],
[
apiEndpointKeys.aws.eks.nodegroups.delete,
{
path: "/eks/clusters/:name/nodegroups/:nodegroup",
method: "DELETE",
telemetry: { provider: "aws", service: "eks" },
},
],
[
apiEndpointKeys.aws.eks.fargateProfiles.list,
{
path: "/eks/clusters/:name/fargate-profiles",
method: "GET",
telemetry: { provider: "aws", service: "eks" },
},
],
[
apiEndpointKeys.aws.eks.fargateProfiles.describe,
{
path: "/eks/clusters/:name/fargate-profiles/:profile",
method: "GET",
telemetry: { provider: "aws", service: "eks" },
},
],
[
apiEndpointKeys.aws.eks.fargateProfiles.create,
{
path: "/eks/clusters/:name/fargate-profiles",
method: "POST",
telemetry: { provider: "aws", service: "eks" },
},
],
[
apiEndpointKeys.aws.eks.fargateProfiles.delete,
{
path: "/eks/clusters/:name/fargate-profiles/:profile",
method: "DELETE",
telemetry: { provider: "aws", service: "eks" },
},
],
// AWS Secrets Manager
[
apiEndpointKeys.aws.secretsmanager.secrets.list,
{
path: "/secretsmanager/secrets",
method: "GET",
telemetry: { provider: "aws", service: "secretsmanager" },
},
],
[
apiEndpointKeys.aws.secretsmanager.secrets.create,
{
path: "/secretsmanager/secrets",
method: "POST",
telemetry: { provider: "aws", service: "secretsmanager" },
},
],
[
apiEndpointKeys.aws.secretsmanager.secrets.describe,
{
path: "/secretsmanager/secret",
method: "GET",
telemetry: { provider: "aws", service: "secretsmanager" },
},
],
[
apiEndpointKeys.aws.secretsmanager.secrets.delete,
{
path: "/secretsmanager/secret",
method: "DELETE",
telemetry: { provider: "aws", service: "secretsmanager" },
},
],
[
apiEndpointKeys.aws.secretsmanager.secrets.value.get,
{
path: "/secretsmanager/secret/value",
method: "GET",
telemetry: { provider: "aws", service: "secretsmanager" },
},
],
[
apiEndpointKeys.aws.secretsmanager.secrets.value.put,
{
path: "/secretsmanager/secret/value",
method: "PUT",
telemetry: { provider: "aws", service: "secretsmanager" },
},
],
// AWS EC2
[apiEndpointKeys.aws.ec2.instances.list, { path: "/ec2/instances", method: "GET", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.instances.describe, { path: "/ec2/instances/:instanceId", method: "GET", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.instances.create, { path: "/ec2/instances", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.instances.start, { path: "/ec2/instances/:instanceId/start", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.instances.stop, { path: "/ec2/instances/:instanceId/stop", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.instances.reboot, { path: "/ec2/instances/:instanceId/reboot", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.instances.terminate,{ path: "/ec2/instances/:instanceId/terminate", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.instances.tags, { path: "/ec2/instances/:instanceId/tags", method: "PUT", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.instances.createImage, { path: "/ec2/instances/:instanceId/image", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.instances.console, { path: "/ec2/instances/:instanceId/console", method: "GET", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.amis.list, { path: "/ec2/amis", method: "GET", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.amis.deregister, { path: "/ec2/amis/:imageId/deregister", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.keyPairs.list, { path: "/ec2/key-pairs", method: "GET", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.keyPairs.create, { path: "/ec2/key-pairs", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.keyPairs.delete, { path: "/ec2/key-pairs/:name", method: "DELETE", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.securityGroups.list, { path: "/ec2/security-groups", method: "GET", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.securityGroups.create, { path: "/ec2/security-groups", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.securityGroups.delete, { path: "/ec2/security-groups/:groupId", method: "DELETE", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.securityGroups.authorize, { path: "/ec2/security-groups/:groupId/ingress", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.securityGroups.revoke, { path: "/ec2/security-groups/:groupId/ingress", method: "DELETE", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.securityGroups.authorizeEgress, { path: "/ec2/security-groups/:groupId/egress", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.securityGroups.revokeEgress, { path: "/ec2/security-groups/:groupId/egress", method: "DELETE", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.vpcs.list, { path: "/ec2/vpcs", method: "GET", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.vpcs.create, { path: "/ec2/vpcs", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.vpcs.delete, { path: "/ec2/vpcs/:vpcId", method: "DELETE", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.vpcs.getAttributes, { path: "/ec2/vpcs/:vpcId/attributes", method: "GET", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.vpcs.modifyAttribute, { path: "/ec2/vpcs/:vpcId/attributes", method: "PUT", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.subnets.list, { path: "/ec2/subnets", method: "GET", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.subnets.create, { path: "/ec2/subnets", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.subnets.delete, { path: "/ec2/subnets/:subnetId", method: "DELETE", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.subnets.modifyAttribute, { path: "/ec2/subnets/:subnetId/attributes", method: "PUT", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.internetGateways.list, { path: "/ec2/internet-gateways", method: "GET", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.internetGateways.create, { path: "/ec2/internet-gateways", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.internetGateways.attach, { path: "/ec2/internet-gateways/:igwId/attach", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.internetGateways.detach, { path: "/ec2/internet-gateways/:igwId/detach", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.internetGateways.delete, { path: "/ec2/internet-gateways/:igwId", method: "DELETE", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.natGateways.list, { path: "/ec2/nat-gateways", method: "GET", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.natGateways.create, { path: "/ec2/nat-gateways", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.natGateways.delete, { path: "/ec2/nat-gateways/:natId", method: "DELETE", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.routeTables.list, { path: "/ec2/route-tables", method: "GET", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.routeTables.create, { path: "/ec2/route-tables", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.routeTables.delete, { path: "/ec2/route-tables/:rtbId", method: "DELETE", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.routeTables.createRoute, { path: "/ec2/route-tables/:rtbId/routes", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.routeTables.deleteRoute, { path: "/ec2/route-tables/:rtbId/routes", method: "DELETE", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.routeTables.associate, { path: "/ec2/route-tables/:rtbId/associations", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.routeTables.disassociate,{ path: "/ec2/route-table-associations/:associationId", method: "DELETE", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.elasticIps.list, { path: "/ec2/elastic-ips", method: "GET", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.elasticIps.create, { path: "/ec2/elastic-ips", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.elasticIps.release, { path: "/ec2/elastic-ips/:allocationId/release", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.elasticIps.associate, { path: "/ec2/elastic-ips/:allocationId/associate", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.elasticIps.disassociate, { path: "/ec2/elastic-ips/:allocationId/disassociate", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.availabilityZones, { path: "/ec2/availability-zones", method: "GET", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.instanceTypes, { path: "/ec2/instance-types", method: "GET", telemetry: { provider: "aws", service: "ec2" } }],
[apiEndpointKeys.aws.ec2.vpcWizard, { path: "/ec2/vpc-wizard", method: "POST", telemetry: { provider: "aws", service: "ec2" } }],
]);
// ─── Client Factory ───────────────────────────────────────────────────────────
export function createApiClient(
getToken?: () => string | null | Promise<string | null>,
) {
const client = new HttpClient(
{
baseUrl: API_BASE_URL,
defaultHeaders: { Accept: "application/json" },
timeout: 10_000,
},
endpointRegistry,
);
// Account scope — every request carries the active AWS account so Floci can
// isolate resources per account (the API maps this header to its SDK creds).
client.addRequestInterceptor((url, init) => {
const headers = (init.headers ?? {}) as Record<string, string>;
headers[ACCOUNT_HEADER] = getAccountId();
init.headers = headers;
return { url, init };
});
// Auth interceptor — attaches Bearer token if available
if (getToken) {
client.addRequestInterceptor(async (url, init) => {
const token = await getToken();
if (token) {
(init.headers as Record<string, string>)["Authorization"] =
`Bearer ${token}`;
}
return { url, init };
});
}
// Logging interceptor (dev only)
if (import.meta.env.DEV) {
client.addResponseInterceptor((res) => {
console.debug(`[HTTP] ${res.status} ${res.url}`);
return res;
});
}
// client.addResponseInterceptor((res) => {
// if (res.status === 401) {
// handleUnauthorizedApiResponse();
// }
// return res;
// });
return client;
}
export const apiClient = createApiClient();
@@ -0,0 +1,44 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createEc2SecurityGroup } from "./ec2.api";
describe("createEc2SecurityGroup", () => {
const fetchMock = vi.fn<typeof fetch>();
beforeEach(() => {
fetchMock.mockResolvedValue(
new Response(JSON.stringify({ groupId: "sg-created" }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
);
vi.stubGlobal("fetch", fetchMock);
});
afterEach(() => {
vi.unstubAllGlobals();
fetchMock.mockReset();
});
it.each(["vpc-selected", undefined])(
"sends one JSON object through HttpClient with VPC %s",
async (vpcId) => {
const name = "web-servers";
const description = 'Allow HTTP for "public" clients';
await expect(createEc2SecurityGroup(name, description, vpcId)).resolves.toEqual({
groupId: "sg-created",
});
expect(fetchMock).toHaveBeenCalledTimes(1);
const [url, init] = fetchMock.mock.calls[0];
expect(String(url)).toContain("/api/ec2/security-groups");
expect(init?.method).toBe("POST");
expect(JSON.parse(String(init?.body))).toEqual({
name,
description,
...(vpcId === undefined ? {} : { vpcId }),
});
expect(new Headers(init?.headers).get("content-type")).toBe("application/json");
},
);
});
+730
View File
@@ -0,0 +1,730 @@
import { apiClient, apiEndpointKeys } from "@/api/api";
// ─── Types ────────────────────────────────────────────────────────────────────
export type Ec2Tag = {
key: string;
value: string;
};
export type Ec2SecurityGroupRef = {
id?: string;
name?: string;
};
export type Ec2IpPermission = {
protocol: string;
fromPort: number | null;
toPort: number | null;
ipRanges: string[];
ipv6Ranges: string[];
};
export type Ec2SecurityGroup = {
groupId: string;
groupName: string;
description: string;
vpcId?: string;
inboundRules: Ec2IpPermission[];
outboundRules: Ec2IpPermission[];
tags: Ec2Tag[];
};
export type Ec2Image = {
imageId: string;
name: string;
description?: string;
state?: string;
architecture?: string;
platform?: string;
virtualizationType?: string;
rootDeviceType?: string;
createdAt?: string;
ownerId?: string;
public?: boolean;
tags: Ec2Tag[];
};
export type Ec2Instance = {
instanceId: string;
name: string;
state?: string;
instanceType?: string;
availabilityZone?: string;
publicIpAddress?: string;
privateIpAddress?: string;
vpcId?: string;
subnetId?: string;
imageId?: string;
keyName?: string;
launchTime?: string;
architecture?: string;
platform?: string;
securityGroups: Ec2SecurityGroupRef[];
tags: Ec2Tag[];
};
export type Ec2KeyPair = {
keyPairId: string;
keyName: string;
keyFingerprint?: string;
tags: Ec2Tag[];
};
export type Ec2KeyPairMaterial = {
keyPairId: string;
keyName: string;
keyMaterial: string;
};
export type Ec2Vpc = {
vpcId: string;
cidrBlock: string;
state?: string;
isDefault: boolean;
tags: Ec2Tag[];
};
export type Ec2Subnet = {
subnetId: string;
vpcId: string;
cidrBlock: string;
availabilityZone: string;
availableIpAddressCount?: number;
mapPublicIpOnLaunch: boolean;
state?: string;
tags: Ec2Tag[];
};
export type Ec2AvailabilityZone = {
zoneName: string;
zoneId: string;
state: string;
};
export type Ec2InstanceType = {
instanceType: string;
vcpu: number;
memoryMiB: number;
};
export type Ec2VpcAttributes = {
enableDnsHostnames: boolean;
enableDnsSupport: boolean;
};
export type Ec2ConsoleOutput = {
instanceId: string;
output: string;
timestamp?: string;
};
export type RunEc2InstanceInput = {
name: string;
imageId: string;
instanceType: string;
keyName?: string;
subnetId?: string;
securityGroupIds?: string[];
userData?: string;
iamInstanceProfileName?: string;
associatePublicIpAddress?: boolean;
rootVolumeSize?: number;
rootVolumeType?: string;
};
export type CreateAmiInput = {
name: string;
description?: string;
noReboot?: boolean;
};
export type IpPermissionInput = {
protocol: string;
fromPort: number;
toPort: number;
cidr: string;
};
export type SubnetGroup = {
name: string;
count: number;
isPublic: boolean;
az?: string;
prefix?: number;
};
export type VpcWizardInput = {
name: string;
cidrBlock: string;
subnetGroups: SubnetGroup[];
natGateway: boolean;
availabilityZone?: string;
};
export type VpcWizardResult = {
vpcId: string;
igwId: string;
publicSubnetIds: string[];
privateSubnetIds: string[];
subnetGroups: Array<{ name: string; isPublic: boolean; subnetIds: string[] }>;
natGatewayId?: string;
eipAllocationId?: string;
publicRouteTableId: string;
privateRouteTableId?: string;
};
export type Ec2InternetGateway = {
internetGatewayId: string;
attachments: Array<{ vpcId: string; state: string }>;
tags: Ec2Tag[];
};
export type Ec2NatGateway = {
natGatewayId: string;
subnetId: string;
vpcId: string;
state?: string;
publicIp?: string;
privateIp?: string;
eipAllocationId?: string;
tags: Ec2Tag[];
};
export type Ec2Route = {
destinationCidrBlock?: string;
gatewayId?: string;
natGatewayId?: string;
vpcPeeringConnectionId?: string;
state?: string;
origin?: string;
};
export type Ec2RouteTableAssociation = {
associationId: string;
subnetId?: string;
isMain: boolean;
};
export type Ec2RouteTable = {
routeTableId: string;
vpcId: string;
routes: Ec2Route[];
associations: Ec2RouteTableAssociation[];
tags: Ec2Tag[];
};
export type Ec2ElasticIp = {
allocationId: string;
publicIp: string;
domain?: string;
associationId?: string;
instanceId?: string;
networkInterfaceId?: string;
tags: Ec2Tag[];
};
export type CreateNatGatewayInput = {
name?: string;
subnetId: string;
allocationId?: string;
};
export type CreateRouteInput = {
destinationCidrBlock: string;
gatewayId?: string;
natGatewayId?: string;
vpcPeeringConnectionId?: string;
};
export type ResourceSummary = {
id: string;
name: string;
status?: string;
metadata?: Record<string, unknown>;
};
// ─── Functions ────────────────────────────────────────────────────────────────
export async function listEc2Instances(signal?: AbortSignal): Promise<Ec2Instance[]> {
const res = await apiClient.call<Ec2Instance[]>(apiEndpointKeys.aws.ec2.instances.list, { signal });
return res.data;
}
export async function describeEc2Instance(instanceId: string, signal?: AbortSignal): Promise<Ec2Instance> {
const res = await apiClient.call<Ec2Instance>(
apiEndpointKeys.aws.ec2.instances.describe,
{ signal },
{ instanceId },
);
return res.data;
}
export async function runEc2Instance(input: RunEc2InstanceInput): Promise<Ec2Instance> {
const res = await apiClient.call<Ec2Instance>(
apiEndpointKeys.aws.ec2.instances.create,
{ body: JSON.stringify(input), headers: { "content-type": "application/json" } },
);
return res.data;
}
export async function startEc2Instance(instanceId: string): Promise<void> {
await apiClient.call(
apiEndpointKeys.aws.ec2.instances.start,
{ body: "{}", headers: { "content-type": "application/json" } },
{ instanceId },
);
}
export async function stopEc2Instance(instanceId: string): Promise<void> {
await apiClient.call(
apiEndpointKeys.aws.ec2.instances.stop,
{ body: "{}", headers: { "content-type": "application/json" } },
{ instanceId },
);
}
export async function rebootEc2Instance(instanceId: string): Promise<void> {
await apiClient.call(
apiEndpointKeys.aws.ec2.instances.reboot,
{ body: "{}", headers: { "content-type": "application/json" } },
{ instanceId },
);
}
export async function terminateEc2Instance(instanceId: string): Promise<void> {
await apiClient.call(
apiEndpointKeys.aws.ec2.instances.terminate,
{},
{ instanceId },
);
}
export async function updateEc2InstanceTags(
instanceId: string,
toAdd: Ec2Tag[],
toRemove: string[],
): Promise<void> {
await apiClient.call(
apiEndpointKeys.aws.ec2.instances.tags,
{ body: JSON.stringify({ toAdd, toRemove }), headers: { "content-type": "application/json" } },
{ instanceId },
);
}
export async function createEc2Ami(instanceId: string, input: CreateAmiInput): Promise<{ imageId: string }> {
const res = await apiClient.call<{ imageId: string }>(
apiEndpointKeys.aws.ec2.instances.createImage,
{ body: JSON.stringify(input), headers: { "content-type": "application/json" } },
{ instanceId },
);
return res.data;
}
export async function getEc2ConsoleOutput(instanceId: string, signal?: AbortSignal): Promise<Ec2ConsoleOutput> {
const res = await apiClient.call<Ec2ConsoleOutput>(
apiEndpointKeys.aws.ec2.instances.console,
{ signal },
{ instanceId },
);
return res.data;
}
export async function listEc2Amis(signal?: AbortSignal): Promise<Ec2Image[]> {
const res = await apiClient.call<Ec2Image[]>(apiEndpointKeys.aws.ec2.amis.list, { signal });
return res.data;
}
export async function deregisterEc2Ami(imageId: string): Promise<void> {
await apiClient.call(apiEndpointKeys.aws.ec2.amis.deregister, {}, { imageId });
}
export async function listEc2KeyPairs(signal?: AbortSignal): Promise<Ec2KeyPair[]> {
const res = await apiClient.call<Ec2KeyPair[]>(apiEndpointKeys.aws.ec2.keyPairs.list, { signal });
return res.data;
}
export async function createEc2KeyPair(name: string): Promise<Ec2KeyPairMaterial> {
const res = await apiClient.call<Ec2KeyPairMaterial>(
apiEndpointKeys.aws.ec2.keyPairs.create,
{ body: JSON.stringify({ name }), headers: { "content-type": "application/json" } },
);
return res.data;
}
export async function deleteEc2KeyPair(name: string): Promise<void> {
await apiClient.call(apiEndpointKeys.aws.ec2.keyPairs.delete, {}, { name });
}
export async function listEc2SecurityGroups(vpcId?: string, signal?: AbortSignal): Promise<Ec2SecurityGroup[]> {
const res = await apiClient.call<Ec2SecurityGroup[]>(
apiEndpointKeys.aws.ec2.securityGroups.list,
{ params: { vpcId }, signal },
);
return res.data;
}
export async function createEc2SecurityGroup(
name: string,
description: string,
vpcId?: string,
): Promise<{ groupId: string }> {
const res = await apiClient.call<
{ groupId: string },
{ name: string; description: string; vpcId?: string }
>(
apiEndpointKeys.aws.ec2.securityGroups.create,
{ body: { name, description, vpcId } },
);
return res.data;
}
export async function deleteEc2SecurityGroup(groupId: string): Promise<void> {
await apiClient.call(apiEndpointKeys.aws.ec2.securityGroups.delete, {}, { groupId });
}
export async function authorizeEc2SecurityGroupIngress(
groupId: string,
permission: IpPermissionInput,
): Promise<void> {
await apiClient.call(
apiEndpointKeys.aws.ec2.securityGroups.authorize,
{ body: JSON.stringify(permission), headers: { "content-type": "application/json" } },
{ groupId },
);
}
export async function revokeEc2SecurityGroupIngress(
groupId: string,
permission: IpPermissionInput,
): Promise<void> {
await apiClient.call(
apiEndpointKeys.aws.ec2.securityGroups.revoke,
{ body: JSON.stringify(permission), headers: { "content-type": "application/json" } },
{ groupId },
);
}
export async function authorizeEc2SecurityGroupEgress(
groupId: string,
permission: IpPermissionInput,
): Promise<void> {
await apiClient.call(
apiEndpointKeys.aws.ec2.securityGroups.authorizeEgress,
{ body: JSON.stringify(permission), headers: { "content-type": "application/json" } },
{ groupId },
);
}
export async function revokeEc2SecurityGroupEgress(
groupId: string,
permission: IpPermissionInput,
): Promise<void> {
await apiClient.call(
apiEndpointKeys.aws.ec2.securityGroups.revokeEgress,
{ body: JSON.stringify(permission), headers: { "content-type": "application/json" } },
{ groupId },
);
}
export async function listEc2AvailabilityZones(signal?: AbortSignal): Promise<Ec2AvailabilityZone[]> {
const res = await apiClient.call<Ec2AvailabilityZone[]>(apiEndpointKeys.aws.ec2.availabilityZones, { signal });
return res.data;
}
export async function listEc2InstanceTypes(signal?: AbortSignal): Promise<Ec2InstanceType[]> {
const res = await apiClient.call<Ec2InstanceType[]>(apiEndpointKeys.aws.ec2.instanceTypes, { signal });
return res.data;
}
export async function modifyEc2SubnetAttribute(subnetId: string, mapPublicIpOnLaunch: boolean): Promise<void> {
await apiClient.call(
apiEndpointKeys.aws.ec2.subnets.modifyAttribute,
{ body: JSON.stringify({ mapPublicIpOnLaunch }), headers: { "content-type": "application/json" } },
{ subnetId },
);
}
export async function getEc2VpcAttributes(vpcId: string, signal?: AbortSignal): Promise<Ec2VpcAttributes> {
const res = await apiClient.call<Ec2VpcAttributes>(
apiEndpointKeys.aws.ec2.vpcs.getAttributes,
{ signal },
{ vpcId },
);
return res.data;
}
export async function modifyEc2VpcAttribute(
vpcId: string,
attribute: "enableDnsHostnames" | "enableDnsSupport",
value: boolean,
): Promise<void> {
await apiClient.call(
apiEndpointKeys.aws.ec2.vpcs.modifyAttribute,
{ body: JSON.stringify({ attribute, value }), headers: { "content-type": "application/json" } },
{ vpcId },
);
}
export async function associateEc2ElasticIp(
allocationId: string,
instanceId: string,
): Promise<{ associationId: string }> {
const res = await apiClient.call<{ associationId: string }>(
apiEndpointKeys.aws.ec2.elasticIps.associate,
{ body: JSON.stringify({ instanceId }), headers: { "content-type": "application/json" } },
{ allocationId },
);
return res.data;
}
export async function disassociateEc2ElasticIp(allocationId: string, associationId: string): Promise<void> {
await apiClient.call(
apiEndpointKeys.aws.ec2.elasticIps.disassociate,
{ body: JSON.stringify({ associationId }), headers: { "content-type": "application/json" } },
{ allocationId },
);
}
export async function listEc2Vpcs(signal?: AbortSignal): Promise<Ec2Vpc[]> {
const res = await apiClient.call<Ec2Vpc[]>(apiEndpointKeys.aws.ec2.vpcs.list, { signal });
return res.data;
}
export async function createEc2Vpc(cidrBlock: string): Promise<Ec2Vpc> {
const res = await apiClient.call<Ec2Vpc>(
apiEndpointKeys.aws.ec2.vpcs.create,
{ body: JSON.stringify({ cidrBlock }), headers: { "content-type": "application/json" } },
);
return res.data;
}
export async function deleteEc2Vpc(vpcId: string): Promise<void> {
await apiClient.call(apiEndpointKeys.aws.ec2.vpcs.delete, {}, { vpcId });
}
export async function listEc2Subnets(vpcId?: string, signal?: AbortSignal): Promise<Ec2Subnet[]> {
const res = await apiClient.call<Ec2Subnet[]>(
apiEndpointKeys.aws.ec2.subnets.list,
{ params: { vpcId }, signal },
);
return res.data;
}
export async function createEc2Subnet(
vpcId: string,
cidrBlock: string,
availabilityZone?: string,
): Promise<Ec2Subnet> {
const res = await apiClient.call<Ec2Subnet>(
apiEndpointKeys.aws.ec2.subnets.create,
{ body: JSON.stringify({ vpcId, cidrBlock, availabilityZone }), headers: { "content-type": "application/json" } },
);
return res.data;
}
export async function deleteEc2Subnet(subnetId: string): Promise<void> {
await apiClient.call(apiEndpointKeys.aws.ec2.subnets.delete, {}, { subnetId });
}
export async function listEc2InternetGateways(signal?: AbortSignal): Promise<Ec2InternetGateway[]> {
const res = await apiClient.call<Ec2InternetGateway[]>(apiEndpointKeys.aws.ec2.internetGateways.list, { signal });
return res.data;
}
export async function createEc2InternetGateway(name?: string): Promise<Ec2InternetGateway> {
const res = await apiClient.call<Ec2InternetGateway>(
apiEndpointKeys.aws.ec2.internetGateways.create,
{ body: JSON.stringify({ name }), headers: { "content-type": "application/json" } },
);
return res.data;
}
export async function attachEc2InternetGateway(igwId: string, vpcId: string): Promise<void> {
await apiClient.call(
apiEndpointKeys.aws.ec2.internetGateways.attach,
{ body: JSON.stringify({ vpcId }), headers: { "content-type": "application/json" } },
{ igwId },
);
}
export async function detachEc2InternetGateway(igwId: string, vpcId: string): Promise<void> {
await apiClient.call(
apiEndpointKeys.aws.ec2.internetGateways.detach,
{ body: JSON.stringify({ vpcId }), headers: { "content-type": "application/json" } },
{ igwId },
);
}
export async function deleteEc2InternetGateway(igwId: string): Promise<void> {
await apiClient.call(apiEndpointKeys.aws.ec2.internetGateways.delete, {}, { igwId });
}
export async function listEc2NatGateways(vpcId?: string, signal?: AbortSignal): Promise<Ec2NatGateway[]> {
const res = await apiClient.call<Ec2NatGateway[]>(
apiEndpointKeys.aws.ec2.natGateways.list,
{ params: { vpcId }, signal },
);
return res.data;
}
export async function createEc2NatGateway(input: CreateNatGatewayInput): Promise<Ec2NatGateway> {
const res = await apiClient.call<Ec2NatGateway>(
apiEndpointKeys.aws.ec2.natGateways.create,
{ body: JSON.stringify(input), headers: { "content-type": "application/json" } },
);
return res.data;
}
export async function deleteEc2NatGateway(natId: string): Promise<void> {
await apiClient.call(apiEndpointKeys.aws.ec2.natGateways.delete, {}, { natId });
}
export async function listEc2RouteTables(vpcId?: string, signal?: AbortSignal): Promise<Ec2RouteTable[]> {
const res = await apiClient.call<Ec2RouteTable[]>(
apiEndpointKeys.aws.ec2.routeTables.list,
{ params: { vpcId }, signal },
);
return res.data;
}
export async function createEc2RouteTable(vpcId: string, name?: string): Promise<Ec2RouteTable> {
const res = await apiClient.call<Ec2RouteTable>(
apiEndpointKeys.aws.ec2.routeTables.create,
{ body: JSON.stringify({ vpcId, name }), headers: { "content-type": "application/json" } },
);
return res.data;
}
export async function deleteEc2RouteTable(rtbId: string): Promise<void> {
await apiClient.call(apiEndpointKeys.aws.ec2.routeTables.delete, {}, { rtbId });
}
export async function createEc2Route(rtbId: string, input: CreateRouteInput): Promise<void> {
await apiClient.call(
apiEndpointKeys.aws.ec2.routeTables.createRoute,
{ body: JSON.stringify(input), headers: { "content-type": "application/json" } },
{ rtbId },
);
}
export async function deleteEc2Route(rtbId: string, cidr: string): Promise<void> {
await apiClient.call(
apiEndpointKeys.aws.ec2.routeTables.deleteRoute,
{ params: { cidr } },
{ rtbId },
);
}
export async function associateEc2RouteTable(rtbId: string, subnetId: string): Promise<string> {
const res = await apiClient.call<{ associationId: string }>(
apiEndpointKeys.aws.ec2.routeTables.associate,
{ body: JSON.stringify({ subnetId }), headers: { "content-type": "application/json" } },
{ rtbId },
);
return res.data.associationId;
}
export async function disassociateEc2RouteTable(associationId: string): Promise<void> {
await apiClient.call(apiEndpointKeys.aws.ec2.routeTables.disassociate, {}, { associationId });
}
export async function listEc2ElasticIps(signal?: AbortSignal): Promise<Ec2ElasticIp[]> {
const res = await apiClient.call<Ec2ElasticIp[]>(apiEndpointKeys.aws.ec2.elasticIps.list, { signal });
return res.data;
}
export async function allocateEc2ElasticIp(name?: string): Promise<Ec2ElasticIp> {
const res = await apiClient.call<Ec2ElasticIp>(
apiEndpointKeys.aws.ec2.elasticIps.create,
{ body: JSON.stringify({ name }), headers: { "content-type": "application/json" } },
);
return res.data;
}
export async function releaseEc2ElasticIp(allocationId: string): Promise<void> {
await apiClient.call(apiEndpointKeys.aws.ec2.elasticIps.release, {}, { allocationId });
}
export async function createVpcWizard(input: VpcWizardInput): Promise<VpcWizardResult> {
const res = await apiClient.call<VpcWizardResult>(
apiEndpointKeys.aws.ec2.vpcWizard,
{ body: JSON.stringify(input), headers: { "content-type": "application/json" } },
);
return res.data;
}
export async function listEc2Resources(signal?: AbortSignal): Promise<ResourceSummary[]> {
const instances = await listEc2Instances(signal);
return instances.map((instance) => ({
id: instance.instanceId,
name: instance.name,
status: instance.state,
metadata: {
instanceType: instance.instanceType,
az: instance.availabilityZone,
publicIp: instance.publicIpAddress,
privateIp: instance.privateIpAddress,
},
}));
}
// ─── Client ───────────────────────────────────────────────────────────────────
export const ec2Client = {
listInstances: listEc2Instances,
describeInstance: describeEc2Instance,
runInstance: runEc2Instance,
startInstance: startEc2Instance,
stopInstance: stopEc2Instance,
rebootInstance: rebootEc2Instance,
terminateInstance: terminateEc2Instance,
updateInstanceTags: updateEc2InstanceTags,
createAmi: createEc2Ami,
getConsoleOutput: getEc2ConsoleOutput,
listAmis: listEc2Amis,
deregisterAmi: deregisterEc2Ami,
listResources: listEc2Resources,
listKeyPairs: listEc2KeyPairs,
createKeyPair: createEc2KeyPair,
deleteKeyPair: deleteEc2KeyPair,
listSecurityGroups: listEc2SecurityGroups,
createSecurityGroup: createEc2SecurityGroup,
deleteSecurityGroup: deleteEc2SecurityGroup,
authorizeSecurityGroupIngress: authorizeEc2SecurityGroupIngress,
revokeSecurityGroupIngress: revokeEc2SecurityGroupIngress,
authorizeSecurityGroupEgress: authorizeEc2SecurityGroupEgress,
revokeSecurityGroupEgress: revokeEc2SecurityGroupEgress,
listAvailabilityZones: listEc2AvailabilityZones,
listInstanceTypes: listEc2InstanceTypes,
modifySubnetAttribute: modifyEc2SubnetAttribute,
getVpcAttributes: getEc2VpcAttributes,
modifyVpcAttribute: modifyEc2VpcAttribute,
associateElasticIp: associateEc2ElasticIp,
disassociateElasticIp: disassociateEc2ElasticIp,
listVpcs: listEc2Vpcs,
createVpc: createEc2Vpc,
deleteVpc: deleteEc2Vpc,
listSubnets: listEc2Subnets,
createSubnet: createEc2Subnet,
deleteSubnet: deleteEc2Subnet,
listInternetGateways: listEc2InternetGateways,
createInternetGateway: createEc2InternetGateway,
attachInternetGateway: attachEc2InternetGateway,
detachInternetGateway: detachEc2InternetGateway,
deleteInternetGateway: deleteEc2InternetGateway,
listNatGateways: listEc2NatGateways,
createNatGateway: createEc2NatGateway,
deleteNatGateway: deleteEc2NatGateway,
listRouteTables: listEc2RouteTables,
createRouteTable: createEc2RouteTable,
deleteRouteTable: deleteEc2RouteTable,
createRoute: createEc2Route,
deleteRoute: deleteEc2Route,
associateRouteTable: associateEc2RouteTable,
disassociateRouteTable: disassociateEc2RouteTable,
listElasticIps: listEc2ElasticIps,
allocateElasticIp: allocateEc2ElasticIp,
releaseElasticIp: releaseEc2ElasticIp,
createVpcWizard,
};
@@ -0,0 +1,493 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import {
allocateEc2ElasticIp,
associateEc2ElasticIp,
associateEc2RouteTable,
attachEc2InternetGateway,
authorizeEc2SecurityGroupEgress,
authorizeEc2SecurityGroupIngress,
createEc2Ami,
createEc2InternetGateway,
createEc2KeyPair,
createEc2NatGateway,
createEc2Route,
createEc2RouteTable,
createEc2SecurityGroup,
createEc2Subnet,
createEc2Vpc,
deregisterEc2Ami,
deleteEc2InternetGateway,
deleteEc2KeyPair,
deleteEc2NatGateway,
deleteEc2Route,
deleteEc2RouteTable,
deleteEc2SecurityGroup,
deleteEc2Subnet,
deleteEc2Vpc,
detachEc2InternetGateway,
disassociateEc2ElasticIp,
disassociateEc2RouteTable,
modifyEc2SubnetAttribute,
modifyEc2VpcAttribute,
rebootEc2Instance,
releaseEc2ElasticIp,
revokeEc2SecurityGroupEgress,
revokeEc2SecurityGroupIngress,
runEc2Instance,
startEc2Instance,
stopEc2Instance,
terminateEc2Instance,
updateEc2InstanceTags,
} from "./ec2.api";
import type {
CreateAmiInput,
CreateNatGatewayInput,
CreateRouteInput,
Ec2Tag,
IpPermissionInput,
RunEc2InstanceInput,
} from "./ec2.api";
import { ec2QueryKeys } from "./ec2.queries";
// ─── Instance lifecycle ───────────────────────────────────────────────────────
export function useCreateEc2InstanceMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (input: RunEc2InstanceInput) => runEc2Instance(input),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.instances });
},
});
}
export function useStartEc2InstanceMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (instanceId: string) => startEc2Instance(instanceId),
onSuccess: (_data, instanceId) => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.instances });
void qc.invalidateQueries({ queryKey: ec2QueryKeys.instance(instanceId) });
},
});
}
export function useStopEc2InstanceMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (instanceId: string) => stopEc2Instance(instanceId),
onSuccess: (_data, instanceId) => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.instances });
void qc.invalidateQueries({ queryKey: ec2QueryKeys.instance(instanceId) });
},
});
}
export function useRebootEc2InstanceMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (instanceId: string) => rebootEc2Instance(instanceId),
onSuccess: (_data, instanceId) => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.instances });
void qc.invalidateQueries({ queryKey: ec2QueryKeys.instance(instanceId) });
},
});
}
export function useTerminateEc2InstanceMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (instanceId: string) => terminateEc2Instance(instanceId),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.instances });
},
});
}
export function useUpdateEc2InstanceTagsMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ instanceId, toAdd, toRemove }: { instanceId: string; toAdd: Ec2Tag[]; toRemove: string[] }) =>
updateEc2InstanceTags(instanceId, toAdd, toRemove),
onSuccess: (_data, { instanceId }) => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.instance(instanceId) });
void qc.invalidateQueries({ queryKey: ec2QueryKeys.instances });
},
});
}
// ─── AMIs ─────────────────────────────────────────────────────────────────────
export function useCreateAmiMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ instanceId, input }: { instanceId: string; input: CreateAmiInput }) =>
createEc2Ami(instanceId, input),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.amis });
},
});
}
export function useDeregisterAmiMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (imageId: string) => deregisterEc2Ami(imageId),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.amis });
},
});
}
// ─── Key pairs ────────────────────────────────────────────────────────────────
export function useCreateKeyPairMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (name: string) => createEc2KeyPair(name),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.keyPairs });
},
});
}
export function useDeleteKeyPairMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (name: string) => deleteEc2KeyPair(name),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.keyPairs });
},
});
}
// ─── Security groups ──────────────────────────────────────────────────────────
export function useCreateSecurityGroupMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ name, description, vpcId }: { name: string; description: string; vpcId: string }) =>
createEc2SecurityGroup(name, description, vpcId),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.securityGroupsAll });
},
});
}
export function useDeleteSecurityGroupMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (groupId: string) => deleteEc2SecurityGroup(groupId),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.securityGroupsAll });
},
});
}
export function useAuthorizeSecurityGroupIngressMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ groupId, permission }: { groupId: string; permission: IpPermissionInput }) =>
authorizeEc2SecurityGroupIngress(groupId, permission),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.securityGroupsAll });
},
});
}
export function useRevokeSecurityGroupIngressMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ groupId, permission }: { groupId: string; permission: IpPermissionInput }) =>
revokeEc2SecurityGroupIngress(groupId, permission),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.securityGroupsAll });
},
});
}
export function useAuthorizeSecurityGroupEgressMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ groupId, permission }: { groupId: string; permission: IpPermissionInput }) =>
authorizeEc2SecurityGroupEgress(groupId, permission),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.securityGroupsAll });
},
});
}
export function useRevokeSecurityGroupEgressMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ groupId, permission }: { groupId: string; permission: IpPermissionInput }) =>
revokeEc2SecurityGroupEgress(groupId, permission),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.securityGroupsAll });
},
});
}
// ─── VPCs ─────────────────────────────────────────────────────────────────────
export function useCreateVpcMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (cidrBlock: string) => createEc2Vpc(cidrBlock),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.vpcs });
},
});
}
export function useDeleteVpcMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (vpcId: string) => deleteEc2Vpc(vpcId),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.vpcs });
void qc.invalidateQueries({ queryKey: ec2QueryKeys.subnetsAll });
void qc.invalidateQueries({ queryKey: ec2QueryKeys.securityGroupsAll });
void qc.invalidateQueries({ queryKey: ec2QueryKeys.internetGateways });
void qc.invalidateQueries({ queryKey: ec2QueryKeys.routeTablesAll });
void qc.invalidateQueries({ queryKey: ec2QueryKeys.elasticIps });
},
});
}
// ─── Subnets ──────────────────────────────────────────────────────────────────
export function useCreateSubnetMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({
vpcId,
cidrBlock,
availabilityZone,
}: {
vpcId: string;
cidrBlock: string;
availabilityZone?: string;
}) => createEc2Subnet(vpcId, cidrBlock, availabilityZone),
onSuccess: (_data, { vpcId }) => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.subnets(vpcId) });
void qc.invalidateQueries({ queryKey: ec2QueryKeys.subnets() });
},
});
}
export function useDeleteSubnetMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (subnetId: string) => deleteEc2Subnet(subnetId),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.subnetsAll });
},
});
}
// ─── Internet Gateways ────────────────────────────────────────────────────────
export function useCreateInternetGatewayMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (name?: string) => createEc2InternetGateway(name),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.internetGateways });
},
});
}
export function useAttachInternetGatewayMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ igwId, vpcId }: { igwId: string; vpcId: string }) =>
attachEc2InternetGateway(igwId, vpcId),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.internetGateways });
},
});
}
export function useDetachInternetGatewayMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ igwId, vpcId }: { igwId: string; vpcId: string }) =>
detachEc2InternetGateway(igwId, vpcId),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.internetGateways });
},
});
}
export function useDeleteInternetGatewayMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (igwId: string) => deleteEc2InternetGateway(igwId),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.internetGateways });
void qc.invalidateQueries({ queryKey: ec2QueryKeys.routeTablesAll });
},
});
}
// ─── NAT Gateways ─────────────────────────────────────────────────────────────
export function useCreateNatGatewayMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (input: CreateNatGatewayInput) => createEc2NatGateway(input),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.natGatewaysAll });
void qc.invalidateQueries({ queryKey: ec2QueryKeys.elasticIps });
},
});
}
export function useDeleteNatGatewayMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (natId: string) => deleteEc2NatGateway(natId),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.natGatewaysAll });
void qc.invalidateQueries({ queryKey: ec2QueryKeys.routeTablesAll });
},
});
}
// ─── Route Tables ─────────────────────────────────────────────────────────────
export function useCreateRouteTableMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ vpcId, name }: { vpcId: string; name?: string }) =>
createEc2RouteTable(vpcId, name),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.routeTablesAll });
},
});
}
export function useDeleteRouteTableMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (rtbId: string) => deleteEc2RouteTable(rtbId),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.routeTablesAll });
void qc.invalidateQueries({ queryKey: ec2QueryKeys.subnetsAll });
},
});
}
export function useCreateRouteMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ rtbId, input }: { rtbId: string; input: CreateRouteInput }) =>
createEc2Route(rtbId, input),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.routeTablesAll });
},
});
}
export function useDeleteRouteMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ rtbId, cidr }: { rtbId: string; cidr: string }) =>
deleteEc2Route(rtbId, cidr),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.routeTablesAll });
},
});
}
export function useAssociateRouteTableMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ rtbId, subnetId }: { rtbId: string; subnetId: string }) =>
associateEc2RouteTable(rtbId, subnetId),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.routeTablesAll });
void qc.invalidateQueries({ queryKey: ec2QueryKeys.subnetsAll });
},
});
}
export function useDisassociateRouteTableMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (associationId: string) => disassociateEc2RouteTable(associationId),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.routeTablesAll });
void qc.invalidateQueries({ queryKey: ec2QueryKeys.subnetsAll });
},
});
}
// ─── Elastic IPs ──────────────────────────────────────────────────────────────
export function useAllocateElasticIpMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (name?: string) => allocateEc2ElasticIp(name),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.elasticIps });
},
});
}
export function useReleaseElasticIpMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (allocationId: string) => releaseEc2ElasticIp(allocationId),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.elasticIps });
},
});
}
export function useAssociateElasticIpMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ allocationId, instanceId }: { allocationId: string; instanceId: string }) =>
associateEc2ElasticIp(allocationId, instanceId),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.elasticIps });
void qc.invalidateQueries({ queryKey: ec2QueryKeys.instances });
},
});
}
export function useDisassociateElasticIpMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ allocationId, associationId }: { allocationId: string; associationId: string }) =>
disassociateEc2ElasticIp(allocationId, associationId),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.elasticIps });
void qc.invalidateQueries({ queryKey: ec2QueryKeys.instances });
},
});
}
export function useModifySubnetAttributeMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ subnetId, mapPublicIpOnLaunch }: { subnetId: string; mapPublicIpOnLaunch: boolean }) =>
modifyEc2SubnetAttribute(subnetId, mapPublicIpOnLaunch),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.subnetsAll });
},
});
}
export function useModifyVpcAttributeMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ vpcId, attribute, value }: { vpcId: string; attribute: "enableDnsHostnames" | "enableDnsSupport"; value: boolean }) =>
modifyEc2VpcAttribute(vpcId, attribute, value),
onSuccess: (_data, { vpcId }) => {
void qc.invalidateQueries({ queryKey: ec2QueryKeys.vpcAttributes(vpcId) });
},
});
}
@@ -0,0 +1,159 @@
import { useQuery } from "@tanstack/react-query";
import { ec2Client } from "./ec2.api";
export const ec2QueryKeys = {
instances: ["ec2", "instances"] as const,
instance: (instanceId: string | null) => ["ec2", "instance", instanceId] as const,
amis: ["ec2", "amis"] as const,
keyPairs: ["ec2", "key-pairs"] as const,
// Parameterized list queries — call with vpcId for scoped, omit for all
securityGroups: (vpcId?: string) => ["ec2", "security-groups", vpcId ?? "all"] as const,
subnets: (vpcId?: string) => ["ec2", "subnets", vpcId ?? "all"] as const,
natGateways: (vpcId?: string) => ["ec2", "nat-gateways", vpcId ?? "all"] as const,
routeTables: (vpcId?: string) => ["ec2", "route-tables", vpcId ?? "all"] as const,
// Root keys — use in mutations to invalidate all variants of a parameterized query
securityGroupsAll: ["ec2", "security-groups"] as const,
subnetsAll: ["ec2", "subnets"] as const,
natGatewaysAll: ["ec2", "nat-gateways"] as const,
routeTablesAll: ["ec2", "route-tables"] as const,
// Static list keys
vpcs: ["ec2", "vpcs"] as const,
consoleOutput: (instanceId: string | null) => ["ec2", "console", instanceId] as const,
internetGateways: ["ec2", "internet-gateways"] as const,
elasticIps: ["ec2", "elastic-ips"] as const,
availabilityZones: ["ec2", "availability-zones"] as const,
instanceTypes: ["ec2", "instance-types"] as const,
vpcAttributes: (vpcId: string) => ["ec2", "vpc-attributes", vpcId] as const,
};
export function useEc2InstancesQuery() {
return useQuery({
queryKey: ec2QueryKeys.instances,
queryFn: ({ signal }) => ec2Client.listInstances(signal),
refetchInterval: 30_000,
});
}
export function useEc2InstanceQuery(instanceId: string | null) {
return useQuery({
queryKey: ec2QueryKeys.instance(instanceId),
queryFn: ({ signal }) => ec2Client.describeInstance(instanceId!, signal),
enabled: Boolean(instanceId),
refetchInterval: 30_000,
});
}
export function useEc2AmisQuery() {
return useQuery({
queryKey: ec2QueryKeys.amis,
queryFn: ({ signal }) => ec2Client.listAmis(signal),
refetchInterval: 30_000,
});
}
export function useEc2KeyPairsQuery(enabled = true) {
return useQuery({
queryKey: ec2QueryKeys.keyPairs,
queryFn: ({ signal }) => ec2Client.listKeyPairs(signal),
enabled,
staleTime: 60_000,
});
}
export function useEc2SecurityGroupsQuery(vpcId?: string, enabled = true) {
return useQuery({
queryKey: ec2QueryKeys.securityGroups(vpcId),
queryFn: ({ signal }) => ec2Client.listSecurityGroups(vpcId, signal),
enabled,
staleTime: 30_000,
});
}
export function useEc2VpcsQuery(enabled = true) {
return useQuery({
queryKey: ec2QueryKeys.vpcs,
queryFn: ({ signal }) => ec2Client.listVpcs(signal),
enabled,
staleTime: 60_000,
});
}
export function useEc2SubnetsQuery(vpcId?: string, enabled = true) {
return useQuery({
queryKey: ec2QueryKeys.subnets(vpcId),
queryFn: ({ signal }) => ec2Client.listSubnets(vpcId, signal),
enabled,
staleTime: 60_000,
});
}
export function useEc2ConsoleOutputQuery(instanceId: string | null) {
return useQuery({
queryKey: ec2QueryKeys.consoleOutput(instanceId),
queryFn: ({ signal }) => ec2Client.getConsoleOutput(instanceId!, signal),
enabled: Boolean(instanceId),
staleTime: 0,
gcTime: 0,
});
}
export function useEc2InternetGatewaysQuery(enabled = true) {
return useQuery({
queryKey: ec2QueryKeys.internetGateways,
queryFn: ({ signal }) => ec2Client.listInternetGateways(signal),
enabled,
staleTime: 60_000,
});
}
export function useEc2NatGatewaysQuery(vpcId?: string, enabled = true) {
return useQuery({
queryKey: ec2QueryKeys.natGateways(vpcId),
queryFn: ({ signal }) => ec2Client.listNatGateways(vpcId, signal),
enabled,
staleTime: 30_000,
});
}
export function useEc2RouteTablesQuery(vpcId?: string, enabled = true) {
return useQuery({
queryKey: ec2QueryKeys.routeTables(vpcId),
queryFn: ({ signal }) => ec2Client.listRouteTables(vpcId, signal),
enabled,
staleTime: 30_000,
});
}
export function useEc2ElasticIpsQuery(enabled = true) {
return useQuery({
queryKey: ec2QueryKeys.elasticIps,
queryFn: ({ signal }) => ec2Client.listElasticIps(signal),
enabled,
staleTime: 60_000,
});
}
export function useEc2AvailabilityZonesQuery() {
return useQuery({
queryKey: ec2QueryKeys.availabilityZones,
queryFn: ({ signal }) => ec2Client.listAvailabilityZones(signal),
staleTime: Infinity,
});
}
export function useEc2InstanceTypesQuery() {
return useQuery({
queryKey: ec2QueryKeys.instanceTypes,
queryFn: ({ signal }) => ec2Client.listInstanceTypes(signal),
staleTime: Infinity,
});
}
export function useEc2VpcAttributesQuery(vpcId: string | undefined) {
return useQuery({
queryKey: ec2QueryKeys.vpcAttributes(vpcId ?? ""),
queryFn: ({ signal }) => ec2Client.getVpcAttributes(vpcId!, signal),
enabled: Boolean(vpcId),
staleTime: 30_000,
});
}
+260
View File
@@ -0,0 +1,260 @@
import { apiClient, apiEndpointKeys } from "@/api/api";
import type { ResourceSummary } from "@/api/types";
export type EksVpcConfig = {
subnetIds: string[];
securityGroupIds: string[];
clusterSecurityGroupId?: string;
vpcId?: string;
endpointPublicAccess?: boolean;
endpointPrivateAccess?: boolean;
publicAccessCidrs: string[];
};
export type EksCluster = {
name: string;
arn?: string;
createdAt?: string;
version?: string;
endpoint?: string;
roleArn?: string;
status?: string;
platformVersion?: string;
certificateAuthority?: {
data?: string;
};
resourcesVpcConfig?: EksVpcConfig;
tags: Record<string, string>;
nodegroupCount?: number;
fargateProfileCount?: number;
};
export type EksNodegroup = {
name: string;
arn?: string;
clusterName: string;
version?: string;
releaseVersion?: string;
createdAt?: string;
modifiedAt?: string;
status?: string;
capacityType?: string;
instanceTypes: string[];
subnets: string[];
nodeRole?: string;
scalingConfig?: {
minSize?: number;
maxSize?: number;
desiredSize?: number;
};
labels: Record<string, string>;
tags: Record<string, string>;
};
export type EksFargateProfileSelector = {
namespace?: string;
labels: Record<string, string>;
};
export type EksFargateProfile = {
name: string;
arn?: string;
clusterName: string;
createdAt?: string;
status?: string;
podExecutionRoleArn?: string;
subnets: string[];
selectors: EksFargateProfileSelector[];
tags: Record<string, string>;
};
export type CreateEksNodegroupInput = {
name: string;
nodeRole: string;
subnets: string[];
instanceTypes?: string[];
scalingConfig?: {
minSize?: number;
maxSize?: number;
desiredSize?: number;
};
labels?: Record<string, string>;
tags?: Record<string, string>;
};
export type CreateEksFargateProfileInput = {
name: string;
podExecutionRoleArn: string;
subnets?: string[];
selectors: {
namespace?: string;
labels?: Record<string, string>;
}[];
tags?: Record<string, string>;
};
export async function listEksClusters(
signal?: AbortSignal,
): Promise<EksCluster[]> {
const res = await apiClient.call<EksCluster[]>(
apiEndpointKeys.aws.eks.clusters.list,
{ signal },
);
return res.data;
}
export async function describeEksCluster(
name: string,
signal?: AbortSignal,
): Promise<EksCluster> {
const res = await apiClient.call<EksCluster>(
apiEndpointKeys.aws.eks.clusters.describe,
{ signal },
{ name },
);
return res.data;
}
export async function listEksNodegroups(
clusterName: string,
signal?: AbortSignal,
): Promise<EksNodegroup[]> {
const res = await apiClient.call<EksNodegroup[]>(
apiEndpointKeys.aws.eks.nodegroups.list,
{ signal },
{ name: clusterName },
);
return res.data;
}
export async function describeEksNodegroup(
clusterName: string,
nodegroupName: string,
signal?: AbortSignal,
): Promise<EksNodegroup> {
const res = await apiClient.call<EksNodegroup>(
apiEndpointKeys.aws.eks.nodegroups.describe,
{ signal },
{ name: clusterName, nodegroup: nodegroupName },
);
return res.data;
}
export async function createEksNodegroup(
clusterName: string,
input: CreateEksNodegroupInput,
): Promise<EksNodegroup> {
const res = await apiClient.call<EksNodegroup, CreateEksNodegroupInput>(
apiEndpointKeys.aws.eks.nodegroups.create,
{ body: input },
{ name: clusterName },
);
return res.data;
}
export async function deleteEksNodegroup(
clusterName: string,
nodegroupName: string,
): Promise<EksNodegroup> {
const res = await apiClient.call<EksNodegroup>(
apiEndpointKeys.aws.eks.nodegroups.delete,
{},
{ name: clusterName, nodegroup: nodegroupName },
);
return res.data;
}
export async function listEksFargateProfiles(
clusterName: string,
signal?: AbortSignal,
): Promise<EksFargateProfile[]> {
const res = await apiClient.call<EksFargateProfile[]>(
apiEndpointKeys.aws.eks.fargateProfiles.list,
{ signal },
{ name: clusterName },
);
return res.data;
}
export async function describeEksFargateProfile(
clusterName: string,
profileName: string,
signal?: AbortSignal,
): Promise<EksFargateProfile> {
const res = await apiClient.call<EksFargateProfile>(
apiEndpointKeys.aws.eks.fargateProfiles.describe,
{ signal },
{ name: clusterName, profile: profileName },
);
return res.data;
}
export async function createEksFargateProfile(
clusterName: string,
input: CreateEksFargateProfileInput,
): Promise<EksFargateProfile> {
const res = await apiClient.call<
EksFargateProfile,
CreateEksFargateProfileInput
>(
apiEndpointKeys.aws.eks.fargateProfiles.create,
{ body: input },
{ name: clusterName },
);
return res.data;
}
export async function deleteEksFargateProfile(
clusterName: string,
profileName: string,
): Promise<EksFargateProfile> {
const res = await apiClient.call<EksFargateProfile>(
apiEndpointKeys.aws.eks.fargateProfiles.delete,
{},
{ name: clusterName, profile: profileName },
);
return res.data;
}
export async function listEksResources(
signal?: AbortSignal,
): Promise<ResourceSummary[]> {
const clusters = await listEksClusters(signal);
return clusters.map((cluster) => ({
id: cluster.arn ?? cluster.name,
name: cluster.name,
status: cluster.status,
metadata: {
version: cluster.version,
platformVersion: cluster.platformVersion,
nodegroups: cluster.nodegroupCount ?? 0,
fargateProfiles: cluster.fargateProfileCount ?? 0,
vpcId: cluster.resourcesVpcConfig?.vpcId,
},
}));
}
export const eksClient = {
listClusters: listEksClusters,
describeCluster: describeEksCluster,
listNodegroups: listEksNodegroups,
describeNodegroup: describeEksNodegroup,
createNodegroup: createEksNodegroup,
deleteNodegroup: deleteEksNodegroup,
listFargateProfiles: listEksFargateProfiles,
describeFargateProfile: describeEksFargateProfile,
createFargateProfile: createEksFargateProfile,
deleteFargateProfile: deleteEksFargateProfile,
listResources: listEksResources,
};
@@ -0,0 +1,87 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import {
eksClient,
type CreateEksFargateProfileInput,
type CreateEksNodegroupInput,
} from "./eks.api";
import { eksQueryKeys } from "./eks.queries";
export type CreateEksNodegroupMutationInput = {
clusterName: string;
nodegroup: CreateEksNodegroupInput;
};
export type DeleteEksNodegroupMutationInput = {
clusterName: string;
nodegroupName: string;
};
export type CreateEksFargateProfileMutationInput = {
clusterName: string;
profile: CreateEksFargateProfileInput;
};
export type DeleteEksFargateProfileMutationInput = {
clusterName: string;
profileName: string;
};
export function useCreateEksNodegroupMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ clusterName, nodegroup }: CreateEksNodegroupMutationInput) =>
eksClient.createNodegroup(clusterName, nodegroup),
onSuccess: (_, input) => {
void qc.invalidateQueries({
queryKey: eksQueryKeys.nodegroups(input.clusterName),
});
void qc.invalidateQueries({ queryKey: eksQueryKeys.clusters });
},
});
}
export function useDeleteEksNodegroupMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ clusterName, nodegroupName }: DeleteEksNodegroupMutationInput) =>
eksClient.deleteNodegroup(clusterName, nodegroupName),
onSuccess: (_, input) => {
void qc.invalidateQueries({
queryKey: eksQueryKeys.nodegroups(input.clusterName),
});
void qc.invalidateQueries({ queryKey: eksQueryKeys.clusters });
},
});
}
export function useCreateEksFargateProfileMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ clusterName, profile }: CreateEksFargateProfileMutationInput) =>
eksClient.createFargateProfile(clusterName, profile),
onSuccess: (_, input) => {
void qc.invalidateQueries({
queryKey: eksQueryKeys.fargateProfiles(input.clusterName),
});
void qc.invalidateQueries({ queryKey: eksQueryKeys.clusters });
},
});
}
export function useDeleteEksFargateProfileMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ clusterName, profileName }: DeleteEksFargateProfileMutationInput) =>
eksClient.deleteFargateProfile(clusterName, profileName),
onSuccess: (_, input) => {
void qc.invalidateQueries({
queryKey: eksQueryKeys.fargateProfiles(input.clusterName),
});
void qc.invalidateQueries({ queryKey: eksQueryKeys.clusters });
},
});
}
@@ -0,0 +1,76 @@
import { useQuery } from "@tanstack/react-query";
import { eksClient } from "./eks.api";
export const eksQueryKeys = {
clusters: ["eks", "clusters"] as const,
cluster: (name: string | null) => ["eks", "cluster", name] as const,
nodegroups: (clusterName: string | null) =>
["eks", "nodegroups", clusterName] as const,
nodegroup: (clusterName: string | null, nodegroupName: string | null) =>
["eks", "nodegroup", clusterName, nodegroupName] as const,
fargateProfiles: (clusterName: string | null) =>
["eks", "fargate-profiles", clusterName] as const,
fargateProfile: (clusterName: string | null, profileName: string | null) =>
["eks", "fargate-profile", clusterName, profileName] as const,
};
export function useEksClustersQuery() {
return useQuery({
queryKey: eksQueryKeys.clusters,
queryFn: ({ signal }) => eksClient.listClusters(signal),
refetchInterval: 30_000,
});
}
export function useEksClusterQuery(name: string | null) {
return useQuery({
queryKey: eksQueryKeys.cluster(name),
queryFn: ({ signal }) => eksClient.describeCluster(name!, signal),
enabled: Boolean(name),
refetchInterval: 30_000,
});
}
export function useEksNodegroupsQuery(clusterName: string | null) {
return useQuery({
queryKey: eksQueryKeys.nodegroups(clusterName),
queryFn: ({ signal }) => eksClient.listNodegroups(clusterName!, signal),
enabled: Boolean(clusterName),
refetchInterval: 30_000,
});
}
export function useEksNodegroupQuery(
clusterName: string | null,
nodegroupName: string | null,
) {
return useQuery({
queryKey: eksQueryKeys.nodegroup(clusterName, nodegroupName),
queryFn: ({ signal }) =>
eksClient.describeNodegroup(clusterName!, nodegroupName!, signal),
enabled: Boolean(clusterName && nodegroupName),
refetchInterval: 30_000,
});
}
export function useEksFargateProfilesQuery(clusterName: string | null) {
return useQuery({
queryKey: eksQueryKeys.fargateProfiles(clusterName),
queryFn: ({ signal }) => eksClient.listFargateProfiles(clusterName!, signal),
enabled: Boolean(clusterName),
refetchInterval: 30_000,
});
}
export function useEksFargateProfileQuery(
clusterName: string | null,
profileName: string | null,
) {
return useQuery({
queryKey: eksQueryKeys.fargateProfile(clusterName, profileName),
queryFn: ({ signal }) =>
eksClient.describeFargateProfile(clusterName!, profileName!, signal),
enabled: Boolean(clusterName && profileName),
refetchInterval: 30_000,
});
}
@@ -0,0 +1,141 @@
import { apiClient, apiEndpointKeys } from "@/api/api";
import type { ResourceSummary } from "@/api/types";
export interface SecretTag {
key: string;
value: string;
}
export interface SecretSummary {
name: string;
arn?: string;
description?: string;
rotationEnabled: boolean;
kmsKeyId?: string;
lastChangedDate?: string;
lastAccessedDate?: string;
createdDate?: string;
tags: SecretTag[];
}
export interface SecretDetail extends SecretSummary {
deletedDate?: string;
versionIds: string[];
}
export interface SecretValue {
name: string;
arn?: string;
versionId?: string;
secretString?: string;
secretBinary?: string;
createdDate?: string;
}
export async function listSecrets(
signal?: AbortSignal,
): Promise<SecretSummary[]> {
const res = await apiClient.call<SecretSummary[]>(
apiEndpointKeys.aws.secretsmanager.secrets.list,
{ signal },
);
return res.data;
}
export async function describeSecret(
id: string,
signal?: AbortSignal,
): Promise<SecretDetail> {
const res = await apiClient.call<SecretDetail>(
apiEndpointKeys.aws.secretsmanager.secrets.describe,
{ signal, params: { id } },
);
return res.data;
}
export async function getSecretValue(
id: string,
signal?: AbortSignal,
): Promise<SecretValue> {
const res = await apiClient.call<SecretValue>(
apiEndpointKeys.aws.secretsmanager.secrets.value.get,
{ signal, params: { id } },
);
return res.data;
}
export async function createSecret(
name: string,
secretString: string,
description?: string,
signal?: AbortSignal,
): Promise<{ name: string; arn?: string; versionId?: string }> {
const res = await apiClient.call<
{ name: string; arn?: string; versionId?: string },
{ name: string; secretString: string; description?: string }
>(
apiEndpointKeys.aws.secretsmanager.secrets.create,
{ signal, body: { name, secretString, description } },
);
return res.data;
}
export async function putSecretValue(
id: string,
secretString: string,
signal?: AbortSignal,
): Promise<{ arn?: string; versionId?: string }> {
const res = await apiClient.call<
{ arn?: string; versionId?: string },
{ id: string; secretString: string }
>(
apiEndpointKeys.aws.secretsmanager.secrets.value.put,
{ signal, body: { id, secretString } },
);
return res.data;
}
export async function deleteSecret(
id: string,
force = false,
signal?: AbortSignal,
): Promise<void> {
await apiClient.call<void>(
apiEndpointKeys.aws.secretsmanager.secrets.delete,
{ signal, params: { id, force: force ? "true" : undefined } },
);
}
export async function listSecretResources(
signal?: AbortSignal,
): Promise<ResourceSummary[]> {
const secrets = await listSecrets(signal);
return secrets.map((s) => ({
id: s.arn ?? s.name,
name: s.name,
status: s.rotationEnabled ? "rotation enabled" : "available",
description: s.description,
metadata: {
lastChangedDate: s.lastChangedDate,
lastAccessedDate: s.lastAccessedDate,
createdDate: s.createdDate,
tagCount: s.tags.length,
},
}));
}
export const secretsManagerClient = {
listSecrets,
describeSecret,
getSecretValue,
createSecret,
putSecretValue,
deleteSecret,
listResources: listSecretResources,
};
@@ -0,0 +1,89 @@
import {
useMutation,
useQueryClient,
type UseMutationOptions,
} from "@tanstack/react-query";
import { secretsManagerClient } from "./secretsmanager.api";
import { secretsManagerQueryKeys } from "./secretsmanager.queries";
export type CreateSecretMutationInput = {
name: string;
secretString: string;
description?: string;
};
export type PutSecretValueMutationInput = {
id: string;
secretString: string;
};
export type DeleteSecretMutationInput = {
id: string;
force?: boolean;
};
export function useCreateSecretMutation(
options?: UseMutationOptions<
{ name: string; arn?: string; versionId?: string },
Error,
CreateSecretMutationInput
>,
) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ name, secretString, description }) =>
secretsManagerClient.createSecret(name, secretString, description),
...options,
onSuccess: (...args) => {
void queryClient.invalidateQueries({
queryKey: secretsManagerQueryKeys.list,
});
options?.onSuccess?.(...args);
},
});
}
export function usePutSecretValueMutation(
options?: UseMutationOptions<
{ arn?: string; versionId?: string },
Error,
PutSecretValueMutationInput
>,
) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, secretString }) =>
secretsManagerClient.putSecretValue(id, secretString),
...options,
onSuccess: (...args) => {
const [, { id }] = args;
void queryClient.invalidateQueries({
queryKey: secretsManagerQueryKeys.value(id),
});
void queryClient.invalidateQueries({
queryKey: secretsManagerQueryKeys.detail(id),
});
options?.onSuccess?.(...args);
},
});
}
export function useDeleteSecretMutation(
options?: UseMutationOptions<void, Error, DeleteSecretMutationInput>,
) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, force }) =>
secretsManagerClient.deleteSecret(id, force ?? false),
...options,
onSuccess: (...args) => {
void queryClient.invalidateQueries({
queryKey: secretsManagerQueryKeys.list,
});
options?.onSuccess?.(...args);
},
});
}
@@ -0,0 +1,37 @@
import { useQuery } from "@tanstack/react-query";
import { secretsManagerClient } from "./secretsmanager.api";
export const secretsManagerQueryKeys = {
list: ["secretsmanager", "secrets"] as const,
detail: (id: string | null) => ["secretsmanager", "detail", id] as const,
value: (id: string | null) => ["secretsmanager", "value", id] as const,
};
export function useSecretsQuery(enabled = true) {
return useQuery({
queryKey: secretsManagerQueryKeys.list,
queryFn: ({ signal }) => secretsManagerClient.listSecrets(signal),
enabled,
});
}
export function useSecretDetailQuery(id: string | null) {
return useQuery({
queryKey: secretsManagerQueryKeys.detail(id),
queryFn: ({ signal }) => secretsManagerClient.describeSecret(id!, signal),
enabled: Boolean(id),
});
}
export function useSecretValueQuery(id: string | null, enabled = false) {
return useQuery({
queryKey: secretsManagerQueryKeys.value(id),
queryFn: ({ signal }) => secretsManagerClient.getSecretValue(id!, signal),
enabled: Boolean(id) && enabled,
// Keep the plaintext out of the long-lived cache: always refetch on reveal
// and drop it the moment the query loses its observer so a hidden value
// cannot be read back later.
gcTime: 0,
staleTime: 0,
});
}
@@ -0,0 +1,983 @@
import { API_BASE_URL, apiClient, apiEndpointKeys } from "./api";
import type {
CloudDescriptor,
CloudProvider,
CloudServiceDescriptor,
CloudServiceStatus,
CloudServiceType,
CloudStatus,
} from "@/types/cloud";
import type {
AppConfigConfigurationProfile,
AppConfigDeployment,
AppConfigDeploymentStrategy,
AppConfigEnvironment,
AppConfigHostedConfigurationVersion,
ChildCollection,
ChildItem,
CloudResource,
CollectionPage,
CosmosContainer,
CosmosItem,
CosmosQueryResult,
CreateDatabaseSnapshotInput,
CreateKubernetesFargateProfileInput,
CreateKubernetesNodegroupInput,
DatabaseSnapshot,
KubernetesFargateProfile,
KubernetesNodegroup,
LogsInsightsQueryInput,
LogsInsightsQueryResult,
NoSqlItem,
SqlCredentials,
SqlDatabase,
SqlEngine,
SqlQueryResult,
SqlTable,
StorageObjectList,
} from "@/types/resource";
import type { ServiceSchema } from "@/types/schema";
import { getAccountId } from "@/lib/accountStore";
type CloudPathParams = Record<string, string>;
const DATABASE_MUTATION_TIMEOUT_MS = 5 * 60_000;
const SQL_DATA_TIMEOUT_MS = 45_000;
/** Floci completes Insights queries instantly by default, but bounded server-side polling can take longer. */
const LOGS_QUERY_TIMEOUT_MS = 20_000;
/** Cold starts pull a container image; measured at ~60s on a first invoke. */
const INVOKE_TIMEOUT_MS = 120_000;
export async function listClouds(
signal?: AbortSignal,
): Promise<CloudDescriptor[]> {
const res = await apiClient.call<CloudDescriptor[]>(
apiEndpointKeys.clouds.list,
{ signal },
);
return res.data;
}
export async function listCloudServices(
cloud: CloudProvider,
signal?: AbortSignal,
): Promise<CloudServiceDescriptor[]> {
const res = await apiClient.call<CloudServiceDescriptor[]>(
apiEndpointKeys.clouds.services,
requestOptions(cloud, "cloud-proxy", { signal }),
{ cloud },
);
return res.data;
}
export async function getCloudStatus(
cloud: CloudProvider,
signal?: AbortSignal,
): Promise<CloudStatus> {
const res = await apiClient.call<CloudStatus>(
apiEndpointKeys.clouds.status,
requestOptions(cloud, "cloud-proxy", { signal }),
{ cloud },
);
return res.data;
}
export async function getCloudServiceStatus(
cloud: CloudProvider,
service: CloudServiceType,
signal?: AbortSignal,
): Promise<CloudServiceStatus> {
const res = await apiClient.call<CloudServiceStatus>(
apiEndpointKeys.clouds.serviceStatus,
requestOptions(cloud, service, { signal }),
{ cloud, service },
);
return res.data;
}
export async function getServiceSchema(
cloud: CloudProvider,
service: CloudServiceType,
signal?: AbortSignal,
): Promise<ServiceSchema> {
const res = await apiClient.call<ServiceSchema>(
apiEndpointKeys.clouds.schema,
requestOptions(cloud, service, { signal }),
{ cloud, service },
);
return res.data;
}
export async function listCloudResources(
cloud: CloudProvider,
service: CloudServiceType,
search?: string,
signal?: AbortSignal,
): Promise<CloudResource[]> {
const res = await apiClient.call<CloudResource[]>(
apiEndpointKeys.clouds.resources.list,
requestOptions(cloud, service, { signal, params: { search } }),
{ cloud, service },
);
return res.data;
}
export async function getCloudResource(
cloud: CloudProvider,
service: CloudServiceType,
id: string,
signal?: AbortSignal,
): Promise<CloudResource> {
const res = await apiClient.call<CloudResource>(
apiEndpointKeys.clouds.resources.get,
requestOptions(cloud, service, { signal }),
{ cloud, service, id },
);
return res.data;
}
export async function createCloudResource(
cloud: CloudProvider,
service: CloudServiceType,
values: Record<string, unknown>,
signal?: AbortSignal,
): Promise<CloudResource> {
const timeout =
(cloud === "azure" || cloud === "aws") && service === "database"
? DATABASE_MUTATION_TIMEOUT_MS
: undefined;
const res = await apiClient.call<CloudResource, Record<string, unknown>>(
apiEndpointKeys.clouds.resources.create,
requestOptions(cloud, service, { signal, body: values, timeout }),
{ cloud, service },
);
return res.data;
}
export async function updateCloudResource(
cloud: CloudProvider,
service: CloudServiceType,
id: string,
values: Record<string, unknown>,
signal?: AbortSignal,
): Promise<CloudResource> {
const timeout =
(cloud === "azure" || cloud === "aws") && service === "database"
? DATABASE_MUTATION_TIMEOUT_MS
: undefined;
const res = await apiClient.call<CloudResource, Record<string, unknown>>(
apiEndpointKeys.clouds.resources.update,
requestOptions(cloud, service, { signal, body: values, timeout }),
{ cloud, service, id },
);
return res.data;
}
export async function deleteCloudResource(
cloud: CloudProvider,
service: CloudServiceType,
id: string,
signal?: AbortSignal,
): Promise<void> {
await apiClient.call<void>(
apiEndpointKeys.clouds.resources.delete,
requestOptions(cloud, service, { signal }),
{ cloud, service, id },
);
}
export async function clearEmailInbox(
cloud: CloudProvider,
signal?: AbortSignal,
): Promise<void> {
await apiClient.call<void>(
apiEndpointKeys.clouds.email.inbox.clear,
requestOptions(cloud, "email", { signal }),
{ cloud },
);
}
export interface ServerlessInvokeResult {
statusCode: number;
payload: string;
functionError?: string;
logResult?: string;
executionDuration?: number;
}
export interface CreateApiGatewayLambdaRouteInput {
path: string;
method: string;
stage: string;
functionName: string;
}
export interface ApiGatewayLambdaRoute {
resourceId: string;
deploymentId: string;
invokeUrl: string;
}
export async function createApiGatewayLambdaRoute(
cloud: CloudProvider,
apiId: string,
input: CreateApiGatewayLambdaRouteInput,
signal?: AbortSignal,
): Promise<ApiGatewayLambdaRoute> {
const res = await apiClient.call<ApiGatewayLambdaRoute, CreateApiGatewayLambdaRouteInput>(
apiEndpointKeys.clouds.resources.createApiGatewayRoute,
requestOptions(cloud, "apigateway", { signal, body: input }),
{ cloud, id: apiId },
);
return res.data;
}
export type KmsEncryptionAlgorithm =
| "SYMMETRIC_DEFAULT"
| "RSAES_OAEP_SHA_1"
| "RSAES_OAEP_SHA_256";
export interface KmsEncryptRequest {
plaintextBase64: string;
encryptionAlgorithm: KmsEncryptionAlgorithm;
encryptionContext?: Record<string, string>;
}
export interface KmsEncryptResponse {
ciphertextBlobBase64: string;
keyId: string;
encryptionAlgorithm: KmsEncryptionAlgorithm;
}
export interface KmsDecryptRequest {
ciphertextBlobBase64: string;
encryptionAlgorithm: KmsEncryptionAlgorithm;
encryptionContext?: Record<string, string>;
}
export interface KmsDecryptResponse {
plaintextBase64: string;
keyId: string;
encryptionAlgorithm: KmsEncryptionAlgorithm;
}
export async function invokeCloudResource(
cloud: CloudProvider,
service: CloudServiceType,
id: string,
payload: string,
signal?: AbortSignal,
): Promise<ServerlessInvokeResult> {
const res = await apiClient.call<ServerlessInvokeResult, { payload: string }>(
apiEndpointKeys.clouds.resources.invoke,
requestOptions(cloud, service, {
signal,
body: { payload },
// A cold invoke can exceed a minute while the runtime pulls and starts the
// function container, so the 10s default would abort a request that is
// working. The user-visible alternative is a timeout error for a function
// that in fact ran.
timeout: INVOKE_TIMEOUT_MS,
}),
{ cloud, service, id },
);
return res.data;
}
export async function encryptKmsResource(
cloud: CloudProvider,
id: string,
body: KmsEncryptRequest,
signal?: AbortSignal,
): Promise<KmsEncryptResponse> {
const res = await apiClient.call<KmsEncryptResponse, KmsEncryptRequest>(
apiEndpointKeys.clouds.resources.encrypt,
requestOptions(cloud, "kms", { signal, body }),
{ cloud, id },
);
return res.data;
}
export async function decryptKmsResource(
cloud: CloudProvider,
id: string,
body: KmsDecryptRequest,
signal?: AbortSignal,
): Promise<KmsDecryptResponse> {
const res = await apiClient.call<KmsDecryptResponse, KmsDecryptRequest>(
apiEndpointKeys.clouds.resources.decrypt,
requestOptions(cloud, "kms", { signal, body }),
{ cloud, id },
);
return res.data;
}
export async function listStorageObjects(
cloud: CloudProvider,
resourceId: string,
prefix?: string,
signal?: AbortSignal,
): Promise<StorageObjectList> {
const res = await apiClient.call<StorageObjectList>(
apiEndpointKeys.clouds.storage.objects.list,
requestOptions(cloud, "storage", { signal, params: { prefix } }),
storagePathParams(cloud, resourceId),
);
return res.data;
}
export async function uploadStorageObject(
cloud: CloudProvider,
resourceId: string,
key: string,
file: File | Blob,
signal?: AbortSignal,
): Promise<void> {
await apiClient.call<void>(
apiEndpointKeys.clouds.storage.objects.upload,
requestOptions(cloud, "storage", {
signal,
rawBody: file,
params: { key },
headers: { "Content-Type": file.type || "application/octet-stream" },
}),
storagePathParams(cloud, resourceId),
);
}
export function storageObjectDownloadUrl(
cloud: CloudProvider,
resourceId: string,
key: string,
): string {
const path = `/clouds/${encodeURIComponent(
cloud,
)}/services/storage/resources/${encodeURIComponent(resourceId)}/object`;
// This URL is opened directly by the browser (anchor/img), so it bypasses the
// request interceptor — pass the account as a query param the API also reads.
return `${API_BASE_URL}${path}?key=${encodeURIComponent(
key,
)}&account=${encodeURIComponent(getAccountId())}`;
}
export async function deleteStorageObject(
cloud: CloudProvider,
resourceId: string,
key: string,
signal?: AbortSignal,
): Promise<void> {
await apiClient.call<void>(
apiEndpointKeys.clouds.storage.objects.delete,
requestOptions(cloud, "storage", { signal, params: { key } }),
storagePathParams(cloud, resourceId),
);
}
export async function copyStorageObject(
cloud: CloudProvider,
srcResourceId: string,
srcKey: string,
destKey: string,
destResourceId?: string,
signal?: AbortSignal,
): Promise<void> {
await apiClient.call<void, Record<string, unknown>>(
apiEndpointKeys.clouds.storage.objects.copy,
requestOptions(cloud, "storage", {
signal,
body: { srcKey, destKey, ...(destResourceId ? { destResourceId } : {}) },
}),
storagePathParams(cloud, srcResourceId),
);
}
export async function listCosmosContainers(
cloud: CloudProvider,
databaseId: string,
signal?: AbortSignal,
): Promise<CosmosContainer[]> {
const res = await apiClient.call<CosmosContainer[]>(
apiEndpointKeys.clouds.nosql.cosmos.containers.list,
requestOptions(cloud, "nosql", { signal }),
databasePathParams(cloud, databaseId),
);
return res.data;
}
export async function createCosmosContainer(
cloud: CloudProvider,
databaseId: string,
values: Record<string, unknown>,
signal?: AbortSignal,
): Promise<CosmosContainer> {
const res = await apiClient.call<CosmosContainer, Record<string, unknown>>(
apiEndpointKeys.clouds.nosql.cosmos.containers.create,
requestOptions(cloud, "nosql", { signal, body: values }),
databasePathParams(cloud, databaseId),
);
return res.data;
}
export async function deleteCosmosContainer(
cloud: CloudProvider,
databaseId: string,
containerId: string,
signal?: AbortSignal,
): Promise<void> {
await apiClient.call<void>(
apiEndpointKeys.clouds.nosql.cosmos.containers.delete,
requestOptions(cloud, "nosql", { signal }),
{ ...databasePathParams(cloud, databaseId), containerId },
);
}
export async function listCosmosItems(
cloud: CloudProvider,
databaseId: string,
containerId: string,
signal?: AbortSignal,
): Promise<CosmosItem[]> {
const res = await apiClient.call<CosmosItem[]>(
apiEndpointKeys.clouds.nosql.cosmos.items.list,
requestOptions(cloud, "nosql", { signal }),
{ ...databasePathParams(cloud, databaseId), containerId },
);
return res.data;
}
export async function upsertCosmosItem(
cloud: CloudProvider,
databaseId: string,
containerId: string,
document: Record<string, unknown>,
signal?: AbortSignal,
): Promise<CosmosItem> {
const res = await apiClient.call<CosmosItem, Record<string, unknown>>(
apiEndpointKeys.clouds.nosql.cosmos.items.upsert,
requestOptions(cloud, "nosql", { signal, body: document }),
{ ...databasePathParams(cloud, databaseId), containerId },
);
return res.data;
}
export async function deleteCosmosItem(
cloud: CloudProvider,
databaseId: string,
containerId: string,
itemId: string,
partitionKey?: string | null,
signal?: AbortSignal,
): Promise<void> {
await apiClient.call<void>(
apiEndpointKeys.clouds.nosql.cosmos.items.delete,
requestOptions(cloud, "nosql", {
signal,
params: partitionKey ? { partitionKey } : undefined,
}),
{ ...databasePathParams(cloud, databaseId), containerId, itemId },
);
}
export async function queryCosmosItems(
cloud: CloudProvider,
databaseId: string,
containerId: string,
query: string,
signal?: AbortSignal,
): Promise<CosmosQueryResult> {
const res = await apiClient.call<CosmosQueryResult, { query: string }>(
apiEndpointKeys.clouds.nosql.cosmos.items.query,
requestOptions(cloud, "nosql", { signal, body: { query } }),
{ ...databasePathParams(cloud, databaseId), containerId },
);
return res.data;
}
export async function listChildCollections(
cloud: CloudProvider,
service: CloudServiceType,
resourceId: string,
cursor?: string,
signal?: AbortSignal,
): Promise<CollectionPage<ChildCollection>> {
const res = await apiClient.call<CollectionPage<ChildCollection>>(
apiEndpointKeys.clouds.childCollections.list,
requestOptions(cloud, service, { signal, params: cursor ? { cursor } : undefined }),
{ cloud, service, id: resourceId },
);
return res.data;
}
export async function listCollectionItems(
cloud: CloudProvider,
service: CloudServiceType,
resourceId: string,
collectionId: string,
cursor?: string,
signal?: AbortSignal,
): Promise<CollectionPage<ChildItem>> {
const res = await apiClient.call<CollectionPage<ChildItem>>(
apiEndpointKeys.clouds.childCollections.items.list,
requestOptions(cloud, service, { signal, params: cursor ? { cursor } : undefined }),
{ cloud, service, id: resourceId, cid: collectionId },
);
return res.data;
}
export async function listSqlDatabases(
cloud: CloudProvider,
serverId: string,
engine: SqlEngine,
credentials: SqlCredentials,
signal?: AbortSignal,
): Promise<SqlDatabase[]> {
const res = await apiClient.call<SqlDatabase[], SqlCredentials & { engine: SqlEngine }>(
apiEndpointKeys.clouds.database.sql.databases,
requestOptions(cloud, "database", {
signal,
body: { ...credentials, engine },
timeout: SQL_DATA_TIMEOUT_MS,
}),
{ cloud, id: serverId },
);
return res.data;
}
export async function listSqlTables(
cloud: CloudProvider,
serverId: string,
engine: SqlEngine,
database: string,
credentials: SqlCredentials,
signal?: AbortSignal,
): Promise<SqlTable[]> {
const res = await apiClient.call<
SqlTable[],
SqlCredentials & { database: string; engine: SqlEngine }
>(
apiEndpointKeys.clouds.database.sql.tables,
requestOptions(cloud, "database", {
signal,
body: { ...credentials, database, engine },
timeout: SQL_DATA_TIMEOUT_MS,
}),
{ cloud, id: serverId },
);
return res.data;
}
export async function querySql(
cloud: CloudProvider,
serverId: string,
engine: SqlEngine,
database: string,
credentials: SqlCredentials,
query: string,
signal?: AbortSignal,
): Promise<SqlQueryResult> {
const res = await apiClient.call<
SqlQueryResult,
SqlCredentials & { database: string; engine: SqlEngine; query: string }
>(
apiEndpointKeys.clouds.database.sql.query,
requestOptions(cloud, "database", {
signal,
body: { ...credentials, database, engine, query },
timeout: SQL_DATA_TIMEOUT_MS,
}),
{ cloud, id: serverId },
);
return res.data;
}
export async function queryLogs(
cloud: CloudProvider,
logGroupName: string,
input: LogsInsightsQueryInput,
signal?: AbortSignal,
): Promise<LogsInsightsQueryResult> {
const res = await apiClient.call<LogsInsightsQueryResult, LogsInsightsQueryInput>(
apiEndpointKeys.clouds.logs.query,
requestOptions(cloud, "logs", { signal, body: input, timeout: LOGS_QUERY_TIMEOUT_MS }),
{ cloud, id: logGroupName },
);
return res.data;
}
export async function listDatabaseSnapshots(
cloud: CloudProvider,
instanceIdentifier?: string,
signal?: AbortSignal,
): Promise<DatabaseSnapshot[]> {
const res = await apiClient.call<DatabaseSnapshot[]>(
apiEndpointKeys.clouds.database.snapshots.list,
requestOptions(cloud, "database", {
signal,
params: instanceIdentifier ? { instanceIdentifier } : undefined,
}),
{ cloud },
);
return res.data;
}
export async function createDatabaseSnapshot(
cloud: CloudProvider,
input: CreateDatabaseSnapshotInput,
signal?: AbortSignal,
): Promise<DatabaseSnapshot> {
const res = await apiClient.call<DatabaseSnapshot, CreateDatabaseSnapshotInput>(
apiEndpointKeys.clouds.database.snapshots.create,
requestOptions(cloud, "database", { signal, body: input }),
{ cloud },
);
return res.data;
}
export async function listDatabaseOrderableClasses(
cloud: CloudProvider,
engine?: string,
signal?: AbortSignal,
): Promise<string[]> {
const res = await apiClient.call<string[]>(
apiEndpointKeys.clouds.database.orderableClasses.list,
requestOptions(cloud, "database", {
signal,
params: engine ? { engine } : undefined,
}),
{ cloud },
);
return res.data;
}
export async function listNoSqlItems(
cloud: CloudProvider,
resourceId: string,
signal?: AbortSignal,
): Promise<NoSqlItem[]> {
const res = await apiClient.call<NoSqlItem[]>(
apiEndpointKeys.clouds.nosql.items.list,
requestOptions(cloud, "nosql", { signal }),
{ cloud, id: resourceId },
);
return res.data;
}
export async function putNoSqlItem(
cloud: CloudProvider,
resourceId: string,
document: Record<string, unknown>,
signal?: AbortSignal,
): Promise<NoSqlItem> {
const res = await apiClient.call<NoSqlItem, Record<string, unknown>>(
apiEndpointKeys.clouds.nosql.items.put,
requestOptions(cloud, "nosql", { signal, body: document }),
{ cloud, id: resourceId },
);
return res.data;
}
export async function listKubernetesNodegroups(
cloud: CloudProvider,
clusterId: string,
signal?: AbortSignal,
): Promise<KubernetesNodegroup[]> {
const res = await apiClient.call<KubernetesNodegroup[]>(
apiEndpointKeys.clouds.k8s.nodegroups.list,
requestOptions(cloud, "k8s", {signal}),
{cloud, id: clusterId},
)
return res.data
}
export async function createKubernetesNodegroup(
cloud: CloudProvider,
clusterId: string,
input: CreateKubernetesNodegroupInput,
): Promise<KubernetesNodegroup> {
const res = await apiClient.call<KubernetesNodegroup, CreateKubernetesNodegroupInput>(
apiEndpointKeys.clouds.k8s.nodegroups.create,
requestOptions(cloud, "k8s", {body: input}),
{cloud, id: clusterId},
)
return res.data
}
export async function deleteKubernetesNodegroup(
cloud: CloudProvider,
clusterId: string,
nodegroupId: string,
): Promise<void> {
await apiClient.call<void>(
apiEndpointKeys.clouds.k8s.nodegroups.delete,
requestOptions(cloud, "k8s"),
{cloud, id: clusterId, nodegroupId},
)
}
export async function listKubernetesFargateProfiles(
cloud: CloudProvider,
clusterId: string,
signal?: AbortSignal,
): Promise<KubernetesFargateProfile[]> {
const res = await apiClient.call<KubernetesFargateProfile[]>(
apiEndpointKeys.clouds.k8s.fargateProfiles.list,
requestOptions(cloud, "k8s", {signal}),
{cloud, id: clusterId},
)
return res.data
}
export async function createKubernetesFargateProfile(
cloud: CloudProvider,
clusterId: string,
input: CreateKubernetesFargateProfileInput,
): Promise<KubernetesFargateProfile> {
const res = await apiClient.call<KubernetesFargateProfile, CreateKubernetesFargateProfileInput>(
apiEndpointKeys.clouds.k8s.fargateProfiles.create,
requestOptions(cloud, "k8s", {body: input}),
{cloud, id: clusterId},
)
return res.data
}
export async function deleteKubernetesFargateProfile(
cloud: CloudProvider,
clusterId: string,
profileId: string,
): Promise<void> {
await apiClient.call<void>(
apiEndpointKeys.clouds.k8s.fargateProfiles.delete,
requestOptions(cloud, "k8s"),
{cloud, id: clusterId, profileId},
)
}
export async function listAppConfigEnvironments(
cloud: CloudProvider,
applicationId: string,
signal?: AbortSignal,
): Promise<AppConfigEnvironment[]> {
const res = await apiClient.call<AppConfigEnvironment[]>(
apiEndpointKeys.clouds.configuration.environments.list,
requestOptions(cloud, "configuration", {signal}),
{cloud, id: applicationId},
);
return res.data;
}
export async function createAppConfigEnvironment(
cloud: CloudProvider,
applicationId: string,
values: Record<string, unknown>,
): Promise<AppConfigEnvironment> {
const res = await apiClient.call<AppConfigEnvironment, Record<string, unknown>>(
apiEndpointKeys.clouds.configuration.environments.create,
requestOptions(cloud, "configuration", {body: values}),
{cloud, id: applicationId},
);
return res.data;
}
export async function deleteAppConfigEnvironment(
cloud: CloudProvider,
applicationId: string,
environmentId: string,
): Promise<void> {
await apiClient.call<void>(
apiEndpointKeys.clouds.configuration.environments.delete,
requestOptions(cloud, "configuration"),
{cloud, id: applicationId, environmentId},
);
}
export async function listAppConfigConfigurationProfiles(
cloud: CloudProvider,
applicationId: string,
signal?: AbortSignal,
): Promise<AppConfigConfigurationProfile[]> {
const res = await apiClient.call<AppConfigConfigurationProfile[]>(
apiEndpointKeys.clouds.configuration.configurationProfiles.list,
requestOptions(cloud, "configuration", {signal}),
{cloud, id: applicationId},
);
return res.data;
}
export async function createAppConfigConfigurationProfile(
cloud: CloudProvider,
applicationId: string,
values: Record<string, unknown>,
): Promise<AppConfigConfigurationProfile> {
const res = await apiClient.call<AppConfigConfigurationProfile, Record<string, unknown>>(
apiEndpointKeys.clouds.configuration.configurationProfiles.create,
requestOptions(cloud, "configuration", {body: values}),
{cloud, id: applicationId},
);
return res.data;
}
export async function deleteAppConfigConfigurationProfile(
cloud: CloudProvider,
applicationId: string,
profileId: string,
): Promise<void> {
await apiClient.call<void>(
apiEndpointKeys.clouds.configuration.configurationProfiles.delete,
requestOptions(cloud, "configuration"),
{cloud, id: applicationId, profileId},
);
}
export async function listAppConfigHostedConfigurationVersions(
cloud: CloudProvider,
applicationId: string,
profileId: string,
signal?: AbortSignal,
): Promise<AppConfigHostedConfigurationVersion[]> {
const res = await apiClient.call<AppConfigHostedConfigurationVersion[]>(
apiEndpointKeys.clouds.configuration.configurationProfiles.hostedVersions.list,
requestOptions(cloud, "configuration", {signal}),
{cloud, id: applicationId, profileId},
);
return res.data;
}
export async function getAppConfigHostedConfigurationVersion(
cloud: CloudProvider,
applicationId: string,
profileId: string,
versionNumber: number,
signal?: AbortSignal,
): Promise<AppConfigHostedConfigurationVersion> {
const res = await apiClient.call<AppConfigHostedConfigurationVersion>(
apiEndpointKeys.clouds.configuration.configurationProfiles.hostedVersions.get,
requestOptions(cloud, "configuration", {signal}),
{cloud, id: applicationId, profileId, versionNumber: String(versionNumber)},
);
return res.data;
}
export async function createAppConfigHostedConfigurationVersion(
cloud: CloudProvider,
applicationId: string,
profileId: string,
values: Record<string, unknown>,
): Promise<AppConfigHostedConfigurationVersion> {
const res = await apiClient.call<AppConfigHostedConfigurationVersion, Record<string, unknown>>(
apiEndpointKeys.clouds.configuration.configurationProfiles.hostedVersions.create,
requestOptions(cloud, "configuration", {body: values}),
{cloud, id: applicationId, profileId},
);
return res.data;
}
export async function deleteAppConfigHostedConfigurationVersion(
cloud: CloudProvider,
applicationId: string,
profileId: string,
versionNumber: number,
): Promise<void> {
await apiClient.call<void>(
apiEndpointKeys.clouds.configuration.configurationProfiles.hostedVersions.delete,
requestOptions(cloud, "configuration"),
{cloud, id: applicationId, profileId, versionNumber: String(versionNumber)},
);
}
export async function listAppConfigDeploymentStrategies(
cloud: CloudProvider,
signal?: AbortSignal,
): Promise<AppConfigDeploymentStrategy[]> {
const res = await apiClient.call<AppConfigDeploymentStrategy[]>(
apiEndpointKeys.clouds.configuration.deploymentStrategies.list,
requestOptions(cloud, "configuration", {signal}),
{cloud},
);
return res.data;
}
export async function createAppConfigDeploymentStrategy(
cloud: CloudProvider,
values: Record<string, unknown>,
): Promise<AppConfigDeploymentStrategy> {
const res = await apiClient.call<AppConfigDeploymentStrategy, Record<string, unknown>>(
apiEndpointKeys.clouds.configuration.deploymentStrategies.create,
requestOptions(cloud, "configuration", {body: values}),
{cloud},
);
return res.data;
}
export async function deleteAppConfigDeploymentStrategy(
cloud: CloudProvider,
strategyId: string,
): Promise<void> {
await apiClient.call<void>(
apiEndpointKeys.clouds.configuration.deploymentStrategies.delete,
requestOptions(cloud, "configuration"),
{cloud, strategyId},
);
}
export async function startAppConfigDeployment(
cloud: CloudProvider,
applicationId: string,
environmentId: string,
values: Record<string, unknown>,
): Promise<AppConfigDeployment> {
const res = await apiClient.call<AppConfigDeployment, Record<string, unknown>>(
apiEndpointKeys.clouds.configuration.environments.deployments.start,
requestOptions(cloud, "configuration", {body: values}),
{cloud, id: applicationId, environmentId},
);
return res.data;
}
export async function getAppConfigDeployment(
cloud: CloudProvider,
applicationId: string,
environmentId: string,
deploymentNumber: number,
signal?: AbortSignal,
): Promise<AppConfigDeployment> {
const res = await apiClient.call<AppConfigDeployment>(
apiEndpointKeys.clouds.configuration.environments.deployments.get,
requestOptions(cloud, "configuration", {signal}),
{cloud, id: applicationId, environmentId, deploymentNumber: String(deploymentNumber)},
);
return res.data;
}
function requestOptions<TBody = unknown>(
cloud: CloudProvider,
service: string,
options: {
signal?: AbortSignal;
params?: Record<string, string | number | boolean | undefined>;
body?: TBody;
rawBody?: BodyInit;
headers?: HeadersInit;
/** Overrides the client default; needed for calls that can legitimately run long. */
timeout?: number;
} = {},
) {
return {
...options,
telemetry: { provider: cloud, service },
};
}
function storagePathParams(
cloud: CloudProvider,
resourceId: string,
): CloudPathParams {
return { cloud, id: resourceId };
}
function databasePathParams(
cloud: CloudProvider,
databaseId: string,
): CloudPathParams {
return { cloud, id: databaseId };
}
+63
View File
@@ -0,0 +1,63 @@
import { apiClient } from "./api";
import type { HttpMethod } from "./HttpClient";
import {
subscribeApiRequests,
type ApiRequestEvent,
} from "@/telemetry";
export type FlociRequestEvent = ApiRequestEvent;
export function subscribeRequests(
cb: (event: FlociRequestEvent) => void,
): () => void {
return subscribeApiRequests(cb);
}
export function apiGet<T>(
path: string,
service: string,
signal?: AbortSignal,
): Promise<T> {
return apiRequest<T>("GET", path, service, undefined, signal);
}
export function apiPost<T>(
path: string,
service: string,
body: unknown,
signal?: AbortSignal,
): Promise<T> {
return apiRequest<T>("POST", path, service, body, signal);
}
export function apiPut<T>(
path: string,
service: string,
body: unknown,
signal?: AbortSignal,
): Promise<T> {
return apiRequest<T>("PUT", path, service, body, signal);
}
export function apiDelete<T>(
path: string,
service: string,
signal?: AbortSignal,
): Promise<T> {
return apiRequest<T>("DELETE", path, service, undefined, signal);
}
async function apiRequest<T>(
method: HttpMethod,
path: string,
service: string,
body?: unknown,
signal?: AbortSignal,
): Promise<T> {
const res = await apiClient.request<T>(method, path, {
signal,
...(body === undefined ? {} : { body }),
telemetry: { provider: "aws", service },
});
return res.data;
}
@@ -0,0 +1,87 @@
import {useQuery} from '@tanstack/react-query'
import {
getCloudServiceStatus,
getCloudStatus,
listCloudResources,
listCloudServices,
listClouds,
} from '@/api/cloudProxyClient'
import type {CloudProvider, CloudServiceDescriptor, CloudServiceType, CloudStatus} from '@/types/cloud'
/**
* Shared cloud queries.
*
* These live under api/ rather than in a feature folder because the app shell
* (nav, connection indicator) needs them as much as the console home does.
* Key strings are unchanged so existing caches still share.
*/
export const cloudQueryKeys = {
clouds: ['clouds'] as const,
services: (cloud: CloudProvider) => ['cloud-services', cloud] as const,
status: (cloud: CloudProvider) => ['cloud-status', cloud] as const,
serviceStatus: (cloud: CloudProvider, service: CloudServiceType) =>
['cloud-service-status', cloud, service] as const,
resources: (cloud: CloudProvider, service: CloudServiceType) =>
['cloud-console-resources', cloud, service] as const,
}
export function useCloudsQuery() {
return useQuery({
queryKey: cloudQueryKeys.clouds,
queryFn: ({signal}) => listClouds(signal),
})
}
export function useCloudServicesQuery(cloud: CloudProvider) {
return useQuery({
queryKey: cloudQueryKeys.services(cloud),
queryFn: ({signal}) => listCloudServices(cloud, signal),
// The catalog only changes when the API restarts, so this is effectively
// free on every navigation while still driving the whole nav.
staleTime: 60_000,
})
}
export function useCloudStatusQuery(cloud: CloudProvider, options: {refetchInterval?: number} = {}) {
return useQuery({
queryKey: cloudQueryKeys.status(cloud),
queryFn: ({signal}) => getCloudStatus(cloud, signal),
refetchInterval: options.refetchInterval ?? 10_000,
})
}
/** Health of the single service being viewed, rather than of the whole cloud. */
export function useCloudServiceStatusQuery(
cloud: CloudProvider,
service: CloudServiceType,
options: {enabled?: boolean} = {},
) {
return useQuery({
queryKey: cloudQueryKeys.serviceStatus(cloud, service),
queryFn: ({signal}) => getCloudServiceStatus(cloud, service, signal),
refetchInterval: 10_000,
enabled: options.enabled ?? true,
})
}
export function useCloudConsoleResourcesQuery({
cloud,
service,
services,
status,
}: {
cloud: CloudProvider
service: CloudServiceType
services?: CloudServiceDescriptor[]
status?: CloudStatus
}) {
return useQuery({
queryKey: cloudQueryKeys.resources(cloud, service),
queryFn: ({signal}) => listCloudResources(cloud, service, undefined, signal),
enabled: hasAvailableService(services, service) && status?.runtime === 'reachable',
})
}
function hasAvailableService(services: CloudServiceDescriptor[] | undefined, service: CloudServiceType): boolean {
return services?.some((item) => item.service === service && item.availability === 'available') ?? false
}
@@ -0,0 +1,6 @@
export {
apiRequestEventBus,
emitApiRequest,
subscribeApiRequests,
} from "@/telemetry/requestEventBus";
export type { ApiRequestEvent, CloudProvider } from "@/telemetry/types";
+114
View File
@@ -0,0 +1,114 @@
export type ServiceStatus = 'healthy' | 'degraded' | 'unavailable' | 'unknown'
export type ServiceName =
| 's3'
| 'sqs'
| 'dynamodb'
| 'sns'
| 'lambda'
| 'eks'
| 'secretsmanager'
| 'cognito'
| 'rds'
| 'elasticache'
| 'iam'
| 'ssm'
| 'kms'
| 'cloudwatch'
export interface ServiceInfo {
name: ServiceName
displayName: string
status: ServiceStatus
requestCount: number
errorCount: number
latencyP50Ms?: number
lastActivity?: string
}
export interface HealthReport {
status: ServiceStatus
services: ServiceInfo[]
checkedAt: string
version?: string
}
export interface ResourceSummary {
id: string
name: string
status?: string
description?: string
metadata?: Record<string, unknown>
}
export interface ServiceResourceSnapshot {
service: ServiceName
displayName: string
implemented: boolean
status: ServiceStatus
count?: number
latencyMs?: number
error?: string
}
export interface ConsoleOverview {
checkedAt: string
health: HealthReport
resources: ServiceResourceSnapshot[]
logGroupCount: number
alarmCount: number
metricCount: number
totalResourceCount: number
}
export interface CWLogGroup {
name: string
arn?: string
retentionInDays?: number
createdAt: number
storedBytes: number
metricFilterCount: number
}
export interface CWLogStream {
name: string
createdAt?: number
firstEventAt?: number
lastEventAt?: number
lastIngestionAt?: number
storedBytes: number
}
export interface CWLogEvent {
id: string
timestamp: number
message: string
ingestionTime?: number
}
export interface CWAlarm {
alarmName: string
stateValue: 'ALARM' | 'OK' | 'INSUFFICIENT_DATA'
stateReason?: string
metricName?: string
namespace?: string
threshold?: number
}
export interface CWMetric {
id: string
namespace: string
metricName: string
dimensions: Array<{ name: string; value: string }>
}
export class FlociError extends Error {
constructor(
message: string,
public readonly statusCode?: number,
public readonly endpoint?: string
) {
super(message)
this.name = 'FlociError'
}
}