From a4447affaef5a83d97d15157d9b5fe709441ed05 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 3 Oct 2024 17:41:42 -0700 Subject: [PATCH 01/63] Update OMICRON_VERSION --- OMICRON_VERSION | 2 +- app/api/__generated__/Api.ts | 406 +++++++++++++++++++++++++- app/api/__generated__/OMICRON_VERSION | 2 +- app/api/__generated__/msw-handlers.ts | 135 +++++++++ app/api/__generated__/validate.ts | 258 +++++++++++++++- 5 files changed, 782 insertions(+), 21 deletions(-) diff --git a/OMICRON_VERSION b/OMICRON_VERSION index f7a1d4ae6..d3996f17a 100644 --- a/OMICRON_VERSION +++ b/OMICRON_VERSION @@ -1 +1 @@ -c50cf019cd9be35f98266a7f4acacab0236b3a3d +a9fd9dbbbcdaa80b822c3fc14b4f1a20ac72021d diff --git a/app/api/__generated__/Api.ts b/app/api/__generated__/Api.ts index e64990f1f..5ac00ea45 100644 --- a/app/api/__generated__/Api.ts +++ b/app/api/__generated__/Api.ts @@ -1710,6 +1710,16 @@ export type ImageResultsPage = { */ export type ImportBlocksBulkWrite = { base64EncodedData: string; offset: number } +/** + * A policy determining when an instance should be automatically restarted by the control plane. + */ +export type InstanceAutoRestartPolicy = + /** The instance should not be automatically restarted by the control plane if it fails. */ + | 'never' + + /** If this instance is running and unexpectedly fails (e.g. due to a host software crash or unexpected host reboot), the control plane will make a best-effort attempt to restart it. The control plane may choose not to restart the instance to preserve the overall availability of the system. */ + | 'best_effort' + /** * The number of CPUs in an Instance */ @@ -1761,6 +1771,10 @@ If this is not present, then either the instance has never been automatically re autoRestartCooldownExpiration?: Date /** `true` if this instance's auto-restart policy will permit the control plane to automatically restart it if it enters the `Failed` state. */ autoRestartEnabled: boolean + /** The auto-restart policy configured for this instance, or `None` if no explicit policy is configured. + +If this is not present, then this instance uses the default auto-restart policy, which may or may not allow it to be restarted. The `auto_restart_enabled` field indicates whether the instance will be automatically restarted. */ + autoRestartPolicy?: InstanceAutoRestartPolicy /** the ID of the disk used to boot this Instance, if a specific one is assigned. */ bootDiskId?: string /** human-readable free-form text about a resource */ @@ -1789,16 +1803,6 @@ If this is not present, then this instance has not been automatically restarted. timeRunStateUpdated: Date } -/** - * A policy determining when an instance should be automatically restarted by the control plane. - */ -export type InstanceAutoRestartPolicy = - /** The instance should not be automatically restarted by the control plane if it fails. */ - | 'never' - - /** If this instance is running and unexpectedly fails (e.g. due to a host software crash or unexpected host reboot), the control plane will make a best-effort attempt to restart it. The control plane may choose not to restart the instance to preserve the overall availability of the system. */ - | 'best_effort' - /** * Describe the instance's disks at creation time */ @@ -1976,12 +1980,128 @@ export type InstanceSerialConsoleData = { * Parameters of an `Instance` that can be reconfigured after creation. */ export type InstanceUpdate = { + /** The auto-restart policy for this instance. + +If not provided, unset the instance's auto-restart policy. */ + autoRestartPolicy?: InstanceAutoRestartPolicy /** Name or ID of the disk the instance should be instructed to boot from. If not provided, unset the instance's boot disk. */ bootDisk?: NameOrId } +/** + * An internet gateway provides a path between VPC networks and external networks. + */ +export type InternetGateway = { + /** human-readable free-form text about a resource */ + description: string + /** unique, immutable, system-controlled identifier for each resource */ + id: string + /** unique, mutable, user-controlled identifier for each resource */ + name: Name + /** timestamp when this resource was created */ + timeCreated: Date + /** timestamp when this resource was last modified */ + timeModified: Date + /** The VPC to which the gateway belongs. */ + vpcId: string +} + +/** + * Create-time parameters for an `InternetGateway` + */ +export type InternetGatewayCreate = { description: string; name: Name } + +/** + * An IP address that is attached to an internet gateway + */ +export type InternetGatewayIpAddress = { + /** The associated IP address, */ + address: string + /** human-readable free-form text about a resource */ + description: string + /** unique, immutable, system-controlled identifier for each resource */ + id: string + /** The associated internet gateway. */ + internetGatewayId: string + /** unique, mutable, user-controlled identifier for each resource */ + name: Name + /** timestamp when this resource was created */ + timeCreated: Date + /** timestamp when this resource was last modified */ + timeModified: Date +} + +/** + * Create-time identity-related parameters + */ +export type InternetGatewayIpAddressCreate = { + address: string + description: string + gateway: NameOrId + name: Name +} + +/** + * A single page of results + */ +export type InternetGatewayIpAddressResultsPage = { + /** list of items on this page of results */ + items: InternetGatewayIpAddress[] + /** token used to fetch the next page of results (if any) */ + nextPage?: string +} + +/** + * An IP pool that is attached to an internet gateway + */ +export type InternetGatewayIpPool = { + /** human-readable free-form text about a resource */ + description: string + /** unique, immutable, system-controlled identifier for each resource */ + id: string + /** The associated internet gateway. */ + internetGatewayId: string + /** The associated IP pool. */ + ipPoolId: string + /** unique, mutable, user-controlled identifier for each resource */ + name: Name + /** timestamp when this resource was created */ + timeCreated: Date + /** timestamp when this resource was last modified */ + timeModified: Date +} + +/** + * Create-time identity-related parameters + */ +export type InternetGatewayIpPoolCreate = { + description: string + ipPool: NameOrId + name: Name +} + +/** + * A single page of results + */ +export type InternetGatewayIpPoolResultsPage = { + /** list of items on this page of results */ + items: InternetGatewayIpPool[] + /** token used to fetch the next page of results (if any) */ + nextPage?: string +} + +/** + * A single page of results + */ +export type InternetGatewayResultsPage = { + /** list of items on this page of results */ + items: InternetGateway[] + /** token used to fetch the next page of results (if any) */ + nextPage?: string +} + /** * A collection of IP ranges. If a pool is linked to a silo, IP addresses from the pool can be allocated within that silo */ @@ -4363,6 +4483,90 @@ export interface InstanceStopQueryParams { project?: NameOrId } +export interface InternetGatewayIpAddressListQueryParams { + gateway?: NameOrId + limit?: number + pageToken?: string + project?: NameOrId + sortBy?: NameOrIdSortMode + vpc?: NameOrId +} + +export interface InternetGatewayIpAddressCreateQueryParams { + gateway: NameOrId + project?: NameOrId + vpc?: NameOrId +} + +export interface InternetGatewayIpAddressDeletePathParams { + address: NameOrId +} + +export interface InternetGatewayIpAddressDeleteQueryParams { + cascade?: boolean + gateway?: NameOrId + project?: NameOrId + vpc?: NameOrId +} + +export interface InternetGatewayIpPoolListQueryParams { + gateway?: NameOrId + limit?: number + pageToken?: string + project?: NameOrId + sortBy?: NameOrIdSortMode + vpc?: NameOrId +} + +export interface InternetGatewayIpPoolCreateQueryParams { + gateway: NameOrId + project?: NameOrId + vpc?: NameOrId +} + +export interface InternetGatewayIpPoolDeletePathParams { + pool: NameOrId +} + +export interface InternetGatewayIpPoolDeleteQueryParams { + cascade?: boolean + gateway?: NameOrId + project?: NameOrId + vpc?: NameOrId +} + +export interface InternetGatewayListQueryParams { + limit?: number + pageToken?: string + project?: NameOrId + sortBy?: NameOrIdSortMode + vpc?: NameOrId +} + +export interface InternetGatewayCreateQueryParams { + project?: NameOrId + vpc: NameOrId +} + +export interface InternetGatewayViewPathParams { + gateway: NameOrId +} + +export interface InternetGatewayViewQueryParams { + project?: NameOrId + vpc?: NameOrId +} + +export interface InternetGatewayDeletePathParams { + gateway: NameOrId +} + +export interface InternetGatewayDeleteQueryParams { + cascade?: boolean + project?: NameOrId + vpc?: NameOrId +} + export interface ProjectIpPoolListQueryParams { limit?: number pageToken?: string @@ -5103,6 +5307,9 @@ export type ApiListMethods = Pick< | 'instanceDiskList' | 'instanceExternalIpList' | 'instanceSshPublicKeyList' + | 'internetGatewayIpAddressList' + | 'internetGatewayIpPoolList' + | 'internetGatewayList' | 'projectIpPoolList' | 'currentUserSshKeyList' | 'instanceNetworkInterfaceList' @@ -5998,6 +6205,185 @@ export class Api extends HttpClient { ...params, }) }, + /** + * List addresses attached to an internet gateway. + */ + internetGatewayIpAddressList: ( + { query = {} }: { query?: InternetGatewayIpAddressListQueryParams }, + params: FetchParams = {} + ) => { + return this.request({ + path: `/v1/internet-gateway-ip-addresses`, + method: 'GET', + query, + ...params, + }) + }, + /** + * Attach ip pool to internet gateway + */ + internetGatewayIpAddressCreate: ( + { + query, + body, + }: { + query: InternetGatewayIpAddressCreateQueryParams + body: InternetGatewayIpAddressCreate + }, + params: FetchParams = {} + ) => { + return this.request({ + path: `/v1/internet-gateway-ip-addresses`, + method: 'POST', + body, + query, + ...params, + }) + }, + /** + * Detach ip pool from internet gateway + */ + internetGatewayIpAddressDelete: ( + { + path, + query = {}, + }: { + path: InternetGatewayIpAddressDeletePathParams + query?: InternetGatewayIpAddressDeleteQueryParams + }, + params: FetchParams = {} + ) => { + return this.request({ + path: `/v1/internet-gateway-ip-addresses/${path.address}`, + method: 'DELETE', + query, + ...params, + }) + }, + /** + * List IP pools attached to an internet gateway. + */ + internetGatewayIpPoolList: ( + { query = {} }: { query?: InternetGatewayIpPoolListQueryParams }, + params: FetchParams = {} + ) => { + return this.request({ + path: `/v1/internet-gateway-ip-pools`, + method: 'GET', + query, + ...params, + }) + }, + /** + * Attach ip pool to internet gateway + */ + internetGatewayIpPoolCreate: ( + { + query, + body, + }: { + query: InternetGatewayIpPoolCreateQueryParams + body: InternetGatewayIpPoolCreate + }, + params: FetchParams = {} + ) => { + return this.request({ + path: `/v1/internet-gateway-ip-pools`, + method: 'POST', + body, + query, + ...params, + }) + }, + /** + * Detach ip pool from internet gateway + */ + internetGatewayIpPoolDelete: ( + { + path, + query = {}, + }: { + path: InternetGatewayIpPoolDeletePathParams + query?: InternetGatewayIpPoolDeleteQueryParams + }, + params: FetchParams = {} + ) => { + return this.request({ + path: `/v1/internet-gateway-ip-pools/${path.pool}`, + method: 'DELETE', + query, + ...params, + }) + }, + /** + * List internet gateways + */ + internetGatewayList: ( + { query = {} }: { query?: InternetGatewayListQueryParams }, + params: FetchParams = {} + ) => { + return this.request({ + path: `/v1/internet-gateways`, + method: 'GET', + query, + ...params, + }) + }, + /** + * Create VPC internet gateway + */ + internetGatewayCreate: ( + { + query, + body, + }: { query: InternetGatewayCreateQueryParams; body: InternetGatewayCreate }, + params: FetchParams = {} + ) => { + return this.request({ + path: `/v1/internet-gateways`, + method: 'POST', + body, + query, + ...params, + }) + }, + /** + * Fetch internet gateway + */ + internetGatewayView: ( + { + path, + query = {}, + }: { path: InternetGatewayViewPathParams; query?: InternetGatewayViewQueryParams }, + params: FetchParams = {} + ) => { + return this.request({ + path: `/v1/internet-gateways/${path.gateway}`, + method: 'GET', + query, + ...params, + }) + }, + /** + * Delete internet gateway + */ + internetGatewayDelete: ( + { + path, + query = {}, + }: { + path: InternetGatewayDeletePathParams + query?: InternetGatewayDeleteQueryParams + }, + params: FetchParams = {} + ) => { + return this.request({ + path: `/v1/internet-gateways/${path.gateway}`, + method: 'DELETE', + query, + ...params, + }) + }, /** * List IP pools */ diff --git a/app/api/__generated__/OMICRON_VERSION b/app/api/__generated__/OMICRON_VERSION index 2c1de3ebd..258888bf6 100644 --- a/app/api/__generated__/OMICRON_VERSION +++ b/app/api/__generated__/OMICRON_VERSION @@ -1,2 +1,2 @@ # generated file. do not update manually. see docs/update-pinned-api.md -c50cf019cd9be35f98266a7f4acacab0236b3a3d +a9fd9dbbbcdaa80b822c3fc14b4f1a20ac72021d diff --git a/app/api/__generated__/msw-handlers.ts b/app/api/__generated__/msw-handlers.ts index 49df51a41..0e455163a 100644 --- a/app/api/__generated__/msw-handlers.ts +++ b/app/api/__generated__/msw-handlers.ts @@ -407,6 +407,73 @@ export interface MSWHandlers { req: Request cookies: Record }) => Promisable> + /** `GET /v1/internet-gateway-ip-addresses` */ + internetGatewayIpAddressList: (params: { + query: Api.InternetGatewayIpAddressListQueryParams + req: Request + cookies: Record + }) => Promisable> + /** `POST /v1/internet-gateway-ip-addresses` */ + internetGatewayIpAddressCreate: (params: { + query: Api.InternetGatewayIpAddressCreateQueryParams + body: Json + req: Request + cookies: Record + }) => Promisable> + /** `DELETE /v1/internet-gateway-ip-addresses/:address` */ + internetGatewayIpAddressDelete: (params: { + path: Api.InternetGatewayIpAddressDeletePathParams + query: Api.InternetGatewayIpAddressDeleteQueryParams + req: Request + cookies: Record + }) => Promisable + /** `GET /v1/internet-gateway-ip-pools` */ + internetGatewayIpPoolList: (params: { + query: Api.InternetGatewayIpPoolListQueryParams + req: Request + cookies: Record + }) => Promisable> + /** `POST /v1/internet-gateway-ip-pools` */ + internetGatewayIpPoolCreate: (params: { + query: Api.InternetGatewayIpPoolCreateQueryParams + body: Json + req: Request + cookies: Record + }) => Promisable> + /** `DELETE /v1/internet-gateway-ip-pools/:pool` */ + internetGatewayIpPoolDelete: (params: { + path: Api.InternetGatewayIpPoolDeletePathParams + query: Api.InternetGatewayIpPoolDeleteQueryParams + req: Request + cookies: Record + }) => Promisable + /** `GET /v1/internet-gateways` */ + internetGatewayList: (params: { + query: Api.InternetGatewayListQueryParams + req: Request + cookies: Record + }) => Promisable> + /** `POST /v1/internet-gateways` */ + internetGatewayCreate: (params: { + query: Api.InternetGatewayCreateQueryParams + body: Json + req: Request + cookies: Record + }) => Promisable> + /** `GET /v1/internet-gateways/:gateway` */ + internetGatewayView: (params: { + path: Api.InternetGatewayViewPathParams + query: Api.InternetGatewayViewQueryParams + req: Request + cookies: Record + }) => Promisable> + /** `DELETE /v1/internet-gateways/:gateway` */ + internetGatewayDelete: (params: { + path: Api.InternetGatewayDeletePathParams + query: Api.InternetGatewayDeleteQueryParams + req: Request + cookies: Record + }) => Promisable /** `GET /v1/ip-pools` */ projectIpPoolList: (params: { query: Api.ProjectIpPoolListQueryParams @@ -1685,6 +1752,74 @@ export function makeHandlers(handlers: MSWHandlers): HttpHandler[] { '/v1/instances/:instance/stop', handler(handlers['instanceStop'], schema.InstanceStopParams, null) ), + http.get( + '/v1/internet-gateway-ip-addresses', + handler( + handlers['internetGatewayIpAddressList'], + schema.InternetGatewayIpAddressListParams, + null + ) + ), + http.post( + '/v1/internet-gateway-ip-addresses', + handler( + handlers['internetGatewayIpAddressCreate'], + schema.InternetGatewayIpAddressCreateParams, + schema.InternetGatewayIpAddressCreate + ) + ), + http.delete( + '/v1/internet-gateway-ip-addresses/:address', + handler( + handlers['internetGatewayIpAddressDelete'], + schema.InternetGatewayIpAddressDeleteParams, + null + ) + ), + http.get( + '/v1/internet-gateway-ip-pools', + handler( + handlers['internetGatewayIpPoolList'], + schema.InternetGatewayIpPoolListParams, + null + ) + ), + http.post( + '/v1/internet-gateway-ip-pools', + handler( + handlers['internetGatewayIpPoolCreate'], + schema.InternetGatewayIpPoolCreateParams, + schema.InternetGatewayIpPoolCreate + ) + ), + http.delete( + '/v1/internet-gateway-ip-pools/:pool', + handler( + handlers['internetGatewayIpPoolDelete'], + schema.InternetGatewayIpPoolDeleteParams, + null + ) + ), + http.get( + '/v1/internet-gateways', + handler(handlers['internetGatewayList'], schema.InternetGatewayListParams, null) + ), + http.post( + '/v1/internet-gateways', + handler( + handlers['internetGatewayCreate'], + schema.InternetGatewayCreateParams, + schema.InternetGatewayCreate + ) + ), + http.get( + '/v1/internet-gateways/:gateway', + handler(handlers['internetGatewayView'], schema.InternetGatewayViewParams, null) + ), + http.delete( + '/v1/internet-gateways/:gateway', + handler(handlers['internetGatewayDelete'], schema.InternetGatewayDeleteParams, null) + ), http.get( '/v1/ip-pools', handler(handlers['projectIpPoolList'], schema.ProjectIpPoolListParams, null) diff --git a/app/api/__generated__/validate.ts b/app/api/__generated__/validate.ts index 77402ef0d..b45ccf9a5 100644 --- a/app/api/__generated__/validate.ts +++ b/app/api/__generated__/validate.ts @@ -1645,6 +1645,14 @@ export const ImportBlocksBulkWrite = z.preprocess( z.object({ base64EncodedData: z.string(), offset: z.number().min(0) }) ) +/** + * A policy determining when an instance should be automatically restarted by the control plane. + */ +export const InstanceAutoRestartPolicy = z.preprocess( + processResponseBody, + z.enum(['never', 'best_effort']) +) + /** * The number of CPUs in an Instance */ @@ -1682,6 +1690,7 @@ export const Instance = z.preprocess( z.object({ autoRestartCooldownExpiration: z.coerce.date().optional(), autoRestartEnabled: SafeBoolean, + autoRestartPolicy: InstanceAutoRestartPolicy.optional(), bootDiskId: z.string().uuid().optional(), description: z.string(), hostname: z.string(), @@ -1698,14 +1707,6 @@ export const Instance = z.preprocess( }) ) -/** - * A policy determining when an instance should be automatically restarted by the control plane. - */ -export const InstanceAutoRestartPolicy = z.preprocess( - processResponseBody, - z.enum(['never', 'best_effort']) -) - /** * Describe the instance's disks at creation time */ @@ -1852,7 +1853,110 @@ export const InstanceSerialConsoleData = z.preprocess( */ export const InstanceUpdate = z.preprocess( processResponseBody, - z.object({ bootDisk: NameOrId.optional() }) + z.object({ + autoRestartPolicy: InstanceAutoRestartPolicy.optional(), + bootDisk: NameOrId.optional(), + }) +) + +/** + * An internet gateway provides a path between VPC networks and external networks. + */ +export const InternetGateway = z.preprocess( + processResponseBody, + z.object({ + description: z.string(), + id: z.string().uuid(), + name: Name, + timeCreated: z.coerce.date(), + timeModified: z.coerce.date(), + vpcId: z.string().uuid(), + }) +) + +/** + * Create-time parameters for an `InternetGateway` + */ +export const InternetGatewayCreate = z.preprocess( + processResponseBody, + z.object({ description: z.string(), name: Name }) +) + +/** + * An IP address that is attached to an internet gateway + */ +export const InternetGatewayIpAddress = z.preprocess( + processResponseBody, + z.object({ + address: z.string().ip(), + description: z.string(), + id: z.string().uuid(), + internetGatewayId: z.string().uuid(), + name: Name, + timeCreated: z.coerce.date(), + timeModified: z.coerce.date(), + }) +) + +/** + * Create-time identity-related parameters + */ +export const InternetGatewayIpAddressCreate = z.preprocess( + processResponseBody, + z.object({ + address: z.string().ip(), + description: z.string(), + gateway: NameOrId, + name: Name, + }) +) + +/** + * A single page of results + */ +export const InternetGatewayIpAddressResultsPage = z.preprocess( + processResponseBody, + z.object({ items: InternetGatewayIpAddress.array(), nextPage: z.string().optional() }) +) + +/** + * An IP pool that is attached to an internet gateway + */ +export const InternetGatewayIpPool = z.preprocess( + processResponseBody, + z.object({ + description: z.string(), + id: z.string().uuid(), + internetGatewayId: z.string().uuid(), + ipPoolId: z.string().uuid(), + name: Name, + timeCreated: z.coerce.date(), + timeModified: z.coerce.date(), + }) +) + +/** + * Create-time identity-related parameters + */ +export const InternetGatewayIpPoolCreate = z.preprocess( + processResponseBody, + z.object({ description: z.string(), ipPool: NameOrId, name: Name }) +) + +/** + * A single page of results + */ +export const InternetGatewayIpPoolResultsPage = z.preprocess( + processResponseBody, + z.object({ items: InternetGatewayIpPool.array(), nextPage: z.string().optional() }) +) + +/** + * A single page of results + */ +export const InternetGatewayResultsPage = z.preprocess( + processResponseBody, + z.object({ items: InternetGateway.array(), nextPage: z.string().optional() }) ) /** @@ -4348,6 +4452,142 @@ export const InstanceStopParams = z.preprocess( }) ) +export const InternetGatewayIpAddressListParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({}), + query: z.object({ + gateway: NameOrId.optional(), + limit: z.number().min(1).max(4294967295).optional(), + pageToken: z.string().optional(), + project: NameOrId.optional(), + sortBy: NameOrIdSortMode.optional(), + vpc: NameOrId.optional(), + }), + }) +) + +export const InternetGatewayIpAddressCreateParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({}), + query: z.object({ + gateway: NameOrId, + project: NameOrId.optional(), + vpc: NameOrId.optional(), + }), + }) +) + +export const InternetGatewayIpAddressDeleteParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({ + address: NameOrId, + }), + query: z.object({ + cascade: SafeBoolean.optional(), + gateway: NameOrId.optional(), + project: NameOrId.optional(), + vpc: NameOrId.optional(), + }), + }) +) + +export const InternetGatewayIpPoolListParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({}), + query: z.object({ + gateway: NameOrId.optional(), + limit: z.number().min(1).max(4294967295).optional(), + pageToken: z.string().optional(), + project: NameOrId.optional(), + sortBy: NameOrIdSortMode.optional(), + vpc: NameOrId.optional(), + }), + }) +) + +export const InternetGatewayIpPoolCreateParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({}), + query: z.object({ + gateway: NameOrId, + project: NameOrId.optional(), + vpc: NameOrId.optional(), + }), + }) +) + +export const InternetGatewayIpPoolDeleteParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({ + pool: NameOrId, + }), + query: z.object({ + cascade: SafeBoolean.optional(), + gateway: NameOrId.optional(), + project: NameOrId.optional(), + vpc: NameOrId.optional(), + }), + }) +) + +export const InternetGatewayListParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({}), + query: z.object({ + limit: z.number().min(1).max(4294967295).optional(), + pageToken: z.string().optional(), + project: NameOrId.optional(), + sortBy: NameOrIdSortMode.optional(), + vpc: NameOrId.optional(), + }), + }) +) + +export const InternetGatewayCreateParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({}), + query: z.object({ + project: NameOrId.optional(), + vpc: NameOrId, + }), + }) +) + +export const InternetGatewayViewParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({ + gateway: NameOrId, + }), + query: z.object({ + project: NameOrId.optional(), + vpc: NameOrId.optional(), + }), + }) +) + +export const InternetGatewayDeleteParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({ + gateway: NameOrId, + }), + query: z.object({ + cascade: SafeBoolean.optional(), + project: NameOrId.optional(), + vpc: NameOrId.optional(), + }), + }) +) + export const ProjectIpPoolListParams = z.preprocess( processResponseBody, z.object({ From 07d96975c2269cfe373c88ae16285d451b380b65 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 3 Oct 2024 17:46:04 -0700 Subject: [PATCH 02/63] update msw handlers --- mock-api/msw/handlers.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/mock-api/msw/handlers.ts b/mock-api/msw/handlers.ts index c81f53d95..317bcfed0 100644 --- a/mock-api/msw/handlers.ts +++ b/mock-api/msw/handlers.ts @@ -1519,6 +1519,16 @@ export const handlers = makeHandlers({ certificateView: NotImplemented, instanceSerialConsoleStream: NotImplemented, instanceSshPublicKeyList: NotImplemented, + internetGatewayIpAddressCreate: NotImplemented, + internetGatewayIpAddressDelete: NotImplemented, + internetGatewayIpAddressList: NotImplemented, + internetGatewayIpPoolCreate: NotImplemented, + internetGatewayIpPoolDelete: NotImplemented, + internetGatewayIpPoolList: NotImplemented, + internetGatewayCreate: NotImplemented, + internetGatewayDelete: NotImplemented, + internetGatewayList: NotImplemented, + internetGatewayView: NotImplemented, ipPoolServiceRangeAdd: NotImplemented, ipPoolServiceRangeList: NotImplemented, ipPoolServiceRangeRemove: NotImplemented, From 954700504fa6c13c2b4993ca8a6cc61769083f7f Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 4 Oct 2024 09:06:40 -0700 Subject: [PATCH 03/63] Other changes from initial branch --- app/api/path-params.ts | 1 + app/pages/project/vpcs/VpcPage/VpcPage.tsx | 1 + .../vpcs/VpcPage/tabs/VpcGatewaysTab.tsx | 68 +++++++++ app/routes.tsx | 11 ++ app/util/path-builder.ts | 6 + mock-api/index.ts | 1 + mock-api/internet-gateway.ts | 129 ++++++++++++++++++ mock-api/msw/db.ts | 3 + mock-api/msw/handlers.ts | 6 +- 9 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 mock-api/internet-gateway.ts diff --git a/app/api/path-params.ts b/app/api/path-params.ts index eaeab465a..baa7674c6 100644 --- a/app/api/path-params.ts +++ b/app/api/path-params.ts @@ -19,6 +19,7 @@ export type VpcRouter = Merge export type VpcRouterRoute = Merge export type VpcSubnet = Merge export type FirewallRule = Merge +export type InternetGateway = Merge export type Silo = { silo?: string } export type IdentityProvider = Merge export type SystemUpdate = { version: string } diff --git a/app/pages/project/vpcs/VpcPage/VpcPage.tsx b/app/pages/project/vpcs/VpcPage/VpcPage.tsx index 5c5c5d912..b79fce4e8 100644 --- a/app/pages/project/vpcs/VpcPage/VpcPage.tsx +++ b/app/pages/project/vpcs/VpcPage/VpcPage.tsx @@ -103,6 +103,7 @@ export function VpcPage() { Firewall Rules Subnets Routers + Internet Gateways ) diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx index 262139f28..44013b1c6 100644 --- a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx @@ -5,3 +5,71 @@ * * Copyright Oxide Computer Company */ + +import { createColumnHelper } from '@tanstack/react-table' +import { useMemo } from 'react' +import { Outlet, type LoaderFunctionArgs } from 'react-router-dom' + +import { apiQueryClient, usePrefetchedApiQuery, type InternetGateway } from '~/api' +import { getVpcSelector, useVpcSelector } from '~/hooks/use-params' +import { makeLinkCell } from '~/table/cells/LinkCell' +import { Columns } from '~/table/columns/common' +import { useQueryTable } from '~/table/QueryTable' +import { CreateLink } from '~/ui/lib/CreateButton' +import { EmptyMessage } from '~/ui/lib/EmptyMessage' +import { ALL_ISH } from '~/util/consts' +import { pb } from '~/util/path-builder' + +const colHelper = createColumnHelper() + +VpcInternetGatewaysTab.loader = async ({ params }: LoaderFunctionArgs) => { + const { project, vpc } = getVpcSelector(params) + await apiQueryClient.prefetchQuery('internetGatewayList', { + query: { project, vpc, limit: ALL_ISH }, + }) + return null +} + +export function VpcInternetGatewaysTab() { + const vpcSelector = useVpcSelector() + const { project, vpc } = vpcSelector + const igs = usePrefetchedApiQuery('internetGatewayList', { + query: { project, vpc, limit: ALL_ISH }, + }) + const { Table } = useQueryTable('internetGatewayList', { + query: { project, vpc, limit: ALL_ISH }, + }) + + console.log({ igs }) + const emptyState = ( + + ) + + const staticColumns = useMemo( + () => [ + colHelper.accessor('name', { + cell: makeLinkCell((gateway) => pb.vpcInternetGateway({ ...vpcSelector, gateway })), + }), + colHelper.accessor('description', Columns.description), + colHelper.accessor('timeCreated', Columns.timeCreated), + ], + [vpcSelector] + ) + + return ( + <> +
+ + New internet gateway + +
+ + + + ) +} diff --git a/app/routes.tsx b/app/routes.tsx index 86b159f5f..f4a7d0017 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -68,6 +68,7 @@ import { InstancesPage } from './pages/project/instances/InstancesPage' import { SnapshotsPage } from './pages/project/snapshots/SnapshotsPage' import { RouterPage } from './pages/project/vpcs/RouterPage' import { VpcFirewallRulesTab } from './pages/project/vpcs/VpcPage/tabs/VpcFirewallRulesTab' +import { VpcInternetGatewaysTab } from './pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab' import { VpcRoutersTab } from './pages/project/vpcs/VpcPage/tabs/VpcRoutersTab' import { VpcSubnetsTab } from './pages/project/vpcs/VpcPage/tabs/VpcSubnetsTab' import { VpcPage } from './pages/project/vpcs/VpcPage/VpcPage' @@ -412,6 +413,16 @@ export const routes = createRoutesFromElements( handle={{ crumb: 'New Router' }} /> + } + loader={VpcInternetGatewaysTab.loader} + > + + diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts index 1709b19c5..6426584e9 100644 --- a/app/util/path-builder.ts +++ b/app/util/path-builder.ts @@ -25,6 +25,7 @@ type FirewallRule = Required type VpcRouter = Required type VpcRouterRoute = Required type VpcSubnet = Required +type VpcInternetGateway = Required // these are used as the basis for many routes but are not themselves routes we // ever want to link to. so we use this to build the routes but pb.project() is @@ -97,6 +98,11 @@ export const pb = { vpcSubnetsNew: (params: Vpc) => `${vpcBase(params)}/subnets-new`, vpcSubnetsEdit: (params: VpcSubnet) => `${pb.vpcSubnets(params)}/${params.subnet}/edit`, + vpcInternetGateways: (params: Vpc) => `${vpcBase(params)}/internet-gateways`, + vpcInternetGateway: (params: VpcInternetGateway) => + `${pb.vpcInternetGateways(params)}/${params.gateway}`, + vpcInternetGatewaysNew: (params: Vpc) => `${vpcBase(params)}/internet-gateways-new`, + floatingIps: (params: Project) => `${projectBase(params)}/floating-ips`, floatingIpsNew: (params: Project) => `${projectBase(params)}/floating-ips-new`, floatingIp: (params: FloatingIp) => `${pb.floatingIps(params)}/${params.floatingIp}`, diff --git a/mock-api/index.ts b/mock-api/index.ts index e03311145..0256808bf 100644 --- a/mock-api/index.ts +++ b/mock-api/index.ts @@ -11,6 +11,7 @@ export * from './external-ip' export * from './floating-ip' export * from './image' export * from './instance' +export * from './internet-gateway' export * from './ip-pool' export * from './network-interface' export * from './physical-disk' diff --git a/mock-api/internet-gateway.ts b/mock-api/internet-gateway.ts new file mode 100644 index 000000000..c8bb4d688 --- /dev/null +++ b/mock-api/internet-gateway.ts @@ -0,0 +1,129 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import type { + InternetGateway, + InternetGatewayIpAddress, + InternetGatewayIpPool, +} from '@oxide/api' + +import { ipPools } from './ip-pool' +import type { Json } from './json-type' +import { vpc, vpc2 } from './vpc' + +const time_created = new Date(2021, 0, 1).toISOString() +const time_modified = new Date(2021, 0, 2).toISOString() + +// An internet gateway for VPC 1 +const internetGateway1: Json = { + id: 'f1d5e5a1-0b2b-4d5b-8b9d-2d4b3e0c6b9a', + name: 'internet-gateway-1', + description: 'internet gateway 1', + vpc_id: vpc.id, + time_created, + time_modified, +} + +// Another internet gateway for VPC 1 +const internetGateway2: Json = { + id: 'f1d5e5a1-0b2b-4d5b-8b9d-2d4b3e0c6b9b', + name: 'internet-gateway-2', + description: 'internet gateway 2', + vpc_id: vpc.id, + time_created, + time_modified, +} + +// An internet gateway for VPC 2 +const internetGateway3: Json = { + id: 'f1d5e5a1-0b2b-4d5b-8b9d-2d4b3e0c6b9c', + name: 'internet-gateway-3', + description: 'internet gateway 3', + vpc_id: vpc2.id, + time_created, + time_modified, +} + +export const internetGateways: Json[] = [ + internetGateway1, + internetGateway2, + internetGateway3, +] + +const internetGatewayIpAddress1: Json = { + id: 'f1d5e5a1-0b2b-4d5b-8b9d-2d4b3e0c6b9d', + address: '87.114.25.166', + description: 'the IP address for an internet gateway', + internet_gateway_id: internetGateway1.id, + name: 'internet-gateway-ip-1', + time_created, + time_modified, +} + +const internetGatewayIpAddress2: Json = { + id: 'f1d5e5a1-0b2b-4d5b-8b9d-2d4b3e0c6b9e', + address: '292a:a05c:3b36:a053:9166:6510:2d6b:3322', + description: 'an IPv6 address for an internet gateway', + internet_gateway_id: internetGateway1.id, + name: 'internet-gateway-ip-2', + time_created, + time_modified, +} + +const internetGatewayIpAddress3: Json = { + id: 'f1d5e5a1-0b2b-4d5b-8b9d-2d4b3e0c6b9f', + address: '178.125.253.126', + description: 'an IPv4 address for internet gateway 2', + internet_gateway_id: internetGateway2.id, + name: 'internet-gateway-ip-3', + time_created, + time_modified, +} + +export const internetGatewayIpAddresses: Json[] = [ + internetGatewayIpAddress1, + internetGatewayIpAddress2, + internetGatewayIpAddress3, +] + +const [ipPool1, ipPool2, ipPool3] = ipPools + +const internetGatewayIpPool1: Json = { + id: 'f1d5e5a1-0b2b-4d5b-8b9d-2d4b3e0c6b9g', + name: 'internet-gateway-ip-pool-1', + description: 'an IP pool for an internet gateway', + internet_gateway_id: internetGateway1.id, + ip_pool_id: ipPool1.id, + time_created, + time_modified, +} + +const internetGatewayIpPool2: Json = { + id: 'f1d5e5a1-0b2b-4d5b-8b9d-2d4b3e0c6b9h', + name: 'internet-gateway-ip-pool-2', + description: 'a set of VPN IPs in an IP pool for an internet gateway', + internet_gateway_id: internetGateway1.id, + ip_pool_id: ipPool2.id, + time_created, + time_modified, +} + +const internetGatewayIpPool3: Json = { + id: 'f1d5e5a1-0b2b-4d5b-8b9d-2d4b3e0c6b9i', + name: 'internet-gateway-ip-pool-3', + description: 'another IP pool for an internet gateway', + internet_gateway_id: internetGateway2.id, + ip_pool_id: ipPool3.id, + time_created, + time_modified, +} + +export const internetGatewayIpPools: Json[] = [ + internetGatewayIpPool1, + internetGatewayIpPool2, + internetGatewayIpPool3, +] diff --git a/mock-api/msw/db.ts b/mock-api/msw/db.ts index f9e1b26ed..22e9079ae 100644 --- a/mock-api/msw/db.ts +++ b/mock-api/msw/db.ts @@ -404,6 +404,9 @@ const initDb = { images: [...mock.images], ephemeralIps: [...mock.ephemeralIps], instances: [...mock.instances], + internetGatewayIpAddresses: [...mock.internetGatewayIpAddresses], + internetGatewayIpPools: [...mock.internetGatewayIpPools], + internetGateways: [...mock.internetGateways], ipPools: [...mock.ipPools], ipPoolSilos: [...mock.ipPoolSilos], ipPoolRanges: [...mock.ipPoolRanges], diff --git a/mock-api/msw/handlers.ts b/mock-api/msw/handlers.ts index 317bcfed0..53aca842b 100644 --- a/mock-api/msw/handlers.ts +++ b/mock-api/msw/handlers.ts @@ -1155,6 +1155,11 @@ export const handlers = makeHandlers({ return { rules: R.sortBy(rules, (r) => r.name) } }, + internetGatewayList({ query }) { + const vpc = lookup.vpc(query) + const gateways = db.internetGateways.filter((g) => g.vpc_id === vpc.id) + return paginated(query, gateways) + }, vpcRouterList({ query }) { const vpc = lookup.vpc(query) const routers = db.vpcRouters.filter((r) => r.vpc_id === vpc.id) @@ -1527,7 +1532,6 @@ export const handlers = makeHandlers({ internetGatewayIpPoolList: NotImplemented, internetGatewayCreate: NotImplemented, internetGatewayDelete: NotImplemented, - internetGatewayList: NotImplemented, internetGatewayView: NotImplemented, ipPoolServiceRangeAdd: NotImplemented, ipPoolServiceRangeList: NotImplemented, From f7c4b10b040e789ac44b55e85d6a06c98f3c7674 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 4 Oct 2024 23:18:09 -0700 Subject: [PATCH 04/63] IP Pools tab in place --- app/api/path-params.ts | 2 + app/components/ErrorBoundary.tsx | 1 + app/hooks/use-params.ts | 3 + .../InternetGatewayPage.tsx | 66 +++++++++++++ .../tabs/InternetGatewayIpAddressesTab.tsx | 31 +++++++ .../tabs/InternetGatewayIpPoolsTab.tsx | 93 +++++++++++++++++++ .../vpcs/VpcPage/tabs/VpcGatewaysTab.tsx | 55 ++++++++--- app/pages/system/networking/IpPoolsPage.tsx | 11 +-- app/routes.tsx | 28 ++++++ app/table/cells/UtilizationCell.tsx | 17 ++++ app/util/path-builder.ts | 4 + mock-api/msw/db.ts | 38 ++++++++ mock-api/msw/handlers.ts | 18 +++- 13 files changed, 343 insertions(+), 24 deletions(-) create mode 100644 app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx create mode 100644 app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpAddressesTab.tsx create mode 100644 app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx create mode 100644 app/table/cells/UtilizationCell.tsx diff --git a/app/api/path-params.ts b/app/api/path-params.ts index baa7674c6..5cc088a19 100644 --- a/app/api/path-params.ts +++ b/app/api/path-params.ts @@ -20,6 +20,8 @@ export type VpcRouterRoute = Merge export type VpcSubnet = Merge export type FirewallRule = Merge export type InternetGateway = Merge +export type InternetGatewayIpAddress = Merge +export type InternetGatewayIpPool = Merge export type Silo = { silo?: string } export type IdentityProvider = Merge export type SystemUpdate = { version: string } diff --git a/app/components/ErrorBoundary.tsx b/app/components/ErrorBoundary.tsx index 91085e08f..52db88e5c 100644 --- a/app/components/ErrorBoundary.tsx +++ b/app/components/ErrorBoundary.tsx @@ -38,5 +38,6 @@ export const ErrorBoundary = (props: { children: React.ReactNode }) => ( export function RouterDataErrorBoundary() { // TODO: validate this unknown at runtime _before_ passing to ErrorFallback const error = useRouteError() as Props['error'] + console.error(error) return } diff --git a/app/hooks/use-params.ts b/app/hooks/use-params.ts index 348e48d3b..d55792c21 100644 --- a/app/hooks/use-params.ts +++ b/app/hooks/use-params.ts @@ -40,6 +40,7 @@ export const getFirewallRuleSelector = requireParams('project', 'vpc', 'rule') export const getVpcRouterSelector = requireParams('project', 'vpc', 'router') export const getVpcRouterRouteSelector = requireParams('project', 'vpc', 'router', 'route') export const getVpcSubnetSelector = requireParams('project', 'vpc', 'subnet') +export const getInternetGatewaySelector = requireParams('project', 'vpc', 'gateway') export const getSiloSelector = requireParams('silo') export const getSiloImageSelector = requireParams('image') export const getIdpSelector = requireParams('silo', 'provider') @@ -84,6 +85,8 @@ export const useVpcSelector = () => useSelectedParams(getVpcSelector) export const useVpcRouterSelector = () => useSelectedParams(getVpcRouterSelector) export const useVpcRouterRouteSelector = () => useSelectedParams(getVpcRouterRouteSelector) export const useVpcSubnetSelector = () => useSelectedParams(getVpcSubnetSelector) +export const useInternetGatewaySelector = () => + useSelectedParams(getInternetGatewaySelector) export const useFirewallRuleSelector = () => useSelectedParams(getFirewallRuleSelector) export const useSiloSelector = () => useSelectedParams(getSiloSelector) export const useSiloImageSelector = () => useSelectedParams(getSiloImageSelector) diff --git a/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx b/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx new file mode 100644 index 000000000..4575bcd25 --- /dev/null +++ b/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx @@ -0,0 +1,66 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +import { type LoaderFunctionArgs } from 'react-router-dom' + +import { Networking24Icon } from '@oxide/design-system/icons/react' + +import { apiQueryClient, usePrefetchedApiQuery } from '~/api' +import { RouteTabs, Tab } from '~/components/RouteTabs' +import { getInternetGatewaySelector, useInternetGatewaySelector } from '~/hooks/use-params' +import { PageHeader, PageTitle } from '~/ui/lib/PageHeader' +import { ALL_ISH } from '~/util/consts' +import { pb } from '~/util/path-builder' + +InternetGatewayPage.loader = async function ({ params }: LoaderFunctionArgs) { + console.log('InternetGatewayPage.loader') + const { project, vpc, gateway } = getInternetGatewaySelector(params) + console.log({ project, vpc, gateway }) + const query = { project, vpc, gateway, limit: ALL_ISH } + await Promise.all([ + apiQueryClient.prefetchQuery('internetGatewayView', { + query: { project, vpc }, + path: { gateway }, + }), + apiQueryClient.prefetchQuery('internetGatewayIpAddressList', { query }), + apiQueryClient.prefetchQuery('internetGatewayIpPoolList', { query }), + ]) + return null +} + +export function InternetGatewayPage() { + const gatewaySelector = useInternetGatewaySelector() + const { project, vpc, gateway } = gatewaySelector + // const query = { project, vpc, limit: ALL_ISH } + + const { data: internetGateway } = usePrefetchedApiQuery('internetGatewayView', { + query: { project, vpc }, + path: { gateway }, + }) + + console.log(internetGateway) + // const { Table: IpPoolsTable } = useQueryTable('internetGatewayIpPoolsList', { + // query, + // }) + + // const { Table: IpAddressesTable } = useQueryTable('internetGatewayIpAddressList', { + // query, + // }) + + return ( + <> + + }>{gateway} + + + IP Pools + IP Addresses + + + ) +} diff --git a/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpAddressesTab.tsx b/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpAddressesTab.tsx new file mode 100644 index 000000000..27e5e6c9a --- /dev/null +++ b/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpAddressesTab.tsx @@ -0,0 +1,31 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +import type { LoaderFunctionArgs } from 'react-router-dom' + +import { apiQueryClient } from '~/api' +import { getInternetGatewaySelector, useInternetGatewaySelector } from '~/hooks/use-params' +import { ALL_ISH } from '~/util/consts' + +InternetGatewayIpAddressesTab.loader = async function ({ params }: LoaderFunctionArgs) { + const { project, vpc, gateway } = getInternetGatewaySelector(params) + await Promise.all([ + apiQueryClient.prefetchQuery('internetGatewayIpAddressList', { + query: { project, vpc, gateway, limit: ALL_ISH }, + }), + ]) + return null +} + +export function InternetGatewayIpAddressesTab() { + const gatewaySelector = useInternetGatewaySelector() + const { project, vpc, gateway } = gatewaySelector + // const query = { project, vpc, limit: ALL_ISH } + console.log({ project, vpc, gateway }) + return <>IP Addresses stuff will go here +} diff --git a/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx b/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx new file mode 100644 index 000000000..6a573bb1b --- /dev/null +++ b/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx @@ -0,0 +1,93 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +import { createColumnHelper } from '@tanstack/react-table' +import { useMemo } from 'react' +import type { LoaderFunctionArgs } from 'react-router-dom' + +import { apiQueryClient, usePrefetchedApiQuery, type InternetGatewayIpPool } from '~/api' +import { getInternetGatewaySelector, useInternetGatewaySelector } from '~/hooks/use-params' +import { DescriptionCell } from '~/table/cells/DescriptionCell' +import { EmptyCell } from '~/table/cells/EmptyCell' +import { UtilizationCell } from '~/table/cells/UtilizationCell' +import { Columns } from '~/table/columns/common' +import { useQueryTable } from '~/table/QueryTable' +import { EmptyMessage } from '~/ui/lib/EmptyMessage' +import { ALL_ISH } from '~/util/consts' + +InternetGatewayIpPoolsTab.loader = async function ({ params }: LoaderFunctionArgs) { + const { project, vpc, gateway } = getInternetGatewaySelector(params) + await Promise.all([ + apiQueryClient.prefetchQuery('internetGatewayIpPoolList', { + query: { project, vpc, gateway, limit: ALL_ISH }, + }), + // get IP Pools + apiQueryClient.prefetchQuery('ipPoolList', { query: { limit: ALL_ISH } }), + ]) + return null +} + +const colHelper = createColumnHelper() + +export function InternetGatewayIpPoolsTab() { + const { project, vpc, gateway } = useInternetGatewaySelector() + const { Table } = useQueryTable('internetGatewayIpPoolList', { + query: { project, vpc, gateway, limit: ALL_ISH }, + }) + const { data: ipPools } = usePrefetchedApiQuery('ipPoolList', { + query: { limit: ALL_ISH }, + }) + + const emptyState = ( + + ) + + const staticColumns = useMemo( + () => [ + colHelper.accessor('name', {}), + colHelper.accessor('description', Columns.description), + colHelper.accessor('ipPoolId', { + header: 'IP Pool Name', + cell: (info) => { + const ipPool = ipPools.items.find((item) => item.id === info.getValue()) + return ipPool?.name || + }, + }), + colHelper.accessor('ipPoolId', { + header: 'IP Pool Description', + cell: (info) => { + const ipPool = ipPools.items.find((item) => item.id === info.getValue()) + return + }, + }), + colHelper.accessor('ipPoolId', { + header: 'IP Pool Utilization', + cell: (info) => { + const ipPool = ipPools.items.find((item) => item.id === info.getValue()) + return + }, + }), + ], + [ipPools] + ) + + // const makeActions = (info) => { + // return [ + // + // ] + // } + + return ( + <> +
+ + ) +} diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx index 44013b1c6..d2887bf1d 100644 --- a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx @@ -10,12 +10,17 @@ import { createColumnHelper } from '@tanstack/react-table' import { useMemo } from 'react' import { Outlet, type LoaderFunctionArgs } from 'react-router-dom' -import { apiQueryClient, usePrefetchedApiQuery, type InternetGateway } from '~/api' +import { + apiQueryClient, + type InternetGateway, + // type InternetGatewayIpAddress, + // type InternetGatewayIpPool, +} from '~/api' import { getVpcSelector, useVpcSelector } from '~/hooks/use-params' import { makeLinkCell } from '~/table/cells/LinkCell' import { Columns } from '~/table/columns/common' import { useQueryTable } from '~/table/QueryTable' -import { CreateLink } from '~/ui/lib/CreateButton' +// import { CreateLink } from '~/ui/lib/CreateButton' import { EmptyMessage } from '~/ui/lib/EmptyMessage' import { ALL_ISH } from '~/util/consts' import { pb } from '~/util/path-builder' @@ -24,23 +29,50 @@ const colHelper = createColumnHelper() VpcInternetGatewaysTab.loader = async ({ params }: LoaderFunctionArgs) => { const { project, vpc } = getVpcSelector(params) - await apiQueryClient.prefetchQuery('internetGatewayList', { - query: { project, vpc, limit: ALL_ISH }, - }) + const query = { project, vpc, limit: ALL_ISH } + await Promise.all([ + apiQueryClient.prefetchQuery('internetGatewayList', { query }), + apiQueryClient.prefetchQuery('internetGatewayIpAddressList', { query }), + apiQueryClient.prefetchQuery('internetGatewayIpPoolList', { query }), + ]) return null } export function VpcInternetGatewaysTab() { const vpcSelector = useVpcSelector() const { project, vpc } = vpcSelector - const igs = usePrefetchedApiQuery('internetGatewayList', { - query: { project, vpc, limit: ALL_ISH }, - }) + // const { data: internetGatewayIpAddresses } = usePrefetchedApiQuery( + // 'internetGatewayIpAddressList', + // { query } + // ) + // const { data: internetGatewayIpPools } = usePrefetchedApiQuery( + // 'internetGatewayIpPoolList', + // { query } + // ) const { Table } = useQueryTable('internetGatewayList', { query: { project, vpc, limit: ALL_ISH }, }) - console.log({ igs }) + // type PartitionedIpAddresses = Record + // const partitionedIpAddresses: PartitionedIpAddresses = useMemo(() => { + // if (!internetGatewayIpAddresses) return {} + // return internetGatewayIpAddresses.items.reduce((acc, ip: InternetGatewayIpAddress) => { + // acc[ip.internetGatewayId] = acc[ip.internetGatewayId] || [] + // acc[ip.internetGatewayId].push(ip) + // return acc + // }, {} as PartitionedIpAddresses) + // }, [internetGatewayIpAddresses]) + + // type PartitionedIpPools = Record + // const partitionedIpPools: PartitionedIpPools = useMemo(() => { + // if (!internetGatewayIpPools) return {} + // return internetGatewayIpPools.items.reduce((acc, ip: InternetGatewayIpPool) => { + // acc[ip.internetGatewayId] = acc[ip.internetGatewayId] || [] + // acc[ip.internetGatewayId].push(ip) + // return acc + // }, {} as PartitionedIpPools) + // }, [internetGatewayIpPools]) + const emptyState = (
- + {/* Add this back in when moving out of read-only mode */} + {/* New internet gateway - + */}
diff --git a/app/pages/system/networking/IpPoolsPage.tsx b/app/pages/system/networking/IpPoolsPage.tsx index 8084dc77f..b1ea91fb5 100644 --- a/app/pages/system/networking/IpPoolsPage.tsx +++ b/app/pages/system/networking/IpPoolsPage.tsx @@ -13,19 +13,17 @@ import { Outlet, useNavigate } from 'react-router-dom' import { apiQueryClient, useApiMutation, - useApiQuery, usePrefetchedApiQuery, type IpPool, } from '@oxide/api' import { IpGlobal16Icon, IpGlobal24Icon } from '@oxide/design-system/icons/react' import { DocsPopover } from '~/components/DocsPopover' -import { IpUtilCell } from '~/components/IpPoolUtilization' import { useQuickActions } from '~/hooks/use-quick-actions' import { confirmDelete } from '~/stores/confirm-delete' import { addToast } from '~/stores/toast' -import { SkeletonCell } from '~/table/cells/EmptyCell' import { makeLinkCell } from '~/table/cells/LinkCell' +import { UtilizationCell } from '~/table/cells/UtilizationCell' import { useColsWithActions, type MenuAction } from '~/table/columns/action-col' import { Columns } from '~/table/columns/common' import { PAGE_SIZE, useQueryTable } from '~/table/QueryTable' @@ -46,13 +44,6 @@ const EmptyState = () => ( /> ) -function UtilizationCell({ pool }: { pool: string }) { - const { data } = useApiQuery('ipPoolUtilizationView', { path: { pool } }) - - if (!data) return - return -} - const colHelper = createColumnHelper() const staticColumns = [ diff --git a/app/routes.tsx b/app/routes.tsx index f4a7d0017..18ed21560 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -66,6 +66,9 @@ import { NetworkingTab } from './pages/project/instances/instance/tabs/Networkin import { StorageTab } from './pages/project/instances/instance/tabs/StorageTab' import { InstancesPage } from './pages/project/instances/InstancesPage' import { SnapshotsPage } from './pages/project/snapshots/SnapshotsPage' +import { InternetGatewayPage } from './pages/project/vpcs/InternetGatewayPage/InternetGatewayPage' +import { InternetGatewayIpAddressesTab } from './pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpAddressesTab' +import { InternetGatewayIpPoolsTab } from './pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab' import { RouterPage } from './pages/project/vpcs/RouterPage' import { VpcFirewallRulesTab } from './pages/project/vpcs/VpcPage/tabs/VpcFirewallRulesTab' import { VpcInternetGatewaysTab } from './pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab' @@ -94,6 +97,7 @@ import { pb } from './util/path-builder' const projectCrumb: CrumbFunc = (m) => m.params.project! const instanceCrumb: CrumbFunc = (m) => m.params.instance! const vpcCrumb: CrumbFunc = (m) => m.params.vpc! +const internetGatewayCrumb: CrumbFunc = (m) => m.params.gateway! const siloCrumb: CrumbFunc = (m) => m.params.silo! const poolCrumb: CrumbFunc = (m) => m.params.pool! @@ -445,6 +449,30 @@ export const routes = createRoutesFromElements( handle={{ crumb: 'Edit Route' }} /> + + } loader={InternetGatewayPage.loader}> + } + loader={InternetGatewayIpPoolsTab.loader} + /> + } + loader={InternetGatewayIpPoolsTab.loader} + handle={{ crumb: 'IP Pools' }} + /> + } + loader={InternetGatewayIpAddressesTab.loader} + handle={{ crumb: 'IP Addresses' }} + /> + + } loader={FloatingIpsPage.loader}> { + const { data } = useApiQuery('ipPoolUtilizationView', { path: { pool } }) + + if (!data) return + return +} diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts index 6426584e9..5ed1e89cf 100644 --- a/app/util/path-builder.ts +++ b/app/util/path-builder.ts @@ -102,6 +102,10 @@ export const pb = { vpcInternetGateway: (params: VpcInternetGateway) => `${pb.vpcInternetGateways(params)}/${params.gateway}`, vpcInternetGatewaysNew: (params: Vpc) => `${vpcBase(params)}/internet-gateways-new`, + vpcInternetGatewayIpPools: (params: VpcInternetGateway) => + `${pb.vpcInternetGateway(params)}/ip-pools`, + vpcInternetGatewayIpAddresses: (params: VpcInternetGateway) => + `${pb.vpcInternetGateway(params)}/ip-addresses`, floatingIps: (params: Project) => `${projectBase(params)}/floating-ips`, floatingIpsNew: (params: Project) => `${projectBase(params)}/floating-ips-new`, diff --git a/mock-api/msw/db.ts b/mock-api/msw/db.ts index 22e9079ae..8474a88f2 100644 --- a/mock-api/msw/db.ts +++ b/mock-api/msw/db.ts @@ -214,6 +214,44 @@ export const lookup = { return subnet }, + internetGateway({ + gateway: id, + ...vpcSelector + }: PP.InternetGateway): Json { + if (!id) throw notFoundErr('no internet gateway specified') + + if (isUuid(id)) { + ensureNoParentSelectors('internet gateway', vpcSelector) + return lookupById(db.internetGateways, id) + } + + const vpc = lookup.vpc(vpcSelector) + const internetGateway = db.internetGateways.find( + (ig) => ig.vpc_id === vpc.id && ig.name === id + ) + if (!internetGateway) throw notFoundErr(`internet gateway '${id}'`) + + return internetGateway + }, + internetGatewayIpAddress({ + address: id, + ...gatewaySelector + }: PP.InternetGatewayIpAddress): Json { + if (!id) throw notFoundErr('no IP address specified') + + if (isUuid(id)) { + ensureNoParentSelectors('IP address', gatewaySelector) + return lookupById(db.internetGatewayIpAddresses, id) + } + + const gateway = lookup.internetGateway(gatewaySelector) + const ip = db.internetGatewayIpAddresses.find( + (i) => i.internet_gateway_id === gateway.id && i.name === id + ) + if (!ip) throw notFoundErr(`IP address '${id}'`) + + return ip + }, image({ image: id, project: projectId }: PP.Image): Json { if (!id) throw notFoundErr('no image specified') diff --git a/mock-api/msw/handlers.ts b/mock-api/msw/handlers.ts index 53aca842b..e0e391434 100644 --- a/mock-api/msw/handlers.ts +++ b/mock-api/msw/handlers.ts @@ -1160,6 +1160,21 @@ export const handlers = makeHandlers({ const gateways = db.internetGateways.filter((g) => g.vpc_id === vpc.id) return paginated(query, gateways) }, + internetGatewayView: ({ path, query }) => lookup.internetGateway({ ...path, ...query }), + internetGatewayIpPoolList({ query }) { + const gateway = lookup.internetGateway(query) + const pools = db.internetGatewayIpPools.filter( + (p) => p.internet_gateway_id === gateway.id + ) + return paginated(query, pools) + }, + internetGatewayIpAddressList({ query }) { + const gateway = lookup.internetGateway(query) + const addresses = db.internetGatewayIpAddresses.filter( + (a) => a.internet_gateway_id === gateway.id + ) + return paginated(query, addresses) + }, vpcRouterList({ query }) { const vpc = lookup.vpc(query) const routers = db.vpcRouters.filter((r) => r.vpc_id === vpc.id) @@ -1526,13 +1541,10 @@ export const handlers = makeHandlers({ instanceSshPublicKeyList: NotImplemented, internetGatewayIpAddressCreate: NotImplemented, internetGatewayIpAddressDelete: NotImplemented, - internetGatewayIpAddressList: NotImplemented, internetGatewayIpPoolCreate: NotImplemented, internetGatewayIpPoolDelete: NotImplemented, - internetGatewayIpPoolList: NotImplemented, internetGatewayCreate: NotImplemented, internetGatewayDelete: NotImplemented, - internetGatewayView: NotImplemented, ipPoolServiceRangeAdd: NotImplemented, ipPoolServiceRangeList: NotImplemented, ipPoolServiceRangeRemove: NotImplemented, From aa2f363a18cdfe48855140f3864f35d3870d1bcc Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Sun, 6 Oct 2024 10:20:06 -0700 Subject: [PATCH 05/63] IP Pool ID is copyable from IP Pools tab --- .../tabs/InternetGatewayIpPoolsTab.tsx | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx b/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx index 6a573bb1b..5b6d3e7fd 100644 --- a/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx +++ b/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx @@ -7,7 +7,7 @@ */ import { createColumnHelper } from '@tanstack/react-table' -import { useMemo } from 'react' +import { useCallback, useMemo } from 'react' import type { LoaderFunctionArgs } from 'react-router-dom' import { apiQueryClient, usePrefetchedApiQuery, type InternetGatewayIpPool } from '~/api' @@ -15,6 +15,7 @@ import { getInternetGatewaySelector, useInternetGatewaySelector } from '~/hooks/ import { DescriptionCell } from '~/table/cells/DescriptionCell' import { EmptyCell } from '~/table/cells/EmptyCell' import { UtilizationCell } from '~/table/cells/UtilizationCell' +import { useColsWithActions, type MenuAction } from '~/table/columns/action-col' import { Columns } from '~/table/columns/common' import { useQueryTable } from '~/table/QueryTable' import { EmptyMessage } from '~/ui/lib/EmptyMessage' @@ -79,15 +80,23 @@ export function InternetGatewayIpPoolsTab() { [ipPools] ) - // const makeActions = (info) => { - // return [ - // - // ] - // } + // The user can copy the ID of the IP Pool attached to this internet gateway + const makeActions = useCallback( + (internetGatewayIpPool: InternetGatewayIpPool): MenuAction[] => [ + { + label: 'Copy IP pool ID', + onActivate() { + window.navigator.clipboard.writeText(internetGatewayIpPool.ipPoolId) + }, + }, + ], + [] + ) + const columns = useColsWithActions(staticColumns, makeActions) return ( <> -
+
) } From 302cd1971fc6f92c1e0713f796d2ae56d9889cf2 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Sun, 6 Oct 2024 14:46:46 -0700 Subject: [PATCH 06/63] Add content to IP Addresses tab and root page --- .../InternetGatewayPage.tsx | 74 +++++++++++++++---- .../tabs/InternetGatewayIpAddressesTab.tsx | 44 +++++++++-- .../tabs/InternetGatewayIpPoolsTab.tsx | 6 +- 3 files changed, 100 insertions(+), 24 deletions(-) diff --git a/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx b/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx index 4575bcd25..e25ebc698 100644 --- a/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx +++ b/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx @@ -6,15 +6,23 @@ * Copyright Oxide Computer Company */ +import { useMemo } from 'react' import { type LoaderFunctionArgs } from 'react-router-dom' -import { Networking24Icon } from '@oxide/design-system/icons/react' +import { Networking16Icon, Networking24Icon } from '@oxide/design-system/icons/react' import { apiQueryClient, usePrefetchedApiQuery } from '~/api' +import { DocsPopover } from '~/components/DocsPopover' +import { MoreActionsMenu } from '~/components/MoreActionsMenu' import { RouteTabs, Tab } from '~/components/RouteTabs' import { getInternetGatewaySelector, useInternetGatewaySelector } from '~/hooks/use-params' +import { DescriptionCell } from '~/table/cells/DescriptionCell' +import { DateTime } from '~/ui/lib/DateTime' +import { Message } from '~/ui/lib/Message' import { PageHeader, PageTitle } from '~/ui/lib/PageHeader' +import { PropertiesTable } from '~/ui/lib/PropertiesTable' import { ALL_ISH } from '~/util/consts' +import { docLinks } from '~/util/links' import { pb } from '~/util/path-builder' InternetGatewayPage.loader = async function ({ params }: LoaderFunctionArgs) { @@ -36,27 +44,67 @@ InternetGatewayPage.loader = async function ({ params }: LoaderFunctionArgs) { export function InternetGatewayPage() { const gatewaySelector = useInternetGatewaySelector() const { project, vpc, gateway } = gatewaySelector - // const query = { project, vpc, limit: ALL_ISH } - - const { data: internetGateway } = usePrefetchedApiQuery('internetGatewayView', { + const { + data: { id, description, name, timeCreated, timeModified }, + } = usePrefetchedApiQuery('internetGatewayView', { query: { project, vpc }, path: { gateway }, }) - console.log(internetGateway) - // const { Table: IpPoolsTable } = useQueryTable('internetGatewayIpPoolsList', { - // query, - // }) - - // const { Table: IpAddressesTable } = useQueryTable('internetGatewayIpAddressList', { - // query, - // }) + const actions = useMemo( + () => [ + { + label: 'Copy ID', + onActivate() { + window.navigator.clipboard.writeText(id || '') + }, + }, + ], + [id] + ) return ( <> - }>{gateway} + }>{name} +
+ } + summary="Internet gateways … 🚨🙈🚨🙉🚨🙊🚨 … just using emojis here so we spot it more easily in the PR; this copy needs eyes 👀" + links={[docLinks.vpcs, docLinks.firewallRules]} + /> + +
+ + + + + + + + + + + + + + + + + + This is a read-only copy of this internet gateway. Use the CLI to create and + update internet gateways. More functionality for internet gateways will be + included in future releases of the Oxide console. + + } + /> + IP Pools IP Addresses diff --git a/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpAddressesTab.tsx b/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpAddressesTab.tsx index 27e5e6c9a..0017870fe 100644 --- a/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpAddressesTab.tsx +++ b/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpAddressesTab.tsx @@ -6,10 +6,17 @@ * Copyright Oxide Computer Company */ +import { createColumnHelper } from '@tanstack/react-table' +import { useMemo } from 'react' import type { LoaderFunctionArgs } from 'react-router-dom' -import { apiQueryClient } from '~/api' +import { apiQueryClient, type InternetGatewayIpAddress } from '~/api' import { getInternetGatewaySelector, useInternetGatewaySelector } from '~/hooks/use-params' +import { useColsWithActions, type MenuAction } from '~/table/columns/action-col' +import { Columns } from '~/table/columns/common' +import { useQueryTable } from '~/table/QueryTable' +import { CopyableIp } from '~/ui/lib/CopyableIp' +import { EmptyMessage } from '~/ui/lib/EmptyMessage' import { ALL_ISH } from '~/util/consts' InternetGatewayIpAddressesTab.loader = async function ({ params }: LoaderFunctionArgs) { @@ -22,10 +29,35 @@ InternetGatewayIpAddressesTab.loader = async function ({ params }: LoaderFunctio return null } +const colHelper = createColumnHelper() + export function InternetGatewayIpAddressesTab() { - const gatewaySelector = useInternetGatewaySelector() - const { project, vpc, gateway } = gatewaySelector - // const query = { project, vpc, limit: ALL_ISH } - console.log({ project, vpc, gateway }) - return <>IP Addresses stuff will go here + const { project, vpc, gateway } = useInternetGatewaySelector() + const { Table } = useQueryTable('internetGatewayIpAddressList', { + query: { project, vpc, gateway, limit: ALL_ISH }, + }) + + const emptyState = ( + + ) + + const staticColumns = useMemo( + () => [ + colHelper.accessor('name', {}), + colHelper.accessor('description', Columns.description), + colHelper.accessor('address', { + header: 'Address', + cell: (info) => , + }), + ], + [] + ) + + const makeActions = (): MenuAction[] => [] + + const columns = useColsWithActions(staticColumns, makeActions) + return
} diff --git a/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx b/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx index 5b6d3e7fd..50adae7d2 100644 --- a/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx +++ b/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx @@ -94,9 +94,5 @@ export function InternetGatewayIpPoolsTab() { ) const columns = useColsWithActions(staticColumns, makeActions) - return ( - <> -
- - ) + return
} From c038387cb8faf1cd3099464a31b685365d837238 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 7 Oct 2024 00:12:10 -0400 Subject: [PATCH 07/63] Update OMICRON_VERSION to sha with internet gateways --- OMICRON_VERSION | 2 +- app/api/__generated__/OMICRON_VERSION | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OMICRON_VERSION b/OMICRON_VERSION index d3996f17a..2ac1cc36f 100644 --- a/OMICRON_VERSION +++ b/OMICRON_VERSION @@ -1 +1 @@ -a9fd9dbbbcdaa80b822c3fc14b4f1a20ac72021d +e51641064a9aeb62b6461055505c53e43fbbe58c diff --git a/app/api/__generated__/OMICRON_VERSION b/app/api/__generated__/OMICRON_VERSION index 258888bf6..998389f18 100644 --- a/app/api/__generated__/OMICRON_VERSION +++ b/app/api/__generated__/OMICRON_VERSION @@ -1,2 +1,2 @@ # generated file. do not update manually. see docs/update-pinned-api.md -a9fd9dbbbcdaa80b822c3fc14b4f1a20ac72021d +e51641064a9aeb62b6461055505c53e43fbbe58c From d778b70de262f23b57095655c17e2de823458c44 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 7 Oct 2024 00:24:43 -0400 Subject: [PATCH 08/63] small tweaks --- .../InternetGatewayPage/InternetGatewayPage.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx b/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx index e25ebc698..4dfe30db6 100644 --- a/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx +++ b/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx @@ -77,11 +77,14 @@ export function InternetGatewayPage() { - + + + + @@ -94,13 +97,14 @@ export function InternetGatewayPage() { - This is a read-only copy of this internet gateway. Use the CLI to create and - update internet gateways. More functionality for internet gateways will be - included in future releases of the Oxide console. + This is a read-only copy of this internet gateway and its IP pools and + addresses. Use the CLI to create and update internet gateways. More + functionality for internet gateways will be included in future releases of the + Oxide console. } /> From 0a62befe1dc8cbdc7f108f0e4f26d6e64a119803 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 7 Oct 2024 00:27:40 -0400 Subject: [PATCH 09/63] remove commented-out code --- .../vpcs/VpcPage/tabs/VpcGatewaysTab.tsx | 42 +------------------ 1 file changed, 1 insertion(+), 41 deletions(-) diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx index d2887bf1d..0544f6ec8 100644 --- a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx @@ -10,17 +10,11 @@ import { createColumnHelper } from '@tanstack/react-table' import { useMemo } from 'react' import { Outlet, type LoaderFunctionArgs } from 'react-router-dom' -import { - apiQueryClient, - type InternetGateway, - // type InternetGatewayIpAddress, - // type InternetGatewayIpPool, -} from '~/api' +import { apiQueryClient, type InternetGateway } from '~/api' import { getVpcSelector, useVpcSelector } from '~/hooks/use-params' import { makeLinkCell } from '~/table/cells/LinkCell' import { Columns } from '~/table/columns/common' import { useQueryTable } from '~/table/QueryTable' -// import { CreateLink } from '~/ui/lib/CreateButton' import { EmptyMessage } from '~/ui/lib/EmptyMessage' import { ALL_ISH } from '~/util/consts' import { pb } from '~/util/path-builder' @@ -41,38 +35,10 @@ VpcInternetGatewaysTab.loader = async ({ params }: LoaderFunctionArgs) => { export function VpcInternetGatewaysTab() { const vpcSelector = useVpcSelector() const { project, vpc } = vpcSelector - // const { data: internetGatewayIpAddresses } = usePrefetchedApiQuery( - // 'internetGatewayIpAddressList', - // { query } - // ) - // const { data: internetGatewayIpPools } = usePrefetchedApiQuery( - // 'internetGatewayIpPoolList', - // { query } - // ) const { Table } = useQueryTable('internetGatewayList', { query: { project, vpc, limit: ALL_ISH }, }) - // type PartitionedIpAddresses = Record - // const partitionedIpAddresses: PartitionedIpAddresses = useMemo(() => { - // if (!internetGatewayIpAddresses) return {} - // return internetGatewayIpAddresses.items.reduce((acc, ip: InternetGatewayIpAddress) => { - // acc[ip.internetGatewayId] = acc[ip.internetGatewayId] || [] - // acc[ip.internetGatewayId].push(ip) - // return acc - // }, {} as PartitionedIpAddresses) - // }, [internetGatewayIpAddresses]) - - // type PartitionedIpPools = Record - // const partitionedIpPools: PartitionedIpPools = useMemo(() => { - // if (!internetGatewayIpPools) return {} - // return internetGatewayIpPools.items.reduce((acc, ip: InternetGatewayIpPool) => { - // acc[ip.internetGatewayId] = acc[ip.internetGatewayId] || [] - // acc[ip.internetGatewayId].push(ip) - // return acc - // }, {} as PartitionedIpPools) - // }, [internetGatewayIpPools]) - const emptyState = ( -
- {/* Add this back in when moving out of read-only mode */} - {/* - New internet gateway - */} -
From f3d8d41241edf3879dfefa644149cac173961d79 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 7 Oct 2024 00:30:43 -0400 Subject: [PATCH 10/63] Use proper truncate component --- .../project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx b/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx index 4dfe30db6..c6a3893c3 100644 --- a/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx +++ b/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx @@ -21,6 +21,7 @@ import { DateTime } from '~/ui/lib/DateTime' import { Message } from '~/ui/lib/Message' import { PageHeader, PageTitle } from '~/ui/lib/PageHeader' import { PropertiesTable } from '~/ui/lib/PropertiesTable' +import { Truncate } from '~/ui/lib/Truncate' import { ALL_ISH } from '~/util/consts' import { docLinks } from '~/util/links' import { pb } from '~/util/path-builder' @@ -83,7 +84,7 @@ export function InternetGatewayPage() { - + From bc1a1038d5ecc7ce646c08f7cd660cd5924d0de3 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 7 Oct 2024 01:20:09 -0400 Subject: [PATCH 11/63] adjust path-builder spec --- app/util/path-builder.spec.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/util/path-builder.spec.ts b/app/util/path-builder.spec.ts index 76e39846e..62933f089 100644 --- a/app/util/path-builder.spec.ts +++ b/app/util/path-builder.spec.ts @@ -12,6 +12,7 @@ import { pb } from './path-builder' // params can be the same for all of them because they only use what they need const params = { floatingIp: 'f', + gateway: 'g', project: 'p', instance: 'i', vpc: 'v', @@ -94,6 +95,11 @@ test('path builder', () => { "vpcFirewallRuleEdit": "/projects/p/vpcs/v/firewall-rules/fr/edit", "vpcFirewallRules": "/projects/p/vpcs/v/firewall-rules", "vpcFirewallRulesNew": "/projects/p/vpcs/v/firewall-rules-new", + "vpcInternetGateway": "/projects/p/vpcs/v/internet-gateways/g", + "vpcInternetGatewayIpAddresses": "/projects/p/vpcs/v/internet-gateways/g/ip-addresses", + "vpcInternetGatewayIpPools": "/projects/p/vpcs/v/internet-gateways/g/ip-pools", + "vpcInternetGateways": "/projects/p/vpcs/v/internet-gateways", + "vpcInternetGatewaysNew": "/projects/p/vpcs/v/internet-gateways-new", "vpcRouter": "/projects/p/vpcs/v/routers/r", "vpcRouterEdit": "/projects/p/vpcs/v/routers/r/edit", "vpcRouterRouteEdit": "/projects/p/vpcs/v/routers/r/routes/rr/edit", From 786393960a307552b02eca601e1163bec39a0239 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 7 Oct 2024 06:12:42 -0400 Subject: [PATCH 12/63] Add breadcrumb nav for internet gateways --- app/components/TopBarPicker.tsx | 24 ++++++++++++++++++++++++ app/layouts/ProjectLayout.tsx | 4 +++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/app/components/TopBarPicker.tsx b/app/components/TopBarPicker.tsx index 9e3c51599..323af00cd 100644 --- a/app/components/TopBarPicker.tsx +++ b/app/components/TopBarPicker.tsx @@ -17,6 +17,7 @@ import { import { useInstanceSelector, + useInternetGatewaySelector, useIpPoolSelector, useSiloSelector, useVpcRouterSelector, @@ -295,6 +296,29 @@ export function VpcRouterPicker() { ) } +/** Used when drilling down into a VPC Internet Gateway from the Silo view. */ +export function VpcGatewayPicker() { + // picker only shows up when an internet gateway is in scope + const { project, vpc, gateway } = useInternetGatewaySelector() + const { data } = useApiQuery('internetGatewayList', { + query: { project, vpc, limit: PAGE_SIZE }, + }) + const items = (data?.items || []).map((g) => ({ + label: g.name, + to: pb.vpcInternetGateway({ vpc, project, gateway: g.name }), + })) + + return ( + + ) +} + const NoProjectLogo = () => (
diff --git a/app/layouts/ProjectLayout.tsx b/app/layouts/ProjectLayout.tsx index 5c186c213..9ce5f6811 100644 --- a/app/layouts/ProjectLayout.tsx +++ b/app/layouts/ProjectLayout.tsx @@ -30,6 +30,7 @@ import { InstancePicker, ProjectPicker, SiloSystemPicker, + VpcGatewayPicker, VpcPicker, VpcRouterPicker, } from '~/components/TopBarPicker' @@ -61,7 +62,7 @@ export function ProjectLayout({ overrideContentPane }: ProjectLayoutProps) { const projectSelector = useProjectSelector() const { data: project } = usePrefetchedApiQuery('projectView', { path: projectSelector }) - const { instance, router, vpc } = useParams() + const { gateway, instance, router, vpc } = useParams() const { pathname } = useLocation() useQuickActions( useMemo( @@ -94,6 +95,7 @@ export function ProjectLayout({ overrideContentPane }: ProjectLayoutProps) { {instance && } {vpc && } {router && } + {gateway && } From 721a900ac6d847b6017859912c51564bf7103130 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 7 Oct 2024 11:52:14 -0400 Subject: [PATCH 13/63] Update default internet gateway IP pool to reflect actual default values --- .../tabs/InternetGatewayIpAddressesTab.tsx | 4 +-- mock-api/internet-gateway.ts | 28 +++++++++++++++++-- mock-api/ip-pool.ts | 19 ++++++++++++- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpAddressesTab.tsx b/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpAddressesTab.tsx index 0017870fe..fb7f60486 100644 --- a/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpAddressesTab.tsx +++ b/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpAddressesTab.tsx @@ -39,8 +39,8 @@ export function InternetGatewayIpAddressesTab() { const emptyState = ( ) diff --git a/mock-api/internet-gateway.ts b/mock-api/internet-gateway.ts index c8bb4d688..a8859e4fd 100644 --- a/mock-api/internet-gateway.ts +++ b/mock-api/internet-gateway.ts @@ -90,13 +90,33 @@ export const internetGatewayIpAddresses: Json[] = [ internetGatewayIpAddress3, ] -const [ipPool1, ipPool2, ipPool3] = ipPools +const [defaultIpPool, ipPool1, ipPool2, ipPool3] = ipPools + +const defaultInternetGatewayIpPool1: Json = { + id: '1d5e5a1f-0b2b-4d5b-8b9d-2d4b3e0c6gb9', + name: 'default', + description: 'Default internet gateway IP pool', + internet_gateway_id: internetGateway1.id, + ip_pool_id: defaultIpPool.id, + time_created, + time_modified, +} + +const defaultInternetGatewayIpPool2: Json = { + id: 'd5e5a1f1-0b2b-4d5b-8b9d-2d4b3e0c6b9c', + name: 'default', + description: 'Default internet gateway IP pool', + internet_gateway_id: internetGateway2.id, + ip_pool_id: defaultIpPool.id, + time_created, + time_modified, +} const internetGatewayIpPool1: Json = { id: 'f1d5e5a1-0b2b-4d5b-8b9d-2d4b3e0c6b9g', name: 'internet-gateway-ip-pool-1', description: 'an IP pool for an internet gateway', - internet_gateway_id: internetGateway1.id, + internet_gateway_id: internetGateway2.id, ip_pool_id: ipPool1.id, time_created, time_modified, @@ -106,7 +126,7 @@ const internetGatewayIpPool2: Json = { id: 'f1d5e5a1-0b2b-4d5b-8b9d-2d4b3e0c6b9h', name: 'internet-gateway-ip-pool-2', description: 'a set of VPN IPs in an IP pool for an internet gateway', - internet_gateway_id: internetGateway1.id, + internet_gateway_id: internetGateway2.id, ip_pool_id: ipPool2.id, time_created, time_modified, @@ -123,6 +143,8 @@ const internetGatewayIpPool3: Json = { } export const internetGatewayIpPools: Json[] = [ + defaultInternetGatewayIpPool1, + defaultInternetGatewayIpPool2, internetGatewayIpPool1, internetGatewayIpPool2, internetGatewayIpPool3, diff --git a/mock-api/ip-pool.ts b/mock-api/ip-pool.ts index 534125ff8..e7725c6c4 100644 --- a/mock-api/ip-pool.ts +++ b/mock-api/ip-pool.ts @@ -11,6 +11,14 @@ import { type IpPool, type IpPoolRange, type IpPoolSiloLink } from '@oxide/api' import type { Json } from './json-type' import { defaultSilo } from './silo' +export const defaultIpPool: Json = { + id: 'cadb8535-e32f-4be2-9259-e45dec9fa3cd', + name: 'default', + description: 'default IP pool', + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), +} + export const ipPool1: Json = { id: '69b5c583-74a9-451a-823d-0741c1ec66e2', name: 'ip-pool-1', @@ -43,7 +51,7 @@ const ipPool4: Json = { time_modified: new Date().toISOString(), } -export const ipPools: Json[] = [ipPool1, ipPool2, ipPool3, ipPool4] +export const ipPools: Json[] = [defaultIpPool, ipPool1, ipPool2, ipPool3, ipPool4] export const ipPoolSilos: Json[] = [ { @@ -59,6 +67,15 @@ export const ipPoolSilos: Json[] = [ ] export const ipPoolRanges: Json = [ + { + id: 'f6b3b9b5-7e3d-4b8c-9f6b-9b7b5e3d8c4b', + ip_pool_id: defaultIpPool.id, + range: { + first: '170.20.26.11', + last: '170.20.26.254', + }, + time_created: new Date().toISOString(), + }, { id: 'bbfcf3f2-061e-4334-a0e7-dfcd8171f87e', ip_pool_id: ipPool1.id, From b72ae0e816f8990a471350e280458463b8741061 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 7 Oct 2024 12:11:50 -0400 Subject: [PATCH 14/63] update unrelated test --- test/e2e/ip-pools.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/ip-pools.e2e.ts b/test/e2e/ip-pools.e2e.ts index df0de16b0..031090b7b 100644 --- a/test/e2e/ip-pools.e2e.ts +++ b/test/e2e/ip-pools.e2e.ts @@ -19,7 +19,7 @@ test('IP pool list', async ({ page }) => { const table = page.getByRole('table') - await expect(table.getByRole('row')).toHaveCount(5) // header + 4 rows + await expect(table.getByRole('row')).toHaveCount(6) // header + 5 rows await expectRowVisible(table, { name: 'ip-pool-1', Utilization: '6 / 24' }) await expectRowVisible(table, { From cdbd4a2ad33f4c4edf2de62d540ff2853538c751 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 9 Oct 2024 12:15:13 -0400 Subject: [PATCH 15/63] Remove n+1 query on IpPools --- .../project/floating-ips/FloatingIpsPage.tsx | 50 ++++++++----------- .../tabs/InternetGatewayIpPoolsTab.tsx | 25 ++-------- 2 files changed, 23 insertions(+), 52 deletions(-) diff --git a/app/pages/project/floating-ips/FloatingIpsPage.tsx b/app/pages/project/floating-ips/FloatingIpsPage.tsx index a8e35941a..35874c4ca 100644 --- a/app/pages/project/floating-ips/FloatingIpsPage.tsx +++ b/app/pages/project/floating-ips/FloatingIpsPage.tsx @@ -13,7 +13,6 @@ import { Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router-dom' import { apiQueryClient, useApiMutation, - useApiQuery, useApiQueryClient, usePrefetchedApiQuery, type FloatingIp, @@ -28,8 +27,8 @@ import { getProjectSelector, useProjectSelector } from '~/hooks/use-params' import { confirmAction } from '~/stores/confirm-action' import { confirmDelete } from '~/stores/confirm-delete' import { addToast } from '~/stores/toast' -import { EmptyCell } from '~/table/cells/EmptyCell' import { InstanceLinkCell } from '~/table/cells/InstanceLinkCell' +import { IpPoolCell } from '~/table/cells/IpPoolCell' import { useColsWithActions, type MenuAction } from '~/table/columns/action-col' import { Columns } from '~/table/columns/common' import { PAGE_SIZE, useQueryTable } from '~/table/QueryTable' @@ -39,7 +38,6 @@ import { Message } from '~/ui/lib/Message' import { Modal } from '~/ui/lib/Modal' import { PageHeader, PageTitle } from '~/ui/lib/PageHeader' import { TableActions } from '~/ui/lib/Table' -import { Tooltip } from '~/ui/lib/Tooltip' import { ALL_ISH } from '~/util/consts' import { docLinks } from '~/util/links' import { pb } from '~/util/path-builder' @@ -78,34 +76,7 @@ FloatingIpsPage.loader = async ({ params }: LoaderFunctionArgs) => { return null } -const IpPoolCell = ({ ipPoolId }: { ipPoolId: string }) => { - const pool = useApiQuery('projectIpPoolView', { path: { pool: ipPoolId } }).data - if (!pool) return - return pool.description ? ( - - {pool.name} - - ) : ( - <>{pool.name} - ) -} - const colHelper = createColumnHelper() -const staticCols = [ - colHelper.accessor('name', {}), - colHelper.accessor('description', Columns.description), - colHelper.accessor('ip', { - header: 'IP address', - }), - colHelper.accessor('ipPoolId', { - cell: (info) => , - header: 'IP pool', - }), - colHelper.accessor('instanceId', { - cell: (info) => , - header: 'Attached to instance', - }), -] export function FloatingIpsPage() { const [floatingIpToModify, setFloatingIpToModify] = useState(null) @@ -114,8 +85,27 @@ export function FloatingIpsPage() { const { data: instances } = usePrefetchedApiQuery('instanceList', { query: { project }, }) + const { data: ipPools } = usePrefetchedApiQuery('projectIpPoolList', { + query: { limit: ALL_ISH }, + }) const navigate = useNavigate() + const staticCols = [ + colHelper.accessor('name', {}), + colHelper.accessor('description', Columns.description), + colHelper.accessor('ip', { + header: 'IP address', + }), + colHelper.accessor('ipPoolId', { + cell: (info) => , + header: 'IP pool', + }), + colHelper.accessor('instanceId', { + cell: (info) => , + header: 'Attached to instance', + }), + ] + const { mutateAsync: floatingIpDetach } = useApiMutation('floatingIpDetach', { onSuccess() { queryClient.invalidateQueries('floatingIpList') diff --git a/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx b/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx index 50adae7d2..0c97d9c17 100644 --- a/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx +++ b/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx @@ -12,9 +12,7 @@ import type { LoaderFunctionArgs } from 'react-router-dom' import { apiQueryClient, usePrefetchedApiQuery, type InternetGatewayIpPool } from '~/api' import { getInternetGatewaySelector, useInternetGatewaySelector } from '~/hooks/use-params' -import { DescriptionCell } from '~/table/cells/DescriptionCell' -import { EmptyCell } from '~/table/cells/EmptyCell' -import { UtilizationCell } from '~/table/cells/UtilizationCell' +import { IpPoolCell } from '~/table/cells/IpPoolCell' import { useColsWithActions, type MenuAction } from '~/table/columns/action-col' import { Columns } from '~/table/columns/common' import { useQueryTable } from '~/table/QueryTable' @@ -56,25 +54,8 @@ export function InternetGatewayIpPoolsTab() { colHelper.accessor('name', {}), colHelper.accessor('description', Columns.description), colHelper.accessor('ipPoolId', { - header: 'IP Pool Name', - cell: (info) => { - const ipPool = ipPools.items.find((item) => item.id === info.getValue()) - return ipPool?.name || - }, - }), - colHelper.accessor('ipPoolId', { - header: 'IP Pool Description', - cell: (info) => { - const ipPool = ipPools.items.find((item) => item.id === info.getValue()) - return - }, - }), - colHelper.accessor('ipPoolId', { - header: 'IP Pool Utilization', - cell: (info) => { - const ipPool = ipPools.items.find((item) => item.id === info.getValue()) - return - }, + header: 'IP Pool', + cell: (info) => , }), ], [ipPools] From a53479be846fa06571bea31ffad0532e1daddd9f Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 9 Oct 2024 12:20:54 -0400 Subject: [PATCH 16/63] Add IpPoolCell --- app/table/cells/IpPoolCell.tsx | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 app/table/cells/IpPoolCell.tsx diff --git a/app/table/cells/IpPoolCell.tsx b/app/table/cells/IpPoolCell.tsx new file mode 100644 index 000000000..cbf2a899c --- /dev/null +++ b/app/table/cells/IpPoolCell.tsx @@ -0,0 +1,29 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { type IpPool } from '~/api' +import { Tooltip } from '~/ui/lib/Tooltip' + +import { EmptyCell } from './EmptyCell' + +export const IpPoolCell = ({ + ipPoolId, + ipPools, +}: { + ipPoolId: string + ipPools: IpPool[] +}) => { + const pool = ipPools.find((item) => item.id === ipPoolId) + if (!pool) return + return pool.description ? ( + + {pool.name} + + ) : ( + <>{pool.name} + ) +} From 55e49bccdf9511c86fbdad2409a9abd113c6d280 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 9 Oct 2024 18:13:02 -0400 Subject: [PATCH 17/63] Remove code that we'll add separately --- app/components/ErrorBoundary.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/components/ErrorBoundary.tsx b/app/components/ErrorBoundary.tsx index 52db88e5c..91085e08f 100644 --- a/app/components/ErrorBoundary.tsx +++ b/app/components/ErrorBoundary.tsx @@ -38,6 +38,5 @@ export const ErrorBoundary = (props: { children: React.ReactNode }) => ( export function RouterDataErrorBoundary() { // TODO: validate this unknown at runtime _before_ passing to ErrorFallback const error = useRouteError() as Props['error'] - console.error(error) return } From 89a6eabbdd0b77a5d2bc8a2a6b5c3f0a8262c24b Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 9 Oct 2024 18:13:19 -0400 Subject: [PATCH 18/63] Upgrade OMICRON_VERSION --- OMICRON_VERSION | 2 +- app/api/__generated__/Api.ts | 23 +++++++++++------------ app/api/__generated__/OMICRON_VERSION | 2 +- app/api/__generated__/validate.ts | 11 +++-------- 4 files changed, 16 insertions(+), 22 deletions(-) diff --git a/OMICRON_VERSION b/OMICRON_VERSION index 2ac1cc36f..cb340659d 100644 --- a/OMICRON_VERSION +++ b/OMICRON_VERSION @@ -1 +1 @@ -e51641064a9aeb62b6461055505c53e43fbbe58c +0640bb277df110f3e740464bf4bacdf2bd24c897 diff --git a/app/api/__generated__/Api.ts b/app/api/__generated__/Api.ts index 5ac00ea45..02a553344 100644 --- a/app/api/__generated__/Api.ts +++ b/app/api/__generated__/Api.ts @@ -2039,7 +2039,6 @@ export type InternetGatewayIpAddress = { export type InternetGatewayIpAddressCreate = { address: string description: string - gateway: NameOrId name: Name } @@ -2730,7 +2729,7 @@ export type Route = { /** The route gateway. */ gw: string /** Local preference for route. Higher preference indictes precedence within and across protocols. */ - localPref?: number + ribPriority?: number /** VLAN id the gateway is reachable over. */ vid?: number } @@ -2807,7 +2806,7 @@ export type RouterRouteKind = export type RouterRoute = { /** human-readable free-form text about a resource */ description: string - /** Selects which traffic this routing rule will apply to. */ + /** Selects which traffic this routing rule will apply to */ destination: RouteDestination /** unique, immutable, system-controlled identifier for each resource */ id: string @@ -2815,7 +2814,7 @@ export type RouterRoute = { kind: RouterRouteKind /** unique, mutable, user-controlled identifier for each resource */ name: Name - /** The location that matched packets should be forwarded to. */ + /** The location that matched packets should be forwarded to */ target: RouteTarget /** timestamp when this resource was created */ timeCreated: Date @@ -3517,10 +3516,10 @@ export type SwitchPortRouteConfig = { gw: IpNet /** The interface name this route configuration is assigned to. */ interfaceName: string - /** Local preference indicating priority within and across protocols. */ - localPref?: number /** The port settings object this route configuration belongs to. */ portSettingsId: string + /** RIB Priority indicating priority within and across protocols. */ + ribPriority?: number /** The VLAN identifier for the route. Use this if the gateway is reachable over an 802.1Q tagged L2 segment. */ vlanId?: number } @@ -6206,7 +6205,7 @@ export class Api extends HttpClient { }) }, /** - * List addresses attached to an internet gateway. + * List IP addresses attached to internet gateway */ internetGatewayIpAddressList: ( { query = {} }: { query?: InternetGatewayIpAddressListQueryParams }, @@ -6220,7 +6219,7 @@ export class Api extends HttpClient { }) }, /** - * Attach ip pool to internet gateway + * Attach IP address to internet gateway */ internetGatewayIpAddressCreate: ( { @@ -6241,7 +6240,7 @@ export class Api extends HttpClient { }) }, /** - * Detach ip pool from internet gateway + * Detach IP address from internet gateway */ internetGatewayIpAddressDelete: ( { @@ -6261,7 +6260,7 @@ export class Api extends HttpClient { }) }, /** - * List IP pools attached to an internet gateway. + * List IP pools attached to internet gateway */ internetGatewayIpPoolList: ( { query = {} }: { query?: InternetGatewayIpPoolListQueryParams }, @@ -6275,7 +6274,7 @@ export class Api extends HttpClient { }) }, /** - * Attach ip pool to internet gateway + * Attach IP pool to internet gateway */ internetGatewayIpPoolCreate: ( { @@ -6296,7 +6295,7 @@ export class Api extends HttpClient { }) }, /** - * Detach ip pool from internet gateway + * Detach IP pool from internet gateway */ internetGatewayIpPoolDelete: ( { diff --git a/app/api/__generated__/OMICRON_VERSION b/app/api/__generated__/OMICRON_VERSION index 998389f18..7278501cd 100644 --- a/app/api/__generated__/OMICRON_VERSION +++ b/app/api/__generated__/OMICRON_VERSION @@ -1,2 +1,2 @@ # generated file. do not update manually. see docs/update-pinned-api.md -e51641064a9aeb62b6461055505c53e43fbbe58c +0640bb277df110f3e740464bf4bacdf2bd24c897 diff --git a/app/api/__generated__/validate.ts b/app/api/__generated__/validate.ts index b45ccf9a5..1fea712f6 100644 --- a/app/api/__generated__/validate.ts +++ b/app/api/__generated__/validate.ts @@ -1903,12 +1903,7 @@ export const InternetGatewayIpAddress = z.preprocess( */ export const InternetGatewayIpAddressCreate = z.preprocess( processResponseBody, - z.object({ - address: z.string().ip(), - description: z.string(), - gateway: NameOrId, - name: Name, - }) + z.object({ address: z.string().ip(), description: z.string(), name: Name }) ) /** @@ -2587,7 +2582,7 @@ export const Route = z.preprocess( z.object({ dst: IpNet, gw: z.string().ip(), - localPref: z.number().min(0).max(4294967295).optional(), + ribPriority: z.number().min(0).max(255).optional(), vid: z.number().min(0).max(65535).optional(), }) ) @@ -3242,8 +3237,8 @@ export const SwitchPortRouteConfig = z.preprocess( dst: IpNet, gw: IpNet, interfaceName: z.string(), - localPref: z.number().min(0).max(4294967295).optional(), portSettingsId: z.string().uuid(), + ribPriority: z.number().min(0).max(255).optional(), vlanId: z.number().min(0).max(65535).optional(), }) ) From 39c8ca0ac0725408e03c8dd23133a425ea5cf7ea Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 9 Oct 2024 18:22:41 -0400 Subject: [PATCH 19/63] No need to extract UtilizationCell now --- app/pages/system/networking/IpPoolsPage.tsx | 11 ++++++++++- app/table/cells/UtilizationCell.tsx | 17 ----------------- 2 files changed, 10 insertions(+), 18 deletions(-) delete mode 100644 app/table/cells/UtilizationCell.tsx diff --git a/app/pages/system/networking/IpPoolsPage.tsx b/app/pages/system/networking/IpPoolsPage.tsx index b1ea91fb5..8084dc77f 100644 --- a/app/pages/system/networking/IpPoolsPage.tsx +++ b/app/pages/system/networking/IpPoolsPage.tsx @@ -13,17 +13,19 @@ import { Outlet, useNavigate } from 'react-router-dom' import { apiQueryClient, useApiMutation, + useApiQuery, usePrefetchedApiQuery, type IpPool, } from '@oxide/api' import { IpGlobal16Icon, IpGlobal24Icon } from '@oxide/design-system/icons/react' import { DocsPopover } from '~/components/DocsPopover' +import { IpUtilCell } from '~/components/IpPoolUtilization' import { useQuickActions } from '~/hooks/use-quick-actions' import { confirmDelete } from '~/stores/confirm-delete' import { addToast } from '~/stores/toast' +import { SkeletonCell } from '~/table/cells/EmptyCell' import { makeLinkCell } from '~/table/cells/LinkCell' -import { UtilizationCell } from '~/table/cells/UtilizationCell' import { useColsWithActions, type MenuAction } from '~/table/columns/action-col' import { Columns } from '~/table/columns/common' import { PAGE_SIZE, useQueryTable } from '~/table/QueryTable' @@ -44,6 +46,13 @@ const EmptyState = () => ( /> ) +function UtilizationCell({ pool }: { pool: string }) { + const { data } = useApiQuery('ipPoolUtilizationView', { path: { pool } }) + + if (!data) return + return +} + const colHelper = createColumnHelper() const staticColumns = [ diff --git a/app/table/cells/UtilizationCell.tsx b/app/table/cells/UtilizationCell.tsx deleted file mode 100644 index 6b594af53..000000000 --- a/app/table/cells/UtilizationCell.tsx +++ /dev/null @@ -1,17 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -import { useApiQuery } from '~/api' -import { IpUtilCell } from '~/components/IpPoolUtilization' -import { EmptyCell } from '~/table/cells/EmptyCell' - -export const UtilizationCell = ({ pool }: { pool: string }) => { - const { data } = useApiQuery('ipPoolUtilizationView', { path: { pool } }) - - if (!data) return - return -} From 6f5e7ffe68c51bf4ef36efeec848c28dfaafe6c7 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 9 Oct 2024 23:41:32 -0400 Subject: [PATCH 20/63] Get IP Pool tab for Internet Gateways working --- app/components/ErrorBoundary.tsx | 1 + .../tabs/InternetGatewayIpPoolsTab.tsx | 43 +++++++++++-------- mock-api/ip-pool.ts | 7 ++- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/app/components/ErrorBoundary.tsx b/app/components/ErrorBoundary.tsx index 91085e08f..52db88e5c 100644 --- a/app/components/ErrorBoundary.tsx +++ b/app/components/ErrorBoundary.tsx @@ -38,5 +38,6 @@ export const ErrorBoundary = (props: { children: React.ReactNode }) => ( export function RouterDataErrorBoundary() { // TODO: validate this unknown at runtime _before_ passing to ErrorFallback const error = useRouteError() as Props['error'] + console.error(error) return } diff --git a/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx b/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx index 0c97d9c17..609149221 100644 --- a/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx +++ b/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx @@ -7,10 +7,10 @@ */ import { createColumnHelper } from '@tanstack/react-table' -import { useCallback, useMemo } from 'react' +import { useCallback } from 'react' import type { LoaderFunctionArgs } from 'react-router-dom' -import { apiQueryClient, usePrefetchedApiQuery, type InternetGatewayIpPool } from '~/api' +import { apiQueryClient, type InternetGatewayIpPool } from '~/api' import { getInternetGatewaySelector, useInternetGatewaySelector } from '~/hooks/use-params' import { IpPoolCell } from '~/table/cells/IpPoolCell' import { useColsWithActions, type MenuAction } from '~/table/columns/action-col' @@ -25,8 +25,21 @@ InternetGatewayIpPoolsTab.loader = async function ({ params }: LoaderFunctionArg apiQueryClient.prefetchQuery('internetGatewayIpPoolList', { query: { project, vpc, gateway, limit: ALL_ISH }, }), - // get IP Pools - apiQueryClient.prefetchQuery('ipPoolList', { query: { limit: ALL_ISH } }), + // fetch IP Pools and preload into RQ cache so fetches by ID in + // IpPoolCell can be mostly instant yet gracefully fall back to + // fetching individually if we don't fetch them all here + apiQueryClient + .fetchQuery('projectIpPoolList', { query: { limit: ALL_ISH } }) + .then((pools) => { + console.log({ pools }) + for (const pool of pools.items) { + apiQueryClient.setQueryData( + 'projectIpPoolView', + { path: { pool: pool.id } }, + pool + ) + } + }), ]) return null } @@ -38,9 +51,6 @@ export function InternetGatewayIpPoolsTab() { const { Table } = useQueryTable('internetGatewayIpPoolList', { query: { project, vpc, gateway, limit: ALL_ISH }, }) - const { data: ipPools } = usePrefetchedApiQuery('ipPoolList', { - query: { limit: ALL_ISH }, - }) const emptyState = ( ) - const staticColumns = useMemo( - () => [ - colHelper.accessor('name', {}), - colHelper.accessor('description', Columns.description), - colHelper.accessor('ipPoolId', { - header: 'IP Pool', - cell: (info) => , - }), - ], - [ipPools] - ) + const staticColumns = [ + colHelper.accessor('name', {}), + colHelper.accessor('description', Columns.description), + colHelper.accessor('ipPoolId', { + header: 'IP Pool', + cell: (info) => , + }), + ] // The user can copy the ID of the IP Pool attached to this internet gateway const makeActions = useCallback( diff --git a/mock-api/ip-pool.ts b/mock-api/ip-pool.ts index e7725c6c4..d1ac26dd4 100644 --- a/mock-api/ip-pool.ts +++ b/mock-api/ip-pool.ts @@ -55,10 +55,15 @@ export const ipPools: Json[] = [defaultIpPool, ipPool1, ipPool2, ipPool3 export const ipPoolSilos: Json[] = [ { - ip_pool_id: ipPool1.id, + ip_pool_id: defaultIpPool.id, silo_id: defaultSilo.id, is_default: true, }, + { + ip_pool_id: ipPool1.id, + silo_id: defaultSilo.id, + is_default: false, + }, { ip_pool_id: ipPool2.id, silo_id: defaultSilo.id, From 1b63138275580b7876d5ac2f102b146f75d0b765 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 19 Nov 2024 15:35:33 -0800 Subject: [PATCH 21/63] Update snapshot, but there might still be some other issues to work out --- .../__snapshots__/path-builder.spec.ts.snap | 109 ++++++++++++++++++ app/util/path-builder.spec.ts | 1 + 2 files changed, 110 insertions(+) diff --git a/app/util/__snapshots__/path-builder.spec.ts.snap b/app/util/__snapshots__/path-builder.spec.ts.snap index a4c74b445..a7ac8eaf9 100644 --- a/app/util/__snapshots__/path-builder.spec.ts.snap +++ b/app/util/__snapshots__/path-builder.spec.ts.snap @@ -709,6 +709,115 @@ exports[`breadcrumbs 2`] = ` "path": "/projects/p/vpcs/v/", }, ], + "vpcInternetGateway (/projects/p/vpcs/v/internet-gateways/g)": [ + { + "label": "Projects", + "path": "/projects", + }, + { + "label": "p", + "path": "/projects/p/instances", + }, + { + "label": "VPCs", + "path": "/projects/p/vpcs", + }, + { + "label": "v", + "path": "/projects/p/vpcs/v", + }, + { + "label": "Internet Gateways", + "path": "/projects/p/vpcs/v/internet-gateways", + }, + { + "label": "g", + "path": "/projects/p/vpcs/v/internet-gateways/g", + }, + ], + "vpcInternetGatewayIpAddresses (/projects/p/vpcs/v/internet-gateways/g/ip-addresses)": [ + { + "label": "Projects", + "path": "/projects", + }, + { + "label": "p", + "path": "/projects/p/instances", + }, + { + "label": "VPCs", + "path": "/projects/p/vpcs", + }, + { + "label": "v", + "path": "/projects/p/vpcs/v", + }, + { + "label": "Internet Gateways", + "path": "/projects/p/vpcs/v/internet-gateways", + }, + { + "label": "g", + "path": "/projects/p/vpcs/v/internet-gateways/g", + }, + { + "label": "IP Addresses", + "path": "/projects/p/vpcs/v/internet-gateways/g/ip-addresses", + }, + ], + "vpcInternetGatewayIpPools (/projects/p/vpcs/v/internet-gateways/g/ip-pools)": [ + { + "label": "Projects", + "path": "/projects", + }, + { + "label": "p", + "path": "/projects/p/instances", + }, + { + "label": "VPCs", + "path": "/projects/p/vpcs", + }, + { + "label": "v", + "path": "/projects/p/vpcs/v", + }, + { + "label": "Internet Gateways", + "path": "/projects/p/vpcs/v/internet-gateways", + }, + { + "label": "g", + "path": "/projects/p/vpcs/v/internet-gateways/g", + }, + { + "label": "IP Pools", + "path": "/projects/p/vpcs/v/internet-gateways/g/ip-pools", + }, + ], + "vpcInternetGateways (/projects/p/vpcs/v/internet-gateways)": [ + { + "label": "Projects", + "path": "/projects", + }, + { + "label": "p", + "path": "/projects/p/instances", + }, + { + "label": "VPCs", + "path": "/projects/p/vpcs", + }, + { + "label": "v", + "path": "/projects/p/vpcs/v/firewall-rules", + }, + { + "label": "Internet gateways", + "path": "/projects/p/vpcs/v/internet-gateways", + }, + ], + "vpcInternetGatewaysNew (/projects/p/vpcs/v/internet-gateways-new)": [], "vpcRouter (/projects/p/vpcs/v/routers/r)": [ { "label": "Projects", diff --git a/app/util/path-builder.spec.ts b/app/util/path-builder.spec.ts index f1a922134..ccf703055 100644 --- a/app/util/path-builder.spec.ts +++ b/app/util/path-builder.spec.ts @@ -141,6 +141,7 @@ test('breadcrumbs', () => { .map(([key]) => key) expect(zeroCrumbKeys).toMatchInlineSnapshot(` [ + "vpcInternetGatewaysNew (/projects/p/vpcs/v/internet-gateways-new)", "deviceSuccess (/device/success)", ] `) From 1caf6cd17a36b31664c1b49b419943ae63ee626a Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 19 Nov 2024 16:19:12 -0800 Subject: [PATCH 22/63] Some headway, but screen is still blank --- app/routes.tsx | 66 ++++++++++++++++++-------------------------------- 1 file changed, 24 insertions(+), 42 deletions(-) diff --git a/app/routes.tsx b/app/routes.tsx index 7b3b36b4f..ef4637518 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -383,22 +383,11 @@ export const routes = createRoutesFromElements( /> } loader={VpcInternetGatewaysTab.loader} - > - - - pb.vpcInternetGateways(getVpcSelector(p)) - )} - /> - - + handle={{ crumb: 'Internet gateways' }} + /> @@ -425,40 +414,33 @@ export const routes = createRoutesFromElements( - pb.vpcInternetGateways(getVpcSelector(p)) + element={} + loader={InternetGatewayPage.loader} + handle={makeCrumb( + (p) => p.gateway!, + (p) => pb.vpcInternetGateway(getInternetGatewaySelector(p)) )} > } - loader={InternetGatewayPage.loader} - handle={makeCrumb( - (p) => p.gateway!, - (p) => pb.vpcInternetGateway(getInternetGatewaySelector(p)) - )} - > - } - loader={InternetGatewayIpPoolsTab.loader} - /> - } - loader={InternetGatewayIpPoolsTab.loader} - handle={{ crumb: 'IP Pools' }} - /> - } - loader={InternetGatewayIpAddressesTab.loader} - handle={{ crumb: 'IP Addresses' }} - /> - + index + element={} + // loader={InternetGatewayIpPoolsTab.loader} + /> + } + loader={InternetGatewayIpPoolsTab.loader} + handle={{ crumb: 'IP Pools' }} + /> + } + loader={InternetGatewayIpAddressesTab.loader} + handle={{ crumb: 'IP Addresses' }} + /> - } loader={FloatingIpsPage.loader} From 0c64c545a4e38c346a7d4fd7d42b61da3fc817d8 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 20 Nov 2024 11:30:23 -0800 Subject: [PATCH 23/63] Update snapshots for test --- app/routes.tsx | 2 +- app/util/__snapshots__/path-builder.spec.ts.snap | 14 +------------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/app/routes.tsx b/app/routes.tsx index ef4637518..6abe48772 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -386,7 +386,7 @@ export const routes = createRoutesFromElements( path="internet-gateways" element={} loader={VpcInternetGatewaysTab.loader} - handle={{ crumb: 'Internet gateways' }} + handle={{ crumb: 'Internet Gateways' }} /> diff --git a/app/util/__snapshots__/path-builder.spec.ts.snap b/app/util/__snapshots__/path-builder.spec.ts.snap index a7ac8eaf9..19cf96e23 100644 --- a/app/util/__snapshots__/path-builder.spec.ts.snap +++ b/app/util/__snapshots__/path-builder.spec.ts.snap @@ -726,10 +726,6 @@ exports[`breadcrumbs 2`] = ` "label": "v", "path": "/projects/p/vpcs/v", }, - { - "label": "Internet Gateways", - "path": "/projects/p/vpcs/v/internet-gateways", - }, { "label": "g", "path": "/projects/p/vpcs/v/internet-gateways/g", @@ -752,10 +748,6 @@ exports[`breadcrumbs 2`] = ` "label": "v", "path": "/projects/p/vpcs/v", }, - { - "label": "Internet Gateways", - "path": "/projects/p/vpcs/v/internet-gateways", - }, { "label": "g", "path": "/projects/p/vpcs/v/internet-gateways/g", @@ -782,10 +774,6 @@ exports[`breadcrumbs 2`] = ` "label": "v", "path": "/projects/p/vpcs/v", }, - { - "label": "Internet Gateways", - "path": "/projects/p/vpcs/v/internet-gateways", - }, { "label": "g", "path": "/projects/p/vpcs/v/internet-gateways/g", @@ -813,7 +801,7 @@ exports[`breadcrumbs 2`] = ` "path": "/projects/p/vpcs/v/firewall-rules", }, { - "label": "Internet gateways", + "label": "Internet Gateways", "path": "/projects/p/vpcs/v/internet-gateways", }, ], From 1b9fecc3d91e2fbbf390121f6d7f5d19a9940f90 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 20 Nov 2024 12:27:46 -0800 Subject: [PATCH 24/63] Update routes; fix mock data --- app/routes.tsx | 46 ++++++++++--------- .../__snapshots__/path-builder.spec.ts.snap | 12 +++++ mock-api/internet-gateway.ts | 13 +----- 3 files changed, 37 insertions(+), 34 deletions(-) diff --git a/app/routes.tsx b/app/routes.tsx index 6abe48772..1b6cbb0dd 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -413,31 +413,33 @@ export const routes = createRoutesFromElements( } + path="internet-gateways" + handle={{ crumb: 'Internet Gateways' }} loader={InternetGatewayPage.loader} - handle={makeCrumb( - (p) => p.gateway!, - (p) => pb.vpcInternetGateway(getInternetGatewaySelector(p)) - )} > } - // loader={InternetGatewayIpPoolsTab.loader} - /> - } - loader={InternetGatewayIpPoolsTab.loader} - handle={{ crumb: 'IP Pools' }} - /> - } - loader={InternetGatewayIpAddressesTab.loader} - handle={{ crumb: 'IP Addresses' }} - /> + path=":gateway" + element={} + loader={InternetGatewayPage.loader} + handle={makeCrumb( + (p) => p.gateway!, + (p) => pb.vpcInternetGateway(getInternetGatewaySelector(p)) + )} + > + } /> + } + loader={InternetGatewayIpPoolsTab.loader} + handle={{ crumb: 'IP Pools' }} + /> + } + loader={InternetGatewayIpAddressesTab.loader} + handle={{ crumb: 'IP Addresses' }} + /> + diff --git a/app/util/__snapshots__/path-builder.spec.ts.snap b/app/util/__snapshots__/path-builder.spec.ts.snap index 19cf96e23..540b8765b 100644 --- a/app/util/__snapshots__/path-builder.spec.ts.snap +++ b/app/util/__snapshots__/path-builder.spec.ts.snap @@ -726,6 +726,10 @@ exports[`breadcrumbs 2`] = ` "label": "v", "path": "/projects/p/vpcs/v", }, + { + "label": "Internet Gateways", + "path": "/projects/p/vpcs/v/internet-gateways", + }, { "label": "g", "path": "/projects/p/vpcs/v/internet-gateways/g", @@ -748,6 +752,10 @@ exports[`breadcrumbs 2`] = ` "label": "v", "path": "/projects/p/vpcs/v", }, + { + "label": "Internet Gateways", + "path": "/projects/p/vpcs/v/internet-gateways", + }, { "label": "g", "path": "/projects/p/vpcs/v/internet-gateways/g", @@ -774,6 +782,10 @@ exports[`breadcrumbs 2`] = ` "label": "v", "path": "/projects/p/vpcs/v", }, + { + "label": "Internet Gateways", + "path": "/projects/p/vpcs/v/internet-gateways", + }, { "label": "g", "path": "/projects/p/vpcs/v/internet-gateways/g", diff --git a/mock-api/internet-gateway.ts b/mock-api/internet-gateway.ts index a8859e4fd..239da4ec3 100644 --- a/mock-api/internet-gateway.ts +++ b/mock-api/internet-gateway.ts @@ -90,7 +90,7 @@ export const internetGatewayIpAddresses: Json[] = [ internetGatewayIpAddress3, ] -const [defaultIpPool, ipPool1, ipPool2, ipPool3] = ipPools +const [defaultIpPool, ipPool1, ipPool2] = ipPools const defaultInternetGatewayIpPool1: Json = { id: '1d5e5a1f-0b2b-4d5b-8b9d-2d4b3e0c6gb9', @@ -132,20 +132,9 @@ const internetGatewayIpPool2: Json = { time_modified, } -const internetGatewayIpPool3: Json = { - id: 'f1d5e5a1-0b2b-4d5b-8b9d-2d4b3e0c6b9i', - name: 'internet-gateway-ip-pool-3', - description: 'another IP pool for an internet gateway', - internet_gateway_id: internetGateway2.id, - ip_pool_id: ipPool3.id, - time_created, - time_modified, -} - export const internetGatewayIpPools: Json[] = [ defaultInternetGatewayIpPool1, defaultInternetGatewayIpPool2, internetGatewayIpPool1, internetGatewayIpPool2, - internetGatewayIpPool3, ] From 38ef9c117d0756b20456a72bd6ba925b81bf7a0d Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 20 Nov 2024 13:03:40 -0800 Subject: [PATCH 25/63] update tests with mock data, but this should probably get pulled to a new branch --- test/e2e/instance-create.e2e.ts | 2 +- test/e2e/ip-pools.e2e.ts | 8 ++++---- test/e2e/silos.e2e.ts | 12 +++++++----- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/test/e2e/instance-create.e2e.ts b/test/e2e/instance-create.e2e.ts index 4e0a5a6b8..fb90ebd72 100644 --- a/test/e2e/instance-create.e2e.ts +++ b/test/e2e/instance-create.e2e.ts @@ -475,7 +475,7 @@ test('attaches a floating IP; disables button when no IPs available', async ({ p // ensure External IPs table has rows for the Ephemeral IP and the Floating IP await expectRowVisible(page.getByRole('table'), { - ip: '123.4.56.0', + ip: '170.20.26.11', Kind: 'ephemeral', name: '—', }) diff --git a/test/e2e/ip-pools.e2e.ts b/test/e2e/ip-pools.e2e.ts index 9221d1d29..2de5e1ff5 100644 --- a/test/e2e/ip-pools.e2e.ts +++ b/test/e2e/ip-pools.e2e.ts @@ -54,13 +54,13 @@ test.describe('german locale', () => { test('IP pool silo list', async ({ page }) => { await page.goto('/system/networking/ip-pools') - await page.getByRole('link', { name: 'ip-pool-1' }).click() - await expect(page).toHaveTitle('ip-pool-1 / IP Pools / Oxide Console') + await page.getByRole('link', { name: 'default' }).click() + await expect(page).toHaveTitle('default / IP Pools / Oxide Console') await page.getByRole('tab', { name: 'Linked silos' }).click() // this is here because waiting for the `tab` query param to show up avoids // flake after the goBack bit below - await expect(page).toHaveURL('/system/networking/ip-pools/ip-pool-1?tab=silos') + await expect(page).toHaveURL('/system/networking/ip-pools/default?tab=silos') const table = page.getByRole('table') await expectRowVisible(table, { Silo: 'maze-war', 'Pool is silo default': 'default' }) @@ -79,7 +79,7 @@ test('IP pool silo list', async ({ page }) => { }) test('IP pool link silo', async ({ page }) => { - await page.goto('/system/networking/ip-pools/ip-pool-1?tab=silos') + await page.goto('/system/networking/ip-pools/default?tab=silos') const table = page.getByRole('table') await expectRowVisible(table, { Silo: 'maze-war', 'Pool is silo default': 'default' }) diff --git a/test/e2e/silos.e2e.ts b/test/e2e/silos.e2e.ts index 4dccf3656..635307200 100644 --- a/test/e2e/silos.e2e.ts +++ b/test/e2e/silos.e2e.ts @@ -251,9 +251,10 @@ test('Silo IP pools', async ({ page }) => { await page.goto('/system/silos/maze-war?tab=ip-pools') const table = page.getByRole('table') - await expectRowVisible(table, { name: 'ip-pool-1', Default: 'default' }) + await expectRowVisible(table, { name: 'default', Default: 'default' }) + await expectRowVisible(table, { name: 'ip-pool-1', Default: '' }) await expectRowVisible(table, { name: 'ip-pool-2', Default: '' }) - await expect(table.getByRole('row')).toHaveCount(3) // header + 2 + await expect(table.getByRole('row')).toHaveCount(4) // header + 3 // clicking on pool goes to pool detail await page.getByRole('link', { name: 'ip-pool-1' }).click() @@ -266,7 +267,7 @@ test('Silo IP pools', async ({ page }) => { page .getByRole('dialog', { name: 'Confirm change default' }) .getByText( - 'Are you sure you want to change the default pool from ip-pool-1 to ip-pool-2?' + 'Are you sure you want to change the default pool from default to ip-pool-2?' ) ).toBeVisible() await page.getByRole('button', { name: 'Confirm' }).click() @@ -299,9 +300,10 @@ test('Silo IP pools link pool', async ({ page }) => { await page.goto('/system/silos/maze-war?tab=ip-pools') const table = page.getByRole('table') - await expectRowVisible(table, { name: 'ip-pool-1', Default: 'default' }) + await expectRowVisible(table, { name: 'default', Default: 'default' }) + await expectRowVisible(table, { name: 'ip-pool-1', Default: '' }) await expectRowVisible(table, { name: 'ip-pool-2', Default: '' }) - await expect(table.getByRole('row')).toHaveCount(3) // header + 2 + await expect(table.getByRole('row')).toHaveCount(4) // header + 3 const modal = page.getByRole('dialog', { name: 'Link pool' }) await expect(modal).toBeHidden() From 8fb80573ab7ab58e556d6d12e7141f5048832536 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 20 Nov 2024 19:47:24 -0800 Subject: [PATCH 26/63] Simplify mock data; renaming the default IP Pool to default was unnecessary and confusing --- mock-api/internet-gateway.ts | 34 ++++++++++++++++----------------- mock-api/ip-pool.ts | 32 +++++-------------------------- test/e2e/instance-create.e2e.ts | 2 +- test/e2e/ip-pools.e2e.ts | 10 +++++----- test/e2e/silos.e2e.ts | 12 +++++------- 5 files changed, 32 insertions(+), 58 deletions(-) diff --git a/mock-api/internet-gateway.ts b/mock-api/internet-gateway.ts index 239da4ec3..c1365bad2 100644 --- a/mock-api/internet-gateway.ts +++ b/mock-api/internet-gateway.ts @@ -11,7 +11,7 @@ import type { InternetGatewayIpPool, } from '@oxide/api' -import { ipPools } from './ip-pool' +import { ipPool1, ipPool2 } from './ip-pool' import type { Json } from './json-type' import { vpc, vpc2 } from './vpc' @@ -56,30 +56,30 @@ export const internetGateways: Json[] = [ const internetGatewayIpAddress1: Json = { id: 'f1d5e5a1-0b2b-4d5b-8b9d-2d4b3e0c6b9d', + name: 'internet-gateway-address-1', address: '87.114.25.166', description: 'the IP address for an internet gateway', internet_gateway_id: internetGateway1.id, - name: 'internet-gateway-ip-1', time_created, time_modified, } const internetGatewayIpAddress2: Json = { id: 'f1d5e5a1-0b2b-4d5b-8b9d-2d4b3e0c6b9e', + name: 'internet-gateway-address-2', address: '292a:a05c:3b36:a053:9166:6510:2d6b:3322', description: 'an IPv6 address for an internet gateway', internet_gateway_id: internetGateway1.id, - name: 'internet-gateway-ip-2', time_created, time_modified, } const internetGatewayIpAddress3: Json = { id: 'f1d5e5a1-0b2b-4d5b-8b9d-2d4b3e0c6b9f', + name: 'internet-gateway-address-3', address: '178.125.253.126', description: 'an IPv4 address for internet gateway 2', internet_gateway_id: internetGateway2.id, - name: 'internet-gateway-ip-3', time_created, time_modified, } @@ -90,31 +90,29 @@ export const internetGatewayIpAddresses: Json[] = [ internetGatewayIpAddress3, ] -const [defaultIpPool, ipPool1, ipPool2] = ipPools - -const defaultInternetGatewayIpPool1: Json = { +const internetGatewayIpPool1: Json = { id: '1d5e5a1f-0b2b-4d5b-8b9d-2d4b3e0c6gb9', - name: 'default', + name: 'internet-gateway-pool-1', description: 'Default internet gateway IP pool', internet_gateway_id: internetGateway1.id, - ip_pool_id: defaultIpPool.id, + ip_pool_id: ipPool1.id, time_created, time_modified, } -const defaultInternetGatewayIpPool2: Json = { +const internetGatewayIpPool2: Json = { id: 'd5e5a1f1-0b2b-4d5b-8b9d-2d4b3e0c6b9c', - name: 'default', + name: 'interent-gateway-pool-2', description: 'Default internet gateway IP pool', internet_gateway_id: internetGateway2.id, - ip_pool_id: defaultIpPool.id, + ip_pool_id: ipPool1.id, time_created, time_modified, } -const internetGatewayIpPool1: Json = { +const internetGatewayIpPool3: Json = { id: 'f1d5e5a1-0b2b-4d5b-8b9d-2d4b3e0c6b9g', - name: 'internet-gateway-ip-pool-1', + name: 'internet-gateway-pool-3', description: 'an IP pool for an internet gateway', internet_gateway_id: internetGateway2.id, ip_pool_id: ipPool1.id, @@ -122,9 +120,9 @@ const internetGatewayIpPool1: Json = { time_modified, } -const internetGatewayIpPool2: Json = { +const internetGatewayIpPool4: Json = { id: 'f1d5e5a1-0b2b-4d5b-8b9d-2d4b3e0c6b9h', - name: 'internet-gateway-ip-pool-2', + name: 'internet-gateway-pool-4', description: 'a set of VPN IPs in an IP pool for an internet gateway', internet_gateway_id: internetGateway2.id, ip_pool_id: ipPool2.id, @@ -133,8 +131,8 @@ const internetGatewayIpPool2: Json = { } export const internetGatewayIpPools: Json[] = [ - defaultInternetGatewayIpPool1, - defaultInternetGatewayIpPool2, internetGatewayIpPool1, internetGatewayIpPool2, + internetGatewayIpPool3, + internetGatewayIpPool4, ] diff --git a/mock-api/ip-pool.ts b/mock-api/ip-pool.ts index d1ac26dd4..550699be6 100644 --- a/mock-api/ip-pool.ts +++ b/mock-api/ip-pool.ts @@ -11,14 +11,6 @@ import { type IpPool, type IpPoolRange, type IpPoolSiloLink } from '@oxide/api' import type { Json } from './json-type' import { defaultSilo } from './silo' -export const defaultIpPool: Json = { - id: 'cadb8535-e32f-4be2-9259-e45dec9fa3cd', - name: 'default', - description: 'default IP pool', - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), -} - export const ipPool1: Json = { id: '69b5c583-74a9-451a-823d-0741c1ec66e2', name: 'ip-pool-1', @@ -27,7 +19,7 @@ export const ipPool1: Json = { time_modified: new Date().toISOString(), } -const ipPool2: Json = { +export const ipPool2: Json = { id: 'af2fbe06-b21d-4364-96b7-a58220bc3242', name: 'ip-pool-2', description: 'VPN IPs', @@ -35,7 +27,7 @@ const ipPool2: Json = { time_modified: new Date().toISOString(), } -const ipPool3: Json = { +export const ipPool3: Json = { id: '8929a9ec-03d7-4027-8bf3-dda76627de07', name: 'ip-pool-3', description: '', @@ -43,7 +35,7 @@ const ipPool3: Json = { time_modified: new Date().toISOString(), } -const ipPool4: Json = { +export const ipPool4: Json = { id: 'a5f395a8-650e-44c9-9af8-ec21d890f61c', name: 'ip-pool-4', description: '', @@ -51,18 +43,13 @@ const ipPool4: Json = { time_modified: new Date().toISOString(), } -export const ipPools: Json[] = [defaultIpPool, ipPool1, ipPool2, ipPool3, ipPool4] +export const ipPools: Json[] = [ipPool1, ipPool2, ipPool3, ipPool4] export const ipPoolSilos: Json[] = [ - { - ip_pool_id: defaultIpPool.id, - silo_id: defaultSilo.id, - is_default: true, - }, { ip_pool_id: ipPool1.id, silo_id: defaultSilo.id, - is_default: false, + is_default: true, }, { ip_pool_id: ipPool2.id, @@ -72,15 +59,6 @@ export const ipPoolSilos: Json[] = [ ] export const ipPoolRanges: Json = [ - { - id: 'f6b3b9b5-7e3d-4b8c-9f6b-9b7b5e3d8c4b', - ip_pool_id: defaultIpPool.id, - range: { - first: '170.20.26.11', - last: '170.20.26.254', - }, - time_created: new Date().toISOString(), - }, { id: 'bbfcf3f2-061e-4334-a0e7-dfcd8171f87e', ip_pool_id: ipPool1.id, diff --git a/test/e2e/instance-create.e2e.ts b/test/e2e/instance-create.e2e.ts index fb90ebd72..4e0a5a6b8 100644 --- a/test/e2e/instance-create.e2e.ts +++ b/test/e2e/instance-create.e2e.ts @@ -475,7 +475,7 @@ test('attaches a floating IP; disables button when no IPs available', async ({ p // ensure External IPs table has rows for the Ephemeral IP and the Floating IP await expectRowVisible(page.getByRole('table'), { - ip: '170.20.26.11', + ip: '123.4.56.0', Kind: 'ephemeral', name: '—', }) diff --git a/test/e2e/ip-pools.e2e.ts b/test/e2e/ip-pools.e2e.ts index 2de5e1ff5..70db5b15f 100644 --- a/test/e2e/ip-pools.e2e.ts +++ b/test/e2e/ip-pools.e2e.ts @@ -19,7 +19,7 @@ test('IP pool list', async ({ page }) => { const table = page.getByRole('table') - await expect(table.getByRole('row')).toHaveCount(6) // header + 5 rows + await expect(table.getByRole('row')).toHaveCount(5) // header + 4 rows await expectRowVisible(table, { name: 'ip-pool-1', Utilization: '6 / 24' }) await expectRowVisible(table, { @@ -54,13 +54,13 @@ test.describe('german locale', () => { test('IP pool silo list', async ({ page }) => { await page.goto('/system/networking/ip-pools') - await page.getByRole('link', { name: 'default' }).click() - await expect(page).toHaveTitle('default / IP Pools / Oxide Console') + await page.getByRole('link', { name: 'ip-pool-1' }).click() + await expect(page).toHaveTitle('ip-pool-1 / IP Pools / Oxide Console') await page.getByRole('tab', { name: 'Linked silos' }).click() // this is here because waiting for the `tab` query param to show up avoids // flake after the goBack bit below - await expect(page).toHaveURL('/system/networking/ip-pools/default?tab=silos') + await expect(page).toHaveURL('/system/networking/ip-pools/ip-pool-1?tab=silos') const table = page.getByRole('table') await expectRowVisible(table, { Silo: 'maze-war', 'Pool is silo default': 'default' }) @@ -79,7 +79,7 @@ test('IP pool silo list', async ({ page }) => { }) test('IP pool link silo', async ({ page }) => { - await page.goto('/system/networking/ip-pools/default?tab=silos') + await page.goto('/system/networking/ip-pools/ip-pool-1?tab=silos') const table = page.getByRole('table') await expectRowVisible(table, { Silo: 'maze-war', 'Pool is silo default': 'default' }) diff --git a/test/e2e/silos.e2e.ts b/test/e2e/silos.e2e.ts index 635307200..4dccf3656 100644 --- a/test/e2e/silos.e2e.ts +++ b/test/e2e/silos.e2e.ts @@ -251,10 +251,9 @@ test('Silo IP pools', async ({ page }) => { await page.goto('/system/silos/maze-war?tab=ip-pools') const table = page.getByRole('table') - await expectRowVisible(table, { name: 'default', Default: 'default' }) - await expectRowVisible(table, { name: 'ip-pool-1', Default: '' }) + await expectRowVisible(table, { name: 'ip-pool-1', Default: 'default' }) await expectRowVisible(table, { name: 'ip-pool-2', Default: '' }) - await expect(table.getByRole('row')).toHaveCount(4) // header + 3 + await expect(table.getByRole('row')).toHaveCount(3) // header + 2 // clicking on pool goes to pool detail await page.getByRole('link', { name: 'ip-pool-1' }).click() @@ -267,7 +266,7 @@ test('Silo IP pools', async ({ page }) => { page .getByRole('dialog', { name: 'Confirm change default' }) .getByText( - 'Are you sure you want to change the default pool from default to ip-pool-2?' + 'Are you sure you want to change the default pool from ip-pool-1 to ip-pool-2?' ) ).toBeVisible() await page.getByRole('button', { name: 'Confirm' }).click() @@ -300,10 +299,9 @@ test('Silo IP pools link pool', async ({ page }) => { await page.goto('/system/silos/maze-war?tab=ip-pools') const table = page.getByRole('table') - await expectRowVisible(table, { name: 'default', Default: 'default' }) - await expectRowVisible(table, { name: 'ip-pool-1', Default: '' }) + await expectRowVisible(table, { name: 'ip-pool-1', Default: 'default' }) await expectRowVisible(table, { name: 'ip-pool-2', Default: '' }) - await expect(table.getByRole('row')).toHaveCount(4) // header + 3 + await expect(table.getByRole('row')).toHaveCount(3) // header + 2 const modal = page.getByRole('dialog', { name: 'Link pool' }) await expect(modal).toBeHidden() From dfc32236dbc6e5e9d7121f9b93bf1a68128fcaf9 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Fri, 22 Nov 2024 22:16:23 -0600 Subject: [PATCH 27/63] convert to new useQueryTable --- .../tabs/InternetGatewayIpAddressesTab.tsx | 52 +++++++++---------- .../tabs/InternetGatewayIpPoolsTab.tsx | 41 ++++++++------- .../vpcs/VpcPage/tabs/VpcGatewaysTab.tsx | 35 +++++-------- 3 files changed, 62 insertions(+), 66 deletions(-) diff --git a/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpAddressesTab.tsx b/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpAddressesTab.tsx index fb7f60486..aeff41fb1 100644 --- a/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpAddressesTab.tsx +++ b/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpAddressesTab.tsx @@ -7,35 +7,40 @@ */ import { createColumnHelper } from '@tanstack/react-table' -import { useMemo } from 'react' import type { LoaderFunctionArgs } from 'react-router-dom' -import { apiQueryClient, type InternetGatewayIpAddress } from '~/api' +import { getListQFn, queryClient, type InternetGatewayIpAddress } from '~/api' import { getInternetGatewaySelector, useInternetGatewaySelector } from '~/hooks/use-params' import { useColsWithActions, type MenuAction } from '~/table/columns/action-col' import { Columns } from '~/table/columns/common' import { useQueryTable } from '~/table/QueryTable' import { CopyableIp } from '~/ui/lib/CopyableIp' import { EmptyMessage } from '~/ui/lib/EmptyMessage' -import { ALL_ISH } from '~/util/consts' + +type GatewayParams = { project: string; vpc: string; gateway: string } + +const gatewayIpList = (query: GatewayParams) => + getListQFn('internetGatewayIpAddressList', { query }) InternetGatewayIpAddressesTab.loader = async function ({ params }: LoaderFunctionArgs) { - const { project, vpc, gateway } = getInternetGatewaySelector(params) - await Promise.all([ - apiQueryClient.prefetchQuery('internetGatewayIpAddressList', { - query: { project, vpc, gateway, limit: ALL_ISH }, - }), - ]) + const gatewaySelector = getInternetGatewaySelector(params) + await queryClient.prefetchQuery(gatewayIpList(gatewaySelector).optionsFn()) return null } const colHelper = createColumnHelper() +const staticColumns = [ + colHelper.accessor('name', {}), + colHelper.accessor('description', Columns.description), + colHelper.accessor('address', { + header: 'Address', + cell: (info) => , + }), +] + export function InternetGatewayIpAddressesTab() { - const { project, vpc, gateway } = useInternetGatewaySelector() - const { Table } = useQueryTable('internetGatewayIpAddressList', { - query: { project, vpc, gateway, limit: ALL_ISH }, - }) + const gatewaySelector = useInternetGatewaySelector() const emptyState = ( ) - const staticColumns = useMemo( - () => [ - colHelper.accessor('name', {}), - colHelper.accessor('description', Columns.description), - colHelper.accessor('address', { - header: 'Address', - cell: (info) => , - }), - ], - [] - ) - const makeActions = (): MenuAction[] => [] const columns = useColsWithActions(staticColumns, makeActions) - return
+ + const { table } = useQueryTable({ + query: gatewayIpList(gatewaySelector), + columns, + emptyState, + }) + + return <>{table} } diff --git a/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx b/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx index 609149221..4db260cff 100644 --- a/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx +++ b/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx @@ -10,7 +10,7 @@ import { createColumnHelper } from '@tanstack/react-table' import { useCallback } from 'react' import type { LoaderFunctionArgs } from 'react-router-dom' -import { apiQueryClient, type InternetGatewayIpPool } from '~/api' +import { apiQueryClient, getListQFn, queryClient, type InternetGatewayIpPool } from '~/api' import { getInternetGatewaySelector, useInternetGatewaySelector } from '~/hooks/use-params' import { IpPoolCell } from '~/table/cells/IpPoolCell' import { useColsWithActions, type MenuAction } from '~/table/columns/action-col' @@ -19,12 +19,15 @@ import { useQueryTable } from '~/table/QueryTable' import { EmptyMessage } from '~/ui/lib/EmptyMessage' import { ALL_ISH } from '~/util/consts' +type GatewayParams = { project: string; vpc: string; gateway: string } + +const gatewayIpPoolList = (query: GatewayParams) => + getListQFn('internetGatewayIpPoolList', { query }) + InternetGatewayIpPoolsTab.loader = async function ({ params }: LoaderFunctionArgs) { const { project, vpc, gateway } = getInternetGatewaySelector(params) await Promise.all([ - apiQueryClient.prefetchQuery('internetGatewayIpPoolList', { - query: { project, vpc, gateway, limit: ALL_ISH }, - }), + queryClient.prefetchQuery(gatewayIpPoolList({ project, vpc, gateway }).optionsFn()), // fetch IP Pools and preload into RQ cache so fetches by ID in // IpPoolCell can be mostly instant yet gracefully fall back to // fetching individually if we don't fetch them all here @@ -46,11 +49,17 @@ InternetGatewayIpPoolsTab.loader = async function ({ params }: LoaderFunctionArg const colHelper = createColumnHelper() +const staticColumns = [ + colHelper.accessor('name', {}), + colHelper.accessor('description', Columns.description), + colHelper.accessor('ipPoolId', { + header: 'IP Pool', + cell: (info) => , + }), +] + export function InternetGatewayIpPoolsTab() { - const { project, vpc, gateway } = useInternetGatewaySelector() - const { Table } = useQueryTable('internetGatewayIpPoolList', { - query: { project, vpc, gateway, limit: ALL_ISH }, - }) + const gatewaySelector = useInternetGatewaySelector() const emptyState = ( ) - const staticColumns = [ - colHelper.accessor('name', {}), - colHelper.accessor('description', Columns.description), - colHelper.accessor('ipPoolId', { - header: 'IP Pool', - cell: (info) => , - }), - ] - // The user can copy the ID of the IP Pool attached to this internet gateway const makeActions = useCallback( (internetGatewayIpPool: InternetGatewayIpPool): MenuAction[] => [ @@ -82,5 +82,10 @@ export function InternetGatewayIpPoolsTab() { ) const columns = useColsWithActions(staticColumns, makeActions) - return
+ const { table } = useQueryTable({ + query: gatewayIpPoolList(gatewaySelector), + columns, + emptyState, + }) + return table } diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx index 0544f6ec8..b6ac97ec4 100644 --- a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx @@ -8,47 +8,41 @@ import { createColumnHelper } from '@tanstack/react-table' import { useMemo } from 'react' -import { Outlet, type LoaderFunctionArgs } from 'react-router-dom' +import { type LoaderFunctionArgs } from 'react-router-dom' -import { apiQueryClient, type InternetGateway } from '~/api' +import { getListQFn, queryClient, type InternetGateway } from '~/api' import { getVpcSelector, useVpcSelector } from '~/hooks/use-params' import { makeLinkCell } from '~/table/cells/LinkCell' import { Columns } from '~/table/columns/common' import { useQueryTable } from '~/table/QueryTable' import { EmptyMessage } from '~/ui/lib/EmptyMessage' -import { ALL_ISH } from '~/util/consts' import { pb } from '~/util/path-builder' const colHelper = createColumnHelper() +type VpcParams = { project: string; vpc: string } + +const gatewayList = (query: VpcParams) => getListQFn('internetGatewayList', { query }) + VpcInternetGatewaysTab.loader = async ({ params }: LoaderFunctionArgs) => { - const { project, vpc } = getVpcSelector(params) - const query = { project, vpc, limit: ALL_ISH } - await Promise.all([ - apiQueryClient.prefetchQuery('internetGatewayList', { query }), - apiQueryClient.prefetchQuery('internetGatewayIpAddressList', { query }), - apiQueryClient.prefetchQuery('internetGatewayIpPoolList', { query }), - ]) + const vpcSelector = getVpcSelector(params) + await queryClient.prefetchQuery(gatewayList(vpcSelector).optionsFn()) return null } export function VpcInternetGatewaysTab() { const vpcSelector = useVpcSelector() - const { project, vpc } = vpcSelector - const { Table } = useQueryTable('internetGatewayList', { - query: { project, vpc, limit: ALL_ISH }, - }) const emptyState = ( ) - const staticColumns = useMemo( + const columns = useMemo( () => [ colHelper.accessor('name', { cell: makeLinkCell((gateway) => pb.vpcInternetGateway({ ...vpcSelector, gateway })), @@ -59,10 +53,7 @@ export function VpcInternetGatewaysTab() { [vpcSelector] ) - return ( - <> -
- - - ) + const { table } = useQueryTable({ query: gatewayList(vpcSelector), columns, emptyState }) + + return <>{table} } From fdc2995308f0c9469258000af2bea2ba4a9e3a1c Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 2 Dec 2024 09:58:35 -0800 Subject: [PATCH 28/63] Add internet gateway combobox to router route target field --- app/forms/vpc-router-route-common.tsx | 57 ++++++++++++------- app/forms/vpc-router-route-create.tsx | 8 ++- app/forms/vpc-router-route-edit.tsx | 8 ++- .../vpcs/VpcPage/tabs/VpcGatewaysTab.tsx | 2 + 4 files changed, 51 insertions(+), 24 deletions(-) diff --git a/app/forms/vpc-router-route-common.tsx b/app/forms/vpc-router-route-common.tsx index 186667bef..5d07639ff 100644 --- a/app/forms/vpc-router-route-common.tsx +++ b/app/forms/vpc-router-route-common.tsx @@ -23,6 +23,7 @@ import { TextField } from '~/components/form/fields/TextField' import { useVpcRouterSelector } from '~/hooks/use-params' import { toComboboxItems } from '~/ui/lib/Combobox' import { Message } from '~/ui/lib/Message' +import { ALL_ISH } from '~/util/consts' import { validateIp, validateIpNet } from '~/util/ip' export type RouteFormValues = RouterRouteCreate | Required @@ -30,8 +31,6 @@ export type RouteFormValues = RouterRouteCreate | Required export const routeFormMessage = { vpcSubnetNotModifiable: 'Routes of type VPC Subnet within the system router are not modifiable', - internetGatewayTargetValue: - 'For ‘Internet gateway’ targets, the value must be ‘outbound’', // https://github.com/oxidecomputer/omicron/blob/914f5fd7d51f9b060dcc0382a30b607e25df49b2/nexus/src/app/vpc_router.rs#L201-L204 noNewRoutesOnSystemRouter: 'User-provided routes cannot be added to a system router', // https://github.com/oxidecomputer/omicron/blob/914f5fd7d51f9b060dcc0382a30b607e25df49b2/nexus/src/app/vpc_router.rs#L300-L304 @@ -75,7 +74,7 @@ const destinationValueDescription: Record = { ip: 'Enter an IP', instance: 'Select an instance', - internet_gateway: undefined, + internet_gateway: 'Select an internet gateway', drop: undefined, subnet: undefined, vpc: undefined, @@ -84,7 +83,7 @@ const targetValuePlaceholder: Record = const targetValueDescription: Record = { ip: 'An IP address, like 10.0.1.5', instance: undefined, - internet_gateway: routeFormMessage.internetGatewayTargetValue, + internet_gateway: undefined, drop: undefined, subnet: undefined, vpc: undefined, @@ -103,10 +102,15 @@ export const RouteFormFields = ({ form, disabled }: RouteFormFieldsProps) => { // usePrefetchedApiQuery items below are initially fetched in the loaders in vpc-router-route-create and -edit const { data: { items: vpcSubnets }, - } = usePrefetchedApiQuery('vpcSubnetList', { query: { project, vpc, limit: 1000 } }) + } = usePrefetchedApiQuery('vpcSubnetList', { query: { project, vpc, limit: ALL_ISH } }) const { data: { items: instances }, - } = usePrefetchedApiQuery('instanceList', { query: { project, limit: 1000 } }) + } = usePrefetchedApiQuery('instanceList', { query: { project, limit: ALL_ISH } }) + const { + data: { items: internetGateways }, + } = usePrefetchedApiQuery('internetGatewayList', { + query: { project, vpc, limit: ALL_ISH }, + }) const { control } = form const destinationType = form.watch('destination.type') @@ -129,13 +133,35 @@ export const RouteFormFields = ({ form, disabled }: RouteFormFieldsProps) => { control, placeholder: targetValuePlaceholder[targetType], required: true, - // 'internet_gateway' targetTypes can only have the value 'outbound', so we disable the field - disabled: disabled || targetType === 'internet_gateway', + disabled, description: targetValueDescription[targetType], // need a default to prevent the text field validation function from // sticking around when we switch to the combobox validate: () => undefined, } + + const targetTypeField = () => { + if (targetType === 'drop') { + return null + } + if (targetType === 'instance') { + return + } + if (targetType === 'internet_gateway') { + return ( + + ) + } + return ( + + (target.type === 'ip' && validateIp(value)) || undefined + } + /> + ) + } + return ( <> {disabled && ( @@ -176,22 +202,13 @@ export const RouteFormFields = ({ form, disabled }: RouteFormFieldsProps) => { items={toListboxItems(targetTypes)} placeholder="Select a target type" required - onChange={(value) => { - form.setValue('target.value', value === 'internet_gateway' ? 'outbound' : '') + onChange={() => { + form.setValue('target.value', '') form.clearErrors('target.value') }} disabled={disabled} /> - {targetType === 'drop' ? null : targetType === 'instance' ? ( - - ) : ( - - (target.type === 'ip' && validateIp(value)) || undefined - } - /> - )} + {targetTypeField()} ) } diff --git a/app/forms/vpc-router-route-create.tsx b/app/forms/vpc-router-route-create.tsx index 8030b55dc..3f89dc8f7 100644 --- a/app/forms/vpc-router-route-create.tsx +++ b/app/forms/vpc-router-route-create.tsx @@ -15,6 +15,7 @@ import { HL } from '~/components/HL' import { RouteFormFields, type RouteFormValues } from '~/forms/vpc-router-route-common' import { getVpcRouterSelector, useVpcRouterSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' +import { ALL_ISH } from '~/util/consts' import { pb } from '~/util/path-builder' const defaultValues: RouteFormValues = { @@ -28,10 +29,13 @@ CreateRouterRouteSideModalForm.loader = async ({ params }: LoaderFunctionArgs) = const { project, vpc } = getVpcRouterSelector(params) await Promise.all([ apiQueryClient.prefetchQuery('vpcSubnetList', { - query: { project, vpc, limit: 1000 }, + query: { project, vpc, limit: ALL_ISH }, }), apiQueryClient.prefetchQuery('instanceList', { - query: { project, limit: 1000 }, + query: { project, limit: ALL_ISH }, + }), + apiQueryClient.prefetchQuery('internetGatewayList', { + query: { project, vpc, limit: ALL_ISH }, }), ]) return null diff --git a/app/forms/vpc-router-route-edit.tsx b/app/forms/vpc-router-route-edit.tsx index da1c06338..d54fba40d 100644 --- a/app/forms/vpc-router-route-edit.tsx +++ b/app/forms/vpc-router-route-edit.tsx @@ -25,6 +25,7 @@ import { } from '~/forms/vpc-router-route-common' import { getVpcRouterRouteSelector, useVpcRouterRouteSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' +import { ALL_ISH } from '~/util/consts' import { pb } from '~/util/path-builder' EditRouterRouteSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { @@ -35,10 +36,13 @@ EditRouterRouteSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => query: { project, vpc, router }, }), apiQueryClient.prefetchQuery('vpcSubnetList', { - query: { project, vpc, limit: 1000 }, + query: { project, vpc, limit: ALL_ISH }, }), apiQueryClient.prefetchQuery('instanceList', { - query: { project, limit: 1000 }, + query: { project, limit: ALL_ISH }, + }), + apiQueryClient.prefetchQuery('internetGatewayList', { + query: { project, vpc, limit: ALL_ISH }, }), ]) return null diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx index b6ac97ec4..67499baf1 100644 --- a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx @@ -48,6 +48,8 @@ export function VpcInternetGatewaysTab() { cell: makeLinkCell((gateway) => pb.vpcInternetGateway({ ...vpcSelector, gateway })), }), colHelper.accessor('description', Columns.description), + // add a column for the IP Pool associated with this Internet Gateway + colHelper.accessor('timeCreated', Columns.timeCreated), ], [vpcSelector] From d3f9e1d4974a387998e45d5752f55da3bd503b73 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 5 Dec 2024 05:16:33 -0800 Subject: [PATCH 29/63] Sidebar for Internet Gateway coming together --- .../InternetGatewayPage.tsx | 192 ++++++++++-------- app/routes.tsx | 18 +- app/ui/lib/SideModal.tsx | 2 +- app/util/path-builder.ts | 1 + 4 files changed, 130 insertions(+), 83 deletions(-) diff --git a/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx b/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx index c6a3893c3..9a85b7e7a 100644 --- a/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx +++ b/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx @@ -6,114 +6,144 @@ * Copyright Oxide Computer Company */ -import { useMemo } from 'react' -import { type LoaderFunctionArgs } from 'react-router-dom' +import { useQuery } from '@tanstack/react-query' +import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom' -import { Networking16Icon, Networking24Icon } from '@oxide/design-system/icons/react' - -import { apiQueryClient, usePrefetchedApiQuery } from '~/api' -import { DocsPopover } from '~/components/DocsPopover' -import { MoreActionsMenu } from '~/components/MoreActionsMenu' -import { RouteTabs, Tab } from '~/components/RouteTabs' +import { apiQueryClient, getListQFn, queryClient, usePrefetchedApiQuery } from '~/api' import { getInternetGatewaySelector, useInternetGatewaySelector } from '~/hooks/use-params' import { DescriptionCell } from '~/table/cells/DescriptionCell' -import { DateTime } from '~/ui/lib/DateTime' +import { EmptyCell } from '~/table/cells/EmptyCell' +import { IpPoolCell } from '~/table/cells/IpPoolCell' +import { Button } from '~/ui/lib/Button' +import { CopyableIp } from '~/ui/lib/CopyableIp' import { Message } from '~/ui/lib/Message' -import { PageHeader, PageTitle } from '~/ui/lib/PageHeader' import { PropertiesTable } from '~/ui/lib/PropertiesTable' -import { Truncate } from '~/ui/lib/Truncate' -import { ALL_ISH } from '~/util/consts' -import { docLinks } from '~/util/links' +import { SideModal } from '~/ui/lib/SideModal' import { pb } from '~/util/path-builder' +type GatewayParams = { project: string; vpc: string; gateway: string } + +const gatewayIpPoolList = (query: GatewayParams) => + getListQFn('internetGatewayIpPoolList', { query }) +// const gatewayIpList = (query: GatewayParams) => +// getListQFn('internetGatewayIpAddressList', { query }) +const gatewayIpAddressList = (query: GatewayParams) => + getListQFn('internetGatewayIpAddressList', { query }) + InternetGatewayPage.loader = async function ({ params }: LoaderFunctionArgs) { - console.log('InternetGatewayPage.loader') const { project, vpc, gateway } = getInternetGatewaySelector(params) - console.log({ project, vpc, gateway }) - const query = { project, vpc, gateway, limit: ALL_ISH } + // const query = { project, vpc, gateway, limit: ALL_ISH } await Promise.all([ apiQueryClient.prefetchQuery('internetGatewayView', { query: { project, vpc }, path: { gateway }, }), - apiQueryClient.prefetchQuery('internetGatewayIpAddressList', { query }), - apiQueryClient.prefetchQuery('internetGatewayIpPoolList', { query }), + // apiQueryClient.prefetchQuery('internetGatewayIpAddressList', { query }), + queryClient.prefetchQuery(gatewayIpPoolList({ project, vpc, gateway }).optionsFn()), + queryClient.prefetchQuery(gatewayIpAddressList({ project, vpc, gateway }).optionsFn()), ]) return null } export function InternetGatewayPage() { + const navigate = useNavigate() const gatewaySelector = useInternetGatewaySelector() + const onDismiss = () => navigate(pb.vpcInternetGateways(gatewaySelector)) const { project, vpc, gateway } = gatewaySelector - const { - data: { id, description, name, timeCreated, timeModified }, - } = usePrefetchedApiQuery('internetGatewayView', { + const { data: internetGateway } = usePrefetchedApiQuery('internetGatewayView', { query: { project, vpc }, path: { gateway }, }) - - const actions = useMemo( - () => [ - { - label: 'Copy ID', - onActivate() { - window.navigator.clipboard.writeText(id || '') - }, - }, - ], - [id] + const { data: { items: gatewayIpPools } = {} } = useQuery( + gatewayIpPoolList({ project, vpc, gateway }).optionsFn() ) + const { data: { items: gatewayIpAddresses } = {} } = useQuery( + gatewayIpAddressList({ project, vpc, gateway }).optionsFn() + ) + // const { data: { items: projectIpPools } = {} } = usePrefetchedApiQuery( + // 'projectIpPoolList', + // { + // query: { limit: ALL_ISH }, + // } + // ) return ( - <> - - }>{name} -
- } - summary="Internet gateways … 🚨🙈🚨🙉🚨🙊🚨 … just using emojis here so we spot it more easily in the PR; this copy needs eyes 👀" - links={[docLinks.vpcs, docLinks.firewallRules]} - /> - + + +
+
+ + This is a read-only copy of this internet gateway. Use the CLI to create + and update internet gateways. More functionality for internet gateways + will be included in future releases of the Oxide console. + + } + /> +
+ + {internetGateway.name} + + + + +
+ + Internet Gateway IP Pool + {gatewayIpPools && gatewayIpPools.length > 1 ? 's' : ''} + +
+ {gatewayIpPools ? ( + gatewayIpPools.map((gatewayPool) => ( + + + {gatewayPool.name} + + + + + + + + + )) + ) : ( + + )} +
+
+ + Internet Gateway IP Address + +
+ {gatewayIpAddresses ? ( + gatewayIpAddresses.map((gatewayAddress) => ( + + + {gatewayAddress.name} + + + + + + + + + )) + ) : ( + + )} +
- - - - - - - - - - - - - - - - - - - - - - This is a read-only copy of this internet gateway and its IP pools and - addresses. Use the CLI to create and update internet gateways. More - functionality for internet gateways will be included in future releases of the - Oxide console. - - } - /> - - - IP Pools - IP Addresses - - +
+ + + +
) } diff --git a/app/routes.tsx b/app/routes.tsx index 1b6cbb0dd..4b00682fa 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -387,7 +387,23 @@ export const routes = createRoutesFromElements( element={} loader={VpcInternetGatewaysTab.loader} handle={{ crumb: 'Internet Gateways' }} - /> + > + + } + loader={InternetGatewayPage.loader} + handle={makeCrumb( + (p) => p.gateway!, + (p) => pb.vpcInternetGateway(getInternetGatewaySelector(p)) + )} + /> + + diff --git a/app/ui/lib/SideModal.tsx b/app/ui/lib/SideModal.tsx index 494901b35..dd3d408b3 100644 --- a/app/ui/lib/SideModal.tsx +++ b/app/ui/lib/SideModal.tsx @@ -87,7 +87,7 @@ export function SideModal({ aria-describedby={undefined} >
- + {title} {subtitle} diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts index 209b37ab4..37637a286 100644 --- a/app/util/path-builder.ts +++ b/app/util/path-builder.ts @@ -101,6 +101,7 @@ export const pb = { vpcInternetGateway: (params: VpcInternetGateway) => `${pb.vpcInternetGateways(params)}/${params.gateway}`, vpcInternetGatewaysNew: (params: Vpc) => `${vpcBase(params)}/internet-gateways-new`, + // the next two should come off when no longer using tabs for internet gateways 👀 vpcInternetGatewayIpPools: (params: VpcInternetGateway) => `${pb.vpcInternetGateway(params)}/ip-pools`, vpcInternetGatewayIpAddresses: (params: VpcInternetGateway) => From 845940f2c361bbef93eb91ddd8e5d550c8e6fe04 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 13:17:58 +0000 Subject: [PATCH 30/63] Bot commit: format with prettier --- app/ui/lib/SideModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/ui/lib/SideModal.tsx b/app/ui/lib/SideModal.tsx index dd3d408b3..494901b35 100644 --- a/app/ui/lib/SideModal.tsx +++ b/app/ui/lib/SideModal.tsx @@ -87,7 +87,7 @@ export function SideModal({ aria-describedby={undefined} >
- + {title} {subtitle} From 000c4f98aa82d8172902c94dd149db43a7912da4 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 5 Dec 2024 09:32:24 -0800 Subject: [PATCH 31/63] DOM shuffling --- .../InternetGatewayPage.tsx | 59 ++++++++----------- 1 file changed, 26 insertions(+), 33 deletions(-) diff --git a/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx b/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx index 9a85b7e7a..4f84f2955 100644 --- a/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx +++ b/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx @@ -25,14 +25,11 @@ type GatewayParams = { project: string; vpc: string; gateway: string } const gatewayIpPoolList = (query: GatewayParams) => getListQFn('internetGatewayIpPoolList', { query }) -// const gatewayIpList = (query: GatewayParams) => -// getListQFn('internetGatewayIpAddressList', { query }) const gatewayIpAddressList = (query: GatewayParams) => getListQFn('internetGatewayIpAddressList', { query }) InternetGatewayPage.loader = async function ({ params }: LoaderFunctionArgs) { const { project, vpc, gateway } = getInternetGatewaySelector(params) - // const query = { project, vpc, gateway, limit: ALL_ISH } await Promise.all([ apiQueryClient.prefetchQuery('internetGatewayView', { query: { project, vpc }, @@ -47,9 +44,8 @@ InternetGatewayPage.loader = async function ({ params }: LoaderFunctionArgs) { export function InternetGatewayPage() { const navigate = useNavigate() - const gatewaySelector = useInternetGatewaySelector() - const onDismiss = () => navigate(pb.vpcInternetGateways(gatewaySelector)) - const { project, vpc, gateway } = gatewaySelector + const { project, vpc, gateway } = useInternetGatewaySelector() + const onDismiss = () => navigate(pb.vpcInternetGateways({ project, vpc })) const { data: internetGateway } = usePrefetchedApiQuery('internetGatewayView', { query: { project, vpc }, path: { gateway }, @@ -60,12 +56,6 @@ export function InternetGatewayPage() { const { data: { items: gatewayIpAddresses } = {} } = useQuery( gatewayIpAddressList({ project, vpc, gateway }).optionsFn() ) - // const { data: { items: projectIpPools } = {} } = usePrefetchedApiQuery( - // 'projectIpPoolList', - // { - // query: { limit: ALL_ISH }, - // } - // ) return ( @@ -89,6 +79,7 @@ export function InternetGatewayPage() { + {/* insert routes that are associated with this gateway */}
@@ -115,27 +106,29 @@ export function InternetGatewayPage() { )}
- - Internet Gateway IP Address - -
- {gatewayIpAddresses ? ( - gatewayIpAddresses.map((gatewayAddress) => ( - - - {gatewayAddress.name} - - - - - - - - - )) - ) : ( - - )} +
+ + Internet Gateway IP Address + +
+ {gatewayIpAddresses ? ( + gatewayIpAddresses.map((gatewayAddress) => ( + + + {gatewayAddress.name} + + + + + + + + + )) + ) : ( + + )} +
From 98238ec7e304278be574dbb25cae8829d976b519 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 5 Dec 2024 11:31:13 -0800 Subject: [PATCH 32/63] Update mock data --- mock-api/internet-gateway.ts | 14 ++------------ mock-api/vpc.ts | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/mock-api/internet-gateway.ts b/mock-api/internet-gateway.ts index c1365bad2..44498f641 100644 --- a/mock-api/internet-gateway.ts +++ b/mock-api/internet-gateway.ts @@ -57,7 +57,8 @@ export const internetGateways: Json[] = [ const internetGatewayIpAddress1: Json = { id: 'f1d5e5a1-0b2b-4d5b-8b9d-2d4b3e0c6b9d', name: 'internet-gateway-address-1', - address: '87.114.25.166', + // This IP address comes from IP Pool 1; a proper IP address will come from one of the IP pools + address: '123.4.56.3', description: 'the IP address for an internet gateway', internet_gateway_id: internetGateway1.id, time_created, @@ -69,16 +70,6 @@ const internetGatewayIpAddress2: Json = { name: 'internet-gateway-address-2', address: '292a:a05c:3b36:a053:9166:6510:2d6b:3322', description: 'an IPv6 address for an internet gateway', - internet_gateway_id: internetGateway1.id, - time_created, - time_modified, -} - -const internetGatewayIpAddress3: Json = { - id: 'f1d5e5a1-0b2b-4d5b-8b9d-2d4b3e0c6b9f', - name: 'internet-gateway-address-3', - address: '178.125.253.126', - description: 'an IPv4 address for internet gateway 2', internet_gateway_id: internetGateway2.id, time_created, time_modified, @@ -87,7 +78,6 @@ const internetGatewayIpAddress3: Json = { export const internetGatewayIpAddresses: Json[] = [ internetGatewayIpAddress1, internetGatewayIpAddress2, - internetGatewayIpAddress3, ] const internetGatewayIpPool1: Json = { diff --git a/mock-api/vpc.ts b/mock-api/vpc.ts index caab75d1a..57d719234 100644 --- a/mock-api/vpc.ts +++ b/mock-api/vpc.ts @@ -134,6 +134,21 @@ export const routerRoutes: Json> = [ type: 'drop', }, }, + { + ...routeBase, + id: '93g7c4a1-5b0de-4efb-8518-e0bf012a5169', + name: 'dc2', + description: 'route to datacenter 2', + kind: 'custom', + target: { + type: 'internet_gateway', + value: 'internet-gateway-1', + }, + destination: { + type: 'ip_net', + value: '45.154.216.0/24', + }, + }, ] export const vpcSubnet: Json = { From 7097dc6093f949dceea0bd6079dd79db499d9367 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 5 Dec 2024 13:41:36 -0800 Subject: [PATCH 33/63] Update routes to handle new sidebar and main tab together --- .../vpcs/VpcPage/tabs/VpcGatewaysTab.tsx | 9 ++- app/routes.tsx | 55 +++---------------- 2 files changed, 16 insertions(+), 48 deletions(-) diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx index 67499baf1..94cdb9f86 100644 --- a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx @@ -8,7 +8,7 @@ import { createColumnHelper } from '@tanstack/react-table' import { useMemo } from 'react' -import { type LoaderFunctionArgs } from 'react-router-dom' +import { Outlet, type LoaderFunctionArgs } from 'react-router-dom' import { getListQFn, queryClient, type InternetGateway } from '~/api' import { getVpcSelector, useVpcSelector } from '~/hooks/use-params' @@ -57,5 +57,10 @@ export function VpcInternetGatewaysTab() { const { table } = useQueryTable({ query: gatewayList(vpcSelector), columns, emptyState }) - return <>{table} + return ( + <> + {table} + + + ) } diff --git a/app/routes.tsx b/app/routes.tsx index 4b00682fa..e66737bab 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -70,8 +70,6 @@ import * as StorageTab from './pages/project/instances/instance/tabs/StorageTab' import { InstancesPage } from './pages/project/instances/InstancesPage' import { SnapshotsPage } from './pages/project/snapshots/SnapshotsPage' import { InternetGatewayPage } from './pages/project/vpcs/InternetGatewayPage/InternetGatewayPage' -import { InternetGatewayIpAddressesTab } from './pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpAddressesTab' -import { InternetGatewayIpPoolsTab } from './pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab' import * as RouterPage from './pages/project/vpcs/RouterPage' import { VpcFirewallRulesTab } from './pages/project/vpcs/VpcPage/tabs/VpcFirewallRulesTab' import { VpcInternetGatewaysTab } from './pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab' @@ -384,25 +382,19 @@ export const routes = createRoutesFromElements( } - loader={VpcInternetGatewaysTab.loader} handle={{ crumb: 'Internet Gateways' }} + loader={VpcInternetGatewaysTab.loader} + element={} > } loader={InternetGatewayPage.loader} - > - } - loader={InternetGatewayPage.loader} - handle={makeCrumb( - (p) => p.gateway!, - (p) => pb.vpcInternetGateway(getInternetGatewaySelector(p)) - )} - /> - + handle={makeCrumb( + (p) => p.gateway!, + (p) => pb.vpcInternetGateway(getInternetGatewaySelector(p)) + )} + /> @@ -428,35 +420,6 @@ export const routes = createRoutesFromElements( - - } - loader={InternetGatewayPage.loader} - handle={makeCrumb( - (p) => p.gateway!, - (p) => pb.vpcInternetGateway(getInternetGatewaySelector(p)) - )} - > - } /> - } - loader={InternetGatewayIpPoolsTab.loader} - handle={{ crumb: 'IP Pools' }} - /> - } - loader={InternetGatewayIpAddressesTab.loader} - handle={{ crumb: 'IP Addresses' }} - /> - - Date: Thu, 5 Dec 2024 13:45:50 -0800 Subject: [PATCH 34/63] Reorder internet gateway sidebar --- .../InternetGatewayPage.tsx | 60 +++++++++---------- mock-api/internet-gateway.ts | 6 +- 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx b/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx index 4f84f2955..e7d3f8de4 100644 --- a/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx +++ b/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx @@ -60,20 +60,18 @@ export function InternetGatewayPage() { return ( -
-
- - This is a read-only copy of this internet gateway. Use the CLI to create - and update internet gateways. More functionality for internet gateways - will be included in future releases of the Oxide console. - - } - /> -
+
+ + This is a read-only copy of this internet gateway. Use the CLI to create and + update internet gateways. More functionality for internet gateways will be + included in future releases of the Oxide console. + + } + /> {internetGateway.name} @@ -83,21 +81,20 @@ export function InternetGatewayPage() {
- Internet Gateway IP Pool - {gatewayIpPools && gatewayIpPools.length > 1 ? 's' : ''} + Internet Gateway IP Address
- {gatewayIpPools ? ( - gatewayIpPools.map((gatewayPool) => ( - + {gatewayIpAddresses ? ( + gatewayIpAddresses.map((gatewayAddress) => ( + - {gatewayPool.name} + {gatewayAddress.name} - + - - + + )) @@ -108,20 +105,21 @@ export function InternetGatewayPage() {
- Internet Gateway IP Address + Internet Gateway IP Pool + {gatewayIpPools && gatewayIpPools.length > 1 ? 's' : ''}
- {gatewayIpAddresses ? ( - gatewayIpAddresses.map((gatewayAddress) => ( - + {gatewayIpPools ? ( + gatewayIpPools.map((gatewayPool) => ( + - {gatewayAddress.name} + {gatewayPool.name} - + - - + + )) diff --git a/mock-api/internet-gateway.ts b/mock-api/internet-gateway.ts index 44498f641..56fad67d1 100644 --- a/mock-api/internet-gateway.ts +++ b/mock-api/internet-gateway.ts @@ -83,7 +83,7 @@ export const internetGatewayIpAddresses: Json[] = [ const internetGatewayIpPool1: Json = { id: '1d5e5a1f-0b2b-4d5b-8b9d-2d4b3e0c6gb9', name: 'internet-gateway-pool-1', - description: 'Default internet gateway IP pool', + description: 'An IP pool for an internet gateway', internet_gateway_id: internetGateway1.id, ip_pool_id: ipPool1.id, time_created, @@ -93,7 +93,7 @@ const internetGatewayIpPool1: Json = { const internetGatewayIpPool2: Json = { id: 'd5e5a1f1-0b2b-4d5b-8b9d-2d4b3e0c6b9c', name: 'interent-gateway-pool-2', - description: 'Default internet gateway IP pool', + description: 'another IP pool for an internet gateway', internet_gateway_id: internetGateway2.id, ip_pool_id: ipPool1.id, time_created, @@ -103,7 +103,7 @@ const internetGatewayIpPool2: Json = { const internetGatewayIpPool3: Json = { id: 'f1d5e5a1-0b2b-4d5b-8b9d-2d4b3e0c6b9g', name: 'internet-gateway-pool-3', - description: 'an IP pool for an internet gateway', + description: 'a third pool for an internet gateway', internet_gateway_id: internetGateway2.id, ip_pool_id: ipPool1.id, time_created, From daa24f0634d5be4a7bb851c40545dc8fe97c8081 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 5 Dec 2024 14:37:30 -0800 Subject: [PATCH 35/63] Update paths and snapshots --- .../__snapshots__/path-builder.spec.ts.snap | 62 +------------------ app/util/path-builder.spec.ts | 2 - app/util/path-builder.ts | 5 -- 3 files changed, 1 insertion(+), 68 deletions(-) diff --git a/app/util/__snapshots__/path-builder.spec.ts.snap b/app/util/__snapshots__/path-builder.spec.ts.snap index 540b8765b..6fd10b759 100644 --- a/app/util/__snapshots__/path-builder.spec.ts.snap +++ b/app/util/__snapshots__/path-builder.spec.ts.snap @@ -724,63 +724,7 @@ exports[`breadcrumbs 2`] = ` }, { "label": "v", - "path": "/projects/p/vpcs/v", - }, - { - "label": "Internet Gateways", - "path": "/projects/p/vpcs/v/internet-gateways", - }, - { - "label": "g", - "path": "/projects/p/vpcs/v/internet-gateways/g", - }, - ], - "vpcInternetGatewayIpAddresses (/projects/p/vpcs/v/internet-gateways/g/ip-addresses)": [ - { - "label": "Projects", - "path": "/projects", - }, - { - "label": "p", - "path": "/projects/p/instances", - }, - { - "label": "VPCs", - "path": "/projects/p/vpcs", - }, - { - "label": "v", - "path": "/projects/p/vpcs/v", - }, - { - "label": "Internet Gateways", - "path": "/projects/p/vpcs/v/internet-gateways", - }, - { - "label": "g", - "path": "/projects/p/vpcs/v/internet-gateways/g", - }, - { - "label": "IP Addresses", - "path": "/projects/p/vpcs/v/internet-gateways/g/ip-addresses", - }, - ], - "vpcInternetGatewayIpPools (/projects/p/vpcs/v/internet-gateways/g/ip-pools)": [ - { - "label": "Projects", - "path": "/projects", - }, - { - "label": "p", - "path": "/projects/p/instances", - }, - { - "label": "VPCs", - "path": "/projects/p/vpcs", - }, - { - "label": "v", - "path": "/projects/p/vpcs/v", + "path": "/projects/p/vpcs/v/firewall-rules", }, { "label": "Internet Gateways", @@ -790,10 +734,6 @@ exports[`breadcrumbs 2`] = ` "label": "g", "path": "/projects/p/vpcs/v/internet-gateways/g", }, - { - "label": "IP Pools", - "path": "/projects/p/vpcs/v/internet-gateways/g/ip-pools", - }, ], "vpcInternetGateways (/projects/p/vpcs/v/internet-gateways)": [ { diff --git a/app/util/path-builder.spec.ts b/app/util/path-builder.spec.ts index ccf703055..3d539d432 100644 --- a/app/util/path-builder.spec.ts +++ b/app/util/path-builder.spec.ts @@ -93,8 +93,6 @@ test('path builder', () => { "vpcFirewallRules": "/projects/p/vpcs/v/firewall-rules", "vpcFirewallRulesNew": "/projects/p/vpcs/v/firewall-rules-new", "vpcInternetGateway": "/projects/p/vpcs/v/internet-gateways/g", - "vpcInternetGatewayIpAddresses": "/projects/p/vpcs/v/internet-gateways/g/ip-addresses", - "vpcInternetGatewayIpPools": "/projects/p/vpcs/v/internet-gateways/g/ip-pools", "vpcInternetGateways": "/projects/p/vpcs/v/internet-gateways", "vpcInternetGatewaysNew": "/projects/p/vpcs/v/internet-gateways-new", "vpcRouter": "/projects/p/vpcs/v/routers/r", diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts index 37637a286..dacff8b90 100644 --- a/app/util/path-builder.ts +++ b/app/util/path-builder.ts @@ -101,11 +101,6 @@ export const pb = { vpcInternetGateway: (params: VpcInternetGateway) => `${pb.vpcInternetGateways(params)}/${params.gateway}`, vpcInternetGatewaysNew: (params: Vpc) => `${vpcBase(params)}/internet-gateways-new`, - // the next two should come off when no longer using tabs for internet gateways 👀 - vpcInternetGatewayIpPools: (params: VpcInternetGateway) => - `${pb.vpcInternetGateway(params)}/ip-pools`, - vpcInternetGatewayIpAddresses: (params: VpcInternetGateway) => - `${pb.vpcInternetGateway(params)}/ip-addresses`, floatingIps: (params: Project) => `${projectBase(params)}/floating-ips`, floatingIpsNew: (params: Project) => `${projectBase(params)}/floating-ips-new`, From 3f042641fca1e66120e4b63025fea1f88c519d10 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 5 Dec 2024 15:15:34 -0800 Subject: [PATCH 36/63] use more common internet-gateway-edit syntax for filename --- app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx | 4 ++++ .../InternetGatewayPage.tsx => internet-gateway-edit.tsx} | 4 ++-- app/routes.tsx | 6 +++--- app/util/__snapshots__/path-builder.spec.ts.snap | 1 - app/util/path-builder.spec.ts | 2 -- app/util/path-builder.ts | 2 +- 6 files changed, 10 insertions(+), 9 deletions(-) rename app/pages/project/vpcs/{InternetGatewayPage/InternetGatewayPage.tsx => internet-gateway-edit.tsx} (97%) diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx index 94cdb9f86..a5c99a74a 100644 --- a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx @@ -49,6 +49,10 @@ export function VpcInternetGatewaysTab() { }), colHelper.accessor('description', Columns.description), // add a column for the IP Pool associated with this Internet Gateway + colHelper.accessor('id', { + header: 'IP Pool', + cell: (info) => info.getValue(), + }), colHelper.accessor('timeCreated', Columns.timeCreated), ], diff --git a/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx b/app/pages/project/vpcs/internet-gateway-edit.tsx similarity index 97% rename from app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx rename to app/pages/project/vpcs/internet-gateway-edit.tsx index e7d3f8de4..69216ea70 100644 --- a/app/pages/project/vpcs/InternetGatewayPage/InternetGatewayPage.tsx +++ b/app/pages/project/vpcs/internet-gateway-edit.tsx @@ -28,7 +28,7 @@ const gatewayIpPoolList = (query: GatewayParams) => const gatewayIpAddressList = (query: GatewayParams) => getListQFn('internetGatewayIpAddressList', { query }) -InternetGatewayPage.loader = async function ({ params }: LoaderFunctionArgs) { +EditInternetGatewayForm.loader = async function ({ params }: LoaderFunctionArgs) { const { project, vpc, gateway } = getInternetGatewaySelector(params) await Promise.all([ apiQueryClient.prefetchQuery('internetGatewayView', { @@ -42,7 +42,7 @@ InternetGatewayPage.loader = async function ({ params }: LoaderFunctionArgs) { return null } -export function InternetGatewayPage() { +export function EditInternetGatewayForm() { const navigate = useNavigate() const { project, vpc, gateway } = useInternetGatewaySelector() const onDismiss = () => navigate(pb.vpcInternetGateways({ project, vpc })) diff --git a/app/routes.tsx b/app/routes.tsx index e66737bab..4329efad4 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -69,7 +69,7 @@ import * as NetworkingTab from './pages/project/instances/instance/tabs/Networki import * as StorageTab from './pages/project/instances/instance/tabs/StorageTab' import { InstancesPage } from './pages/project/instances/InstancesPage' import { SnapshotsPage } from './pages/project/snapshots/SnapshotsPage' -import { InternetGatewayPage } from './pages/project/vpcs/InternetGatewayPage/InternetGatewayPage' +import { EditInternetGatewayForm } from './pages/project/vpcs/internet-gateway-edit' import * as RouterPage from './pages/project/vpcs/RouterPage' import { VpcFirewallRulesTab } from './pages/project/vpcs/VpcPage/tabs/VpcFirewallRulesTab' import { VpcInternetGatewaysTab } from './pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab' @@ -388,8 +388,8 @@ export const routes = createRoutesFromElements( > } - loader={InternetGatewayPage.loader} + element={} + loader={EditInternetGatewayForm.loader} handle={makeCrumb( (p) => p.gateway!, (p) => pb.vpcInternetGateway(getInternetGatewaySelector(p)) diff --git a/app/util/__snapshots__/path-builder.spec.ts.snap b/app/util/__snapshots__/path-builder.spec.ts.snap index 6fd10b759..5c9a5fe0d 100644 --- a/app/util/__snapshots__/path-builder.spec.ts.snap +++ b/app/util/__snapshots__/path-builder.spec.ts.snap @@ -757,7 +757,6 @@ exports[`breadcrumbs 2`] = ` "path": "/projects/p/vpcs/v/internet-gateways", }, ], - "vpcInternetGatewaysNew (/projects/p/vpcs/v/internet-gateways-new)": [], "vpcRouter (/projects/p/vpcs/v/routers/r)": [ { "label": "Projects", diff --git a/app/util/path-builder.spec.ts b/app/util/path-builder.spec.ts index 3d539d432..bfb919679 100644 --- a/app/util/path-builder.spec.ts +++ b/app/util/path-builder.spec.ts @@ -94,7 +94,6 @@ test('path builder', () => { "vpcFirewallRulesNew": "/projects/p/vpcs/v/firewall-rules-new", "vpcInternetGateway": "/projects/p/vpcs/v/internet-gateways/g", "vpcInternetGateways": "/projects/p/vpcs/v/internet-gateways", - "vpcInternetGatewaysNew": "/projects/p/vpcs/v/internet-gateways-new", "vpcRouter": "/projects/p/vpcs/v/routers/r", "vpcRouterEdit": "/projects/p/vpcs/v/routers/r/edit", "vpcRouterRouteEdit": "/projects/p/vpcs/v/routers/r/routes/rr/edit", @@ -139,7 +138,6 @@ test('breadcrumbs', () => { .map(([key]) => key) expect(zeroCrumbKeys).toMatchInlineSnapshot(` [ - "vpcInternetGatewaysNew (/projects/p/vpcs/v/internet-gateways-new)", "deviceSuccess (/device/success)", ] `) diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts index dacff8b90..4c291c370 100644 --- a/app/util/path-builder.ts +++ b/app/util/path-builder.ts @@ -100,7 +100,7 @@ export const pb = { vpcInternetGateways: (params: Vpc) => `${vpcBase(params)}/internet-gateways`, vpcInternetGateway: (params: VpcInternetGateway) => `${pb.vpcInternetGateways(params)}/${params.gateway}`, - vpcInternetGatewaysNew: (params: Vpc) => `${vpcBase(params)}/internet-gateways-new`, + // vpcInternetGatewaysNew: (params: Vpc) => `${vpcBase(params)}/internet-gateways-new`, floatingIps: (params: Project) => `${projectBase(params)}/floating-ips`, floatingIpsNew: (params: Project) => `${projectBase(params)}/floating-ips-new`, From 1df7ad467b5da0ae802ae663fea7839c17e3d5cc Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Fri, 6 Dec 2024 16:53:44 +0000 Subject: [PATCH 37/63] Internet gateways modal tweak (#2607) Internet gateway modal tweaks --- .../vpcs/VpcPage/tabs/VpcGatewaysTab.tsx | 4 +- .../project/vpcs/internet-gateway-edit.tsx | 181 ++++++++++-------- 2 files changed, 101 insertions(+), 84 deletions(-) diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx index a5c99a74a..6f206da56 100644 --- a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx @@ -37,8 +37,8 @@ export function VpcInternetGatewaysTab() { ) diff --git a/app/pages/project/vpcs/internet-gateway-edit.tsx b/app/pages/project/vpcs/internet-gateway-edit.tsx index 69216ea70..dc583fb60 100644 --- a/app/pages/project/vpcs/internet-gateway-edit.tsx +++ b/app/pages/project/vpcs/internet-gateway-edit.tsx @@ -7,18 +7,23 @@ */ import { useQuery } from '@tanstack/react-query' +import { useForm } from 'react-hook-form' import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom' +import { Gateway16Icon } from '@oxide/design-system/icons/react' + import { apiQueryClient, getListQFn, queryClient, usePrefetchedApiQuery } from '~/api' +import { SideModalForm } from '~/components/form/SideModalForm' import { getInternetGatewaySelector, useInternetGatewaySelector } from '~/hooks/use-params' import { DescriptionCell } from '~/table/cells/DescriptionCell' import { EmptyCell } from '~/table/cells/EmptyCell' -import { IpPoolCell } from '~/table/cells/IpPoolCell' -import { Button } from '~/ui/lib/Button' -import { CopyableIp } from '~/ui/lib/CopyableIp' +import { LinkCell } from '~/table/cells/LinkCell' +import { FormDivider } from '~/ui/lib/Divider' import { Message } from '~/ui/lib/Message' import { PropertiesTable } from '~/ui/lib/PropertiesTable' -import { SideModal } from '~/ui/lib/SideModal' +import { ResourceLabel, SideModal } from '~/ui/lib/SideModal' +import { Table } from '~/ui/lib/Table' +import { Truncate } from '~/ui/lib/Truncate' import { pb } from '~/util/path-builder' type GatewayParams = { project: string; vpc: string; gateway: string } @@ -57,84 +62,96 @@ export function EditInternetGatewayForm() { gatewayIpAddressList({ project, vpc, gateway }).optionsFn() ) + const form = useForm({}) + return ( - - -
- - This is a read-only copy of this internet gateway. Use the CLI to create and - update internet gateways. More functionality for internet gateways will be - included in future releases of the Oxide console. - - } - /> - - {internetGateway.name} - - - - {/* insert routes that are associated with this gateway */} - -
- - Internet Gateway IP Address - -
- {gatewayIpAddresses ? ( - gatewayIpAddresses.map((gatewayAddress) => ( - - - {gatewayAddress.name} - - - - - - - - - )) - ) : ( - - )} -
-
-
- - Internet Gateway IP Pool - {gatewayIpPools && gatewayIpPools.length > 1 ? 's' : ''} - -
- {gatewayIpPools ? ( - gatewayIpPools.map((gatewayPool) => ( - - - {gatewayPool.name} - - - - - - - - - )) - ) : ( - - )} -
-
-
-
- - - -
+ + {internetGateway.name} + + } + form={form} + // TODO: pass actual error when this form is hooked up + submitError={null} + loading={false} + > + + {internetGateway.name} + + + + + + This is a read-only copy of this internet gateway. Use the CLI to create and + update internet gateways. More functionality for internet gateways will be + included in future releases of the Oxide console. + + } + /> + + IP Addresses + + {gatewayIpAddresses ? ( +
+ + Name + Address + + + {gatewayIpAddresses.map((gatewayIpAddress) => ( + + + + + + + + + ))} + +
+ ) : ( + + )} + + + IP Pools + + {gatewayIpPools ? ( + + + Name + Description + + + {gatewayIpPools.map((gatewayIpPool) => ( + + + + + + + + + + + ))} + +
+ ) : ( + + )} + + {/* insert routes that are associated with this gateway */} + ) } From ec8b67b42eeab5684ed22248ad5f271e16ef7f20 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 6 Dec 2024 09:58:47 -0800 Subject: [PATCH 38/63] Add IP Address and IP Pool columns to Gateway table --- .../vpcs/VpcPage/tabs/VpcGatewaysTab.tsx | 62 +++++++++++++++++-- mock-api/internet-gateway.ts | 33 ---------- mock-api/msw/db.ts | 4 +- 3 files changed, 61 insertions(+), 38 deletions(-) diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx index 6f206da56..12bbff3de 100644 --- a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx @@ -6,18 +6,43 @@ * Copyright Oxide Computer Company */ +import { useQuery } from '@tanstack/react-query' import { createColumnHelper } from '@tanstack/react-table' import { useMemo } from 'react' import { Outlet, type LoaderFunctionArgs } from 'react-router-dom' -import { getListQFn, queryClient, type InternetGateway } from '~/api' +import { apiq, getListQFn, queryClient, type InternetGateway } from '~/api' import { getVpcSelector, useVpcSelector } from '~/hooks/use-params' +import { EmptyCell } from '~/table/cells/EmptyCell' +import { IpPoolCell } from '~/table/cells/IpPoolCell' import { makeLinkCell } from '~/table/cells/LinkCell' import { Columns } from '~/table/columns/common' import { useQueryTable } from '~/table/QueryTable' +import { CopyableIp } from '~/ui/lib/CopyableIp' import { EmptyMessage } from '~/ui/lib/EmptyMessage' +import { ALL_ISH } from '~/util/consts' import { pb } from '~/util/path-builder' +const InternetGatewayIpAddressCell = ({ gatewayId }: { gatewayId: string }) => { + const { data: addresses } = useQuery( + getListQFn('internetGatewayIpAddressList', { + query: { gateway: gatewayId }, + }).optionsFn() + ) + if (!addresses || addresses.items.length < 1) return + return +} + +const InternetGatewayIpPoolCell = ({ gatewayId }: { gatewayId: string }) => { + const { data: gateways } = useQuery( + getListQFn('internetGatewayIpPoolList', { + query: { gateway: gatewayId }, + }).optionsFn() + ) + if (!gateways || gateways.items.length < 1) return + return +} + const colHelper = createColumnHelper() type VpcParams = { project: string; vpc: string } @@ -26,7 +51,33 @@ const gatewayList = (query: VpcParams) => getListQFn('internetGatewayList', { qu VpcInternetGatewaysTab.loader = async ({ params }: LoaderFunctionArgs) => { const vpcSelector = getVpcSelector(params) - await queryClient.prefetchQuery(gatewayList(vpcSelector).optionsFn()) + const gateways = await queryClient.fetchQuery(gatewayList(vpcSelector).optionsFn()) + await Promise.all([ + ...gateways.items.flatMap((gateway: InternetGateway) => { + return [ + queryClient.prefetchQuery( + getListQFn('internetGatewayIpAddressList', { + query: { gateway: gateway.id }, + }).optionsFn() + ), + queryClient.prefetchQuery( + getListQFn('internetGatewayIpPoolList', { + query: { gateway: gateway.id }, + }).optionsFn() + ), + ] + }), + queryClient + .fetchQuery( + getListQFn('projectIpPoolList', { query: { limit: ALL_ISH } }).optionsFn() + ) + .then((pools) => { + for (const pool of pools.items) { + const { queryKey } = apiq('projectIpPoolView', { path: { pool: pool.id } }) + queryClient.setQueryData(queryKey, pool) + } + }), + ]) return null } @@ -49,11 +100,14 @@ export function VpcInternetGatewaysTab() { }), colHelper.accessor('description', Columns.description), // add a column for the IP Pool associated with this Internet Gateway + colHelper.accessor('id', { + header: 'IP Address', + cell: (info) => , + }), colHelper.accessor('id', { header: 'IP Pool', - cell: (info) => info.getValue(), + cell: (info) => , }), - colHelper.accessor('timeCreated', Columns.timeCreated), ], [vpcSelector] diff --git a/mock-api/internet-gateway.ts b/mock-api/internet-gateway.ts index 56fad67d1..93c1cfeb1 100644 --- a/mock-api/internet-gateway.ts +++ b/mock-api/internet-gateway.ts @@ -65,19 +65,8 @@ const internetGatewayIpAddress1: Json = { time_modified, } -const internetGatewayIpAddress2: Json = { - id: 'f1d5e5a1-0b2b-4d5b-8b9d-2d4b3e0c6b9e', - name: 'internet-gateway-address-2', - address: '292a:a05c:3b36:a053:9166:6510:2d6b:3322', - description: 'an IPv6 address for an internet gateway', - internet_gateway_id: internetGateway2.id, - time_created, - time_modified, -} - export const internetGatewayIpAddresses: Json[] = [ internetGatewayIpAddress1, - internetGatewayIpAddress2, ] const internetGatewayIpPool1: Json = { @@ -95,26 +84,6 @@ const internetGatewayIpPool2: Json = { name: 'interent-gateway-pool-2', description: 'another IP pool for an internet gateway', internet_gateway_id: internetGateway2.id, - ip_pool_id: ipPool1.id, - time_created, - time_modified, -} - -const internetGatewayIpPool3: Json = { - id: 'f1d5e5a1-0b2b-4d5b-8b9d-2d4b3e0c6b9g', - name: 'internet-gateway-pool-3', - description: 'a third pool for an internet gateway', - internet_gateway_id: internetGateway2.id, - ip_pool_id: ipPool1.id, - time_created, - time_modified, -} - -const internetGatewayIpPool4: Json = { - id: 'f1d5e5a1-0b2b-4d5b-8b9d-2d4b3e0c6b9h', - name: 'internet-gateway-pool-4', - description: 'a set of VPN IPs in an IP pool for an internet gateway', - internet_gateway_id: internetGateway2.id, ip_pool_id: ipPool2.id, time_created, time_modified, @@ -123,6 +92,4 @@ const internetGatewayIpPool4: Json = { export const internetGatewayIpPools: Json[] = [ internetGatewayIpPool1, internetGatewayIpPool2, - internetGatewayIpPool3, - internetGatewayIpPool4, ] diff --git a/mock-api/msw/db.ts b/mock-api/msw/db.ts index 8474a88f2..9f41dd5d2 100644 --- a/mock-api/msw/db.ts +++ b/mock-api/msw/db.ts @@ -30,6 +30,8 @@ export const lookupById = (table: T[], id: string) => return item } +const paginationParams = ['limit', 'page_token', 'sort_by'] + /** * Given an object representing (potentially) parent selectors for a resource, * throw an error if any of the keys in that object have truthy values. For @@ -43,7 +45,7 @@ function ensureNoParentSelectors( parentSelector: Record ) { const keysWithValues = Object.entries(parentSelector) - .filter(([_, v]) => v) + .filter(([k, v]) => v && !paginationParams.includes(k)) .map(([k]) => k) if (keysWithValues.length > 0) { const message = `when ${resourceLabel} is specified by ID, ${commaSeries(keysWithValues, 'and')} should not be specified` From 001971ac89ecdcb6ae53e7536d361f832794c311 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 6 Dec 2024 13:22:50 -0800 Subject: [PATCH 39/63] test update --- test/e2e/vpcs.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/vpcs.e2e.ts b/test/e2e/vpcs.e2e.ts index dadcc16be..248d5ea59 100644 --- a/test/e2e/vpcs.e2e.ts +++ b/test/e2e/vpcs.e2e.ts @@ -210,7 +210,7 @@ test('can’t create or delete Routes on system routers', async ({ page }) => { // expect to see table of routes const table = page.getByRole('table') const routeRows = table.locator('tbody >> tr') - await expect(routeRows).toHaveCount(3) + await expect(routeRows).toHaveCount(4) await expectRowVisible(table, { Name: 'default' }) await expectRowVisible(table, { Name: 'default-v4' }) await expectRowVisible(table, { Name: 'default-v6' }) From 79991a77b67626be08b6d1c7a2261d5d66519615 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 6 Dec 2024 13:57:05 -0800 Subject: [PATCH 40/63] small tweaks to sidebar --- .../project/vpcs/internet-gateway-edit.tsx | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/app/pages/project/vpcs/internet-gateway-edit.tsx b/app/pages/project/vpcs/internet-gateway-edit.tsx index d20fe65eb..8565384dc 100644 --- a/app/pages/project/vpcs/internet-gateway-edit.tsx +++ b/app/pages/project/vpcs/internet-gateway-edit.tsx @@ -17,7 +17,7 @@ import { SideModalForm } from '~/components/form/SideModalForm' import { getInternetGatewaySelector, useInternetGatewaySelector } from '~/hooks/use-params' import { DescriptionCell } from '~/table/cells/DescriptionCell' import { EmptyCell } from '~/table/cells/EmptyCell' -import { LinkCell } from '~/table/cells/LinkCell' +import { IpPoolCell } from '~/table/cells/IpPoolCell' import { FormDivider } from '~/ui/lib/Divider' import { Message } from '~/ui/lib/Message' import { PropertiesTable } from '~/ui/lib/PropertiesTable' @@ -97,21 +97,25 @@ export function EditInternetGatewayForm() { } /> - IP Addresses + Internet Gateway IP Addresses {gatewayIpAddresses ? ( Name + Description Address {gatewayIpAddresses.map((gatewayIpAddress) => ( - + - + + + + @@ -123,25 +127,27 @@ export function EditInternetGatewayForm() { )} - IP Pools + Internet Gateway IP Pools {gatewayIpPools ? (
Name Description + IP Pool {gatewayIpPools.map((gatewayIpPool) => ( - - - - + + - + + + + ))} From 109a60c49b87dbc1cffc192f1aaebdd69d682c35 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 6 Dec 2024 15:47:00 -0800 Subject: [PATCH 41/63] reverting back to vertical table for now, to render IP Pool alonside join table info --- .../tabs/InternetGatewayIpAddressesTab.tsx | 62 ---------- .../tabs/InternetGatewayIpPoolsTab.tsx | 90 -------------- .../project/vpcs/internet-gateway-edit.tsx | 110 ++++++++---------- 3 files changed, 51 insertions(+), 211 deletions(-) delete mode 100644 app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpAddressesTab.tsx delete mode 100644 app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx diff --git a/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpAddressesTab.tsx b/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpAddressesTab.tsx deleted file mode 100644 index b395f3d1e..000000000 --- a/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpAddressesTab.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ - -import { createColumnHelper } from '@tanstack/react-table' -import type { LoaderFunctionArgs } from 'react-router-dom' - -import { getListQFn, queryClient, type InternetGatewayIpAddress } from '~/api' -import { getInternetGatewaySelector, useInternetGatewaySelector } from '~/hooks/use-params' -import { useColsWithActions, type MenuAction } from '~/table/columns/action-col' -import { Columns } from '~/table/columns/common' -import { useQueryTable } from '~/table/QueryTable' -import { CopyableIp } from '~/ui/lib/CopyableIp' -import { EmptyMessage } from '~/ui/lib/EmptyMessage' -import type * as PP from '~/util/path-params' - -const gatewayIpList = (query: PP.VpcInternetGateway) => - getListQFn('internetGatewayIpAddressList', { query }) - -InternetGatewayIpAddressesTab.loader = async function ({ params }: LoaderFunctionArgs) { - const gatewaySelector = getInternetGatewaySelector(params) - await queryClient.prefetchQuery(gatewayIpList(gatewaySelector).optionsFn()) - return null -} - -const colHelper = createColumnHelper() - -const staticColumns = [ - colHelper.accessor('name', {}), - colHelper.accessor('description', Columns.description), - colHelper.accessor('address', { - header: 'Address', - cell: (info) => , - }), -] - -export function InternetGatewayIpAddressesTab() { - const gatewaySelector = useInternetGatewaySelector() - - const emptyState = ( - - ) - - const makeActions = (): MenuAction[] => [] - - const columns = useColsWithActions(staticColumns, makeActions) - - const { table } = useQueryTable({ - query: gatewayIpList(gatewaySelector), - columns, - emptyState, - }) - - return <>{table} -} diff --git a/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx b/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx deleted file mode 100644 index 8e0c716ba..000000000 --- a/app/pages/project/vpcs/InternetGatewayPage/tabs/InternetGatewayIpPoolsTab.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ - -import { createColumnHelper } from '@tanstack/react-table' -import { useCallback } from 'react' -import type { LoaderFunctionArgs } from 'react-router-dom' - -import { apiQueryClient, getListQFn, queryClient, type InternetGatewayIpPool } from '~/api' -import { getInternetGatewaySelector, useInternetGatewaySelector } from '~/hooks/use-params' -import { IpPoolCell } from '~/table/cells/IpPoolCell' -import { useColsWithActions, type MenuAction } from '~/table/columns/action-col' -import { Columns } from '~/table/columns/common' -import { useQueryTable } from '~/table/QueryTable' -import { EmptyMessage } from '~/ui/lib/EmptyMessage' -import { ALL_ISH } from '~/util/consts' -import type * as PP from '~/util/path-params' - -const gatewayIpPoolList = (query: PP.VpcInternetGateway) => - getListQFn('internetGatewayIpPoolList', { query }) - -InternetGatewayIpPoolsTab.loader = async function ({ params }: LoaderFunctionArgs) { - const { project, vpc, gateway } = getInternetGatewaySelector(params) - await Promise.all([ - queryClient.prefetchQuery(gatewayIpPoolList({ project, vpc, gateway }).optionsFn()), - // fetch IP Pools and preload into RQ cache so fetches by ID in - // IpPoolCell can be mostly instant yet gracefully fall back to - // fetching individually if we don't fetch them all here - apiQueryClient - .fetchQuery('projectIpPoolList', { query: { limit: ALL_ISH } }) - .then((pools) => { - console.log({ pools }) - for (const pool of pools.items) { - apiQueryClient.setQueryData( - 'projectIpPoolView', - { path: { pool: pool.id } }, - pool - ) - } - }), - ]) - return null -} - -const colHelper = createColumnHelper() - -const staticColumns = [ - colHelper.accessor('name', {}), - colHelper.accessor('description', Columns.description), - colHelper.accessor('ipPoolId', { - header: 'IP Pool', - cell: (info) => , - }), -] - -export function InternetGatewayIpPoolsTab() { - const gatewaySelector = useInternetGatewaySelector() - - const emptyState = ( - - ) - - // The user can copy the ID of the IP Pool attached to this internet gateway - const makeActions = useCallback( - (internetGatewayIpPool: InternetGatewayIpPool): MenuAction[] => [ - { - label: 'Copy IP pool ID', - onActivate() { - window.navigator.clipboard.writeText(internetGatewayIpPool.ipPoolId) - }, - }, - ], - [] - ) - - const columns = useColsWithActions(staticColumns, makeActions) - const { table } = useQueryTable({ - query: gatewayIpPoolList(gatewaySelector), - columns, - emptyState, - }) - return table -} diff --git a/app/pages/project/vpcs/internet-gateway-edit.tsx b/app/pages/project/vpcs/internet-gateway-edit.tsx index 8565384dc..3569e971f 100644 --- a/app/pages/project/vpcs/internet-gateway-edit.tsx +++ b/app/pages/project/vpcs/internet-gateway-edit.tsx @@ -16,14 +16,12 @@ import { apiQueryClient, getListQFn, queryClient, usePrefetchedApiQuery } from ' import { SideModalForm } from '~/components/form/SideModalForm' import { getInternetGatewaySelector, useInternetGatewaySelector } from '~/hooks/use-params' import { DescriptionCell } from '~/table/cells/DescriptionCell' -import { EmptyCell } from '~/table/cells/EmptyCell' import { IpPoolCell } from '~/table/cells/IpPoolCell' +import { CopyableIp } from '~/ui/lib/CopyableIp' import { FormDivider } from '~/ui/lib/Divider' import { Message } from '~/ui/lib/Message' import { PropertiesTable } from '~/ui/lib/PropertiesTable' import { ResourceLabel, SideModal } from '~/ui/lib/SideModal' -import { Table } from '~/ui/lib/Table' -import { Truncate } from '~/ui/lib/Truncate' import { pb } from '~/util/path-builder' import type * as PP from '~/util/path-params' @@ -63,6 +61,7 @@ export function EditInternetGatewayForm() { const form = useForm({}) + const hasAttachedPool = gatewayIpPools && gatewayIpPools.length > 0 return ( - Internet Gateway IP Addresses +
+ + Internet Gateway IP Address + {gatewayIpAddresses && gatewayIpAddresses.length > 1 ? 'es' : ''} + + {gatewayIpAddresses && gatewayIpAddresses.length > 0 ? ( + gatewayIpAddresses.map((gatewayIpAddress) => ( + + + {gatewayIpAddress.name} + + + + + + + + + )) + ) : ( +
+ {'This internet gateway does not have any specific IP addresses attached. '} + {hasAttachedPool && 'It will use an address from the attached IP pool.'} +
+ )} +
- {gatewayIpAddresses ? ( -
- - Name - Description - Address - - - {gatewayIpAddresses.map((gatewayIpAddress) => ( - - - - - - - - - - - - ))} - -
- ) : ( - - )} - Internet Gateway IP Pools - - {gatewayIpPools ? ( - - - Name - Description - IP Pool - - - {gatewayIpPools.map((gatewayIpPool) => ( - - - - - - - - - - - - ))} - -
- ) : ( - - )} - +
+ + Internet Gateway IP Pool + {gatewayIpPools && gatewayIpPools.length > 1 ? 's' : ''} + + {hasAttachedPool ? ( + gatewayIpPools.map((gatewayIpPool) => ( + + {gatewayIpPool.name} + + + + + + + + )) + ) : ( +
+ This internet gateway does not have any IP pools attached. +
+ )} +
{/* insert routes that are associated with this gateway */} ) From abb82ba683954ca0ac539412d1b17f074e9b31d0 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 6 Dec 2024 15:49:20 -0800 Subject: [PATCH 42/63] Update copy when missing pool or ip address --- app/pages/project/vpcs/internet-gateway-edit.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/pages/project/vpcs/internet-gateway-edit.tsx b/app/pages/project/vpcs/internet-gateway-edit.tsx index 3569e971f..13ddb11e5 100644 --- a/app/pages/project/vpcs/internet-gateway-edit.tsx +++ b/app/pages/project/vpcs/internet-gateway-edit.tsx @@ -118,7 +118,9 @@ export function EditInternetGatewayForm() { ) : (
{'This internet gateway does not have any specific IP addresses attached. '} - {hasAttachedPool && 'It will use an address from the attached IP pool.'} + {hasAttachedPool + ? 'It will use an address from the attached IP pool.' + : 'Use the CLI to attach an IP Pool or specify an IP address to use with this gateway.'}
)} From c5013d0e18dd3c99dd247a43134b5e8552f36295 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 9 Dec 2024 12:58:51 -0800 Subject: [PATCH 43/63] Add a test for internet gateway list and sidemodal --- test/e2e/vpcs.e2e.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/e2e/vpcs.e2e.ts b/test/e2e/vpcs.e2e.ts index 248d5ea59..a6718c46d 100644 --- a/test/e2e/vpcs.e2e.ts +++ b/test/e2e/vpcs.e2e.ts @@ -319,3 +319,40 @@ test('edit and delete router route', async ({ page }) => { await expect(table).toBeHidden() await expect(page.getByText('No routes')).toBeVisible() }) + +test('can view internet gateways', async ({ page }) => { + await page.goto('/projects/mock-project/vpcs/mock-vpc') + await page.getByRole('tab', { name: 'Internet Gateways' }).click() + + const table = page.getByRole('table') + const rows = table.locator('tbody >> tr') + await expect(rows).toHaveCount(2) + + await expectRowVisible(table, { + name: 'internet-gateway-1', + description: 'internet gateway 1', + 'IP Address': '123.4.56.3', + 'IP Pool': 'ip-pool-1', + }) + await expectRowVisible(table, { + name: 'internet-gateway-2', + description: 'internet gateway 2', + 'IP Address': '—', + 'IP Pool': 'ip-pool-2', + }) + + await page.getByRole('link', { name: 'internet-gateway-1' }).click() + await expect(page).toHaveURL( + '/projects/mock-project/vpcs/mock-vpc/internet-gateways/internet-gateway-1' + ) + const sidemodal = page.getByLabel('Internet Gateway') + + await expect(sidemodal.getByText('123.4.56.3')).toBeVisible() + + // close the sidemodal + await sidemodal.getByRole('button', { name: 'Close' }).click() + await expect(sidemodal).toBeHidden() + + await page.getByRole('link', { name: 'internet-gateway-2' }).click() + await expect(sidemodal.getByText('This internet gateway does not have any')).toBeVisible() +}) From c758b5cb5f8db29e6128b6f19edc17071cd394ce Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 10 Dec 2024 10:34:46 -0800 Subject: [PATCH 44/63] Add routes targeting gateway to table --- app/forms/vpc-router-route-edit.tsx | 1 + .../vpcs/VpcPage/tabs/VpcGatewaysTab.tsx | 153 ++++++++++++------ 2 files changed, 109 insertions(+), 45 deletions(-) diff --git a/app/forms/vpc-router-route-edit.tsx b/app/forms/vpc-router-route-edit.tsx index d54fba40d..78a94f066 100644 --- a/app/forms/vpc-router-route-edit.tsx +++ b/app/forms/vpc-router-route-edit.tsx @@ -69,6 +69,7 @@ export function EditRouterRouteSideModalForm() { const updateRouterRoute = useApiMutation('vpcRouterRouteUpdate', { onSuccess(updatedRoute) { queryClient.invalidateQueries('vpcRouterRouteList') + queryClient.invalidateQueries('vpcRouterRouteView') addToast(<>Route {updatedRoute.name} updated) // prettier-ignore navigate(pb.vpcRouter(routerSelector)) }, diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx index 12bbff3de..d0a9ed8b1 100644 --- a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx @@ -9,7 +9,7 @@ import { useQuery } from '@tanstack/react-query' import { createColumnHelper } from '@tanstack/react-table' import { useMemo } from 'react' -import { Outlet, type LoaderFunctionArgs } from 'react-router-dom' +import { Link, Outlet, type LoaderFunctionArgs } from 'react-router-dom' import { apiq, getListQFn, queryClient, type InternetGateway } from '~/api' import { getVpcSelector, useVpcSelector } from '~/hooks/use-params' @@ -23,66 +23,110 @@ import { EmptyMessage } from '~/ui/lib/EmptyMessage' import { ALL_ISH } from '~/util/consts' import { pb } from '~/util/path-builder' +type VpcParams = { project: string; vpc: string } +type GatewayParams = VpcParams & { gateway: string } +type RouterParams = GatewayParams & { router: string } + +const gatewayList = (query: VpcParams) => + getListQFn('internetGatewayList', { query: { ...query, limit: ALL_ISH } }) +const routerList = (query: VpcParams) => + getListQFn('vpcRouterList', { query: { ...query, limit: ALL_ISH } }) +const routeList = (query: VpcParams & { router: string }) => + getListQFn('vpcRouterRouteList', { query: { ...query, limit: ALL_ISH } }) +const gatewayIpAddressList = (query: { gatewayId: string }) => + getListQFn('internetGatewayIpAddressList', { query: { gateway: query.gatewayId } }) +const gatewayIpPoolList = (query: { gatewayId: string }) => + getListQFn('internetGatewayIpPoolList', { query: { gateway: query.gatewayId } }) +const projectIpPoolList = getListQFn('projectIpPoolList', { query: { limit: ALL_ISH } }) + const InternetGatewayIpAddressCell = ({ gatewayId }: { gatewayId: string }) => { - const { data: addresses } = useQuery( - getListQFn('internetGatewayIpAddressList', { - query: { gateway: gatewayId }, - }).optionsFn() - ) + const { data: addresses } = useQuery(gatewayIpAddressList({ gatewayId }).optionsFn()) if (!addresses || addresses.items.length < 1) return return } const InternetGatewayIpPoolCell = ({ gatewayId }: { gatewayId: string }) => { - const { data: gateways } = useQuery( - getListQFn('internetGatewayIpPoolList', { - query: { gateway: gatewayId }, - }).optionsFn() - ) + const { data: gateways } = useQuery(gatewayIpPoolList({ gatewayId }).optionsFn()) if (!gateways || gateways.items.length < 1) return return } -const colHelper = createColumnHelper() +// called by InternetGatewayAttachedRoutesCell to get the routes per router +// we need to have this in its own function because useQuery cannot be called inside a loop +const InternetGatewayIndividualRoute = ({ + project, + vpc, + gateway, + router, +}: RouterParams) => { + const matchingRoutes: JSX.Element[] = [] + const { data: routes } = useQuery(routeList({ project, vpc, router }).optionsFn()) + if (!routes || routes.items.length < 1) return + routes.items.forEach((route) => { + if (route.target.type === 'internet_gateway' && route.target.value === gateway) { + matchingRoutes.push( + + {route.name} + + ) + } + }) + return matchingRoutes +} -type VpcParams = { project: string; vpc: string } +const InternetGatewayAttachedRoutesCell = ({ project, vpc, gateway }: GatewayParams) => { + const { data: routers } = useQuery(routerList({ project, vpc }).optionsFn()) + const matchingRoutes = routers?.items.flatMap((router) => + InternetGatewayIndividualRoute({ project, vpc, gateway, router: router.name }) + ) + return matchingRoutes?.length ? <>{matchingRoutes} : +} -const gatewayList = (query: VpcParams) => getListQFn('internetGatewayList', { query }) +const colHelper = createColumnHelper() VpcInternetGatewaysTab.loader = async ({ params }: LoaderFunctionArgs) => { - const vpcSelector = getVpcSelector(params) - const gateways = await queryClient.fetchQuery(gatewayList(vpcSelector).optionsFn()) + const { project, vpc } = getVpcSelector(params) await Promise.all([ - ...gateways.items.flatMap((gateway: InternetGateway) => { - return [ + (await queryClient.fetchQuery(gatewayList({ project, vpc }).optionsFn())).items.flatMap( + (gateway: InternetGateway) => { + return [ + queryClient.prefetchQuery( + gatewayIpAddressList({ gatewayId: gateway.id }).optionsFn() + ), + queryClient.prefetchQuery( + gatewayIpPoolList({ gatewayId: gateway.id }).optionsFn() + ), + ] + } + ), + (await queryClient.fetchQuery(routerList({ project, vpc }).optionsFn())).items.map( + (router) => { queryClient.prefetchQuery( - getListQFn('internetGatewayIpAddressList', { - query: { gateway: gateway.id }, - }).optionsFn() - ), - queryClient.prefetchQuery( - getListQFn('internetGatewayIpPoolList', { - query: { gateway: gateway.id }, - }).optionsFn() - ), - ] + routeList({ project, vpc, router: router.name }).optionsFn() + ) + } + ), + queryClient.fetchQuery(projectIpPoolList.optionsFn()).then((pools) => { + for (const pool of pools.items) { + const { queryKey } = apiq('projectIpPoolView', { path: { pool: pool.id } }) + queryClient.setQueryData(queryKey, pool) + } }), - queryClient - .fetchQuery( - getListQFn('projectIpPoolList', { query: { limit: ALL_ISH } }).optionsFn() - ) - .then((pools) => { - for (const pool of pools.items) { - const { queryKey } = apiq('projectIpPoolView', { path: { pool: pool.id } }) - queryClient.setQueryData(queryKey, pool) - } - }), ]) return null } export function VpcInternetGatewaysTab() { - const vpcSelector = useVpcSelector() + const { project, vpc } = useVpcSelector() const emptyState = ( [ colHelper.accessor('name', { - cell: makeLinkCell((gateway) => pb.vpcInternetGateway({ ...vpcSelector, gateway })), + cell: makeLinkCell((gateway) => pb.vpcInternetGateway({ project, vpc, gateway })), }), colHelper.accessor('description', Columns.description), - // add a column for the IP Pool associated with this Internet Gateway colHelper.accessor('id', { - header: 'IP Address', + // ID needed to avoid key collision with other name column + id: 'ip-address', + header: 'Attached IP Address', cell: (info) => , }), colHelper.accessor('id', { - header: 'IP Pool', + // ID needed to avoid key collision with other name column + id: 'ip-pool', + header: 'Attached IP Pool', cell: (info) => , }), + colHelper.accessor('name', { + // ID needed to avoid key collision with other name column + id: 'routes', + header: 'Routes targeting this gateway', + cell: (info) => ( + + ), + }), colHelper.accessor('timeCreated', Columns.timeCreated), ], - [vpcSelector] + [project, vpc] ) - const { table } = useQueryTable({ query: gatewayList(vpcSelector), columns, emptyState }) + const { table } = useQueryTable({ + query: gatewayList({ project, vpc }), + columns, + emptyState, + }) return ( <> From 7ec29f9e3c550c11891e18963e50666066b7f9e0 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 10 Dec 2024 10:46:36 -0800 Subject: [PATCH 45/63] Better handle multiple route spacing; fix test --- app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx | 8 ++++++-- test/e2e/vpcs.e2e.ts | 8 ++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx index d0a9ed8b1..d6b074d30 100644 --- a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx @@ -88,7 +88,11 @@ const InternetGatewayAttachedRoutesCell = ({ project, vpc, gateway }: GatewayPar const matchingRoutes = routers?.items.flatMap((router) => InternetGatewayIndividualRoute({ project, vpc, gateway, router: router.name }) ) - return matchingRoutes?.length ? <>{matchingRoutes} : + return matchingRoutes?.length ? ( +
{matchingRoutes}
+ ) : ( + + ) } const colHelper = createColumnHelper() @@ -158,7 +162,7 @@ export function VpcInternetGatewaysTab() { colHelper.accessor('name', { // ID needed to avoid key collision with other name column id: 'routes', - header: 'Routes targeting this gateway', + header: 'Routes', cell: (info) => ( { await expectRowVisible(table, { name: 'internet-gateway-1', description: 'internet gateway 1', - 'IP Address': '123.4.56.3', - 'IP Pool': 'ip-pool-1', + 'Attached IP Address': '123.4.56.3', + 'Attached IP Pool': 'ip-pool-1', }) await expectRowVisible(table, { name: 'internet-gateway-2', description: 'internet gateway 2', - 'IP Address': '—', - 'IP Pool': 'ip-pool-2', + 'Attached IP Address': '—', + 'Attached IP Pool': 'ip-pool-2', }) await page.getByRole('link', { name: 'internet-gateway-1' }).click() From a39c154900f2e3b997181fbdc2967ae2ad4c6b7d Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 10 Dec 2024 13:28:42 -0800 Subject: [PATCH 46/63] Update side modal with routes targeting gateway --- .../vpcs/VpcPage/tabs/VpcGatewaysTab.tsx | 19 ++-- .../project/vpcs/internet-gateway-edit.tsx | 87 ++++++++++++++++++- 2 files changed, 93 insertions(+), 13 deletions(-) diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx index d6b074d30..295f58725 100644 --- a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx @@ -22,16 +22,13 @@ import { CopyableIp } from '~/ui/lib/CopyableIp' import { EmptyMessage } from '~/ui/lib/EmptyMessage' import { ALL_ISH } from '~/util/consts' import { pb } from '~/util/path-builder' +import type * as PP from '~/util/path-params' -type VpcParams = { project: string; vpc: string } -type GatewayParams = VpcParams & { gateway: string } -type RouterParams = GatewayParams & { router: string } - -const gatewayList = (query: VpcParams) => +const gatewayList = (query: PP.Vpc) => getListQFn('internetGatewayList', { query: { ...query, limit: ALL_ISH } }) -const routerList = (query: VpcParams) => +const routerList = (query: PP.Vpc) => getListQFn('vpcRouterList', { query: { ...query, limit: ALL_ISH } }) -const routeList = (query: VpcParams & { router: string }) => +const routeList = (query: PP.VpcRouter) => getListQFn('vpcRouterRouteList', { query: { ...query, limit: ALL_ISH } }) const gatewayIpAddressList = (query: { gatewayId: string }) => getListQFn('internetGatewayIpAddressList', { query: { gateway: query.gatewayId } }) @@ -58,7 +55,7 @@ const InternetGatewayIndividualRoute = ({ vpc, gateway, router, -}: RouterParams) => { +}: PP.VpcInternetGateway & { router: string }) => { const matchingRoutes: JSX.Element[] = [] const { data: routes } = useQuery(routeList({ project, vpc, router }).optionsFn()) if (!routes || routes.items.length < 1) return @@ -83,7 +80,11 @@ const InternetGatewayIndividualRoute = ({ return matchingRoutes } -const InternetGatewayAttachedRoutesCell = ({ project, vpc, gateway }: GatewayParams) => { +const InternetGatewayAttachedRoutesCell = ({ + project, + vpc, + gateway, +}: PP.VpcInternetGateway) => { const { data: routers } = useQuery(routerList({ project, vpc }).optionsFn()) const matchingRoutes = routers?.items.flatMap((router) => InternetGatewayIndividualRoute({ project, vpc, gateway, router: router.name }) diff --git a/app/pages/project/vpcs/internet-gateway-edit.tsx b/app/pages/project/vpcs/internet-gateway-edit.tsx index 13ddb11e5..dfa4d401d 100644 --- a/app/pages/project/vpcs/internet-gateway-edit.tsx +++ b/app/pages/project/vpcs/internet-gateway-edit.tsx @@ -8,7 +8,7 @@ import { useQuery } from '@tanstack/react-query' import { useForm } from 'react-hook-form' -import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom' +import { Link, useNavigate, type LoaderFunctionArgs } from 'react-router-dom' import { Gateway16Icon } from '@oxide/design-system/icons/react' @@ -22,13 +22,67 @@ import { FormDivider } from '~/ui/lib/Divider' import { Message } from '~/ui/lib/Message' import { PropertiesTable } from '~/ui/lib/PropertiesTable' import { ResourceLabel, SideModal } from '~/ui/lib/SideModal' +import { Table } from '~/ui/lib/Table' +import { ALL_ISH } from '~/util/consts' import { pb } from '~/util/path-builder' import type * as PP from '~/util/path-params' +const RouterRow = ({ + project, + vpc, + gateway, + router, +}: PP.VpcInternetGateway & { router: string }) => { + const matchingRoutes: JSX.Element[] = [] + const { data: routes } = useQuery(routeList({ project, vpc, router }).optionsFn()) + if (!routes || routes.items.length < 1) return + routes.items.forEach((route) => { + if (route.target.type === 'internet_gateway' && route.target.value === gateway) { + matchingRoutes.push( + + {router} + + + {route.name} + + + + ) + } + }) + return matchingRoutes +} + +const RouterRows = ({ project, vpc, gateway }: PP.VpcInternetGateway) => { + const { data: routers } = useQuery(routerList({ project, vpc }).optionsFn()) + const matchingRoutes = routers?.items.flatMap((router) => + RouterRow({ project, vpc, gateway, router: router.name }) + ) + return matchingRoutes?.length ? ( + matchingRoutes + ) : ( + + No VPC routes target this gateway. + + ) +} + const gatewayIpPoolList = (query: PP.VpcInternetGateway) => getListQFn('internetGatewayIpPoolList', { query }) const gatewayIpAddressList = (query: PP.VpcInternetGateway) => getListQFn('internetGatewayIpAddressList', { query }) +const routerList = (query: PP.Vpc) => + getListQFn('vpcRouterList', { query: { ...query, limit: ALL_ISH } }) +const routeList = (query: PP.VpcRouter) => + getListQFn('vpcRouterRouteList', { query: { ...query, limit: ALL_ISH } }) EditInternetGatewayForm.loader = async function ({ params }: LoaderFunctionArgs) { const { project, vpc, gateway } = getInternetGatewaySelector(params) @@ -40,6 +94,13 @@ EditInternetGatewayForm.loader = async function ({ params }: LoaderFunctionArgs) // apiQueryClient.prefetchQuery('internetGatewayIpAddressList', { query }), queryClient.prefetchQuery(gatewayIpPoolList({ project, vpc, gateway }).optionsFn()), queryClient.prefetchQuery(gatewayIpAddressList({ project, vpc, gateway }).optionsFn()), + (await queryClient.fetchQuery(routerList({ project, vpc }).optionsFn())).items.map( + (router) => { + queryClient.prefetchQuery( + routeList({ project, vpc, router: router.name }).optionsFn() + ) + } + ), ]) return null } @@ -62,6 +123,7 @@ export function EditInternetGatewayForm() { const form = useForm({}) const hasAttachedPool = gatewayIpPools && gatewayIpPools.length > 0 + return ( -
+
Internet Gateway IP Address {gatewayIpAddresses && gatewayIpAddresses.length > 1 ? 'es' : ''} @@ -127,7 +189,7 @@ export function EditInternetGatewayForm() { -
+
Internet Gateway IP Pool {gatewayIpPools && gatewayIpPools.length > 1 ? 's' : ''} @@ -139,7 +201,7 @@ export function EditInternetGatewayForm() { - + @@ -151,6 +213,23 @@ export function EditInternetGatewayForm() { )}
{/* insert routes that are associated with this gateway */} + + +
+ + Routes targeting this gateway + {gatewayIpPools && gatewayIpPools.length > 1 ? 's' : ''} + + + + Router + Route + + + + +
+
) } From 0f5b849545a6e4cf094bbadfdb5a0994527f9b36 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 10 Dec 2024 15:13:29 -0800 Subject: [PATCH 47/63] Update test for showing route --- .../project/vpcs/internet-gateway-edit.tsx | 7 +-- test/e2e/vpcs.e2e.ts | 56 +++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/app/pages/project/vpcs/internet-gateway-edit.tsx b/app/pages/project/vpcs/internet-gateway-edit.tsx index dfa4d401d..e0ab46197 100644 --- a/app/pages/project/vpcs/internet-gateway-edit.tsx +++ b/app/pages/project/vpcs/internet-gateway-edit.tsx @@ -76,9 +76,9 @@ const RouterRows = ({ project, vpc, gateway }: PP.VpcInternetGateway) => { } const gatewayIpPoolList = (query: PP.VpcInternetGateway) => - getListQFn('internetGatewayIpPoolList', { query }) + getListQFn('internetGatewayIpPoolList', { query: { ...query, limit: ALL_ISH } }) const gatewayIpAddressList = (query: PP.VpcInternetGateway) => - getListQFn('internetGatewayIpAddressList', { query }) + getListQFn('internetGatewayIpAddressList', { query: { ...query, limit: ALL_ISH } }) const routerList = (query: PP.Vpc) => getListQFn('vpcRouterList', { query: { ...query, limit: ALL_ISH } }) const routeList = (query: PP.VpcRouter) => @@ -91,7 +91,6 @@ EditInternetGatewayForm.loader = async function ({ params }: LoaderFunctionArgs) query: { project, vpc }, path: { gateway }, }), - // apiQueryClient.prefetchQuery('internetGatewayIpAddressList', { query }), queryClient.prefetchQuery(gatewayIpPoolList({ project, vpc, gateway }).optionsFn()), queryClient.prefetchQuery(gatewayIpAddressList({ project, vpc, gateway }).optionsFn()), (await queryClient.fetchQuery(routerList({ project, vpc }).optionsFn())).items.map( @@ -212,7 +211,7 @@ export function EditInternetGatewayForm() {
)}
- {/* insert routes that are associated with this gateway */} +
diff --git a/test/e2e/vpcs.e2e.ts b/test/e2e/vpcs.e2e.ts index 8f5311676..da04502bd 100644 --- a/test/e2e/vpcs.e2e.ts +++ b/test/e2e/vpcs.e2e.ts @@ -356,3 +356,59 @@ test('can view internet gateways', async ({ page }) => { await page.getByRole('link', { name: 'internet-gateway-2' }).click() await expect(sidemodal.getByText('This internet gateway does not have any')).toBeVisible() }) + +test('internet gateway shows proper list of routes targeting it', async ({ page }) => { + // open up the internet gateway detail page for internet-gateway-1 + await page.goto( + '/projects/mock-project/vpcs/mock-vpc/internet-gateways/internet-gateway-1' + ) + // verify that it has a table with the row showing "mock-system-router" and "dc2" + const sidemodal = page.getByRole('dialog', { name: 'Internet Gateway' }) + const table = sidemodal.getByRole('table') + await expectRowVisible(table, { Router: 'mock-system-router', Route: 'dc2' }) + await expect(table.getByText('mock-custom-router')).toBeHidden() + + // close the sidemodal + await sidemodal.getByRole('button', { name: 'Close' }).click() + await expect(sidemodal).toBeHidden() + // go to the Routers tab + await page.getByRole('tab', { name: 'Routers' }).click() + // click on the mock-custom-router to go to the router detail page + await page.getByRole('link', { name: 'mock-custom-router' }).click() + // expect to be on the view page + await expect(page).toHaveURL( + '/projects/mock-project/vpcs/mock-vpc/routers/mock-custom-router' + ) + + await page.getByRole('link', { name: 'mock-custom-router' }).click() + // create a new route + await page.getByRole('link', { name: 'New route' }).click() + await page.getByRole('textbox', { name: 'Name' }).fill('new-route') + await page.getByRole('textbox', { name: 'Destination value' }).fill('1.2.3.4') + await selectOption(page, 'Target type', 'Internet gateway') + await selectOption(page, 'Target value', 'internet-gateway-1') + await page.getByRole('button', { name: 'Create route' }).click() + + // go back to the mock-vpc page by clicking on the link in the header + await page.getByRole('link', { name: 'mock-vpc' }).click() + // click on the internet gateways tab and then the internet-gateway-1 link to go to the detail page + await page.getByRole('tab', { name: 'Internet Gateways' }).click() + await page.getByRole('link', { name: 'internet-gateway-1' }).click() + + // the table should now say "dc2 new-route" + await expect(page.getByText('dc2new-route')).toBeVisible() + // click on the link to go to the detail page + await page.getByRole('link', { name: 'internet-gateway-1' }).click() + + // the new route should be visible in the table + await expectRowVisible(table, { Router: 'mock-system-router', Route: 'dc2' }) + await expectRowVisible(table, { Router: 'mock-custom-router', Route: 'new-route' }) + await expect(table.locator('tbody >> tr')).toHaveCount(2) + + // click on the new-route link to go to the detail page + await sidemodal.getByRole('link', { name: 'new-route' }).click() + // expect to be on the view page + await expect(page).toHaveURL( + '/projects/mock-project/vpcs/mock-vpc/routers/mock-custom-router/routes/new-route/edit' + ) +}) From 2ab18595f70c0d7192806198b0f715ecf563d869 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 10 Dec 2024 16:41:05 -0800 Subject: [PATCH 48/63] Tweaks to sidemodal --- .../project/vpcs/internet-gateway-edit.tsx | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/app/pages/project/vpcs/internet-gateway-edit.tsx b/app/pages/project/vpcs/internet-gateway-edit.tsx index e0ab46197..369cc6ce4 100644 --- a/app/pages/project/vpcs/internet-gateway-edit.tsx +++ b/app/pages/project/vpcs/internet-gateway-edit.tsx @@ -40,8 +40,8 @@ const RouterRow = ({ if (route.target.type === 'internet_gateway' && route.target.value === gateway) { matchingRoutes.push( - {router} - + {router} + { matchingRoutes ) : ( - No VPC routes target this gateway. + + No VPC routes target this gateway. + ) } @@ -139,12 +141,6 @@ export function EditInternetGatewayForm() { submitError={null} loading={false} > - - {internetGateway.name} - - - - } /> + + {internetGateway.name} + + + + +
From bd896232547b4863e4115849af61be09a48c55e4 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 10 Dec 2024 19:32:55 -0800 Subject: [PATCH 49/63] move example gateway route to custom router, not default router --- mock-api/msw/handlers.ts | 4 ++-- mock-api/vpc.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mock-api/msw/handlers.ts b/mock-api/msw/handlers.ts index 0a8390777..9ea126fd9 100644 --- a/mock-api/msw/handlers.ts +++ b/mock-api/msw/handlers.ts @@ -1543,12 +1543,12 @@ export const handlers = makeHandlers({ certificateView: NotImplemented, instanceSerialConsoleStream: NotImplemented, instanceSshPublicKeyList: NotImplemented, + internetGatewayCreate: NotImplemented, + internetGatewayDelete: NotImplemented, internetGatewayIpAddressCreate: NotImplemented, internetGatewayIpAddressDelete: NotImplemented, internetGatewayIpPoolCreate: NotImplemented, internetGatewayIpPoolDelete: NotImplemented, - internetGatewayCreate: NotImplemented, - internetGatewayDelete: NotImplemented, ipPoolServiceRangeAdd: NotImplemented, ipPoolServiceRangeList: NotImplemented, ipPoolServiceRangeRemove: NotImplemented, diff --git a/mock-api/vpc.ts b/mock-api/vpc.ts index 57d719234..7c45dc655 100644 --- a/mock-api/vpc.ts +++ b/mock-api/vpc.ts @@ -136,6 +136,7 @@ export const routerRoutes: Json> = [ }, { ...routeBase, + vpc_router_id: customRouter.id, id: '93g7c4a1-5b0de-4efb-8518-e0bf012a5169', name: 'dc2', description: 'route to datacenter 2', From efb0c5082bd3737f1a4ca4e60041d73a849061df Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 10 Dec 2024 19:38:20 -0800 Subject: [PATCH 50/63] Update tests to reflect gateway route existing on custom router --- test/e2e/vpcs.e2e.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/test/e2e/vpcs.e2e.ts b/test/e2e/vpcs.e2e.ts index da04502bd..d6de8039e 100644 --- a/test/e2e/vpcs.e2e.ts +++ b/test/e2e/vpcs.e2e.ts @@ -210,7 +210,7 @@ test('can’t create or delete Routes on system routers', async ({ page }) => { // expect to see table of routes const table = page.getByRole('table') const routeRows = table.locator('tbody >> tr') - await expect(routeRows).toHaveCount(4) + await expect(routeRows).toHaveCount(3) await expectRowVisible(table, { Name: 'default' }) await expectRowVisible(table, { Name: 'default-v4' }) await expectRowVisible(table, { Name: 'default-v6' }) @@ -279,6 +279,9 @@ test('create router route', async ({ page }) => { test('edit and delete router route', async ({ page }) => { await page.goto('/projects/mock-project/vpcs/mock-vpc/routers/mock-custom-router') + const table = page.getByRole('table') + await expect(table.locator('tbody >> tr')).toHaveCount(2) + const form = page.getByRole('dialog', { name: 'Edit route' }) await expect(form).toBeHidden() @@ -306,7 +309,6 @@ test('edit and delete router route', async ({ page }) => { await submitButton.click() await expect(form).toBeHidden() - const table = page.getByRole('table') await expectRowVisible(table, { Name: 'new-name', Destination: 'VPC subnetmock-subnet', @@ -316,8 +318,8 @@ test('edit and delete router route', async ({ page }) => { // delete the route await clickRowAction(page, 'new-name', 'Delete') await page.getByRole('button', { name: 'Confirm' }).click() - await expect(table).toBeHidden() - await expect(page.getByText('No routes')).toBeVisible() + // expect 1 row in table + await expect(table.locator('tbody >> tr')).toHaveCount(1) }) test('can view internet gateways', async ({ page }) => { @@ -362,11 +364,11 @@ test('internet gateway shows proper list of routes targeting it', async ({ page await page.goto( '/projects/mock-project/vpcs/mock-vpc/internet-gateways/internet-gateway-1' ) - // verify that it has a table with the row showing "mock-system-router" and "dc2" + // verify that it has a table with the row showing "mock-custom-router" and "dc2" const sidemodal = page.getByRole('dialog', { name: 'Internet Gateway' }) const table = sidemodal.getByRole('table') - await expectRowVisible(table, { Router: 'mock-system-router', Route: 'dc2' }) - await expect(table.getByText('mock-custom-router')).toBeHidden() + await expectRowVisible(table, { Router: 'mock-custom-router', Route: 'dc2' }) + await expect(table.locator('tbody >> tr')).toHaveCount(1) // close the sidemodal await sidemodal.getByRole('button', { name: 'Close' }).click() @@ -401,7 +403,7 @@ test('internet gateway shows proper list of routes targeting it', async ({ page await page.getByRole('link', { name: 'internet-gateway-1' }).click() // the new route should be visible in the table - await expectRowVisible(table, { Router: 'mock-system-router', Route: 'dc2' }) + await expectRowVisible(table, { Router: 'mock-custom-router', Route: 'dc2' }) await expectRowVisible(table, { Router: 'mock-custom-router', Route: 'new-route' }) await expect(table.locator('tbody >> tr')).toHaveCount(2) From 49dfca37cccce8e72accf841ccf4217b11ccb862 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 11 Dec 2024 11:34:13 -0800 Subject: [PATCH 51/63] use more specific params in queries --- .../vpcs/VpcPage/tabs/VpcGatewaysTab.tsx | 20 +++++++++---------- .../project/vpcs/internet-gateway-edit.tsx | 20 +++++++++++-------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx index 295f58725..713347788 100644 --- a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx @@ -24,16 +24,16 @@ import { ALL_ISH } from '~/util/consts' import { pb } from '~/util/path-builder' import type * as PP from '~/util/path-params' -const gatewayList = (query: PP.Vpc) => - getListQFn('internetGatewayList', { query: { ...query, limit: ALL_ISH } }) -const routerList = (query: PP.Vpc) => - getListQFn('vpcRouterList', { query: { ...query, limit: ALL_ISH } }) -const routeList = (query: PP.VpcRouter) => - getListQFn('vpcRouterRouteList', { query: { ...query, limit: ALL_ISH } }) -const gatewayIpAddressList = (query: { gatewayId: string }) => - getListQFn('internetGatewayIpAddressList', { query: { gateway: query.gatewayId } }) -const gatewayIpPoolList = (query: { gatewayId: string }) => - getListQFn('internetGatewayIpPoolList', { query: { gateway: query.gatewayId } }) +const gatewayList = ({ project, vpc }: PP.Vpc) => + getListQFn('internetGatewayList', { query: { project, vpc, limit: ALL_ISH } }) +const routerList = ({ project, vpc }: PP.Vpc) => + getListQFn('vpcRouterList', { query: { project, vpc, limit: ALL_ISH } }) +const routeList = ({ project, vpc, router }: PP.VpcRouter) => + getListQFn('vpcRouterRouteList', { query: { project, vpc, router, limit: ALL_ISH } }) +const gatewayIpAddressList = ({ gatewayId }: { gatewayId: string }) => + getListQFn('internetGatewayIpAddressList', { query: { gateway: gatewayId } }) +const gatewayIpPoolList = ({ gatewayId }: { gatewayId: string }) => + getListQFn('internetGatewayIpPoolList', { query: { gateway: gatewayId } }) const projectIpPoolList = getListQFn('projectIpPoolList', { query: { limit: ALL_ISH } }) const InternetGatewayIpAddressCell = ({ gatewayId }: { gatewayId: string }) => { diff --git a/app/pages/project/vpcs/internet-gateway-edit.tsx b/app/pages/project/vpcs/internet-gateway-edit.tsx index 369cc6ce4..55e25a61b 100644 --- a/app/pages/project/vpcs/internet-gateway-edit.tsx +++ b/app/pages/project/vpcs/internet-gateway-edit.tsx @@ -77,14 +77,18 @@ const RouterRows = ({ project, vpc, gateway }: PP.VpcInternetGateway) => { ) } -const gatewayIpPoolList = (query: PP.VpcInternetGateway) => - getListQFn('internetGatewayIpPoolList', { query: { ...query, limit: ALL_ISH } }) -const gatewayIpAddressList = (query: PP.VpcInternetGateway) => - getListQFn('internetGatewayIpAddressList', { query: { ...query, limit: ALL_ISH } }) -const routerList = (query: PP.Vpc) => - getListQFn('vpcRouterList', { query: { ...query, limit: ALL_ISH } }) -const routeList = (query: PP.VpcRouter) => - getListQFn('vpcRouterRouteList', { query: { ...query, limit: ALL_ISH } }) +const gatewayIpPoolList = ({ project, vpc, gateway }: PP.VpcInternetGateway) => + getListQFn('internetGatewayIpPoolList', { + query: { project, vpc, gateway, limit: ALL_ISH }, + }) +const gatewayIpAddressList = ({ project, vpc, gateway }: PP.VpcInternetGateway) => + getListQFn('internetGatewayIpAddressList', { + query: { project, vpc, gateway, limit: ALL_ISH }, + }) +const routerList = ({ project, vpc }: PP.Vpc) => + getListQFn('vpcRouterList', { query: { project, vpc, limit: ALL_ISH } }) +const routeList = ({ project, vpc, router }: PP.VpcRouter) => + getListQFn('vpcRouterRouteList', { query: { project, vpc, router, limit: ALL_ISH } }) EditInternetGatewayForm.loader = async function ({ params }: LoaderFunctionArgs) { const { project, vpc, gateway } = getInternetGatewaySelector(params) From 7c7f9b763b199ae02d6becd03f8642ced0489b32 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 11 Dec 2024 12:16:57 -0800 Subject: [PATCH 52/63] use titleCrumb for Edit Internet Gateway --- app/routes.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/app/routes.tsx b/app/routes.tsx index 4329efad4..797be42ab 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -37,12 +37,7 @@ import { EditRouterSideModalForm } from './forms/vpc-router-edit' import { CreateRouterRouteSideModalForm } from './forms/vpc-router-route-create' import { EditRouterRouteSideModalForm } from './forms/vpc-router-route-edit' import { makeCrumb, titleCrumb } from './hooks/use-crumbs' -import { - getInstanceSelector, - getInternetGatewaySelector, - getProjectSelector, - getVpcSelector, -} from './hooks/use-params' +import { getInstanceSelector, getProjectSelector, getVpcSelector } from './hooks/use-params' import { AuthenticatedLayout } from './layouts/AuthenticatedLayout' import { AuthLayout } from './layouts/AuthLayout' import { SerialConsoleContentPane } from './layouts/helpers' @@ -390,10 +385,7 @@ export const routes = createRoutesFromElements( path=":gateway" element={} loader={EditInternetGatewayForm.loader} - handle={makeCrumb( - (p) => p.gateway!, - (p) => pb.vpcInternetGateway(getInternetGatewaySelector(p)) - )} + handle={titleCrumb('Edit Internet Gateway')} /> From fb4ede508910f5f14587a80e57a7af0b7d58b41c Mon Sep 17 00:00:00 2001 From: David Crespo Date: Wed, 11 Dec 2024 14:43:45 -0600 Subject: [PATCH 53/63] fix RR leaf route without element warning --- app/routes.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routes.tsx b/app/routes.tsx index 797be42ab..36b84ec45 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -396,7 +396,7 @@ export const routes = createRoutesFromElements( p.router!)}> - + } From 6f91091fc4dc5553138f53ffbf55ee870985c2fa Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 11 Dec 2024 12:50:08 -0800 Subject: [PATCH 54/63] let's use a valid UUID --- mock-api/vpc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mock-api/vpc.ts b/mock-api/vpc.ts index 7c45dc655..e3614f9ac 100644 --- a/mock-api/vpc.ts +++ b/mock-api/vpc.ts @@ -137,7 +137,7 @@ export const routerRoutes: Json> = [ { ...routeBase, vpc_router_id: customRouter.id, - id: '93g7c4a1-5b0de-4efb-8518-e0bf012a5169', + id: '550e8400-e29b-41d4-a716-446655440000', name: 'dc2', description: 'route to datacenter 2', kind: 'custom', From 2135c928f9ab003b59ea6c83318a1d4a69c41d93 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Wed, 11 Dec 2024 14:53:48 -0600 Subject: [PATCH 55/63] clean up InternetGatewayRoutes and call it as a component --- .../vpcs/VpcPage/tabs/VpcGatewaysTab.tsx | 49 +++++++------------ 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx index 713347788..b461b714f 100644 --- a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx @@ -50,34 +50,25 @@ const InternetGatewayIpPoolCell = ({ gatewayId }: { gatewayId: string }) => { // called by InternetGatewayAttachedRoutesCell to get the routes per router // we need to have this in its own function because useQuery cannot be called inside a loop -const InternetGatewayIndividualRoute = ({ +const InternetGatewayRoutes = ({ project, vpc, gateway, router, }: PP.VpcInternetGateway & { router: string }) => { - const matchingRoutes: JSX.Element[] = [] const { data: routes } = useQuery(routeList({ project, vpc, router }).optionsFn()) - if (!routes || routes.items.length < 1) return - routes.items.forEach((route) => { - if (route.target.type === 'internet_gateway' && route.target.value === gateway) { - matchingRoutes.push( - - {route.name} - - ) - } - }) - return matchingRoutes + if (!routes || routes.items.length < 1) return null + return routes.items + .filter((r) => r.target.type === 'internet_gateway' && r.target.value === gateway) + .map((route) => ( + + {route.name} + + )) } const InternetGatewayAttachedRoutesCell = ({ @@ -86,14 +77,12 @@ const InternetGatewayAttachedRoutesCell = ({ gateway, }: PP.VpcInternetGateway) => { const { data: routers } = useQuery(routerList({ project, vpc }).optionsFn()) - const matchingRoutes = routers?.items.flatMap((router) => - InternetGatewayIndividualRoute({ project, vpc, gateway, router: router.name }) - ) - return matchingRoutes?.length ? ( -
{matchingRoutes}
- ) : ( - - ) + const matchingRoutes = routers?.items.map((router) => { + const props = { project, vpc, gateway, router: router.name } + return + }) + if (!matchingRoutes?.length) return + return
{matchingRoutes}
} const colHelper = createColumnHelper() From ca4962d4d83059002d3f8c0fccca03b6ee96833d Mon Sep 17 00:00:00 2001 From: David Crespo Date: Wed, 11 Dec 2024 15:05:22 -0600 Subject: [PATCH 56/63] update the snapshot! --- app/util/__snapshots__/path-builder.spec.ts.snap | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/util/__snapshots__/path-builder.spec.ts.snap b/app/util/__snapshots__/path-builder.spec.ts.snap index 5c9a5fe0d..80a255614 100644 --- a/app/util/__snapshots__/path-builder.spec.ts.snap +++ b/app/util/__snapshots__/path-builder.spec.ts.snap @@ -730,10 +730,6 @@ exports[`breadcrumbs 2`] = ` "label": "Internet Gateways", "path": "/projects/p/vpcs/v/internet-gateways", }, - { - "label": "g", - "path": "/projects/p/vpcs/v/internet-gateways/g", - }, ], "vpcInternetGateways (/projects/p/vpcs/v/internet-gateways)": [ { From c2d20ae17664efba41d8231defcae8649ed73c9d Mon Sep 17 00:00:00 2001 From: David Crespo Date: Wed, 11 Dec 2024 15:31:29 -0600 Subject: [PATCH 57/63] fix gnarly dependent promises in gateways loader --- .../vpcs/VpcPage/tabs/VpcGatewaysTab.tsx | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx index b461b714f..3bc72c23d 100644 --- a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx @@ -89,25 +89,22 @@ const colHelper = createColumnHelper() VpcInternetGatewaysTab.loader = async ({ params }: LoaderFunctionArgs) => { const { project, vpc } = getVpcSelector(params) + const [gateways, routers] = await Promise.all([ + queryClient.fetchQuery(gatewayList({ project, vpc }).optionsFn()), + queryClient.fetchQuery(routerList({ project, vpc }).optionsFn()), + ]) + await Promise.all([ - (await queryClient.fetchQuery(gatewayList({ project, vpc }).optionsFn())).items.flatMap( - (gateway: InternetGateway) => { - return [ - queryClient.prefetchQuery( - gatewayIpAddressList({ gatewayId: gateway.id }).optionsFn() - ), - queryClient.prefetchQuery( - gatewayIpPoolList({ gatewayId: gateway.id }).optionsFn() - ), - ] - } - ), - (await queryClient.fetchQuery(routerList({ project, vpc }).optionsFn())).items.map( - (router) => { - queryClient.prefetchQuery( - routeList({ project, vpc, router: router.name }).optionsFn() - ) - } + ...gateways.items.flatMap((gateway: InternetGateway) => [ + queryClient.prefetchQuery( + gatewayIpAddressList({ gatewayId: gateway.id }).optionsFn() + ), + queryClient.prefetchQuery(gatewayIpPoolList({ gatewayId: gateway.id }).optionsFn()), + ]), + ...routers.items.map((router) => + queryClient.prefetchQuery( + routeList({ project, vpc, router: router.name }).optionsFn() + ) ), queryClient.fetchQuery(projectIpPoolList.optionsFn()).then((pools) => { for (const pool of pools.items) { @@ -115,7 +112,8 @@ VpcInternetGatewaysTab.loader = async ({ params }: LoaderFunctionArgs) => { queryClient.setQueryData(queryKey, pool) } }), - ]) + ] satisfies Promise[]) + return null } From 49cfa1503c0d6cf18f958e301357c0afab5978dc Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 12 Dec 2024 12:00:04 -0600 Subject: [PATCH 58/63] update read only info box --- app/forms/firewall-rules-common.tsx | 4 ++-- app/pages/project/vpcs/internet-gateway-edit.tsx | 15 +++++++++++---- app/util/links.ts | 2 ++ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/app/forms/firewall-rules-common.tsx b/app/forms/firewall-rules-common.tsx index 0f6f20059..809799b72 100644 --- a/app/forms/firewall-rules-common.tsx +++ b/app/forms/firewall-rules-common.tsx @@ -323,9 +323,9 @@ export const CommonFields = ({ control, nameTaken, error }: CommonFieldsProps) = target="_blank" rel="noreferrer" > - guest networking guide + Networking {' '} - and{' '} + guide and the{' '} - This is a read-only copy of this internet gateway. Use the CLI to create and - update internet gateways. More functionality for internet gateways will be - included in future releases of the Oxide console. + For now, gateways can only be modified through the API. Learn more in the{' '} + + Networking + {' '} + guide. } /> diff --git a/app/util/links.ts b/app/util/links.ts index ef5a5aec4..b3bc96256 100644 --- a/app/util/links.ts +++ b/app/util/links.ts @@ -16,6 +16,8 @@ export const links = { firewallRulesDocs: 'https://docs.oxide.computer/guides/configuring-guest-networking#_firewall_rules', floatingIpsDocs: 'https://docs.oxide.computer/guides/managing-floating-ips', + gatewaysDocs: + 'https://docs.oxide.computer/guides/configuring-guest-networking#internet-gateway', imagesDocs: 'https://docs.oxide.computer/guides/creating-and-sharing-images', preparingImagesDocs: 'https://docs.oxide.computer/guides/creating-and-sharing-images#_preparing_images_for_import', From 24dc84427e74cde625b9af753e33de4e49b7a625 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 12 Dec 2024 12:00:04 -0600 Subject: [PATCH 59/63] clean up gateway routes fetch logic by extracting shared hook --- .../vpcs/VpcPage/tabs/VpcGatewaysTab.tsx | 51 +++----- .../project/vpcs/internet-gateway-edit.tsx | 113 ++++++++++-------- 2 files changed, 75 insertions(+), 89 deletions(-) diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx index 3bc72c23d..64582fa6b 100644 --- a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx @@ -24,6 +24,8 @@ import { ALL_ISH } from '~/util/consts' import { pb } from '~/util/path-builder' import type * as PP from '~/util/path-params' +import { useGatewayRoutes } from '../../internet-gateway-edit' + const gatewayList = ({ project, vpc }: PP.Vpc) => getListQFn('internetGatewayList', { query: { project, vpc, limit: ALL_ISH } }) const routerList = ({ project, vpc }: PP.Vpc) => @@ -48,41 +50,20 @@ const InternetGatewayIpPoolCell = ({ gatewayId }: { gatewayId: string }) => { return } -// called by InternetGatewayAttachedRoutesCell to get the routes per router -// we need to have this in its own function because useQuery cannot be called inside a loop -const InternetGatewayRoutes = ({ - project, - vpc, - gateway, - router, -}: PP.VpcInternetGateway & { router: string }) => { - const { data: routes } = useQuery(routeList({ project, vpc, router }).optionsFn()) - if (!routes || routes.items.length < 1) return null - return routes.items - .filter((r) => r.target.type === 'internet_gateway' && r.target.value === gateway) - .map((route) => ( - +const GatewayRoutes = ({ project, vpc, gateway }: PP.VpcInternetGateway) => { + const matchingRoutes = useGatewayRoutes({ project, vpc, gateway }) + + if (!matchingRoutes?.length) return + + return matchingRoutes.map(([router, route]) => { + const to = pb.vpcRouterRouteEdit({ project, vpc, router, route: route.name }) + const key = `${router}-${route.name}` + return ( + {route.name} - )) -} - -const InternetGatewayAttachedRoutesCell = ({ - project, - vpc, - gateway, -}: PP.VpcInternetGateway) => { - const { data: routers } = useQuery(routerList({ project, vpc }).optionsFn()) - const matchingRoutes = routers?.items.map((router) => { - const props = { project, vpc, gateway, router: router.name } - return + ) }) - if (!matchingRoutes?.length) return - return
{matchingRoutes}
} const colHelper = createColumnHelper() @@ -152,11 +133,7 @@ export function VpcInternetGatewaysTab() { id: 'routes', header: 'Routes', cell: (info) => ( - + ), }), colHelper.accessor('timeCreated', Columns.timeCreated), diff --git a/app/pages/project/vpcs/internet-gateway-edit.tsx b/app/pages/project/vpcs/internet-gateway-edit.tsx index 1f7369ae9..52549a4fb 100644 --- a/app/pages/project/vpcs/internet-gateway-edit.tsx +++ b/app/pages/project/vpcs/internet-gateway-edit.tsx @@ -6,13 +6,20 @@ * Copyright Oxide Computer Company */ -import { useQuery } from '@tanstack/react-query' +import { useQueries, useQuery } from '@tanstack/react-query' import { useForm } from 'react-hook-form' import { Link, useNavigate, type LoaderFunctionArgs } from 'react-router-dom' +import * as R from 'remeda' import { Gateway16Icon } from '@oxide/design-system/icons/react' -import { apiQueryClient, getListQFn, queryClient, usePrefetchedApiQuery } from '~/api' +import { + apiQueryClient, + getListQFn, + queryClient, + usePrefetchedApiQuery, + usePrefetchedQuery, +} from '~/api' import { SideModalForm } from '~/components/form/SideModalForm' import { getInternetGatewaySelector, useInternetGatewaySelector } from '~/hooks/use-params' import { DescriptionCell } from '~/table/cells/DescriptionCell' @@ -28,54 +35,55 @@ import { links } from '~/util/links' import { pb } from '~/util/path-builder' import type * as PP from '~/util/path-params' -const RouterRow = ({ - project, - vpc, - gateway, - router, -}: PP.VpcInternetGateway & { router: string }) => { - const matchingRoutes: JSX.Element[] = [] - const { data: routes } = useQuery(routeList({ project, vpc, router }).optionsFn()) - if (!routes || routes.items.length < 1) return - routes.items.forEach((route) => { - if (route.target.type === 'internet_gateway' && route.target.value === gateway) { - matchingRoutes.push( - - {router} - - - {route.name} - - - - ) - } +const RoutesEmpty = () => ( + + + No VPC router routes target this gateway. + + +) + +/** + * For a given gateway, return a list of [router name, RouterRoute] pairs + */ +export function useGatewayRoutes({ project, vpc, gateway }: PP.VpcInternetGateway) { + const { data: routers } = usePrefetchedQuery(routerList({ project, vpc }).optionsFn()) + const routerNames = routers.items.map((r) => r.name) + + const routesQueries = useQueries({ + queries: routerNames.map((router) => routeList({ project, vpc, router }).optionsFn()), }) - return matchingRoutes -} + const loadedRoutesLists = routesQueries.filter((q) => !!q.data).map((q) => q.data.items) + + // loading. should never happen because of prefetches + if (loadedRoutesLists.length < routers.items.length) return null -const RouterRows = ({ project, vpc, gateway }: PP.VpcInternetGateway) => { - const { data: routers } = useQuery(routerList({ project, vpc }).optionsFn()) - const matchingRoutes = routers?.items.flatMap((router) => - RouterRow({ project, vpc, gateway, router: router.name }) + return R.pipe( + R.zip(routerNames, loadedRoutesLists), + R.flatMap(([router, routes]) => routes.map((route) => [router, route] as const)), + R.filter(([_, r]) => r.target.type === 'internet_gateway' && r.target.value === gateway) ) - return matchingRoutes?.length ? ( - matchingRoutes - ) : ( - - - No VPC routes target this gateway. +} + +function RouteRows({ project, vpc, gateway }: PP.VpcInternetGateway) { + const matchingRoutes = useGatewayRoutes({ project, vpc, gateway }) + + if (!matchingRoutes) return null + if (matchingRoutes.length === 0) return + + return matchingRoutes.map(([router, route]) => ( + + {router} + + + {route.name} + - ) + )) } const gatewayIpPoolList = ({ project, vpc, gateway }: PP.VpcInternetGateway) => @@ -100,14 +108,13 @@ EditInternetGatewayForm.loader = async function ({ params }: LoaderFunctionArgs) }), queryClient.prefetchQuery(gatewayIpPoolList({ project, vpc, gateway }).optionsFn()), queryClient.prefetchQuery(gatewayIpAddressList({ project, vpc, gateway }).optionsFn()), - (await queryClient.fetchQuery(routerList({ project, vpc }).optionsFn())).items.map( - (router) => { + ...(await queryClient.fetchQuery(routerList({ project, vpc }).optionsFn())).items.map( + (router) => queryClient.prefetchQuery( routeList({ project, vpc, router: router.name }).optionsFn() ) - } ), - ]) + ] satisfies Promise[]) return null } @@ -235,11 +242,13 @@ export function EditInternetGatewayForm() {
- Router - Route + + Router + Route + - +
From a6c0e7bd4f262e9566bc6e65fcf8c89f8dd3805e Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 12 Dec 2024 17:47:48 -0600 Subject: [PATCH 60/63] extract gateway data logic into a separate file --- .../vpcs/VpcPage/tabs/VpcGatewaysTab.tsx | 42 +++++++------- app/pages/project/vpcs/gateway-data.ts | 49 +++++++++++++++++ .../project/vpcs/internet-gateway-edit.tsx | 55 ++++--------------- 3 files changed, 82 insertions(+), 64 deletions(-) create mode 100644 app/pages/project/vpcs/gateway-data.ts diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx index 64582fa6b..6b0e219cd 100644 --- a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx @@ -24,28 +24,26 @@ import { ALL_ISH } from '~/util/consts' import { pb } from '~/util/path-builder' import type * as PP from '~/util/path-params' -import { useGatewayRoutes } from '../../internet-gateway-edit' +import { + gatewayIpAddressList, + gatewayIpPoolList, + routeList, + routerList, + useGatewayRoutes, +} from '../../gateway-data' const gatewayList = ({ project, vpc }: PP.Vpc) => getListQFn('internetGatewayList', { query: { project, vpc, limit: ALL_ISH } }) -const routerList = ({ project, vpc }: PP.Vpc) => - getListQFn('vpcRouterList', { query: { project, vpc, limit: ALL_ISH } }) -const routeList = ({ project, vpc, router }: PP.VpcRouter) => - getListQFn('vpcRouterRouteList', { query: { project, vpc, router, limit: ALL_ISH } }) -const gatewayIpAddressList = ({ gatewayId }: { gatewayId: string }) => - getListQFn('internetGatewayIpAddressList', { query: { gateway: gatewayId } }) -const gatewayIpPoolList = ({ gatewayId }: { gatewayId: string }) => - getListQFn('internetGatewayIpPoolList', { query: { gateway: gatewayId } }) const projectIpPoolList = getListQFn('projectIpPoolList', { query: { limit: ALL_ISH } }) -const InternetGatewayIpAddressCell = ({ gatewayId }: { gatewayId: string }) => { - const { data: addresses } = useQuery(gatewayIpAddressList({ gatewayId }).optionsFn()) +const IpAddressCell = (gatewaySelector: PP.VpcInternetGateway) => { + const { data: addresses } = useQuery(gatewayIpAddressList(gatewaySelector).optionsFn()) if (!addresses || addresses.items.length < 1) return return } -const InternetGatewayIpPoolCell = ({ gatewayId }: { gatewayId: string }) => { - const { data: gateways } = useQuery(gatewayIpPoolList({ gatewayId }).optionsFn()) +const GatewayIpPoolCell = (gatewaySelector: PP.VpcInternetGateway) => { + const { data: gateways } = useQuery(gatewayIpPoolList(gatewaySelector).optionsFn()) if (!gateways || gateways.items.length < 1) return return } @@ -78,9 +76,11 @@ VpcInternetGatewaysTab.loader = async ({ params }: LoaderFunctionArgs) => { await Promise.all([ ...gateways.items.flatMap((gateway: InternetGateway) => [ queryClient.prefetchQuery( - gatewayIpAddressList({ gatewayId: gateway.id }).optionsFn() + gatewayIpAddressList({ project, vpc, gateway: gateway.name }).optionsFn() + ), + queryClient.prefetchQuery( + gatewayIpPoolList({ project, vpc, gateway: gateway.name }).optionsFn() ), - queryClient.prefetchQuery(gatewayIpPoolList({ gatewayId: gateway.id }).optionsFn()), ]), ...routers.items.map((router) => queryClient.prefetchQuery( @@ -116,17 +116,21 @@ export function VpcInternetGatewaysTab() { cell: makeLinkCell((gateway) => pb.vpcInternetGateway({ project, vpc, gateway })), }), colHelper.accessor('description', Columns.description), - colHelper.accessor('id', { + colHelper.accessor('name', { // ID needed to avoid key collision with other name column id: 'ip-address', header: 'Attached IP Address', - cell: (info) => , + cell: (info) => ( + + ), }), - colHelper.accessor('id', { + colHelper.accessor('name', { // ID needed to avoid key collision with other name column id: 'ip-pool', header: 'Attached IP Pool', - cell: (info) => , + cell: (info) => ( + + ), }), colHelper.accessor('name', { // ID needed to avoid key collision with other name column diff --git a/app/pages/project/vpcs/gateway-data.ts b/app/pages/project/vpcs/gateway-data.ts new file mode 100644 index 000000000..d7047ae6f --- /dev/null +++ b/app/pages/project/vpcs/gateway-data.ts @@ -0,0 +1,49 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +import { useQueries } from '@tanstack/react-query' +import * as R from 'remeda' + +import { getListQFn, usePrefetchedQuery } from '~/api' +import { ALL_ISH } from '~/util/consts' +import type * as PP from '~/util/path-params' + +export const routerList = ({ project, vpc }: PP.Vpc) => + getListQFn('vpcRouterList', { query: { project, vpc, limit: ALL_ISH } }) +export const routeList = ({ project, vpc, router }: PP.VpcRouter) => + getListQFn('vpcRouterRouteList', { query: { project, vpc, router, limit: ALL_ISH } }) +export const gatewayIpPoolList = ({ project, vpc, gateway }: PP.VpcInternetGateway) => + getListQFn('internetGatewayIpPoolList', { + query: { project, vpc, gateway, limit: ALL_ISH }, + }) +export const gatewayIpAddressList = ({ project, vpc, gateway }: PP.VpcInternetGateway) => + getListQFn('internetGatewayIpAddressList', { + query: { project, vpc, gateway, limit: ALL_ISH }, + }) + +/** + * For a given gateway, return a list of [router name, RouterRoute] pairs + */ +export function useGatewayRoutes({ project, vpc, gateway }: PP.VpcInternetGateway) { + const { data: routers } = usePrefetchedQuery(routerList({ project, vpc }).optionsFn()) + const routerNames = routers.items.map((r) => r.name) + + const routesQueries = useQueries({ + queries: routerNames.map((router) => routeList({ project, vpc, router }).optionsFn()), + }) + const loadedRoutesLists = routesQueries.filter((q) => !!q.data).map((q) => q.data.items) + + // loading. should never happen because of prefetches + if (loadedRoutesLists.length < routers.items.length) return null + + return R.pipe( + R.zip(routerNames, loadedRoutesLists), + R.flatMap(([router, routes]) => routes.map((route) => [router, route] as const)), + R.filter(([_, r]) => r.target.type === 'internet_gateway' && r.target.value === gateway) + ) +} diff --git a/app/pages/project/vpcs/internet-gateway-edit.tsx b/app/pages/project/vpcs/internet-gateway-edit.tsx index 52549a4fb..a6f2b3437 100644 --- a/app/pages/project/vpcs/internet-gateway-edit.tsx +++ b/app/pages/project/vpcs/internet-gateway-edit.tsx @@ -6,20 +6,13 @@ * Copyright Oxide Computer Company */ -import { useQueries, useQuery } from '@tanstack/react-query' +import { useQuery } from '@tanstack/react-query' import { useForm } from 'react-hook-form' import { Link, useNavigate, type LoaderFunctionArgs } from 'react-router-dom' -import * as R from 'remeda' import { Gateway16Icon } from '@oxide/design-system/icons/react' -import { - apiQueryClient, - getListQFn, - queryClient, - usePrefetchedApiQuery, - usePrefetchedQuery, -} from '~/api' +import { apiQueryClient, queryClient, usePrefetchedApiQuery } from '~/api' import { SideModalForm } from '~/components/form/SideModalForm' import { getInternetGatewaySelector, useInternetGatewaySelector } from '~/hooks/use-params' import { DescriptionCell } from '~/table/cells/DescriptionCell' @@ -30,11 +23,18 @@ import { Message } from '~/ui/lib/Message' import { PropertiesTable } from '~/ui/lib/PropertiesTable' import { ResourceLabel, SideModal } from '~/ui/lib/SideModal' import { Table } from '~/ui/lib/Table' -import { ALL_ISH } from '~/util/consts' import { links } from '~/util/links' import { pb } from '~/util/path-builder' import type * as PP from '~/util/path-params' +import { + gatewayIpAddressList, + gatewayIpPoolList, + routeList, + routerList, + useGatewayRoutes, +} from './gateway-data' + const RoutesEmpty = () => ( @@ -43,28 +43,6 @@ const RoutesEmpty = () => ( ) -/** - * For a given gateway, return a list of [router name, RouterRoute] pairs - */ -export function useGatewayRoutes({ project, vpc, gateway }: PP.VpcInternetGateway) { - const { data: routers } = usePrefetchedQuery(routerList({ project, vpc }).optionsFn()) - const routerNames = routers.items.map((r) => r.name) - - const routesQueries = useQueries({ - queries: routerNames.map((router) => routeList({ project, vpc, router }).optionsFn()), - }) - const loadedRoutesLists = routesQueries.filter((q) => !!q.data).map((q) => q.data.items) - - // loading. should never happen because of prefetches - if (loadedRoutesLists.length < routers.items.length) return null - - return R.pipe( - R.zip(routerNames, loadedRoutesLists), - R.flatMap(([router, routes]) => routes.map((route) => [router, route] as const)), - R.filter(([_, r]) => r.target.type === 'internet_gateway' && r.target.value === gateway) - ) -} - function RouteRows({ project, vpc, gateway }: PP.VpcInternetGateway) { const matchingRoutes = useGatewayRoutes({ project, vpc, gateway }) @@ -86,19 +64,6 @@ function RouteRows({ project, vpc, gateway }: PP.VpcInternetGateway) { )) } -const gatewayIpPoolList = ({ project, vpc, gateway }: PP.VpcInternetGateway) => - getListQFn('internetGatewayIpPoolList', { - query: { project, vpc, gateway, limit: ALL_ISH }, - }) -const gatewayIpAddressList = ({ project, vpc, gateway }: PP.VpcInternetGateway) => - getListQFn('internetGatewayIpAddressList', { - query: { project, vpc, gateway, limit: ALL_ISH }, - }) -const routerList = ({ project, vpc }: PP.Vpc) => - getListQFn('vpcRouterList', { query: { project, vpc, limit: ALL_ISH } }) -const routeList = ({ project, vpc, router }: PP.VpcRouter) => - getListQFn('vpcRouterRouteList', { query: { project, vpc, router, limit: ALL_ISH } }) - EditInternetGatewayForm.loader = async function ({ params }: LoaderFunctionArgs) { const { project, vpc, gateway } = getInternetGatewaySelector(params) await Promise.all([ From 46ef5d86fb3519afa203ac1b5c2de144591836b7 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 12 Dec 2024 23:04:00 -0800 Subject: [PATCH 61/63] Use count of routes; link to sidemodal --- .../vpcs/VpcPage/tabs/VpcGatewaysTab.tsx | 17 ++++------------- test/e2e/vpcs.e2e.ts | 10 ++++------ 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx index 6b0e219cd..fd8fdd586 100644 --- a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx @@ -9,13 +9,13 @@ import { useQuery } from '@tanstack/react-query' import { createColumnHelper } from '@tanstack/react-table' import { useMemo } from 'react' -import { Link, Outlet, type LoaderFunctionArgs } from 'react-router-dom' +import { Outlet, type LoaderFunctionArgs } from 'react-router-dom' import { apiq, getListQFn, queryClient, type InternetGateway } from '~/api' import { getVpcSelector, useVpcSelector } from '~/hooks/use-params' import { EmptyCell } from '~/table/cells/EmptyCell' import { IpPoolCell } from '~/table/cells/IpPoolCell' -import { makeLinkCell } from '~/table/cells/LinkCell' +import { LinkCell, makeLinkCell } from '~/table/cells/LinkCell' import { Columns } from '~/table/columns/common' import { useQueryTable } from '~/table/QueryTable' import { CopyableIp } from '~/ui/lib/CopyableIp' @@ -50,18 +50,9 @@ const GatewayIpPoolCell = (gatewaySelector: PP.VpcInternetGateway) => { const GatewayRoutes = ({ project, vpc, gateway }: PP.VpcInternetGateway) => { const matchingRoutes = useGatewayRoutes({ project, vpc, gateway }) - if (!matchingRoutes?.length) return - - return matchingRoutes.map(([router, route]) => { - const to = pb.vpcRouterRouteEdit({ project, vpc, router, route: route.name }) - const key = `${router}-${route.name}` - return ( - - {route.name} - - ) - }) + const to = pb.vpcInternetGateway({ project, vpc, gateway }) + return {matchingRoutes.length} } const colHelper = createColumnHelper() diff --git a/test/e2e/vpcs.e2e.ts b/test/e2e/vpcs.e2e.ts index d6de8039e..ffa43dbfc 100644 --- a/test/e2e/vpcs.e2e.ts +++ b/test/e2e/vpcs.e2e.ts @@ -373,6 +373,8 @@ test('internet gateway shows proper list of routes targeting it', async ({ page // close the sidemodal await sidemodal.getByRole('button', { name: 'Close' }).click() await expect(sidemodal).toBeHidden() + // check for the route count; which should be 1 + await expect(page.getByRole('link', { name: '1', exact: true })).toBeVisible() // go to the Routers tab await page.getByRole('tab', { name: 'Routers' }).click() // click on the mock-custom-router to go to the router detail page @@ -395,12 +397,8 @@ test('internet gateway shows proper list of routes targeting it', async ({ page await page.getByRole('link', { name: 'mock-vpc' }).click() // click on the internet gateways tab and then the internet-gateway-1 link to go to the detail page await page.getByRole('tab', { name: 'Internet Gateways' }).click() - await page.getByRole('link', { name: 'internet-gateway-1' }).click() - - // the table should now say "dc2 new-route" - await expect(page.getByText('dc2new-route')).toBeVisible() - // click on the link to go to the detail page - await page.getByRole('link', { name: 'internet-gateway-1' }).click() + // verify that the route count is now 2: click on the link to go to the edit gateway sidemodal + await page.getByRole('link', { name: '2', exact: true }).click() // the new route should be visible in the table await expectRowVisible(table, { Router: 'mock-custom-router', Route: 'dc2' }) From 64a7bbe48f36303d57ba415207163034c7c41fc6 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 13 Dec 2024 04:30:13 -0800 Subject: [PATCH 62/63] Use count of 0 instead of EmptyCell for route count --- app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx | 3 +-- app/pages/project/vpcs/internet-gateway-edit.tsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx index fd8fdd586..e7b8f8363 100644 --- a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx @@ -50,9 +50,8 @@ const GatewayIpPoolCell = (gatewaySelector: PP.VpcInternetGateway) => { const GatewayRoutes = ({ project, vpc, gateway }: PP.VpcInternetGateway) => { const matchingRoutes = useGatewayRoutes({ project, vpc, gateway }) - if (!matchingRoutes?.length) return const to = pb.vpcInternetGateway({ project, vpc, gateway }) - return {matchingRoutes.length} + return {matchingRoutes?.length || 0} } const colHelper = createColumnHelper() diff --git a/app/pages/project/vpcs/internet-gateway-edit.tsx b/app/pages/project/vpcs/internet-gateway-edit.tsx index a6f2b3437..a93b10da2 100644 --- a/app/pages/project/vpcs/internet-gateway-edit.tsx +++ b/app/pages/project/vpcs/internet-gateway-edit.tsx @@ -37,7 +37,7 @@ import { const RoutesEmpty = () => ( - + No VPC router routes target this gateway. From c4b35fe540d6b32bea685b68196d168af43ab047 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Fri, 13 Dec 2024 09:58:03 -0600 Subject: [PATCH 63/63] use EmptyCell for zero routes, copy tweaks, sentence case --- .../project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx | 3 ++- app/pages/project/vpcs/internet-gateway-edit.tsx | 12 ++++++------ test/e2e/vpcs.e2e.ts | 2 ++ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx index e7b8f8363..0fd50179d 100644 --- a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx @@ -51,7 +51,8 @@ const GatewayIpPoolCell = (gatewaySelector: PP.VpcInternetGateway) => { const GatewayRoutes = ({ project, vpc, gateway }: PP.VpcInternetGateway) => { const matchingRoutes = useGatewayRoutes({ project, vpc, gateway }) const to = pb.vpcInternetGateway({ project, vpc, gateway }) - return {matchingRoutes?.length || 0} + if (!matchingRoutes?.length) return + return {matchingRoutes.length} } const colHelper = createColumnHelper() diff --git a/app/pages/project/vpcs/internet-gateway-edit.tsx b/app/pages/project/vpcs/internet-gateway-edit.tsx index a93b10da2..0301fe869 100644 --- a/app/pages/project/vpcs/internet-gateway-edit.tsx +++ b/app/pages/project/vpcs/internet-gateway-edit.tsx @@ -104,9 +104,9 @@ export function EditInternetGatewayForm() { return ( @@ -145,7 +145,7 @@ export function EditInternetGatewayForm() {
- Internet Gateway IP Address + Internet gateway IP address {gatewayIpAddresses && gatewayIpAddresses.length > 1 ? 'es' : ''} {gatewayIpAddresses && gatewayIpAddresses.length > 0 ? ( @@ -164,10 +164,10 @@ export function EditInternetGatewayForm() { )) ) : (
- {'This internet gateway does not have any specific IP addresses attached. '} + {'This internet gateway does not have any IP addresses attached. '} {hasAttachedPool ? 'It will use an address from the attached IP pool.' - : 'Use the CLI to attach an IP Pool or specify an IP address to use with this gateway.'} + : 'Use the CLI to attach an IP pool or IP address to this gateway.'}
)}
@@ -176,7 +176,7 @@ export function EditInternetGatewayForm() {
- Internet Gateway IP Pool + Internet gateway IP pool {gatewayIpPools && gatewayIpPools.length > 1 ? 's' : ''} {hasAttachedPool ? ( diff --git a/test/e2e/vpcs.e2e.ts b/test/e2e/vpcs.e2e.ts index ffa43dbfc..f2e9ef10e 100644 --- a/test/e2e/vpcs.e2e.ts +++ b/test/e2e/vpcs.e2e.ts @@ -335,12 +335,14 @@ test('can view internet gateways', async ({ page }) => { description: 'internet gateway 1', 'Attached IP Address': '123.4.56.3', 'Attached IP Pool': 'ip-pool-1', + Routes: '1', }) await expectRowVisible(table, { name: 'internet-gateway-2', description: 'internet gateway 2', 'Attached IP Address': '—', 'Attached IP Pool': 'ip-pool-2', + Routes: '—', }) await page.getByRole('link', { name: 'internet-gateway-1' }).click()