diff --git a/packages/api/src/routes/ec2.test.ts b/packages/api/src/routes/ec2.test.ts
index a321e8a..3e4de8f 100644
--- a/packages/api/src/routes/ec2.test.ts
+++ b/packages/api/src/routes/ec2.test.ts
@@ -216,6 +216,17 @@ describe('PUT /instances/:id/security-groups', () => {
expect(res.status).toBe(200)
expect(receivedGroupIds).toEqual(['sg-123'])
})
+
+ test('rejects an empty selection', async () => {
+ const res = await app.request('/instances/i-test1/security-groups', {
+ method: 'PUT',
+ headers: {'content-type': 'application/json'},
+ body: JSON.stringify({groupIds: []}),
+ })
+
+ expect(res.status).toBe(400)
+ expect(await res.json()).toEqual({error: 'At least one security group is required'})
+ })
})
describe('POST /amis/:imageId/deregister', () => {
diff --git a/packages/frontend/index.html b/packages/frontend/index.html
index a900f4b..d6e8c1d 100644
--- a/packages/frontend/index.html
+++ b/packages/frontend/index.html
@@ -3,8 +3,8 @@
-
- Floci UI
+
+ The Cloud
-
+
{
@@ -84,3 +85,29 @@ describe("EC2 security-group rule requests", () => {
expect(new Headers(init?.headers).get("content-type")).toBe("application/json");
});
});
+
+describe("updateEc2InstanceSecurityGroups", () => {
+ const fetchMock = vi.fn();
+
+ beforeEach(() => {
+ fetchMock.mockResolvedValue(new Response(JSON.stringify({ ok: true }), {
+ status: 200,
+ headers: { "Content-Type": "application/json" },
+ }));
+ vi.stubGlobal("fetch", fetchMock);
+ });
+
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ fetchMock.mockReset();
+ });
+
+ it("sends the selected group IDs as a JSON object", async () => {
+ await updateEc2InstanceSecurityGroups("i-test", ["sg-default", "sg-web"]);
+
+ const [url, init] = fetchMock.mock.calls[0];
+ expect(String(url)).toContain("/api/ec2/instances/i-test/security-groups");
+ expect(init?.method).toBe("PUT");
+ expect(JSON.parse(String(init?.body))).toEqual({groupIds: ["sg-default", "sg-web"]});
+ });
+});
diff --git a/packages/frontend/src/api/aws/ec2.api.ts b/packages/frontend/src/api/aws/ec2.api.ts
index e3fe169..c1237d1 100644
--- a/packages/frontend/src/api/aws/ec2.api.ts
+++ b/packages/frontend/src/api/aws/ec2.api.ts
@@ -338,7 +338,7 @@ export async function updateEc2InstanceTags(
export async function updateEc2InstanceSecurityGroups(instanceId: string, groupIds: string[]): Promise {
await apiClient.call(
apiEndpointKeys.aws.ec2.instances.securityGroups,
- { body: JSON.stringify({ groupIds }), headers: { "content-type": "application/json" } },
+ { body: { groupIds } },
{ instanceId },
);
}
diff --git a/packages/frontend/src/assets/TheCloud.png b/packages/frontend/src/assets/TheCloud.png
new file mode 100644
index 0000000..909b7a5
Binary files /dev/null and b/packages/frontend/src/assets/TheCloud.png differ
diff --git a/packages/frontend/src/components/ComputePanel.tsx b/packages/frontend/src/components/ComputePanel.tsx
index db123f9..363c277 100644
--- a/packages/frontend/src/components/ComputePanel.tsx
+++ b/packages/frontend/src/components/ComputePanel.tsx
@@ -154,7 +154,7 @@ function InlineSecurityGroupEditor({ instanceId, vpcId, initialGroups }: {
const [selected, setSelected] = useState(() => new Set(initialGroups.map((group) => group.id).filter(Boolean) as string[]))
const groups = useQuery({queryKey: ['ec2', 'security-groups', vpcId], queryFn: ({signal}) => listEc2SecurityGroups(vpcId, signal)})
const mutation = useMutation({
- mutationFn: () => updateEc2InstanceSecurityGroups(instanceId, [...selected]),
+ mutationFn: (groupIds: string[]) => updateEc2InstanceSecurityGroups(instanceId, groupIds),
onSuccess: () => {
void qc.invalidateQueries({queryKey: ['ec2', 'instance', instanceId]})
void qc.invalidateQueries({queryKey: ['ec2', 'instances']})
@@ -177,7 +177,7 @@ function InlineSecurityGroupEditor({ instanceId, vpcId, initialGroups }: {
)}
{groups.isError && Unable to load security groups.
}
{mutation.isError && {mutation.error instanceof Error ? mutation.error.message : 'Save failed'}
}
-