mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-21 19:56:33 +00:00
feat: security groups frontend (#97)
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
Network,
|
||||
Server,
|
||||
Shield,
|
||||
ShieldCheck,
|
||||
Users,
|
||||
} from "lucide-react"
|
||||
|
||||
@@ -197,6 +198,21 @@ export function SidebarLayout() {
|
||||
</Link>
|
||||
</SidebarMenuItem>
|
||||
|
||||
<SidebarMenuItem>
|
||||
<Link to="/ec2/describe-security-groups">
|
||||
<SidebarMenuButton
|
||||
isActive={
|
||||
pathname.startsWith("/ec2/describe-security-groups") ||
|
||||
pathname.startsWith("/ec2/create-security-group")
|
||||
}
|
||||
tooltip="Security Groups"
|
||||
>
|
||||
<ShieldCheck className="size-4" />
|
||||
<span>Security Groups</span>
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
</SidebarMenuItem>
|
||||
|
||||
{/* uncomment when route pages are created
|
||||
<SidebarMenuItem>
|
||||
<Link to="/ec2/describe-route-tables">
|
||||
|
||||
@@ -81,8 +81,8 @@ describe("calculateSubnetCidrs", () => {
|
||||
|
||||
it("sets correct labels", () => {
|
||||
const result = calculateSubnetCidrs("10.0.0.0/16", 1, 1)
|
||||
expect(result.publicSubnets[0]?.label).toBe("Public subnet 1")
|
||||
expect(result.privateSubnets[0]?.label).toBe("Private subnet 1")
|
||||
expect(result.publicSubnets[0]?.label).toBe("Public Subnet 1")
|
||||
expect(result.privateSubnets[0]?.label).toBe("Private Subnet 1")
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -13,11 +13,14 @@ import type { CreateVpcWizardFormData } from "@/types/ec2"
|
||||
|
||||
import {
|
||||
useAttachVolume,
|
||||
useAuthorizeSecurityGroupEgress,
|
||||
useAuthorizeSecurityGroupIngress,
|
||||
useCopySnapshot,
|
||||
useCreateImage,
|
||||
useCreateInstance,
|
||||
useCreateKeyPair,
|
||||
useCreatePlacementGroup,
|
||||
useCreateSecurityGroup,
|
||||
useCreateSnapshot,
|
||||
useCreateSubnet,
|
||||
useCreateVolume,
|
||||
@@ -25,6 +28,7 @@ import {
|
||||
useCreateVpcWizard,
|
||||
useDeleteKeyPair,
|
||||
useDeletePlacementGroup,
|
||||
useDeleteSecurityGroup,
|
||||
useDeleteSnapshot,
|
||||
useDeleteSubnet,
|
||||
useDeleteVolume,
|
||||
@@ -35,6 +39,8 @@ import {
|
||||
useModifyInstanceAttribute,
|
||||
useModifyVolume,
|
||||
useRebootInstance,
|
||||
useRevokeSecurityGroupEgress,
|
||||
useRevokeSecurityGroupIngress,
|
||||
useStartInstance,
|
||||
useStopInstance,
|
||||
useTerminateInstance,
|
||||
@@ -617,6 +623,234 @@ describe("useDeletePlacementGroup", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("useCreateSecurityGroup", () => {
|
||||
it("sends CreateSecurityGroupCommand with form data", async () => {
|
||||
createQueryClient()
|
||||
const { result } = renderHook(() => useCreateSecurityGroup(), { wrapper })
|
||||
|
||||
result.current.mutate({
|
||||
groupName: "web-sg",
|
||||
description: "Allow web traffic",
|
||||
vpcId: "vpc-123",
|
||||
})
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(mockSend.mock.calls[0]?.[0].input).toEqual({
|
||||
GroupName: "web-sg",
|
||||
Description: "Allow web traffic",
|
||||
VpcId: "vpc-123",
|
||||
})
|
||||
})
|
||||
|
||||
it("invalidates securityGroups query on success", async () => {
|
||||
createQueryClient()
|
||||
const spy = vi.spyOn(queryClient, "invalidateQueries")
|
||||
const { result } = renderHook(() => useCreateSecurityGroup(), { wrapper })
|
||||
|
||||
result.current.mutate({
|
||||
groupName: "web-sg",
|
||||
description: "Allow web traffic",
|
||||
vpcId: "vpc-123",
|
||||
})
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
queryKey: ["ec2", "securityGroups"],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("useDeleteSecurityGroup", () => {
|
||||
it("sends DeleteSecurityGroupCommand with group ID", async () => {
|
||||
createQueryClient()
|
||||
const { result } = renderHook(() => useDeleteSecurityGroup(), { wrapper })
|
||||
|
||||
result.current.mutate("sg-123")
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(mockSend.mock.calls[0]?.[0].input).toEqual({
|
||||
GroupId: "sg-123",
|
||||
})
|
||||
})
|
||||
|
||||
it("invalidates securityGroups query on success", async () => {
|
||||
createQueryClient()
|
||||
const spy = vi.spyOn(queryClient, "invalidateQueries")
|
||||
const { result } = renderHook(() => useDeleteSecurityGroup(), { wrapper })
|
||||
|
||||
result.current.mutate("sg-123")
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
queryKey: ["ec2", "securityGroups"],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("useAuthorizeSecurityGroupIngress", () => {
|
||||
const ruleParams = {
|
||||
groupId: "sg-123",
|
||||
ipProtocol: "tcp",
|
||||
fromPort: 443,
|
||||
toPort: 443,
|
||||
cidrIp: "0.0.0.0/0",
|
||||
}
|
||||
|
||||
it("sends AuthorizeSecurityGroupIngressCommand with IpPermissions", async () => {
|
||||
createQueryClient()
|
||||
const { result } = renderHook(() => useAuthorizeSecurityGroupIngress(), {
|
||||
wrapper,
|
||||
})
|
||||
|
||||
result.current.mutate(ruleParams)
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(mockSend.mock.calls[0]?.[0].input).toEqual({
|
||||
GroupId: "sg-123",
|
||||
IpPermissions: [
|
||||
{
|
||||
IpProtocol: "tcp",
|
||||
FromPort: 443,
|
||||
ToPort: 443,
|
||||
IpRanges: [{ CidrIp: "0.0.0.0/0" }],
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("invalidates securityGroups queries on success", async () => {
|
||||
createQueryClient()
|
||||
const spy = vi.spyOn(queryClient, "invalidateQueries")
|
||||
const { result } = renderHook(() => useAuthorizeSecurityGroupIngress(), {
|
||||
wrapper,
|
||||
})
|
||||
|
||||
result.current.mutate(ruleParams)
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
queryKey: ["ec2", "securityGroups"],
|
||||
})
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
queryKey: ["ec2", "securityGroups", "sg-123"],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("useAuthorizeSecurityGroupEgress", () => {
|
||||
it("sends AuthorizeSecurityGroupEgressCommand with IpPermissions", async () => {
|
||||
createQueryClient()
|
||||
const { result } = renderHook(() => useAuthorizeSecurityGroupEgress(), {
|
||||
wrapper,
|
||||
})
|
||||
|
||||
result.current.mutate({
|
||||
groupId: "sg-456",
|
||||
ipProtocol: "-1",
|
||||
fromPort: -1,
|
||||
toPort: -1,
|
||||
cidrIp: "0.0.0.0/0",
|
||||
})
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(mockSend.mock.calls[0]?.[0].input).toEqual({
|
||||
GroupId: "sg-456",
|
||||
IpPermissions: [
|
||||
{
|
||||
IpProtocol: "-1",
|
||||
FromPort: -1,
|
||||
ToPort: -1,
|
||||
IpRanges: [{ CidrIp: "0.0.0.0/0" }],
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("useRevokeSecurityGroupIngress", () => {
|
||||
it("sends RevokeSecurityGroupIngressCommand with IpPermissions", async () => {
|
||||
createQueryClient()
|
||||
const { result } = renderHook(() => useRevokeSecurityGroupIngress(), {
|
||||
wrapper,
|
||||
})
|
||||
|
||||
result.current.mutate({
|
||||
groupId: "sg-123",
|
||||
ipProtocol: "tcp",
|
||||
fromPort: 22,
|
||||
toPort: 22,
|
||||
cidrIp: "10.0.0.0/16",
|
||||
})
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(mockSend.mock.calls[0]?.[0].input).toEqual({
|
||||
GroupId: "sg-123",
|
||||
IpPermissions: [
|
||||
{
|
||||
IpProtocol: "tcp",
|
||||
FromPort: 22,
|
||||
ToPort: 22,
|
||||
IpRanges: [{ CidrIp: "10.0.0.0/16" }],
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("invalidates securityGroups queries on success", async () => {
|
||||
createQueryClient()
|
||||
const spy = vi.spyOn(queryClient, "invalidateQueries")
|
||||
const { result } = renderHook(() => useRevokeSecurityGroupIngress(), {
|
||||
wrapper,
|
||||
})
|
||||
|
||||
result.current.mutate({
|
||||
groupId: "sg-123",
|
||||
ipProtocol: "tcp",
|
||||
fromPort: 22,
|
||||
toPort: 22,
|
||||
cidrIp: "10.0.0.0/16",
|
||||
})
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
queryKey: ["ec2", "securityGroups"],
|
||||
})
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
queryKey: ["ec2", "securityGroups", "sg-123"],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("useRevokeSecurityGroupEgress", () => {
|
||||
it("sends RevokeSecurityGroupEgressCommand with IpPermissions", async () => {
|
||||
createQueryClient()
|
||||
const { result } = renderHook(() => useRevokeSecurityGroupEgress(), {
|
||||
wrapper,
|
||||
})
|
||||
|
||||
result.current.mutate({
|
||||
groupId: "sg-456",
|
||||
ipProtocol: "udp",
|
||||
fromPort: 53,
|
||||
toPort: 53,
|
||||
cidrIp: "0.0.0.0/0",
|
||||
})
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(mockSend.mock.calls[0]?.[0].input).toEqual({
|
||||
GroupId: "sg-456",
|
||||
IpPermissions: [
|
||||
{
|
||||
IpProtocol: "udp",
|
||||
FromPort: 53,
|
||||
ToPort: 53,
|
||||
IpRanges: [{ CidrIp: "0.0.0.0/0" }],
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("useCreateVpcWizard", () => {
|
||||
const baseParams: CreateVpcWizardFormData = {
|
||||
mode: "vpc-only",
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
AssociateRouteTableCommand,
|
||||
AttachInternetGatewayCommand,
|
||||
AttachVolumeCommand,
|
||||
AuthorizeSecurityGroupEgressCommand,
|
||||
AuthorizeSecurityGroupIngressCommand,
|
||||
CopySnapshotCommand,
|
||||
CreateImageCommand,
|
||||
CreateInternetGatewayCommand,
|
||||
@@ -14,12 +16,14 @@ import {
|
||||
CreatePlacementGroupCommand,
|
||||
CreateRouteCommand,
|
||||
CreateRouteTableCommand,
|
||||
CreateSecurityGroupCommand,
|
||||
CreateSnapshotCommand,
|
||||
CreateSubnetCommand,
|
||||
CreateVolumeCommand,
|
||||
CreateVpcCommand,
|
||||
DeleteKeyPairCommand,
|
||||
DeletePlacementGroupCommand,
|
||||
DeleteSecurityGroupCommand,
|
||||
DeleteSnapshotCommand,
|
||||
DeleteSubnetCommand,
|
||||
DeleteVolumeCommand,
|
||||
@@ -31,6 +35,8 @@ import {
|
||||
ModifyVolumeCommand,
|
||||
RebootInstancesCommand,
|
||||
ResourceType,
|
||||
RevokeSecurityGroupEgressCommand,
|
||||
RevokeSecurityGroupIngressCommand,
|
||||
RunInstancesCommand,
|
||||
StartInstancesCommand,
|
||||
StopInstancesCommand,
|
||||
@@ -48,6 +54,7 @@ import type {
|
||||
CreateInstanceParams,
|
||||
CreateKeyPairData,
|
||||
CreatePlacementGroupFormData,
|
||||
CreateSecurityGroupFormData,
|
||||
CreateSnapshotFormData,
|
||||
CreateSubnetFormData,
|
||||
CreateVolumeFormData,
|
||||
@@ -57,6 +64,7 @@ import type {
|
||||
ImportKeyPairData,
|
||||
ModifyInstanceTypeFormData,
|
||||
ModifyVolumeParams,
|
||||
SecurityGroupRuleFormData,
|
||||
} from "@/types/ec2"
|
||||
|
||||
const WHITESPACE_REGEX = /\s+/
|
||||
@@ -484,6 +492,142 @@ export function useDeletePlacementGroup() {
|
||||
})
|
||||
}
|
||||
|
||||
export function useCreateSecurityGroup() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (params: CreateSecurityGroupFormData) => {
|
||||
const command = new CreateSecurityGroupCommand({
|
||||
GroupName: params.groupName,
|
||||
Description: params.description,
|
||||
VpcId: params.vpcId,
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "securityGroups"] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useDeleteSecurityGroup() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (groupId: string) => {
|
||||
const command = new DeleteSecurityGroupCommand({
|
||||
GroupId: groupId,
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "securityGroups"] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useAuthorizeSecurityGroupIngress() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (params: SecurityGroupRuleFormData & { groupId: string }) => {
|
||||
const command = new AuthorizeSecurityGroupIngressCommand({
|
||||
GroupId: params.groupId,
|
||||
IpPermissions: [
|
||||
{
|
||||
IpProtocol: params.ipProtocol,
|
||||
FromPort: params.fromPort,
|
||||
ToPort: params.toPort,
|
||||
IpRanges: [{ CidrIp: params.cidrIp }],
|
||||
},
|
||||
],
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: (_data, params) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "securityGroups"] })
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["ec2", "securityGroups", params.groupId],
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useAuthorizeSecurityGroupEgress() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (params: SecurityGroupRuleFormData & { groupId: string }) => {
|
||||
const command = new AuthorizeSecurityGroupEgressCommand({
|
||||
GroupId: params.groupId,
|
||||
IpPermissions: [
|
||||
{
|
||||
IpProtocol: params.ipProtocol,
|
||||
FromPort: params.fromPort,
|
||||
ToPort: params.toPort,
|
||||
IpRanges: [{ CidrIp: params.cidrIp }],
|
||||
},
|
||||
],
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: (_data, params) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "securityGroups"] })
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["ec2", "securityGroups", params.groupId],
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useRevokeSecurityGroupIngress() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (params: SecurityGroupRuleFormData & { groupId: string }) => {
|
||||
const command = new RevokeSecurityGroupIngressCommand({
|
||||
GroupId: params.groupId,
|
||||
IpPermissions: [
|
||||
{
|
||||
IpProtocol: params.ipProtocol,
|
||||
FromPort: params.fromPort,
|
||||
ToPort: params.toPort,
|
||||
IpRanges: [{ CidrIp: params.cidrIp }],
|
||||
},
|
||||
],
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: (_data, params) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "securityGroups"] })
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["ec2", "securityGroups", params.groupId],
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useRevokeSecurityGroupEgress() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (params: SecurityGroupRuleFormData & { groupId: string }) => {
|
||||
const command = new RevokeSecurityGroupEgressCommand({
|
||||
GroupId: params.groupId,
|
||||
IpPermissions: [
|
||||
{
|
||||
IpProtocol: params.ipProtocol,
|
||||
FromPort: params.fromPort,
|
||||
ToPort: params.toPort,
|
||||
IpRanges: [{ CidrIp: params.cidrIp }],
|
||||
},
|
||||
],
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: (_data, params) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "securityGroups"] })
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["ec2", "securityGroups", params.groupId],
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function buildTags(name: string | undefined, extraTags: Tag[]): Tag[] {
|
||||
const tags: Tag[] = []
|
||||
if (name) {
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
ec2PlacementGroupQueryOptions,
|
||||
ec2PlacementGroupsQueryOptions,
|
||||
ec2RegionsQueryOptions,
|
||||
ec2SecurityGroupQueryOptions,
|
||||
ec2SecurityGroupsQueryOptions,
|
||||
ec2SnapshotQueryOptions,
|
||||
ec2SnapshotsQueryOptions,
|
||||
ec2SubnetQueryOptions,
|
||||
@@ -135,6 +137,21 @@ describe("query keys", () => {
|
||||
"pg-123",
|
||||
])
|
||||
})
|
||||
|
||||
it("ec2SecurityGroupsQueryOptions has correct key", () => {
|
||||
expect(ec2SecurityGroupsQueryOptions.queryKey).toEqual([
|
||||
"ec2",
|
||||
"securityGroups",
|
||||
])
|
||||
})
|
||||
|
||||
it("ec2SecurityGroupQueryOptions includes groupId in key", () => {
|
||||
expect(ec2SecurityGroupQueryOptions("sg-123").queryKey).toEqual([
|
||||
"ec2",
|
||||
"securityGroups",
|
||||
"sg-123",
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("staleTime and refetchInterval", () => {
|
||||
@@ -349,4 +366,23 @@ describe("queryFn", () => {
|
||||
GroupIds: ["pg-123"],
|
||||
})
|
||||
})
|
||||
|
||||
it("ec2SecurityGroupsQueryOptions sends DescribeSecurityGroupsCommand", async () => {
|
||||
const queryFn = ec2SecurityGroupsQueryOptions.queryFn as (
|
||||
ctx: never,
|
||||
) => Promise<unknown>
|
||||
await queryFn({} as never)
|
||||
expect(mockSend).toHaveBeenCalledOnce()
|
||||
expect(mockSend.mock.calls[0]?.[0].input).toEqual({})
|
||||
})
|
||||
|
||||
it("ec2SecurityGroupQueryOptions sends DescribeSecurityGroupsCommand with ID", async () => {
|
||||
const queryFn = ec2SecurityGroupQueryOptions("sg-123").queryFn as (
|
||||
ctx: never,
|
||||
) => Promise<unknown>
|
||||
await queryFn({} as never)
|
||||
expect(mockSend.mock.calls[0]?.[0].input).toEqual({
|
||||
GroupIds: ["sg-123"],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
DescribePlacementGroupsCommand,
|
||||
DescribeRegionsCommand,
|
||||
DescribeRouteTablesCommand,
|
||||
DescribeSecurityGroupsCommand,
|
||||
DescribeSnapshotsCommand,
|
||||
DescribeSubnetsCommand,
|
||||
DescribeVolumesCommand,
|
||||
@@ -207,6 +208,25 @@ export const ec2PlacementGroupQueryOptions = (groupId: string) =>
|
||||
},
|
||||
})
|
||||
|
||||
export const ec2SecurityGroupsQueryOptions = queryOptions({
|
||||
queryKey: ["ec2", "securityGroups"],
|
||||
queryFn: () => {
|
||||
const command = new DescribeSecurityGroupsCommand({})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
})
|
||||
|
||||
export const ec2SecurityGroupQueryOptions = (groupId: string) =>
|
||||
queryOptions({
|
||||
queryKey: ["ec2", "securityGroups", groupId],
|
||||
queryFn: () => {
|
||||
const command = new DescribeSecurityGroupsCommand({
|
||||
GroupIds: [groupId],
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
})
|
||||
|
||||
export const ec2VolumesQueryOptions = queryOptions({
|
||||
queryKey: ["ec2", "volumes"],
|
||||
queryFn: () => {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { Route as AuthEc2vpcCreateVpcRouteImport } from './routes/_auth/ec2/(vpc
|
||||
import { Route as AuthEc2volumesCreateVolumeRouteImport } from './routes/_auth/ec2/(volumes)/create-volume'
|
||||
import { Route as AuthEc2subnetCreateSubnetRouteImport } from './routes/_auth/ec2/(subnet)/create-subnet'
|
||||
import { Route as AuthEc2snapshotsCreateSnapshotRouteImport } from './routes/_auth/ec2/(snapshots)/create-snapshot'
|
||||
import { Route as AuthEc2securityGroupsCreateSecurityGroupRouteImport } from './routes/_auth/ec2/(security-groups)/create-security-group'
|
||||
import { Route as AuthEc2placementGroupsCreatePlacementGroupRouteImport } from './routes/_auth/ec2/(placement-groups)/create-placement-group'
|
||||
import { Route as AuthEc2keyImportKeyPairRouteImport } from './routes/_auth/ec2/(key)/import-key-pair'
|
||||
import { Route as AuthEc2keyCreateKeyPairRouteImport } from './routes/_auth/ec2/(key)/create-key-pair'
|
||||
@@ -32,6 +33,7 @@ import { Route as AuthEc2vpcDescribeVpcsIndexRouteImport } from './routes/_auth/
|
||||
import { Route as AuthEc2volumesDescribeVolumesIndexRouteImport } from './routes/_auth/ec2/(volumes)/describe-volumes/index'
|
||||
import { Route as AuthEc2subnetDescribeSubnetsIndexRouteImport } from './routes/_auth/ec2/(subnet)/describe-subnets/index'
|
||||
import { Route as AuthEc2snapshotsDescribeSnapshotsIndexRouteImport } from './routes/_auth/ec2/(snapshots)/describe-snapshots/index'
|
||||
import { Route as AuthEc2securityGroupsDescribeSecurityGroupsIndexRouteImport } from './routes/_auth/ec2/(security-groups)/describe-security-groups/index'
|
||||
import { Route as AuthEc2placementGroupsDescribePlacementGroupsIndexRouteImport } from './routes/_auth/ec2/(placement-groups)/describe-placement-groups/index'
|
||||
import { Route as AuthEc2keyDescribeKeyPairsIndexRouteImport } from './routes/_auth/ec2/(key)/describe-key-pairs/index'
|
||||
import { Route as AuthEc2instancesDescribeInstancesIndexRouteImport } from './routes/_auth/ec2/(instances)/describe-instances/index'
|
||||
@@ -44,6 +46,7 @@ import { Route as AuthEc2volumesModifyVolumeIdRouteImport } from './routes/_auth
|
||||
import { Route as AuthEc2volumesDescribeVolumesIdRouteImport } from './routes/_auth/ec2/(volumes)/describe-volumes/$id'
|
||||
import { Route as AuthEc2subnetDescribeSubnetsIdRouteImport } from './routes/_auth/ec2/(subnet)/describe-subnets/$id'
|
||||
import { Route as AuthEc2snapshotsDescribeSnapshotsIdRouteImport } from './routes/_auth/ec2/(snapshots)/describe-snapshots/$id'
|
||||
import { Route as AuthEc2securityGroupsDescribeSecurityGroupsIdRouteImport } from './routes/_auth/ec2/(security-groups)/describe-security-groups/$id'
|
||||
import { Route as AuthEc2placementGroupsDescribePlacementGroupsIdRouteImport } from './routes/_auth/ec2/(placement-groups)/describe-placement-groups/$id'
|
||||
import { Route as AuthEc2keyDescribeKeyPairsIdRouteImport } from './routes/_auth/ec2/(key)/describe-key-pairs/$id'
|
||||
import { Route as AuthEc2instancesDescribeInstancesIdRouteImport } from './routes/_auth/ec2/(instances)/describe-instances/$id'
|
||||
@@ -108,6 +111,12 @@ const AuthEc2snapshotsCreateSnapshotRoute =
|
||||
path: '/ec2/create-snapshot',
|
||||
getParentRoute: () => AuthRouteRoute,
|
||||
} as any)
|
||||
const AuthEc2securityGroupsCreateSecurityGroupRoute =
|
||||
AuthEc2securityGroupsCreateSecurityGroupRouteImport.update({
|
||||
id: '/ec2/(security-groups)/create-security-group',
|
||||
path: '/ec2/create-security-group',
|
||||
getParentRoute: () => AuthRouteRoute,
|
||||
} as any)
|
||||
const AuthEc2placementGroupsCreatePlacementGroupRoute =
|
||||
AuthEc2placementGroupsCreatePlacementGroupRouteImport.update({
|
||||
id: '/ec2/(placement-groups)/create-placement-group',
|
||||
@@ -176,6 +185,12 @@ const AuthEc2snapshotsDescribeSnapshotsIndexRoute =
|
||||
path: '/ec2/describe-snapshots/',
|
||||
getParentRoute: () => AuthRouteRoute,
|
||||
} as any)
|
||||
const AuthEc2securityGroupsDescribeSecurityGroupsIndexRoute =
|
||||
AuthEc2securityGroupsDescribeSecurityGroupsIndexRouteImport.update({
|
||||
id: '/ec2/(security-groups)/describe-security-groups/',
|
||||
path: '/ec2/describe-security-groups/',
|
||||
getParentRoute: () => AuthRouteRoute,
|
||||
} as any)
|
||||
const AuthEc2placementGroupsDescribePlacementGroupsIndexRoute =
|
||||
AuthEc2placementGroupsDescribePlacementGroupsIndexRouteImport.update({
|
||||
id: '/ec2/(placement-groups)/describe-placement-groups/',
|
||||
@@ -247,6 +262,12 @@ const AuthEc2snapshotsDescribeSnapshotsIdRoute =
|
||||
path: '/ec2/describe-snapshots/$id',
|
||||
getParentRoute: () => AuthRouteRoute,
|
||||
} as any)
|
||||
const AuthEc2securityGroupsDescribeSecurityGroupsIdRoute =
|
||||
AuthEc2securityGroupsDescribeSecurityGroupsIdRouteImport.update({
|
||||
id: '/ec2/(security-groups)/describe-security-groups/$id',
|
||||
path: '/ec2/describe-security-groups/$id',
|
||||
getParentRoute: () => AuthRouteRoute,
|
||||
} as any)
|
||||
const AuthEc2placementGroupsDescribePlacementGroupsIdRoute =
|
||||
AuthEc2placementGroupsDescribePlacementGroupsIdRouteImport.update({
|
||||
id: '/ec2/(placement-groups)/describe-placement-groups/$id',
|
||||
@@ -280,6 +301,7 @@ export interface FileRoutesByFullPath {
|
||||
'/ec2/create-key-pair': typeof AuthEc2keyCreateKeyPairRoute
|
||||
'/ec2/import-key-pair': typeof AuthEc2keyImportKeyPairRoute
|
||||
'/ec2/create-placement-group': typeof AuthEc2placementGroupsCreatePlacementGroupRoute
|
||||
'/ec2/create-security-group': typeof AuthEc2securityGroupsCreateSecurityGroupRoute
|
||||
'/ec2/create-snapshot': typeof AuthEc2snapshotsCreateSnapshotRoute
|
||||
'/ec2/create-subnet': typeof AuthEc2subnetCreateSubnetRoute
|
||||
'/ec2/create-volume': typeof AuthEc2volumesCreateVolumeRoute
|
||||
@@ -292,6 +314,7 @@ export interface FileRoutesByFullPath {
|
||||
'/ec2/describe-instances/$id': typeof AuthEc2instancesDescribeInstancesIdRoute
|
||||
'/ec2/describe-key-pairs/$id': typeof AuthEc2keyDescribeKeyPairsIdRoute
|
||||
'/ec2/describe-placement-groups/$id': typeof AuthEc2placementGroupsDescribePlacementGroupsIdRoute
|
||||
'/ec2/describe-security-groups/$id': typeof AuthEc2securityGroupsDescribeSecurityGroupsIdRoute
|
||||
'/ec2/describe-snapshots/$id': typeof AuthEc2snapshotsDescribeSnapshotsIdRoute
|
||||
'/ec2/describe-subnets/$id': typeof AuthEc2subnetDescribeSubnetsIdRoute
|
||||
'/ec2/describe-volumes/$id': typeof AuthEc2volumesDescribeVolumesIdRoute
|
||||
@@ -304,6 +327,7 @@ export interface FileRoutesByFullPath {
|
||||
'/ec2/describe-instances/': typeof AuthEc2instancesDescribeInstancesIndexRoute
|
||||
'/ec2/describe-key-pairs/': typeof AuthEc2keyDescribeKeyPairsIndexRoute
|
||||
'/ec2/describe-placement-groups/': typeof AuthEc2placementGroupsDescribePlacementGroupsIndexRoute
|
||||
'/ec2/describe-security-groups/': typeof AuthEc2securityGroupsDescribeSecurityGroupsIndexRoute
|
||||
'/ec2/describe-snapshots/': typeof AuthEc2snapshotsDescribeSnapshotsIndexRoute
|
||||
'/ec2/describe-subnets/': typeof AuthEc2subnetDescribeSubnetsIndexRoute
|
||||
'/ec2/describe-volumes/': typeof AuthEc2volumesDescribeVolumesIndexRoute
|
||||
@@ -319,6 +343,7 @@ export interface FileRoutesByTo {
|
||||
'/ec2/create-key-pair': typeof AuthEc2keyCreateKeyPairRoute
|
||||
'/ec2/import-key-pair': typeof AuthEc2keyImportKeyPairRoute
|
||||
'/ec2/create-placement-group': typeof AuthEc2placementGroupsCreatePlacementGroupRoute
|
||||
'/ec2/create-security-group': typeof AuthEc2securityGroupsCreateSecurityGroupRoute
|
||||
'/ec2/create-snapshot': typeof AuthEc2snapshotsCreateSnapshotRoute
|
||||
'/ec2/create-subnet': typeof AuthEc2subnetCreateSubnetRoute
|
||||
'/ec2/create-volume': typeof AuthEc2volumesCreateVolumeRoute
|
||||
@@ -331,6 +356,7 @@ export interface FileRoutesByTo {
|
||||
'/ec2/describe-instances/$id': typeof AuthEc2instancesDescribeInstancesIdRoute
|
||||
'/ec2/describe-key-pairs/$id': typeof AuthEc2keyDescribeKeyPairsIdRoute
|
||||
'/ec2/describe-placement-groups/$id': typeof AuthEc2placementGroupsDescribePlacementGroupsIdRoute
|
||||
'/ec2/describe-security-groups/$id': typeof AuthEc2securityGroupsDescribeSecurityGroupsIdRoute
|
||||
'/ec2/describe-snapshots/$id': typeof AuthEc2snapshotsDescribeSnapshotsIdRoute
|
||||
'/ec2/describe-subnets/$id': typeof AuthEc2subnetDescribeSubnetsIdRoute
|
||||
'/ec2/describe-volumes/$id': typeof AuthEc2volumesDescribeVolumesIdRoute
|
||||
@@ -343,6 +369,7 @@ export interface FileRoutesByTo {
|
||||
'/ec2/describe-instances': typeof AuthEc2instancesDescribeInstancesIndexRoute
|
||||
'/ec2/describe-key-pairs': typeof AuthEc2keyDescribeKeyPairsIndexRoute
|
||||
'/ec2/describe-placement-groups': typeof AuthEc2placementGroupsDescribePlacementGroupsIndexRoute
|
||||
'/ec2/describe-security-groups': typeof AuthEc2securityGroupsDescribeSecurityGroupsIndexRoute
|
||||
'/ec2/describe-snapshots': typeof AuthEc2snapshotsDescribeSnapshotsIndexRoute
|
||||
'/ec2/describe-subnets': typeof AuthEc2subnetDescribeSubnetsIndexRoute
|
||||
'/ec2/describe-volumes': typeof AuthEc2volumesDescribeVolumesIndexRoute
|
||||
@@ -361,6 +388,7 @@ export interface FileRoutesById {
|
||||
'/_auth/ec2/(key)/create-key-pair': typeof AuthEc2keyCreateKeyPairRoute
|
||||
'/_auth/ec2/(key)/import-key-pair': typeof AuthEc2keyImportKeyPairRoute
|
||||
'/_auth/ec2/(placement-groups)/create-placement-group': typeof AuthEc2placementGroupsCreatePlacementGroupRoute
|
||||
'/_auth/ec2/(security-groups)/create-security-group': typeof AuthEc2securityGroupsCreateSecurityGroupRoute
|
||||
'/_auth/ec2/(snapshots)/create-snapshot': typeof AuthEc2snapshotsCreateSnapshotRoute
|
||||
'/_auth/ec2/(subnet)/create-subnet': typeof AuthEc2subnetCreateSubnetRoute
|
||||
'/_auth/ec2/(volumes)/create-volume': typeof AuthEc2volumesCreateVolumeRoute
|
||||
@@ -373,6 +401,7 @@ export interface FileRoutesById {
|
||||
'/_auth/ec2/(instances)/describe-instances/$id': typeof AuthEc2instancesDescribeInstancesIdRoute
|
||||
'/_auth/ec2/(key)/describe-key-pairs/$id': typeof AuthEc2keyDescribeKeyPairsIdRoute
|
||||
'/_auth/ec2/(placement-groups)/describe-placement-groups/$id': typeof AuthEc2placementGroupsDescribePlacementGroupsIdRoute
|
||||
'/_auth/ec2/(security-groups)/describe-security-groups/$id': typeof AuthEc2securityGroupsDescribeSecurityGroupsIdRoute
|
||||
'/_auth/ec2/(snapshots)/describe-snapshots/$id': typeof AuthEc2snapshotsDescribeSnapshotsIdRoute
|
||||
'/_auth/ec2/(subnet)/describe-subnets/$id': typeof AuthEc2subnetDescribeSubnetsIdRoute
|
||||
'/_auth/ec2/(volumes)/describe-volumes/$id': typeof AuthEc2volumesDescribeVolumesIdRoute
|
||||
@@ -385,6 +414,7 @@ export interface FileRoutesById {
|
||||
'/_auth/ec2/(instances)/describe-instances/': typeof AuthEc2instancesDescribeInstancesIndexRoute
|
||||
'/_auth/ec2/(key)/describe-key-pairs/': typeof AuthEc2keyDescribeKeyPairsIndexRoute
|
||||
'/_auth/ec2/(placement-groups)/describe-placement-groups/': typeof AuthEc2placementGroupsDescribePlacementGroupsIndexRoute
|
||||
'/_auth/ec2/(security-groups)/describe-security-groups/': typeof AuthEc2securityGroupsDescribeSecurityGroupsIndexRoute
|
||||
'/_auth/ec2/(snapshots)/describe-snapshots/': typeof AuthEc2snapshotsDescribeSnapshotsIndexRoute
|
||||
'/_auth/ec2/(subnet)/describe-subnets/': typeof AuthEc2subnetDescribeSubnetsIndexRoute
|
||||
'/_auth/ec2/(volumes)/describe-volumes/': typeof AuthEc2volumesDescribeVolumesIndexRoute
|
||||
@@ -403,6 +433,7 @@ export interface FileRouteTypes {
|
||||
| '/ec2/create-key-pair'
|
||||
| '/ec2/import-key-pair'
|
||||
| '/ec2/create-placement-group'
|
||||
| '/ec2/create-security-group'
|
||||
| '/ec2/create-snapshot'
|
||||
| '/ec2/create-subnet'
|
||||
| '/ec2/create-volume'
|
||||
@@ -415,6 +446,7 @@ export interface FileRouteTypes {
|
||||
| '/ec2/describe-instances/$id'
|
||||
| '/ec2/describe-key-pairs/$id'
|
||||
| '/ec2/describe-placement-groups/$id'
|
||||
| '/ec2/describe-security-groups/$id'
|
||||
| '/ec2/describe-snapshots/$id'
|
||||
| '/ec2/describe-subnets/$id'
|
||||
| '/ec2/describe-volumes/$id'
|
||||
@@ -427,6 +459,7 @@ export interface FileRouteTypes {
|
||||
| '/ec2/describe-instances/'
|
||||
| '/ec2/describe-key-pairs/'
|
||||
| '/ec2/describe-placement-groups/'
|
||||
| '/ec2/describe-security-groups/'
|
||||
| '/ec2/describe-snapshots/'
|
||||
| '/ec2/describe-subnets/'
|
||||
| '/ec2/describe-volumes/'
|
||||
@@ -442,6 +475,7 @@ export interface FileRouteTypes {
|
||||
| '/ec2/create-key-pair'
|
||||
| '/ec2/import-key-pair'
|
||||
| '/ec2/create-placement-group'
|
||||
| '/ec2/create-security-group'
|
||||
| '/ec2/create-snapshot'
|
||||
| '/ec2/create-subnet'
|
||||
| '/ec2/create-volume'
|
||||
@@ -454,6 +488,7 @@ export interface FileRouteTypes {
|
||||
| '/ec2/describe-instances/$id'
|
||||
| '/ec2/describe-key-pairs/$id'
|
||||
| '/ec2/describe-placement-groups/$id'
|
||||
| '/ec2/describe-security-groups/$id'
|
||||
| '/ec2/describe-snapshots/$id'
|
||||
| '/ec2/describe-subnets/$id'
|
||||
| '/ec2/describe-volumes/$id'
|
||||
@@ -466,6 +501,7 @@ export interface FileRouteTypes {
|
||||
| '/ec2/describe-instances'
|
||||
| '/ec2/describe-key-pairs'
|
||||
| '/ec2/describe-placement-groups'
|
||||
| '/ec2/describe-security-groups'
|
||||
| '/ec2/describe-snapshots'
|
||||
| '/ec2/describe-subnets'
|
||||
| '/ec2/describe-volumes'
|
||||
@@ -483,6 +519,7 @@ export interface FileRouteTypes {
|
||||
| '/_auth/ec2/(key)/create-key-pair'
|
||||
| '/_auth/ec2/(key)/import-key-pair'
|
||||
| '/_auth/ec2/(placement-groups)/create-placement-group'
|
||||
| '/_auth/ec2/(security-groups)/create-security-group'
|
||||
| '/_auth/ec2/(snapshots)/create-snapshot'
|
||||
| '/_auth/ec2/(subnet)/create-subnet'
|
||||
| '/_auth/ec2/(volumes)/create-volume'
|
||||
@@ -495,6 +532,7 @@ export interface FileRouteTypes {
|
||||
| '/_auth/ec2/(instances)/describe-instances/$id'
|
||||
| '/_auth/ec2/(key)/describe-key-pairs/$id'
|
||||
| '/_auth/ec2/(placement-groups)/describe-placement-groups/$id'
|
||||
| '/_auth/ec2/(security-groups)/describe-security-groups/$id'
|
||||
| '/_auth/ec2/(snapshots)/describe-snapshots/$id'
|
||||
| '/_auth/ec2/(subnet)/describe-subnets/$id'
|
||||
| '/_auth/ec2/(volumes)/describe-volumes/$id'
|
||||
@@ -507,6 +545,7 @@ export interface FileRouteTypes {
|
||||
| '/_auth/ec2/(instances)/describe-instances/'
|
||||
| '/_auth/ec2/(key)/describe-key-pairs/'
|
||||
| '/_auth/ec2/(placement-groups)/describe-placement-groups/'
|
||||
| '/_auth/ec2/(security-groups)/describe-security-groups/'
|
||||
| '/_auth/ec2/(snapshots)/describe-snapshots/'
|
||||
| '/_auth/ec2/(subnet)/describe-subnets/'
|
||||
| '/_auth/ec2/(volumes)/describe-volumes/'
|
||||
@@ -600,6 +639,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthEc2snapshotsCreateSnapshotRouteImport
|
||||
parentRoute: typeof AuthRouteRoute
|
||||
}
|
||||
'/_auth/ec2/(security-groups)/create-security-group': {
|
||||
id: '/_auth/ec2/(security-groups)/create-security-group'
|
||||
path: '/ec2/create-security-group'
|
||||
fullPath: '/ec2/create-security-group'
|
||||
preLoaderRoute: typeof AuthEc2securityGroupsCreateSecurityGroupRouteImport
|
||||
parentRoute: typeof AuthRouteRoute
|
||||
}
|
||||
'/_auth/ec2/(placement-groups)/create-placement-group': {
|
||||
id: '/_auth/ec2/(placement-groups)/create-placement-group'
|
||||
path: '/ec2/create-placement-group'
|
||||
@@ -684,6 +730,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthEc2snapshotsDescribeSnapshotsIndexRouteImport
|
||||
parentRoute: typeof AuthRouteRoute
|
||||
}
|
||||
'/_auth/ec2/(security-groups)/describe-security-groups/': {
|
||||
id: '/_auth/ec2/(security-groups)/describe-security-groups/'
|
||||
path: '/ec2/describe-security-groups'
|
||||
fullPath: '/ec2/describe-security-groups/'
|
||||
preLoaderRoute: typeof AuthEc2securityGroupsDescribeSecurityGroupsIndexRouteImport
|
||||
parentRoute: typeof AuthRouteRoute
|
||||
}
|
||||
'/_auth/ec2/(placement-groups)/describe-placement-groups/': {
|
||||
id: '/_auth/ec2/(placement-groups)/describe-placement-groups/'
|
||||
path: '/ec2/describe-placement-groups'
|
||||
@@ -768,6 +821,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthEc2snapshotsDescribeSnapshotsIdRouteImport
|
||||
parentRoute: typeof AuthRouteRoute
|
||||
}
|
||||
'/_auth/ec2/(security-groups)/describe-security-groups/$id': {
|
||||
id: '/_auth/ec2/(security-groups)/describe-security-groups/$id'
|
||||
path: '/ec2/describe-security-groups/$id'
|
||||
fullPath: '/ec2/describe-security-groups/$id'
|
||||
preLoaderRoute: typeof AuthEc2securityGroupsDescribeSecurityGroupsIdRouteImport
|
||||
parentRoute: typeof AuthRouteRoute
|
||||
}
|
||||
'/_auth/ec2/(placement-groups)/describe-placement-groups/$id': {
|
||||
id: '/_auth/ec2/(placement-groups)/describe-placement-groups/$id'
|
||||
path: '/ec2/describe-placement-groups/$id'
|
||||
@@ -819,6 +879,7 @@ interface AuthRouteRouteChildren {
|
||||
AuthEc2keyCreateKeyPairRoute: typeof AuthEc2keyCreateKeyPairRoute
|
||||
AuthEc2keyImportKeyPairRoute: typeof AuthEc2keyImportKeyPairRoute
|
||||
AuthEc2placementGroupsCreatePlacementGroupRoute: typeof AuthEc2placementGroupsCreatePlacementGroupRoute
|
||||
AuthEc2securityGroupsCreateSecurityGroupRoute: typeof AuthEc2securityGroupsCreateSecurityGroupRoute
|
||||
AuthEc2snapshotsCreateSnapshotRoute: typeof AuthEc2snapshotsCreateSnapshotRoute
|
||||
AuthEc2subnetCreateSubnetRoute: typeof AuthEc2subnetCreateSubnetRoute
|
||||
AuthEc2volumesCreateVolumeRoute: typeof AuthEc2volumesCreateVolumeRoute
|
||||
@@ -831,6 +892,7 @@ interface AuthRouteRouteChildren {
|
||||
AuthEc2instancesDescribeInstancesIdRoute: typeof AuthEc2instancesDescribeInstancesIdRoute
|
||||
AuthEc2keyDescribeKeyPairsIdRoute: typeof AuthEc2keyDescribeKeyPairsIdRoute
|
||||
AuthEc2placementGroupsDescribePlacementGroupsIdRoute: typeof AuthEc2placementGroupsDescribePlacementGroupsIdRoute
|
||||
AuthEc2securityGroupsDescribeSecurityGroupsIdRoute: typeof AuthEc2securityGroupsDescribeSecurityGroupsIdRoute
|
||||
AuthEc2snapshotsDescribeSnapshotsIdRoute: typeof AuthEc2snapshotsDescribeSnapshotsIdRoute
|
||||
AuthEc2subnetDescribeSubnetsIdRoute: typeof AuthEc2subnetDescribeSubnetsIdRoute
|
||||
AuthEc2volumesDescribeVolumesIdRoute: typeof AuthEc2volumesDescribeVolumesIdRoute
|
||||
@@ -842,6 +904,7 @@ interface AuthRouteRouteChildren {
|
||||
AuthEc2instancesDescribeInstancesIndexRoute: typeof AuthEc2instancesDescribeInstancesIndexRoute
|
||||
AuthEc2keyDescribeKeyPairsIndexRoute: typeof AuthEc2keyDescribeKeyPairsIndexRoute
|
||||
AuthEc2placementGroupsDescribePlacementGroupsIndexRoute: typeof AuthEc2placementGroupsDescribePlacementGroupsIndexRoute
|
||||
AuthEc2securityGroupsDescribeSecurityGroupsIndexRoute: typeof AuthEc2securityGroupsDescribeSecurityGroupsIndexRoute
|
||||
AuthEc2snapshotsDescribeSnapshotsIndexRoute: typeof AuthEc2snapshotsDescribeSnapshotsIndexRoute
|
||||
AuthEc2subnetDescribeSubnetsIndexRoute: typeof AuthEc2subnetDescribeSubnetsIndexRoute
|
||||
AuthEc2volumesDescribeVolumesIndexRoute: typeof AuthEc2volumesDescribeVolumesIndexRoute
|
||||
@@ -858,6 +921,8 @@ const AuthRouteRouteChildren: AuthRouteRouteChildren = {
|
||||
AuthEc2keyImportKeyPairRoute: AuthEc2keyImportKeyPairRoute,
|
||||
AuthEc2placementGroupsCreatePlacementGroupRoute:
|
||||
AuthEc2placementGroupsCreatePlacementGroupRoute,
|
||||
AuthEc2securityGroupsCreateSecurityGroupRoute:
|
||||
AuthEc2securityGroupsCreateSecurityGroupRoute,
|
||||
AuthEc2snapshotsCreateSnapshotRoute: AuthEc2snapshotsCreateSnapshotRoute,
|
||||
AuthEc2subnetCreateSubnetRoute: AuthEc2subnetCreateSubnetRoute,
|
||||
AuthEc2volumesCreateVolumeRoute: AuthEc2volumesCreateVolumeRoute,
|
||||
@@ -872,6 +937,8 @@ const AuthRouteRouteChildren: AuthRouteRouteChildren = {
|
||||
AuthEc2keyDescribeKeyPairsIdRoute: AuthEc2keyDescribeKeyPairsIdRoute,
|
||||
AuthEc2placementGroupsDescribePlacementGroupsIdRoute:
|
||||
AuthEc2placementGroupsDescribePlacementGroupsIdRoute,
|
||||
AuthEc2securityGroupsDescribeSecurityGroupsIdRoute:
|
||||
AuthEc2securityGroupsDescribeSecurityGroupsIdRoute,
|
||||
AuthEc2snapshotsDescribeSnapshotsIdRoute:
|
||||
AuthEc2snapshotsDescribeSnapshotsIdRoute,
|
||||
AuthEc2subnetDescribeSubnetsIdRoute: AuthEc2subnetDescribeSubnetsIdRoute,
|
||||
@@ -887,6 +954,8 @@ const AuthRouteRouteChildren: AuthRouteRouteChildren = {
|
||||
AuthEc2keyDescribeKeyPairsIndexRoute: AuthEc2keyDescribeKeyPairsIndexRoute,
|
||||
AuthEc2placementGroupsDescribePlacementGroupsIndexRoute:
|
||||
AuthEc2placementGroupsDescribePlacementGroupsIndexRoute,
|
||||
AuthEc2securityGroupsDescribeSecurityGroupsIndexRoute:
|
||||
AuthEc2securityGroupsDescribeSecurityGroupsIndexRoute,
|
||||
AuthEc2snapshotsDescribeSnapshotsIndexRoute:
|
||||
AuthEc2snapshotsDescribeSnapshotsIndexRoute,
|
||||
AuthEc2subnetDescribeSubnetsIndexRoute:
|
||||
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useSuspenseQuery } from "@tanstack/react-query"
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router"
|
||||
import { Controller, useForm } from "react-hook-form"
|
||||
|
||||
import { BackLink } from "@/components/back-link"
|
||||
import { ErrorBanner } from "@/components/error-banner"
|
||||
import { FormActions } from "@/components/form-actions"
|
||||
import { PageHeading } from "@/components/page-heading"
|
||||
import { Field, FieldError, FieldTitle } from "@/components/ui/field"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { getNameTag } from "@/lib/utils"
|
||||
import { useCreateSecurityGroup } from "@/mutations/ec2"
|
||||
import { ec2VpcsQueryOptions } from "@/queries/ec2"
|
||||
import {
|
||||
type CreateSecurityGroupFormData,
|
||||
createSecurityGroupSchema,
|
||||
} from "@/types/ec2"
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_auth/ec2/(security-groups)/create-security-group",
|
||||
)({
|
||||
loader: async ({ context }) => {
|
||||
await context.queryClient.ensureQueryData(ec2VpcsQueryOptions)
|
||||
},
|
||||
head: () => ({
|
||||
meta: [
|
||||
{
|
||||
title: "Create Security Group | EC2 | Mulga",
|
||||
},
|
||||
],
|
||||
}),
|
||||
component: CreateSecurityGroup,
|
||||
})
|
||||
|
||||
function CreateSecurityGroup() {
|
||||
const navigate = useNavigate()
|
||||
const { data: vpcsData } = useSuspenseQuery(ec2VpcsQueryOptions)
|
||||
const createMutation = useCreateSecurityGroup()
|
||||
|
||||
const vpcs = vpcsData.Vpcs ?? []
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<CreateSecurityGroupFormData>({
|
||||
resolver: zodResolver(createSecurityGroupSchema),
|
||||
defaultValues: {
|
||||
groupName: "",
|
||||
description: "",
|
||||
vpcId: vpcs[0]?.VpcId ?? "",
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit = async (data: CreateSecurityGroupFormData) => {
|
||||
const result = await createMutation.mutateAsync(data)
|
||||
const groupId = result.GroupId
|
||||
if (groupId) {
|
||||
navigate({
|
||||
to: "/ec2/describe-security-groups/$id",
|
||||
params: { id: groupId },
|
||||
})
|
||||
} else {
|
||||
navigate({ to: "/ec2/describe-security-groups" })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<BackLink to="/ec2/describe-security-groups">
|
||||
Back to Security Groups
|
||||
</BackLink>
|
||||
|
||||
<PageHeading title="Create Security Group" />
|
||||
|
||||
{createMutation.error && (
|
||||
<ErrorBanner
|
||||
error={createMutation.error}
|
||||
msg="Failed to create security group"
|
||||
/>
|
||||
)}
|
||||
|
||||
<form className="max-w-4xl space-y-6" onSubmit={handleSubmit(onSubmit)}>
|
||||
<Field>
|
||||
<FieldTitle>
|
||||
<label htmlFor="groupName">Group Name</label>
|
||||
</FieldTitle>
|
||||
<Input
|
||||
aria-invalid={!!errors.groupName}
|
||||
id="groupName"
|
||||
placeholder="my-security-group"
|
||||
{...register("groupName")}
|
||||
/>
|
||||
<FieldError errors={[errors.groupName]} />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldTitle>
|
||||
<label htmlFor="description">Description</label>
|
||||
</FieldTitle>
|
||||
<Input
|
||||
aria-invalid={!!errors.description}
|
||||
id="description"
|
||||
placeholder="My security group description"
|
||||
{...register("description")}
|
||||
/>
|
||||
<FieldError errors={[errors.description]} />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldTitle>
|
||||
<label htmlFor="vpcId">VPC</label>
|
||||
</FieldTitle>
|
||||
<Controller
|
||||
control={control}
|
||||
name="vpcId"
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
onValueChange={(value) => field.onChange(value)}
|
||||
value={field.value ?? ""}
|
||||
>
|
||||
<SelectTrigger
|
||||
aria-invalid={!!errors.vpcId}
|
||||
className="w-full"
|
||||
id="vpcId"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{vpcs.map((vpc) => {
|
||||
const name = getNameTag(vpc.Tags)
|
||||
return (
|
||||
<SelectItem key={vpc.VpcId} value={vpc.VpcId ?? ""}>
|
||||
{name
|
||||
? `${vpc.VpcId} (${name})`
|
||||
: `${vpc.VpcId} (${vpc.CidrBlock})`}
|
||||
</SelectItem>
|
||||
)
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
<FieldError errors={[errors.vpcId]} />
|
||||
</Field>
|
||||
|
||||
<FormActions
|
||||
isPending={createMutation.isPending}
|
||||
isSubmitting={isSubmitting}
|
||||
onCancel={() => navigate({ to: "/ec2/describe-security-groups" })}
|
||||
pendingLabel="Creating\u2026"
|
||||
submitLabel="Create Security Group"
|
||||
/>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
+507
@@ -0,0 +1,507 @@
|
||||
import type { IpPermission } from "@aws-sdk/client-ec2"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useSuspenseQuery } from "@tanstack/react-query"
|
||||
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"
|
||||
import { Plus, Trash2, X } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { Controller, useForm } from "react-hook-form"
|
||||
|
||||
import { BackLink } from "@/components/back-link"
|
||||
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog"
|
||||
import { DetailCard } from "@/components/detail-card"
|
||||
import { DetailRow } from "@/components/detail-row"
|
||||
import { ErrorBanner } from "@/components/error-banner"
|
||||
import { PageHeading } from "@/components/page-heading"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Field, FieldError, FieldTitle } from "@/components/ui/field"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import {
|
||||
useAuthorizeSecurityGroupEgress,
|
||||
useAuthorizeSecurityGroupIngress,
|
||||
useDeleteSecurityGroup,
|
||||
useRevokeSecurityGroupEgress,
|
||||
useRevokeSecurityGroupIngress,
|
||||
} from "@/mutations/ec2"
|
||||
import { ec2SecurityGroupQueryOptions } from "@/queries/ec2"
|
||||
import {
|
||||
type SecurityGroupRuleFormData,
|
||||
securityGroupRuleSchema,
|
||||
} from "@/types/ec2"
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_auth/ec2/(security-groups)/describe-security-groups/$id",
|
||||
)({
|
||||
loader: async ({ context, params }) => {
|
||||
await context.queryClient.ensureQueryData(
|
||||
ec2SecurityGroupQueryOptions(params.id),
|
||||
)
|
||||
},
|
||||
head: ({ params }) => ({
|
||||
meta: [
|
||||
{
|
||||
title: `${params.id} | Security Group | Mulga`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
component: SecurityGroupDetail,
|
||||
})
|
||||
|
||||
function formatProtocol(protocol: string | undefined): string {
|
||||
if (!protocol || protocol === "-1") {
|
||||
return "All traffic"
|
||||
}
|
||||
const upper = protocol.toUpperCase()
|
||||
if (upper === "TCP" || upper === "UDP" || upper === "ICMP") {
|
||||
return upper
|
||||
}
|
||||
return protocol
|
||||
}
|
||||
|
||||
function formatPortRange(
|
||||
fromPort: number | undefined,
|
||||
toPort: number | undefined,
|
||||
protocol: string | undefined,
|
||||
): string {
|
||||
if (!protocol || protocol === "-1") {
|
||||
return "All"
|
||||
}
|
||||
if (fromPort === undefined || toPort === undefined) {
|
||||
return "All"
|
||||
}
|
||||
if (fromPort === -1 && toPort === -1) {
|
||||
return "All"
|
||||
}
|
||||
if (fromPort === toPort) {
|
||||
return fromPort.toString()
|
||||
}
|
||||
return `${fromPort}-${toPort}`
|
||||
}
|
||||
|
||||
interface AddRuleDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
direction: "ingress" | "egress"
|
||||
groupId: string
|
||||
}
|
||||
|
||||
function AddRuleDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
direction,
|
||||
groupId,
|
||||
}: AddRuleDialogProps) {
|
||||
const authorizeIngress = useAuthorizeSecurityGroupIngress()
|
||||
const authorizeEgress = useAuthorizeSecurityGroupEgress()
|
||||
const mutation = direction === "ingress" ? authorizeIngress : authorizeEgress
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
register,
|
||||
reset,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm<SecurityGroupRuleFormData>({
|
||||
resolver: zodResolver(securityGroupRuleSchema),
|
||||
defaultValues: {
|
||||
ipProtocol: "tcp",
|
||||
fromPort: 0,
|
||||
toPort: 0,
|
||||
cidrIp: "0.0.0.0/0",
|
||||
},
|
||||
})
|
||||
|
||||
const selectedProtocol = watch("ipProtocol")
|
||||
const portsDisabled = selectedProtocol === "-1" || selectedProtocol === "icmp"
|
||||
|
||||
const onSubmit = async (data: SecurityGroupRuleFormData) => {
|
||||
const params = {
|
||||
groupId,
|
||||
ipProtocol: data.ipProtocol,
|
||||
fromPort: portsDisabled ? -1 : data.fromPort,
|
||||
toPort: portsDisabled ? -1 : data.toPort,
|
||||
cidrIp: data.cidrIp,
|
||||
}
|
||||
await mutation.mutateAsync(params)
|
||||
reset()
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertDialog onOpenChange={onOpenChange} open={open}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Add {direction === "ingress" ? "Inbound" : "Outbound"} Rule
|
||||
</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
|
||||
{mutation.error && (
|
||||
<ErrorBanner error={mutation.error} msg="Failed to add rule" />
|
||||
)}
|
||||
|
||||
<form
|
||||
className="space-y-4"
|
||||
id="add-rule-form"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<Field>
|
||||
<FieldTitle>
|
||||
<label htmlFor="ipProtocol">Protocol</label>
|
||||
</FieldTitle>
|
||||
<Controller
|
||||
control={control}
|
||||
name="ipProtocol"
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
onValueChange={(value) => field.onChange(value)}
|
||||
value={field.value}
|
||||
>
|
||||
<SelectTrigger className="w-full" id="ipProtocol">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="tcp">TCP</SelectItem>
|
||||
<SelectItem value="udp">UDP</SelectItem>
|
||||
<SelectItem value="icmp">ICMP</SelectItem>
|
||||
<SelectItem value="-1">All traffic (-1)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
<FieldError errors={[errors.ipProtocol]} />
|
||||
</Field>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field>
|
||||
<FieldTitle>
|
||||
<label htmlFor="fromPort">From Port</label>
|
||||
</FieldTitle>
|
||||
<Input
|
||||
aria-invalid={!!errors.fromPort}
|
||||
disabled={portsDisabled}
|
||||
id="fromPort"
|
||||
type="number"
|
||||
{...register("fromPort", { valueAsNumber: true })}
|
||||
/>
|
||||
<FieldError errors={[errors.fromPort]} />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldTitle>
|
||||
<label htmlFor="toPort">To Port</label>
|
||||
</FieldTitle>
|
||||
<Input
|
||||
aria-invalid={!!errors.toPort}
|
||||
disabled={portsDisabled}
|
||||
id="toPort"
|
||||
type="number"
|
||||
{...register("toPort", { valueAsNumber: true })}
|
||||
/>
|
||||
<FieldError errors={[errors.toPort]} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<Field>
|
||||
<FieldTitle>
|
||||
<label htmlFor="cidrIp">
|
||||
{direction === "ingress" ? "Source" : "Destination"} CIDR
|
||||
</label>
|
||||
</FieldTitle>
|
||||
<Input
|
||||
aria-invalid={!!errors.cidrIp}
|
||||
id="cidrIp"
|
||||
placeholder="0.0.0.0/0"
|
||||
{...register("cidrIp")}
|
||||
/>
|
||||
<FieldError errors={[errors.cidrIp]} />
|
||||
</Field>
|
||||
</form>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel
|
||||
onClick={() => {
|
||||
reset()
|
||||
mutation.reset()
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={mutation.isPending}
|
||||
form="add-rule-form"
|
||||
type="submit"
|
||||
>
|
||||
{mutation.isPending ? "Adding\u2026" : "Add Rule"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
||||
interface RuleRowProps {
|
||||
rule: IpPermission
|
||||
cidr: string
|
||||
groupId: string
|
||||
direction: "ingress" | "egress"
|
||||
}
|
||||
|
||||
function RuleRow({ rule, cidr, groupId, direction }: RuleRowProps) {
|
||||
const revokeIngress = useRevokeSecurityGroupIngress()
|
||||
const revokeEgress = useRevokeSecurityGroupEgress()
|
||||
const mutation = direction === "ingress" ? revokeIngress : revokeEgress
|
||||
|
||||
const handleRemove = async () => {
|
||||
try {
|
||||
await mutation.mutateAsync({
|
||||
groupId,
|
||||
ipProtocol: rule.IpProtocol ?? "-1",
|
||||
fromPort: rule.FromPort ?? -1,
|
||||
toPort: rule.ToPort ?? -1,
|
||||
cidrIp: cidr,
|
||||
})
|
||||
} catch {
|
||||
// Error is stored in mutation.error and rendered below
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-0">
|
||||
<div className="flex items-center justify-between border-b px-4 py-3 last:border-b-0">
|
||||
<div className="grid flex-1 grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Protocol: </span>
|
||||
{formatProtocol(rule.IpProtocol)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Port Range: </span>
|
||||
{formatPortRange(rule.FromPort, rule.ToPort, rule.IpProtocol)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">
|
||||
{direction === "ingress" ? "Source" : "Destination"}:{" "}
|
||||
</span>
|
||||
{cidr}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
disabled={mutation.isPending}
|
||||
onClick={handleRemove}
|
||||
size="icon-xs"
|
||||
variant="ghost"
|
||||
>
|
||||
<X className="size-3" />
|
||||
</Button>
|
||||
</div>
|
||||
{mutation.error && (
|
||||
<div className="px-4 pb-2">
|
||||
<ErrorBanner error={mutation.error} msg="Failed to remove rule" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SecurityGroupDetail() {
|
||||
const { id } = Route.useParams()
|
||||
const navigate = useNavigate()
|
||||
const { data } = useSuspenseQuery(ec2SecurityGroupQueryOptions(id))
|
||||
const sg = data.SecurityGroups?.[0]
|
||||
const deleteMutation = useDeleteSecurityGroup()
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
const [showIngressDialog, setShowIngressDialog] = useState(false)
|
||||
const [showEgressDialog, setShowEgressDialog] = useState(false)
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await deleteMutation.mutateAsync(id)
|
||||
navigate({ to: "/ec2/describe-security-groups" })
|
||||
} finally {
|
||||
setShowDeleteDialog(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!sg?.GroupId) {
|
||||
return (
|
||||
<>
|
||||
<BackLink to="/ec2/describe-security-groups">
|
||||
Back to Security Groups
|
||||
</BackLink>
|
||||
<p className="text-muted-foreground">Security group not found.</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ingressRules = sg.IpPermissions ?? []
|
||||
const egressRules = sg.IpPermissionsEgress ?? []
|
||||
|
||||
return (
|
||||
<>
|
||||
<BackLink to="/ec2/describe-security-groups">
|
||||
Back to Security Groups
|
||||
</BackLink>
|
||||
|
||||
{deleteMutation.error && (
|
||||
<ErrorBanner
|
||||
error={deleteMutation.error}
|
||||
msg="Failed to delete security group"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="space-y-6">
|
||||
<PageHeading
|
||||
actions={
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={() => setShowDeleteDialog(true)}
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
subtitle="Security Group Details"
|
||||
title={sg.GroupName ? `${sg.GroupId} (${sg.GroupName})` : sg.GroupId}
|
||||
/>
|
||||
|
||||
<DetailCard>
|
||||
<DetailCard.Header>Security Group Information</DetailCard.Header>
|
||||
<DetailCard.Content>
|
||||
<DetailRow label="Group ID" value={sg.GroupId} />
|
||||
<DetailRow label="Group Name" value={sg.GroupName} />
|
||||
<DetailRow label="Description" value={sg.Description} />
|
||||
<DetailRow
|
||||
label="VPC ID"
|
||||
value={
|
||||
sg.VpcId ? (
|
||||
<Link
|
||||
className="text-primary hover:underline"
|
||||
params={{ id: sg.VpcId }}
|
||||
to="/ec2/describe-vpcs/$id"
|
||||
>
|
||||
{sg.VpcId}
|
||||
</Link>
|
||||
) : (
|
||||
"\u2014"
|
||||
)
|
||||
}
|
||||
/>
|
||||
<DetailRow label="Owner ID" value={sg.OwnerId} />
|
||||
</DetailCard.Content>
|
||||
</DetailCard>
|
||||
|
||||
<DetailCard>
|
||||
<div className="flex items-center justify-between border-b p-4">
|
||||
<h2 className="font-semibold">
|
||||
Inbound Rules ({ingressRules.length})
|
||||
</h2>
|
||||
<Button
|
||||
onClick={() => setShowIngressDialog(true)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
Add Rule
|
||||
</Button>
|
||||
</div>
|
||||
{ingressRules.length > 0 ? (
|
||||
<div>
|
||||
{ingressRules.map((rule) =>
|
||||
(rule.IpRanges ?? []).map((range) => (
|
||||
<RuleRow
|
||||
cidr={range.CidrIp ?? ""}
|
||||
direction="ingress"
|
||||
groupId={sg.GroupId ?? ""}
|
||||
key={`${rule.IpProtocol}-${rule.FromPort}-${rule.ToPort}-${range.CidrIp}`}
|
||||
rule={rule}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="p-4 text-sm text-muted-foreground">
|
||||
No inbound rules.
|
||||
</p>
|
||||
)}
|
||||
</DetailCard>
|
||||
|
||||
<DetailCard>
|
||||
<div className="flex items-center justify-between border-b p-4">
|
||||
<h2 className="font-semibold">
|
||||
Outbound Rules ({egressRules.length})
|
||||
</h2>
|
||||
<Button
|
||||
onClick={() => setShowEgressDialog(true)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
Add Rule
|
||||
</Button>
|
||||
</div>
|
||||
{egressRules.length > 0 ? (
|
||||
<div>
|
||||
{egressRules.map((rule) =>
|
||||
(rule.IpRanges ?? []).map((range) => (
|
||||
<RuleRow
|
||||
cidr={range.CidrIp ?? ""}
|
||||
direction="egress"
|
||||
groupId={sg.GroupId ?? ""}
|
||||
key={`${rule.IpProtocol}-${rule.FromPort}-${rule.ToPort}-${range.CidrIp}`}
|
||||
rule={rule}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="p-4 text-sm text-muted-foreground">
|
||||
No outbound rules.
|
||||
</p>
|
||||
)}
|
||||
</DetailCard>
|
||||
</div>
|
||||
|
||||
<DeleteConfirmationDialog
|
||||
description={`Are you sure you want to delete the security group "${sg.GroupId}"? This action cannot be undone.`}
|
||||
isPending={deleteMutation.isPending}
|
||||
onConfirm={handleDelete}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
open={showDeleteDialog}
|
||||
title="Delete Security Group"
|
||||
/>
|
||||
|
||||
<AddRuleDialog
|
||||
direction="ingress"
|
||||
groupId={sg.GroupId}
|
||||
onOpenChange={setShowIngressDialog}
|
||||
open={showIngressDialog}
|
||||
/>
|
||||
|
||||
<AddRuleDialog
|
||||
direction="egress"
|
||||
groupId={sg.GroupId}
|
||||
onOpenChange={setShowEgressDialog}
|
||||
open={showEgressDialog}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import type { SecurityGroup } from "@aws-sdk/client-ec2"
|
||||
import { useSuspenseQuery } from "@tanstack/react-query"
|
||||
import { createFileRoute, Link } from "@tanstack/react-router"
|
||||
|
||||
import { ListCard } from "@/components/list-card"
|
||||
import { PageHeading } from "@/components/page-heading"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ec2SecurityGroupsQueryOptions } from "@/queries/ec2"
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_auth/ec2/(security-groups)/describe-security-groups/",
|
||||
)({
|
||||
loader: async ({ context }) => {
|
||||
await context.queryClient.ensureQueryData(ec2SecurityGroupsQueryOptions)
|
||||
},
|
||||
head: () => ({
|
||||
meta: [
|
||||
{
|
||||
title: "Security Groups | EC2 | Mulga",
|
||||
},
|
||||
],
|
||||
}),
|
||||
component: SecurityGroups,
|
||||
})
|
||||
|
||||
function SecurityGroups() {
|
||||
const { data } = useSuspenseQuery(ec2SecurityGroupsQueryOptions)
|
||||
|
||||
const securityGroups = data.SecurityGroups ?? []
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeading
|
||||
actions={
|
||||
<Link to="/ec2/create-security-group">
|
||||
<Button>Create Security Group</Button>
|
||||
</Link>
|
||||
}
|
||||
title="Security Groups"
|
||||
/>
|
||||
|
||||
{securityGroups.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{securityGroups.map((sg: SecurityGroup) => {
|
||||
if (!sg.GroupId) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<ListCard
|
||||
key={sg.GroupId}
|
||||
params={{ id: sg.GroupId }}
|
||||
subtitle={sg.VpcId ?? ""}
|
||||
title={
|
||||
sg.GroupName ? `${sg.GroupId} (${sg.GroupName})` : sg.GroupId
|
||||
}
|
||||
to="/ec2/describe-security-groups/$id"
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground">No security groups found.</p>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
createInstanceSchema,
|
||||
createKeyPairSchema,
|
||||
createPlacementGroupSchema,
|
||||
createSecurityGroupSchema,
|
||||
createSnapshotSchema,
|
||||
createSubnetSchema,
|
||||
createVolumeSchema,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
formTagSchema,
|
||||
importKeyPairSchema,
|
||||
modifyVolumeSchema,
|
||||
securityGroupRuleSchema,
|
||||
} from "./ec2"
|
||||
|
||||
describe("createInstanceSchema", () => {
|
||||
@@ -343,6 +345,163 @@ describe("formTagSchema", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("createSecurityGroupSchema", () => {
|
||||
it("accepts valid security group params", () => {
|
||||
const result = createSecurityGroupSchema.safeParse({
|
||||
groupName: "web-sg",
|
||||
description: "Allow web traffic",
|
||||
vpcId: "vpc-123",
|
||||
})
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it("rejects empty group name", () => {
|
||||
const result = createSecurityGroupSchema.safeParse({
|
||||
groupName: "",
|
||||
description: "Allow web traffic",
|
||||
vpcId: "vpc-123",
|
||||
})
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it("rejects group name over 255 chars", () => {
|
||||
const result = createSecurityGroupSchema.safeParse({
|
||||
groupName: "a".repeat(256),
|
||||
description: "Allow web traffic",
|
||||
vpcId: "vpc-123",
|
||||
})
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it("accepts group name at 255 chars", () => {
|
||||
const result = createSecurityGroupSchema.safeParse({
|
||||
groupName: "a".repeat(255),
|
||||
description: "Allow web traffic",
|
||||
vpcId: "vpc-123",
|
||||
})
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it("rejects empty description", () => {
|
||||
const result = createSecurityGroupSchema.safeParse({
|
||||
groupName: "web-sg",
|
||||
description: "",
|
||||
vpcId: "vpc-123",
|
||||
})
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it("rejects description over 255 chars", () => {
|
||||
const result = createSecurityGroupSchema.safeParse({
|
||||
groupName: "web-sg",
|
||||
description: "a".repeat(256),
|
||||
vpcId: "vpc-123",
|
||||
})
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it("rejects empty vpcId", () => {
|
||||
const result = createSecurityGroupSchema.safeParse({
|
||||
groupName: "web-sg",
|
||||
description: "Allow web traffic",
|
||||
vpcId: "",
|
||||
})
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("securityGroupRuleSchema", () => {
|
||||
it("accepts valid TCP rule", () => {
|
||||
const result = securityGroupRuleSchema.safeParse({
|
||||
ipProtocol: "tcp",
|
||||
fromPort: 443,
|
||||
toPort: 443,
|
||||
cidrIp: "0.0.0.0/0",
|
||||
})
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it("accepts all-traffic protocol with port -1", () => {
|
||||
const result = securityGroupRuleSchema.safeParse({
|
||||
ipProtocol: "-1",
|
||||
fromPort: -1,
|
||||
toPort: -1,
|
||||
cidrIp: "10.0.0.0/16",
|
||||
})
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it("accepts port 65535 (upper boundary)", () => {
|
||||
const result = securityGroupRuleSchema.safeParse({
|
||||
ipProtocol: "tcp",
|
||||
fromPort: 65_535,
|
||||
toPort: 65_535,
|
||||
cidrIp: "0.0.0.0/0",
|
||||
})
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it("rejects port 65536 (above boundary)", () => {
|
||||
const result = securityGroupRuleSchema.safeParse({
|
||||
ipProtocol: "tcp",
|
||||
fromPort: 65_536,
|
||||
toPort: 443,
|
||||
cidrIp: "0.0.0.0/0",
|
||||
})
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it("rejects port -2 (below boundary)", () => {
|
||||
const result = securityGroupRuleSchema.safeParse({
|
||||
ipProtocol: "tcp",
|
||||
fromPort: -2,
|
||||
toPort: 443,
|
||||
cidrIp: "0.0.0.0/0",
|
||||
})
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it("rejects fractional port", () => {
|
||||
const result = securityGroupRuleSchema.safeParse({
|
||||
ipProtocol: "tcp",
|
||||
fromPort: 22.5,
|
||||
toPort: 443,
|
||||
cidrIp: "0.0.0.0/0",
|
||||
})
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it("rejects invalid CIDR format", () => {
|
||||
const result = securityGroupRuleSchema.safeParse({
|
||||
ipProtocol: "tcp",
|
||||
fromPort: 22,
|
||||
toPort: 22,
|
||||
cidrIp: "not-a-cidr",
|
||||
})
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it("rejects empty CIDR", () => {
|
||||
const result = securityGroupRuleSchema.safeParse({
|
||||
ipProtocol: "tcp",
|
||||
fromPort: 22,
|
||||
toPort: 22,
|
||||
cidrIp: "",
|
||||
})
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it("rejects empty protocol", () => {
|
||||
const result = securityGroupRuleSchema.safeParse({
|
||||
ipProtocol: "",
|
||||
fromPort: 22,
|
||||
toPort: 22,
|
||||
cidrIp: "0.0.0.0/0",
|
||||
})
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("createVpcWizardSchema", () => {
|
||||
const validBase = {
|
||||
mode: "vpc-only" as const,
|
||||
|
||||
@@ -239,3 +239,31 @@ export const createPlacementGroupSchema = z.object({
|
||||
export type CreatePlacementGroupFormData = z.infer<
|
||||
typeof createPlacementGroupSchema
|
||||
>
|
||||
|
||||
export const createSecurityGroupSchema = z.object({
|
||||
groupName: z
|
||||
.string()
|
||||
.min(1, "Group name is required")
|
||||
.max(255, "Group name must be 255 characters or less"),
|
||||
description: z
|
||||
.string()
|
||||
.min(1, "Description is required")
|
||||
.max(255, "Description must be 255 characters or less"),
|
||||
vpcId: z.string().min(1, "VPC is required"),
|
||||
})
|
||||
|
||||
export type CreateSecurityGroupFormData = z.infer<
|
||||
typeof createSecurityGroupSchema
|
||||
>
|
||||
|
||||
export const securityGroupRuleSchema = z.object({
|
||||
ipProtocol: z.string().min(1, "Protocol is required"),
|
||||
fromPort: z.number().int().min(-1).max(65_535),
|
||||
toPort: z.number().int().min(-1).max(65_535),
|
||||
cidrIp: z
|
||||
.string()
|
||||
.min(1, "CIDR is required")
|
||||
.regex(CIDR_REGEX, "Must be a valid CIDR block (e.g. 0.0.0.0/0)"),
|
||||
})
|
||||
|
||||
export type SecurityGroupRuleFormData = z.infer<typeof securityGroupRuleSchema>
|
||||
|
||||
Reference in New Issue
Block a user