mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 17:11:30 +00:00
feat: frontend features
This commit is contained in:
+7
-7
@@ -4,7 +4,7 @@
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `run-instances` | `--image-id`, `--instance-type`, `--count` (Min/MaxCount), `--key-name`, `--user-data`, `--block-device-mappings` (DeviceName, VolumeSize, VolumeType, Iops, DeleteOnTermination) | `--security-group-ids`, `--subnet-id`, `--placement`, `--tag-specifications`, `--dry-run`, `--client-token`, `--disable-api-termination`, `--ebs-optimized`, `--iam-instance-profile`, `--network-interfaces`, `--private-ip-address`, `--monitoring`, `--credit-specification`, `--cpu-options`, `--metadata-options`, `--launch-template`, `--hibernate-options` | `describe-images` (AMI must exist), `create-key-pair` (optional), VPC/SG (optional) | Gateway parses AWS query → NATS `ec2.runinstances` → daemon creates QEMU/KVM VM with viperblock-backed root volume via NBD → cloud-init injects user-data/keys → returns reservation with instance ID | 1. Launch with valid AMI and key pair<br>2. Launch with invalid AMI ID (error)<br>3. Launch with block device mappings (custom volume size)<br>4. Launch multiple instances (MinCount/MaxCount)<br>5. DryRun returns validation-only<br>6. Invalid instance type returns error | **DONE** |
|
||||
| `run-instances` | `--image-id`, `--instance-type`, `--count` (Min/MaxCount), `--key-name`, `--user-data`, `--subnet-id` (auto-creates ENI, assigns private IP), `--block-device-mappings` (DeviceName, VolumeSize, VolumeType, Iops, DeleteOnTermination) | `--security-group-ids`, `--placement`, `--tag-specifications`, `--dry-run`, `--client-token`, `--disable-api-termination`, `--ebs-optimized`, `--iam-instance-profile`, `--network-interfaces`, `--private-ip-address`, `--monitoring`, `--credit-specification`, `--cpu-options`, `--metadata-options`, `--launch-template`, `--hibernate-options` | `describe-images` (AMI must exist), `create-key-pair` (optional), VPC/SG (optional) | Gateway parses AWS query → NATS `ec2.runinstances` → daemon creates QEMU/KVM VM with viperblock-backed root volume via NBD → if SubnetId provided, auto-creates ENI with private IP → cloud-init injects user-data/keys → returns reservation with instance ID | 1. Launch with valid AMI and key pair<br>2. Launch with invalid AMI ID (error)<br>3. Launch with block device mappings (custom volume size)<br>4. Launch multiple instances (MinCount/MaxCount)<br>5. Launch with subnet-id (auto-creates ENI)<br>6. Invalid instance type returns error | **DONE** |
|
||||
| `describe-instances` | `--instance-ids` | `--filters`, `--max-results`, `--next-token`, `--dry-run` | None | Gateway fans out NATS `ec2.DescribeInstances` to all nodes (no queue group) → each daemon returns local instances → gateway aggregates and returns combined list | 1. Describe all instances (no filter)<br>2. Describe by instance ID<br>3. Describe with filters (e.g. instance-state-name)<br>4. Instance not found returns empty set<br>5. Multi-node aggregation returns instances from all nodes | **DONE** |
|
||||
| `start-instances` | `--instance-ids` | `--dry-run`, `--force` | `run-instances` (instance must exist in stopped state) | Gateway sends NATS `ec2.cmd.{instance-id}` → daemon restarts stopped QEMU process with same config → state transitions stopped→pending→running | 1. Start a stopped instance<br>2. Start already-running instance (error: IncorrectInstanceState)<br>3. Start with invalid instance ID<br>4. Verify volumes re-mount on start | **DONE** |
|
||||
| `stop-instances` | `--instance-ids` | `--force`, `--hibernate`, `--dry-run` | `run-instances` (instance must be running) | Gateway sends NATS to target node → daemon issues QMP `system_powerdown` for graceful shutdown → monitors heartbeat until QEMU exits → state transitions running→stopping→stopped | 1. Graceful stop of running instance<br>2. Force stop (kills QEMU process)<br>3. Stop already-stopped instance (error)<br>4. Verify ~30s heartbeat detection | **DONE** |
|
||||
@@ -12,7 +12,7 @@
|
||||
| `reboot-instances` | — | `--instance-ids`, `--dry-run` | `run-instances` (instance must be running) | Gateway sends NATS to target node → daemon issues QMP `system_reset` → instance reboots without stopping | 1. Reboot running instance<br>2. Reboot stopped instance (error)<br>3. Verify instance stays in running state after reboot | **NOT STARTED** (parser test exists) |
|
||||
| `describe-instance-types` | `--filters` (capacity filter only) | `--instance-types`, `--max-results`, `--next-token`, `--dry-run`, all other filters | None | Gateway fans out NATS `ec2.DescribeInstanceTypes` to all nodes → each daemon reports supported types (t3.micro/small/medium/large) with vCPU/memory specs → gateway deduplicates and returns | 1. List all instance types<br>2. Filter by specific type<br>3. Filter with `capacity=true` shows available slots<br>4. Verify vCPU/memory specs match hardware | **DONE** |
|
||||
| `modify-instance-attribute` | `--instance-id`, `--instance-type`, `--user-data` | `--disable-api-termination`, `--ebs-optimized`, `--source-dest-check`, `--instance-initiated-shutdown-behavior`, `--block-device-mappings`, `--groups`, `--ena-support`, `--sriov-net-support` | Instance must be stopped (in NATS KV) | Gateway validates input (exactly one attribute per call, instance ID format) → NATS `ec2.ModifyInstanceAttribute` with `hive-workers` queue group → daemon loads stopped instance from JetStream KV → applies attribute change → writes back to KV → returns `{}` on success. **InstanceType**: updates vm.InstanceType, Config, and Instance fields; clears StateReason (enables recovery from instance-type-missing bug). **UserData**: stores decoded content in vm.UserData and re-encodes to base64 for RunInstancesInput (cloud-init on next start). No instance type pre-validation (matches AWS — invalid types accepted, fail at StartInstances time). | 1. Change instance type while stopped<br>2. Change user data while stopped<br>3. Modify running instance (error: NotFound — running instances not in KV)<br>4. Instance not found (error: InvalidInstanceID.NotFound)<br>5. Instance not stopped (error: IncorrectInstanceState)<br>6. Invalid instance type accepted (fails on start with InsufficientInstanceCapacity)<br>7. StateReason cleared on type change (recovery from capacity-unavailable)<br>8. Missing/malformed instance ID (error: InvalidInstanceID.Malformed)<br>9. No attribute set (error: InvalidParameterValue)<br>10. Multiple attributes in one call (error: InvalidParameterValue) | **DONE** |
|
||||
| `get-console-output` | `--instance-id` | `--latest` (always returns latest), `--dry-run` | Instance must be running on a node, serial console access must be enabled via `enable-serial-console-access` | Gateway sends NATS `ec2.{instanceId}.GetConsoleOutput` (per-instance topic, routed to owning node) → daemon checks account-level serial console access → reads console log file from disk → returns last 64KB base64-encoded with timestamp. Respects `enable-serial-console-access` / `disable-serial-console-access` toggle. | 1. Get output from running instance<br>2. Empty log file returns empty output<br>3. Instance not found (error: InvalidInstanceID.NotFound)<br>4. Serial console disabled (error: SerialConsoleSessionUnavailable)<br>5. Enable then disable serial console access toggle | **DONE** |
|
||||
| `get-console-output` | `--instance-id` | `--latest` (always returns latest), `--dry-run` | Instance must be running on a node | Gateway sends NATS `ec2.{instanceId}.GetConsoleOutput` (per-instance topic, routed to owning node) → daemon reads console log file from disk → returns last 64KB base64-encoded with timestamp. Always available regardless of serial console access setting (matches AWS behavior). | 1. Get output from running instance<br>2. Empty log file returns empty output<br>3. Instance not found (error: InvalidInstanceID.NotFound) | **DONE** |
|
||||
| `monitor-instances` | — | `--instance-ids` | Instance must exist | Enable basic monitoring (CPU, disk, network) → store metrics in NATS KV → return monitoring state | 1. Enable monitoring<br>2. Verify monitoring state in describe-instances | **NOT STARTED** |
|
||||
| `unmonitor-instances` | — | `--instance-ids` | Instance must exist | Disable detailed monitoring → revert to basic monitoring → return monitoring state | 1. Disable monitoring<br>2. Verify monitoring state reverts in describe-instances | **NOT STARTED** |
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `describe-images` | `--image-ids` (format validation only) | `--owners`, `--executable-users`, `--filters`, `--include-deprecated`, `--include-disabled`, `--max-results`, `--next-token`, `--dry-run` | None | NATS `ec2.DescribeImages` → daemon reads AMI metadata from Predastore S3 buckets (ami-*) → returns image list with state, architecture, block device mappings | 1. List all images<br>2. Filter by image ID<br>3. Filter by owner (self/amazon)<br>4. Non-existent AMI returns empty<br>5. Verify metadata fields (architecture, state, rootDeviceName) | **DONE** |
|
||||
| `create-image` | — | `--instance-id`, `--name`, `--description`, `--no-reboot`, `--block-device-mappings`, `--tag-specifications` | Instance must exist | Stop instance (unless --no-reboot) → snapshot root volume via viperblock → create AMI metadata in S3 → register as new AMI → restart instance → return ami-ID | 1. Create image from running instance<br>2. Create with --no-reboot<br>3. Invalid instance ID (error)<br>4. Verify new AMI appears in describe-images<br>5. Launch new instance from created AMI | **NOT STARTED** |
|
||||
| `describe-images` | `--image-ids` (format validation only), `--owners` (self, account ID, alias) | `--executable-users`, `--filters`, `--include-deprecated`, `--include-disabled`, `--max-results`, `--next-token`, `--dry-run` | None | NATS `ec2.DescribeImages` → daemon reads AMI metadata from Predastore S3 buckets (ami-*) → filters by ImageIds and Owners → returns image list with state, architecture, block device mappings | 1. List all images<br>2. Filter by image ID<br>3. Filter by owner (self/amazon)<br>4. Non-existent AMI returns empty<br>5. Verify metadata fields (architecture, state, rootDeviceName) | **DONE** |
|
||||
| `create-image` | `--instance-id`, `--name`, `--description`, `--tag-specifications` | `--no-reboot`, `--block-device-mappings`, `--dry-run` | Instance must exist (running or stopped) | Gateway validates input → NATS `ec2.{instanceId}.CreateImage` (per-instance topic) → daemon extracts root volume → snapshots via `ebs.snapshot` NATS (running) or offline S3 copy (stopped) → creates AMI metadata in S3 (`{amiId}/config.json`) → stores tags → returns ami-ID. Duplicate AMI name validation enforced. | 1. Create image from running instance<br>2. Create image from stopped instance<br>3. Invalid instance ID (error)<br>4. Duplicate AMI name (error)<br>5. Verify new AMI appears in describe-images<br>6. Launch new instance from created AMI | **DONE** |
|
||||
| `register-image` | — | `--name`, `--description`, `--architecture`, `--root-device-name`, `--virtualization-type`, `--block-device-mappings`, `--boot-mode`, `--ena-support`, `--sriov-net-support`, `--tpm-support`, `--imds-support` | Image data must exist in S3 | Create AMI metadata entry in Predastore → assign ami-ID → set state to available → return ami-ID | 1. Register with valid metadata<br>2. Missing required name (error)<br>3. Verify registered image in describe-images | **NOT STARTED** |
|
||||
| `deregister-image` | — | `--image-id`, `--dry-run` | AMI must exist, no instances using it | Mark AMI metadata as deregistered in S3 → AMI no longer available for new launches → existing instances unaffected | 1. Deregister existing AMI<br>2. Deregister non-existent AMI (error)<br>3. Verify deregistered AMI not in describe-images<br>4. Existing instances from AMI still run | **NOT STARTED** |
|
||||
| `copy-image` | — | `--source-image-id`, `--source-region`, `--name`, `--description`, `--encrypted`, `--kms-key-id`, `--client-token`, `--copy-image-tags`, `--tag-specifications` | Source AMI must exist | Copy AMI data between regions/nodes in Predastore → create new AMI metadata with new ami-ID → return new ami-ID | 1. Copy image within same region<br>2. Copy non-existent image (error)<br>3. Verify copied image independent of source | **NOT STARTED** |
|
||||
@@ -45,7 +45,7 @@
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `describe-volumes` | `--volume-ids` (fast-path lookup), `DeleteOnTermination` (from persisted VolumeMetadata) | `--filters`, `--max-results`, `--next-token`, `--dry-run` | None | NATS `ec2.DescribeVolumes` → daemon queries viperblock for volume metadata → returns volume list with state, size, attachments, type, DeleteOnTermination flag | 1. List all volumes<br>2. Filter by volume ID<br>3. Filter by attachment state<br>4. Non-existent volume returns empty<br>5. DeleteOnTermination reflects persisted value | **DONE** |
|
||||
| `modify-volume` | `--volume-id`, `--size`, `--volume-type`, `--iops` | `--throughput`, `--dry-run`, `--multi-attach-enabled` | Volume must exist | NATS `ec2.ModifyVolume` → daemon sends resize request to viperblock → NBD does not support live resize, instance must be stopped → returns modification state | 1. Increase volume size<br>2. Modify volume type<br>3. Decrease size (error - not supported)<br>4. Modify attached volume (requires stop/start) | **DONE** |
|
||||
| `create-volume` | `--size`, `--availability-zone`, `--volume-type` (gp3 only) | `--iops`, `--encrypted`, `--snapshot-id`, `--tag-specifications` | Valid AZ configured via `hive init` | Gateway validates input → NATS `ec2.CreateVolume` → daemon generates vol-ID via viperblock → creates empty volume of specified size → persists config.json to Predastore S3 → returns vol-ID with state=available | 1. Create 80GB gp3 volume<br>2. Boundary sizes (1 GiB min, 16384 GiB max)<br>3. Invalid AZ (error)<br>4. Verify volume in describe-volumes<br>5. Unsupported volume type (error - only gp3)<br>6. Size out of range (error) | **DONE** |
|
||||
| `create-volume` | `--size`, `--availability-zone`, `--volume-type` (gp3 only), `--snapshot-id` (creates volume from snapshot) | `--iops` (hardcoded 3000), `--encrypted` (hardcoded false), `--throughput`, `--tag-specifications` | Valid AZ configured via `hive init` | Gateway validates input → NATS `ec2.CreateVolume` → daemon generates vol-ID via viperblock → creates volume (empty or from snapshot) of specified size → persists config.json to Predastore S3 → returns vol-ID with state=available | 1. Create 80GB gp3 volume<br>2. Boundary sizes (1 GiB min, 16384 GiB max)<br>3. Invalid AZ (error)<br>4. Verify volume in describe-volumes<br>5. Unsupported volume type (error - only gp3)<br>6. Size out of range (error)<br>7. Create from snapshot | **DONE** |
|
||||
| `delete-volume` | `--volume-id` | `--dry-run` | Volume must exist and be detached (state=available) | Gateway validates vol- prefix → NATS `ec2.DeleteVolume` → daemon confirms state=available and no AttachedInstance → NATS `ebs.delete` to viperblockd (stops nbdkit/WAL) → deletes S3 objects under vol-id/, vol-id-efi/, vol-id-cloudinit/ → returns success | 1. Delete detached volume<br>2. Delete attached volume (error: VolumeInUse)<br>3. Delete non-existent volume (error: InvalidVolume.NotFound)<br>4. Verify volume gone from describe-volumes<br>5. Malformed volume ID (error: InvalidVolumeID.Malformed)<br>6. Double delete (idempotent NotFound) | **DONE** |
|
||||
| `attach-volume` | `--volume-id`, `--instance-id`, `--device` (optional, auto-assigns `/dev/sd[f-p]`) | `--dry-run` | Volume must exist (available), instance must exist (running) | Gateway sends to `ec2.cmd.{instanceId}` → daemon validates volume (Predastore) → `ebs.mount` via NATS (viperblock starts NBD server) → QMP `blockdev-add` (nbd-{volId}) → QMP `device_add` (virtio-blk-pci, vdisk-{volId}) → three-phase rollback on failure → update EBSRequests + BlockDeviceMappings → persist to JetStream + Predastore → respond with VolumeAttachment | 1. Attach volume to running instance<br>2. Auto-assign device name<br>3. Attach already-attached volume (VolumeInUse)<br>4. Attach to non-existent instance (InvalidInstanceID.NotFound)<br>5. Attach to stopped instance (IncorrectInstanceState)<br>6. Volume not found (InvalidVolume.NotFound)<br>7. All device slots full (AttachmentLimitExceeded)<br>8. Volume persists across stop/start | **DONE** |
|
||||
| `detach-volume` | `--volume-id`, `--instance-id` (optional, resolved via DescribeVolumes), `--device` (optional cross-check), `--force` | `--dry-run` | Volume must be attached, instance must be running | Gateway resolves InstanceId if omitted (via DescribeVolumes) → sends to `ec2.cmd.{instanceId}` → daemon validates (running, attached, not boot/EFI/CloudInit, device match) → three-phase hot-unplug: QMP `device_del` (force continues on failure) → QMP `blockdev-del` (abort if fails, preserves state to prevent double-attach) → `ebs.unmount` via NATS (best-effort) → remove from EBSRequests + BlockDeviceMappings → update volume metadata to available → persist state → respond with VolumeAttachment (state=detaching) | 1. Detach with explicit InstanceId<br>2. Detach without InstanceId (gateway resolution)<br>3. Detach with correct --device cross-check<br>4. Missing VolumeId (InvalidParameterValue)<br>5. Volume not attached (IncorrectState)<br>6. Nonexistent volume (InvalidVolume.NotFound)<br>7. Nonexistent instance (InvalidInstanceID.NotFound)<br>8. Instance not running (IncorrectInstanceState)<br>9. Device mismatch (InvalidParameterValue)<br>10. Boot volume protection (OperationNotPermitted)<br>11. Force flag (continues past device_del failure)<br>12. Volume reusability (re-attach after detach) | **DONE** |
|
||||
@@ -92,8 +92,8 @@ Persistence layer works (NATS JetStream KV), but stored values are not yet enfor
|
||||
| `enable-ebs-encryption-by-default` | *(no flags needed)* | `--dry-run` | None | Gateway validates input → NATS `ec2.EnableEbsEncryptionByDefault` with `hive-workers` queue group → daemon stores `EbsEncryptionByDefault=true` in JetStream KV (`hive-ec2-account-settings` bucket) → return `EbsEncryptionByDefault=true`. Enforcement deferred: stored value not yet checked by CreateVolume/CreateSnapshot. | 1. Enable encryption<br>2. Verify via get | **STARTED** (enforcement pending) |
|
||||
| `disable-ebs-encryption-by-default` | *(no flags needed)* | `--dry-run` | None | Gateway validates input → NATS `ec2.DisableEbsEncryptionByDefault` with `hive-workers` queue group → daemon stores `EbsEncryptionByDefault=false` in JetStream KV → return `EbsEncryptionByDefault=false`. | 1. Disable encryption<br>2. Verify via get | **STARTED** (enforcement pending) |
|
||||
| `get-ebs-encryption-by-default` | *(no flags needed)* | `--dry-run` | None | Gateway validates input → NATS `ec2.GetEbsEncryptionByDefault` with `hive-workers` queue group → daemon reads `EbsEncryptionByDefault` from JetStream KV → return current state. | 1. Get default state<br>2. Verify matches last enable/disable | **STARTED** (enforcement pending) |
|
||||
| `enable-serial-console-access` | *(no flags needed)* | `--dry-run` | None | Gateway validates input → NATS `ec2.EnableSerialConsoleAccess` with `hive-workers` queue group → daemon stores `SerialConsoleAccessEnabled=true` in JetStream KV (`hive-ec2-account-settings` bucket) → return enabled=true. Controls `get-console-output` access. | 1. Enable access<br>2. Verify via get | **DONE** |
|
||||
| `disable-serial-console-access` | *(no flags needed)* | `--dry-run` | None | Gateway validates input → NATS `ec2.DisableSerialConsoleAccess` with `hive-workers` queue group → daemon stores `SerialConsoleAccessEnabled=false` in JetStream KV → return enabled=false. Causes `get-console-output` to return `SerialConsoleSessionUnavailable`. | 1. Disable access<br>2. Verify via get<br>3. get-console-output blocked when disabled | **DONE** |
|
||||
| `enable-serial-console-access` | *(no flags needed)* | `--dry-run` | None | Gateway validates input → NATS `ec2.EnableSerialConsoleAccess` with `hive-workers` queue group → daemon stores `SerialConsoleAccessEnabled=true` in JetStream KV (`hive-ec2-account-settings` bucket) → return enabled=true. Stored for future interactive serial console support (not currently enforced by any operation). | 1. Enable access<br>2. Verify via get | **STARTED** (enforcement pending) |
|
||||
| `disable-serial-console-access` | *(no flags needed)* | `--dry-run` | None | Gateway validates input → NATS `ec2.DisableSerialConsoleAccess` with `hive-workers` queue group → daemon stores `SerialConsoleAccessEnabled=false` in JetStream KV → return enabled=false. Stored for future interactive serial console support (not currently enforced by any operation). | 1. Disable access<br>2. Verify via get | **STARTED** (enforcement pending) |
|
||||
| `get-serial-console-access-status` | *(no flags needed)* | `--dry-run` | None | Gateway validates input → NATS `ec2.GetSerialConsoleAccessStatus` with `hive-workers` queue group → daemon reads `SerialConsoleAccessEnabled` from JetStream KV → return current state. | 1. Get current status<br>2. Verify matches last enable/disable | **DONE** |
|
||||
| `enable-snapshot-block-public-access` | — | `--state` | None | Store snapshot block public access state in JetStream KV | 1. Enable with block-all-sharing<br>2. Enable with block-new-sharing | **NOT STARTED** (enforcement pending) |
|
||||
| `disable-snapshot-block-public-access` | — | `--dry-run` | None | Clear snapshot block public access state in JetStream KV | 1. Disable access | **NOT STARTED** (enforcement pending) |
|
||||
|
||||
@@ -31,18 +31,6 @@ func (d *Daemon) handleEC2GetConsoleOutput(msg *nats.Msg) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check account-level serial console access
|
||||
status, err := d.accountService.GetSerialConsoleAccessStatus(&ec2.GetSerialConsoleAccessStatusInput{})
|
||||
if err != nil {
|
||||
slog.Error("Failed to check serial console access status", "err", err)
|
||||
respondWithError(msg, awserrors.ErrorServerInternal)
|
||||
return
|
||||
}
|
||||
if status.SerialConsoleAccessEnabled == nil || !*status.SerialConsoleAccessEnabled {
|
||||
respondWithError(msg, awserrors.ErrorSerialConsoleSessionUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
instanceID := *input.InstanceId
|
||||
|
||||
// Find the instance on this node
|
||||
|
||||
@@ -1309,10 +1309,6 @@ func TestHandleEC2GetConsoleOutput(t *testing.T) {
|
||||
natsURL := sharedNATSURL
|
||||
daemon := createFullTestDaemon(t, natsURL)
|
||||
|
||||
// Enable serial console access
|
||||
_, err := daemon.accountService.EnableSerialConsoleAccess(&ec2.EnableSerialConsoleAccessInput{})
|
||||
require.NoError(t, err)
|
||||
|
||||
instanceID := "i-console-test-001"
|
||||
|
||||
// Create a temp console log file
|
||||
@@ -1361,10 +1357,6 @@ func TestHandleEC2GetConsoleOutput_EmptyLog(t *testing.T) {
|
||||
natsURL := sharedNATSURL
|
||||
daemon := createFullTestDaemon(t, natsURL)
|
||||
|
||||
// Enable serial console access
|
||||
_, err := daemon.accountService.EnableSerialConsoleAccess(&ec2.EnableSerialConsoleAccessInput{})
|
||||
require.NoError(t, err)
|
||||
|
||||
instanceID := "i-console-empty-001"
|
||||
|
||||
// Instance exists but no log file yet
|
||||
@@ -1402,10 +1394,6 @@ func TestHandleEC2GetConsoleOutput_NotFound(t *testing.T) {
|
||||
natsURL := sharedNATSURL
|
||||
daemon := createFullTestDaemon(t, natsURL)
|
||||
|
||||
// Enable serial console access
|
||||
_, err := daemon.accountService.EnableSerialConsoleAccess(&ec2.EnableSerialConsoleAccessInput{})
|
||||
require.NoError(t, err)
|
||||
|
||||
instanceID := "i-nonexistent-console"
|
||||
topic := fmt.Sprintf("ec2.%s.GetConsoleOutput", instanceID)
|
||||
sub, err := daemon.natsConn.Subscribe(topic, daemon.handleEC2GetConsoleOutput)
|
||||
@@ -1423,84 +1411,6 @@ func TestHandleEC2GetConsoleOutput_NotFound(t *testing.T) {
|
||||
assert.Contains(t, string(reply.Data), "InvalidInstanceID.NotFound")
|
||||
}
|
||||
|
||||
func TestHandleEC2GetConsoleOutput_SerialConsoleDisabled(t *testing.T) {
|
||||
natsURL := sharedNATSURL
|
||||
daemon := createFullTestDaemon(t, natsURL)
|
||||
|
||||
// Serial console access defaults to disabled — do NOT enable it
|
||||
|
||||
instanceID := "i-console-disabled-001"
|
||||
daemon.Instances.Mu.Lock()
|
||||
daemon.Instances.VMS[instanceID] = &vm.VM{
|
||||
ID: instanceID,
|
||||
Status: vm.StateRunning,
|
||||
Config: vm.Config{
|
||||
ConsoleLogPath: "/tmp/some-log.log",
|
||||
},
|
||||
}
|
||||
daemon.Instances.Mu.Unlock()
|
||||
|
||||
topic := fmt.Sprintf("ec2.%s.GetConsoleOutput", instanceID)
|
||||
sub, err := daemon.natsConn.Subscribe(topic, daemon.handleEC2GetConsoleOutput)
|
||||
require.NoError(t, err)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
input := &ec2.GetConsoleOutputInput{
|
||||
InstanceId: aws.String(instanceID),
|
||||
}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := daemon.natsConn.Request(topic, reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, string(reply.Data), "SerialConsoleSessionUnavailable")
|
||||
}
|
||||
|
||||
func TestHandleEC2GetConsoleOutput_EnableThenDisable(t *testing.T) {
|
||||
natsURL := sharedNATSURL
|
||||
daemon := createFullTestDaemon(t, natsURL)
|
||||
|
||||
instanceID := "i-console-toggle-001"
|
||||
tmpDir := t.TempDir()
|
||||
logPath := tmpDir + "/console.log"
|
||||
require.NoError(t, os.WriteFile(logPath, []byte("console output"), 0644))
|
||||
|
||||
daemon.Instances.Mu.Lock()
|
||||
daemon.Instances.VMS[instanceID] = &vm.VM{
|
||||
ID: instanceID,
|
||||
Status: vm.StateRunning,
|
||||
Config: vm.Config{
|
||||
ConsoleLogPath: logPath,
|
||||
},
|
||||
}
|
||||
daemon.Instances.Mu.Unlock()
|
||||
|
||||
topic := fmt.Sprintf("ec2.%s.GetConsoleOutput", instanceID)
|
||||
sub, err := daemon.natsConn.Subscribe(topic, daemon.handleEC2GetConsoleOutput)
|
||||
require.NoError(t, err)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
// Enable serial console access — GetConsoleOutput should succeed
|
||||
_, err = daemon.accountService.EnableSerialConsoleAccess(&ec2.EnableSerialConsoleAccessInput{})
|
||||
require.NoError(t, err)
|
||||
|
||||
input := &ec2.GetConsoleOutputInput{InstanceId: aws.String(instanceID)}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := daemon.natsConn.Request(topic, reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var output ec2.GetConsoleOutputOutput
|
||||
require.NoError(t, json.Unmarshal(reply.Data, &output))
|
||||
assert.NotEmpty(t, *output.Output)
|
||||
|
||||
// Disable serial console access — GetConsoleOutput should fail
|
||||
_, err = daemon.accountService.DisableSerialConsoleAccess(&ec2.DisableSerialConsoleAccessInput{})
|
||||
require.NoError(t, err)
|
||||
|
||||
reply, err = daemon.natsConn.Request(topic, reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(reply.Data), "SerialConsoleSessionUnavailable")
|
||||
}
|
||||
|
||||
// TestAttachVolume_ZoneMismatch verifies that attaching a volume in a different AZ
|
||||
// returns InvalidVolume.ZoneMismatch instead of proceeding.
|
||||
func TestAttachVolume_ZoneMismatch(t *testing.T) {
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{c as B,aE as $,aF as F,aB as C,j as s,aA as K,aG as b,aH as k,L as E}from"./index.js";import{u as L}from"./useSuspenseQuery.js";import{B as A}from"./back-link.js";import{E as O}from"./error-banner.js";import{a as S}from"./s3.js";import{U,F as R,O as I}from"./upload-button.js";import"./alert-dialog.js";import"./download.js";import"./trash-2.js";function J(){const e=B.c(24),{bucket:n,_splat:v}=$.useParams(),a=F(v||""),{data:m}=L(C(n,a)),r=S();let d;e[0]!==m.Contents?(d=m.Contents||[],e[0]=m.Contents,e[1]=d):d=e[1];const o=d;let p;e[2]!==m.CommonPrefixes?(p=m.CommonPrefixes||[],e[2]=m.CommonPrefixes,e[3]=p):p=e[3];const i=p,u=a.split("/").filter(Boolean),y=[{name:n,path:`/s3/ls/${n}`},...u.map((t,l)=>{const c=`${u.slice(0,l+1).join("/")}/`;return{name:t,path:`/s3/ls/${n}/${c}`}})],P=u.length>1?`/s3/ls/${n}/${u.slice(0,-1).join("/")}/`:`/s3/ls/${n}`;let f;e[4]!==P?(f=s.jsx(A,{to:P,children:"Back"}),e[4]=P,e[5]=f):f=e[5];let x;e[6]!==n||e[7]!==a||e[8]!==r.isPending||e[9]!==r.mutateAsync?(x=s.jsx(U,{bucket:n,isPending:r.isPending,onUpload:r.mutateAsync,prefix:a}),e[6]=n,e[7]=a,e[8]=r.isPending,e[9]=r.mutateAsync,e[10]=x):x=e[10];let h;e[11]!==r.error?(h=r.error&&s.jsx(O,{error:r.error,msg:"Failed to upload object"}),e[11]=r.error,e[12]=h):h=e[12];let g;e[13]!==n||e[14]!==i||e[15]!==a?(g=i.length>0&&s.jsxs("div",{className:"mb-6",children:[s.jsx("h2",{className:"mb-3 font-semibold text-lg",children:"Folders"}),s.jsx("div",{className:"space-y-2",children:i.map(t=>{if(!t.Prefix)return null;const l=K(b(t.Prefix,a)),c=k(t.Prefix,a);return s.jsx(R,{bucketName:n,folderName:l,fullPrefix:c},t.Prefix)})})]}),e[13]=n,e[14]=i,e[15]=a,e[16]=g):g=e[16];let j;e[17]!==n||e[18]!==o||e[19]!==a?(j=o.length>0&&s.jsxs("div",{className:"space-y-2",children:[s.jsx("h2",{className:"mb-3 font-semibold text-lg",children:"Objects"}),o.map(t=>{if(!t.Key)return null;const l=b(t.Key,a),c=k(t.Key,a);return s.jsx(I,{bucketName:n,displayName:l,fullKey:c,object:t},t.Key)})]}),e[17]=n,e[18]=o,e[19]=a,e[20]=j):j=e[20];let N;return e[21]!==i.length||e[22]!==o.length?(N=o.length===0&&i.length===0&&s.jsx("p",{className:"text-muted-foreground",children:"No objects found in this folder."}),e[21]=i.length,e[22]=o.length,e[23]=N):N=e[23],s.jsxs(s.Fragment,{children:[f,s.jsx("div",{className:"mb-6",children:s.jsxs("div",{className:"mb-2 flex items-center justify-between",children:[s.jsx("div",{className:"flex items-center gap-2 text-muted-foreground text-sm",children:y.map((t,l)=>s.jsxs("div",{className:"flex items-center gap-2",children:[l>0&&s.jsx("span",{children:"/"}),l===y.length-1?s.jsx("span",{className:"text-foreground",children:t.name}):s.jsx(E,{className:"hover:text-foreground",to:t.path,children:t.name})]},t.path))}),x]})}),h,g,j,N]})}export{J as component};
|
||||
import{c as B,y as $,z as C,v as F,j as s,t as K,A as b,C as v,L}from"./index.js";import{u as A}from"./useSuspenseQuery.js";import{B as E}from"./back-link.js";import{E as O}from"./error-banner.js";import{a as S}from"./s3.js";import{U,F as R,O as I}from"./upload-button.js";import"./alert-dialog.js";import"./download.js";import"./trash-2.js";function J(){const e=B.c(24),{bucket:n,_splat:k}=$.useParams(),r=C(k||""),{data:m}=A(F(n,r)),a=S();let d;e[0]!==m.Contents?(d=m.Contents||[],e[0]=m.Contents,e[1]=d):d=e[1];const o=d;let p;e[2]!==m.CommonPrefixes?(p=m.CommonPrefixes||[],e[2]=m.CommonPrefixes,e[3]=p):p=e[3];const i=p,u=r.split("/").filter(Boolean),y=[{name:n,path:`/s3/ls/${n}`},...u.map((t,l)=>{const c=`${u.slice(0,l+1).join("/")}/`;return{name:t,path:`/s3/ls/${n}/${c}`}})],P=u.length>1?`/s3/ls/${n}/${u.slice(0,-1).join("/")}/`:`/s3/ls/${n}`;let f;e[4]!==P?(f=s.jsx(E,{to:P,children:"Back"}),e[4]=P,e[5]=f):f=e[5];let x;e[6]!==n||e[7]!==r||e[8]!==a.isPending||e[9]!==a.mutateAsync?(x=s.jsx(U,{bucket:n,isPending:a.isPending,onUpload:a.mutateAsync,prefix:r}),e[6]=n,e[7]=r,e[8]=a.isPending,e[9]=a.mutateAsync,e[10]=x):x=e[10];let h;e[11]!==a.error?(h=a.error&&s.jsx(O,{error:a.error,msg:"Failed to upload object"}),e[11]=a.error,e[12]=h):h=e[12];let g;e[13]!==n||e[14]!==i||e[15]!==r?(g=i.length>0&&s.jsxs("div",{className:"mb-6",children:[s.jsx("h2",{className:"mb-3 font-semibold text-lg",children:"Folders"}),s.jsx("div",{className:"space-y-2",children:i.map(t=>{if(!t.Prefix)return null;const l=K(b(t.Prefix,r)),c=v(t.Prefix,r);return s.jsx(R,{bucketName:n,folderName:l,fullPrefix:c},t.Prefix)})})]}),e[13]=n,e[14]=i,e[15]=r,e[16]=g):g=e[16];let j;e[17]!==n||e[18]!==o||e[19]!==r?(j=o.length>0&&s.jsxs("div",{className:"space-y-2",children:[s.jsx("h2",{className:"mb-3 font-semibold text-lg",children:"Objects"}),o.map(t=>{if(!t.Key)return null;const l=b(t.Key,r),c=v(t.Key,r);return s.jsx(I,{bucketName:n,displayName:l,fullKey:c,object:t},t.Key)})]}),e[17]=n,e[18]=o,e[19]=r,e[20]=j):j=e[20];let N;return e[21]!==i.length||e[22]!==o.length?(N=o.length===0&&i.length===0&&s.jsx("p",{className:"text-muted-foreground",children:"No objects found in this folder."}),e[21]=i.length,e[22]=o.length,e[23]=N):N=e[23],s.jsxs(s.Fragment,{children:[f,s.jsx("div",{className:"mb-6",children:s.jsxs("div",{className:"mb-2 flex items-center justify-between",children:[s.jsx("div",{className:"flex items-center gap-2 text-muted-foreground text-sm",children:y.map((t,l)=>s.jsxs("div",{className:"flex items-center gap-2",children:[l>0&&s.jsx("span",{children:"/"}),l===y.length-1?s.jsx("span",{className:"text-foreground",children:t.name}):s.jsx(L,{className:"hover:text-foreground",to:t.path,children:t.name})]},t.path))}),x]})}),h,g,j,N]})}export{J as component};
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{c as T,bm as q,u as D,j as s,bn as J,B as Q}from"./index.js";import{a as K,u as U,I as W,F as X,b as Y,c as Z}from"./field.js";import{u as ee}from"./useSuspenseQuery.js";import{B as H}from"./back-link.js";import{E as se}from"./error-banner.js";import{P as te}from"./page-heading.js";import{d as ie}from"./ec2.js";import{m as re}from"./ec22.js";function fe(){const e=T.c(58),{id:R}=q.useParams(),o=D();let S;e[0]!==R?(S=J(R),e[0]=R,e[1]=S):S=e[1];const{data:L}=ee(S),A=L.Volumes?.[0],t=A?.VolumeId,r=ie(),i=A?.Size??1;let n;e[2]!==i?(n=K(re.refine(l=>l.size>i,{message:`New size must be greater than current size (${i} GiB)`,path:["size"]})),e[2]=i,e[3]=n):n=e[3];let a;e[4]!==i?(a={size:i},e[4]=i,e[5]=a):a=e[5];let B;e[6]!==n||e[7]!==a?(B={resolver:n,defaultValues:a},e[6]=n,e[7]=a,e[8]=B):B=e[8];const{handleSubmit:k,register:E,formState:O}=U(B),{errors:V,isSubmitting:$}=O;if(!t){let l;return e[9]===Symbol.for("react.memo_cache_sentinel")?(l=s.jsxs(s.Fragment,{children:[s.jsx(H,{to:"/ec2/describe-volumes",children:"Back to volumes"}),s.jsx("p",{className:"text-muted-foreground",children:"Volume not found."})]}),e[9]=l):l=e[9],l}let F;e[10]!==r||e[11]!==o||e[12]!==t?(F=async l=>{await r.mutateAsync({...l,volumeId:t}),o({to:"/ec2/describe-volumes/$id",params:{id:t}})},e[10]=r,e[11]=o,e[12]=t,e[13]=F):F=e[13];const w=F;let m,c;e[14]!==t?(m=s.jsx(H,{params:{id:t},to:"/ec2/describe-volumes/$id",children:"Back to volume"}),c=s.jsx(te,{subtitle:t,title:"Resize Volume"}),e[14]=t,e[15]=m,e[16]=c):(m=e[15],c=e[16]);let u;e[17]!==r.error?(u=r.error&&s.jsx(se,{error:r.error,msg:"Failed to resize volume"}),e[17]=r.error,e[18]=u):u=e[18];let d;e[19]!==k||e[20]!==w?(d=k(w),e[19]=k,e[20]=w,e[21]=d):d=e[21];let f;e[22]!==i?(f=s.jsx("div",{className:"space-y-2",children:s.jsxs("p",{className:"text-muted-foreground text-sm",children:["Current size: ",i," GiB"]})}),e[22]=i,e[23]=f):f=e[23];let N;e[24]===Symbol.for("react.memo_cache_sentinel")?(N=s.jsx(Y,{children:s.jsx("label",{htmlFor:"size",children:"New size (GiB)"})}),e[24]=N):N=e[24];const I=!!V.size,_=i+1;let p;e[25]!==E?(p=E("size",{valueAsNumber:!0}),e[25]=E,e[26]=p):p=e[26];let x;e[27]!==I||e[28]!==_||e[29]!==p?(x=s.jsx(W,{"aria-invalid":I,id:"size",min:_,type:"number",...p}),e[27]=I,e[28]=_,e[29]=p,e[30]=x):x=e[30];let b;e[31]!==V.size?(b=s.jsx(X,{errors:[V.size]}),e[31]=V.size,e[32]=b):b=e[32];let h;e[33]!==x||e[34]!==b?(h=s.jsxs(Z,{children:[N,x,b]}),e[33]=x,e[34]=b,e[35]=h):h=e[35];const C=$||r.isPending;let j;e[36]!==o||e[37]!==t?(j=()=>o({to:"/ec2/describe-volumes/$id",params:{id:t}}),e[36]=o,e[37]=t,e[38]=j):j=e[38];let z;e[39]!==C||e[40]!==j?(z=s.jsx(Q,{disabled:C,onClick:j,type:"button",variant:"outline",children:"Cancel"}),e[39]=C,e[40]=j,e[41]=z):z=e[41];const G=$||r.isPending,M=$||r.isPending?"Resizing…":"Resize Volume";let g;e[42]!==G||e[43]!==M?(g=s.jsx(Q,{disabled:G,type:"submit",children:M}),e[42]=G,e[43]=M,e[44]=g):g=e[44];let v;e[45]!==z||e[46]!==g?(v=s.jsxs("div",{className:"flex gap-2",children:[z,g]}),e[45]=z,e[46]=g,e[47]=v):v=e[47];let y;e[48]!==f||e[49]!==h||e[50]!==v||e[51]!==d?(y=s.jsxs("form",{className:"max-w-4xl space-y-6",onSubmit:d,children:[f,h,v]}),e[48]=f,e[49]=h,e[50]=v,e[51]=d,e[52]=y):y=e[52];let P;return e[53]!==y||e[54]!==m||e[55]!==c||e[56]!==u?(P=s.jsxs(s.Fragment,{children:[m,c,u,y]}),e[53]=y,e[54]=m,e[55]=c,e[56]=u,e[57]=P):P=e[57],P}export{fe as component};
|
||||
import{ak as D,u as g,al as b,p as f,r as v,j as e,B as C,L as I}from"./index.js";import{u as d}from"./useSuspenseQuery.js";import{B as u}from"./back-link.js";import{D as l,a as t}from"./detail-row.js";import{E as V}from"./error-banner.js";import{P as S}from"./page-heading.js";import{S as y}from"./state-badge.js";import{g as P,f as A,b as k,a as B,c as w,d as N,e as T,h as O}from"./alert-dialog.js";import{g as H}from"./ec2.js";import{T as R}from"./trash-2.js";function q(){const{id:r}=D.useParams(),x=g(),{data:p}=d(b(r)),{data:m}=d(f),s=p.Vpcs?.[0],n=H(),[j,i]=v.useState(!1),o=m.Subnets?.filter(a=>a.VpcId===r)??[],h=async()=>{try{await n.mutateAsync(r),x({to:"/ec2/describe-vpcs"})}finally{i(!1)}};if(!s?.VpcId)return e.jsxs(e.Fragment,{children:[e.jsx(u,{to:"/ec2/describe-vpcs",children:"Back to VPCs"}),e.jsx("p",{className:"text-muted-foreground",children:"VPC not found."})]});const c=s.Tags?.find(a=>a.Key==="Name")?.Value;return e.jsxs(e.Fragment,{children:[e.jsx(u,{to:"/ec2/describe-vpcs",children:"Back to VPCs"}),n.error&&e.jsx(V,{error:n.error,msg:"Failed to delete VPC"}),e.jsxs("div",{className:"space-y-6",children:[e.jsx(S,{actions:e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsxs(C,{onClick:()=>i(!0),size:"sm",variant:"destructive",children:[e.jsx(R,{className:"size-4"}),"Delete"]}),e.jsx(y,{state:s.State})]}),subtitle:"VPC Details",title:c?`${s.VpcId} (${c})`:s.VpcId}),e.jsxs(l,{children:[e.jsx(l.Header,{children:"VPC Information"}),e.jsxs(l.Content,{children:[e.jsx(t,{label:"VPC ID",value:s.VpcId}),e.jsx(t,{label:"State",value:s.State}),e.jsx(t,{label:"CIDR Block",value:s.CidrBlock}),e.jsx(t,{label:"Is Default",value:s.IsDefault?"Yes":"No"}),e.jsx(t,{label:"DHCP Options ID",value:s.DhcpOptionsId}),e.jsx(t,{label:"Instance Tenancy",value:s.InstanceTenancy}),e.jsx(t,{label:"Owner ID",value:s.OwnerId})]})]}),o.length>0&&e.jsxs(l,{children:[e.jsxs(l.Header,{children:["Subnets (",o.length,")"]}),o.map(a=>a.SubnetId?e.jsxs(l.Content,{children:[e.jsx(t,{label:"Subnet ID",value:e.jsx(I,{className:"text-primary hover:underline",params:{id:a.SubnetId},to:"/ec2/describe-subnets/$id",children:a.SubnetId})}),e.jsx(t,{label:"CIDR Block",value:a.CidrBlock}),e.jsx(t,{label:"Availability Zone",value:a.AvailabilityZone}),e.jsx(t,{label:"State",value:a.State})]},a.SubnetId):null)]})]}),e.jsx(P,{onOpenChange:i,open:j,children:e.jsxs(A,{children:[e.jsxs(k,{children:[e.jsx(B,{children:"Delete VPC"}),e.jsxs(w,{children:['Are you sure you want to delete the VPC "',s.VpcId,'"? This action cannot be undone. The VPC must have no associated subnets or internet gateways.']})]}),e.jsxs(N,{children:[e.jsx(T,{children:"Cancel"}),e.jsx(O,{disabled:n.isPending,onClick:h,children:n.isPending?"Deleting…":"Delete"})]})]})})]})}export{q as component};
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{by as D,u as b,bn as p,r as g,j as e,bz as r,B as c,L as f,bA as A}from"./index.js";import{u as y}from"./useSuspenseQuery.js";import{B as m}from"./back-link.js";import{D as t,a as l}from"./detail-row.js";import{E as V}from"./error-banner.js";import{P as I}from"./page-heading.js";import{g as S,f as T,b as C,a as B,c as N,d as k,e as z,h as E}from"./alert-dialog.js";import{B as w}from"./badge.js";import{e as P}from"./ec2.js";import{T as R}from"./trash-2.js";function G(){const{id:n}=D.useParams(),u=b(),{data:d}=y(p(n)),a=d.Volumes?.[0],i=P(),[x,o]=g.useState(!1),j=a?.State==="available"&&(!a.Attachments||a.Attachments.length===0),h=async()=>{try{await i.mutateAsync(n),u({to:"/ec2/describe-volumes"})}finally{o(!1)}};if(!a?.VolumeId)return e.jsxs(e.Fragment,{children:[e.jsx(m,{to:"/ec2/describe-volumes",children:"Back to volumes"}),e.jsx("p",{className:"text-muted-foreground",children:"Volume not found."})]});const v=r(a.CreateTime);return e.jsxs(e.Fragment,{children:[e.jsx(m,{to:"/ec2/describe-volumes",children:"Back to volumes"}),i.error&&e.jsx(V,{error:i.error,msg:"Failed to delete volume"}),e.jsxs("div",{className:"space-y-6",children:[e.jsx(I,{actions:e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsxs(c,{disabled:!j,onClick:()=>o(!0),size:"sm",variant:"destructive",children:[e.jsx(R,{className:"size-4"}),"Delete"]}),a.State==="available"?e.jsx(f,{className:A({variant:"outline"}),params:{id:a.VolumeId},to:"/ec2/modify-volume/$id",children:"Resize Volume"}):e.jsx(c,{disabled:!0,variant:"outline",children:"Resize Volume"}),a.State&&e.jsx(w,{variant:a.State==="available"?"default":"outline",children:a.State})]}),subtitle:"EC2 Volume Details",title:a.VolumeId}),e.jsxs(t,{children:[e.jsx(t.Header,{children:"Volume Information"}),e.jsxs(t.Content,{children:[e.jsx(l,{label:"Volume ID",value:a.VolumeId}),e.jsx(l,{label:"Size",value:`${a.Size} GiB`}),e.jsx(l,{label:"Volume Type",value:a.VolumeType}),e.jsx(l,{label:"State",value:a.State}),e.jsx(l,{label:"Availability Zone",value:a.AvailabilityZone}),e.jsx(l,{label:"Create Time",value:v}),e.jsx(l,{label:"Encrypted",value:a.Encrypted?"Yes":"No"}),e.jsx(l,{label:"KMS Key ID",value:a.KmsKeyId})]})]}),a.Attachments&&a.Attachments.length>0&&e.jsxs(t,{children:[e.jsx(t.Header,{children:"Attachments"}),a.Attachments.map(s=>e.jsxs(t.Content,{children:[e.jsx(l,{label:"Instance ID",value:s.InstanceId}),e.jsx(l,{label:"Device",value:s.Device}),e.jsx(l,{label:"Status",value:s.State}),e.jsx(l,{label:"Attach Time",value:r(s.AttachTime)}),e.jsx(l,{label:"Delete on Termination",value:s.DeleteOnTermination?"Yes":"No"})]},s.InstanceId))]})]}),e.jsx(S,{onOpenChange:o,open:x,children:e.jsxs(T,{children:[e.jsxs(C,{children:[e.jsx(B,{children:"Delete Volume"}),e.jsxs(N,{children:['Are you sure you want to delete the volume "',a.VolumeId,'"? This action cannot be undone.']})]}),e.jsxs(k,{children:[e.jsx(z,{children:"Cancel"}),e.jsx(E,{disabled:i.isPending,onClick:h,children:i.isPending?"Deleting…":"Delete"})]})]})})]})}export{G as component};
|
||||
import{c as T,am as q,u as D,j as s,an as J,B as Q}from"./index.js";import{a as K,u as U}from"./zod.js";import{u as W}from"./useSuspenseQuery.js";import{B as H}from"./back-link.js";import{E as X}from"./error-banner.js";import{P as Y}from"./page-heading.js";import{F as Z,a as ee,b as se}from"./field.js";import{I as te}from"./input.js";import{h as ie}from"./ec2.js";import{m as re}from"./ec22.js";function xe(){const e=T.c(58),{id:R}=q.useParams(),o=D();let S;e[0]!==R?(S=J(R),e[0]=R,e[1]=S):S=e[1];const{data:L}=W(S),A=L.Volumes?.[0],t=A?.VolumeId,r=ie(),i=A?.Size??1;let a;e[2]!==i?(a=K(re.refine(l=>l.size>i,{message:`New size must be greater than current size (${i} GiB)`,path:["size"]})),e[2]=i,e[3]=a):a=e[3];let n;e[4]!==i?(n={size:i},e[4]=i,e[5]=n):n=e[5];let B;e[6]!==a||e[7]!==n?(B={resolver:a,defaultValues:n},e[6]=a,e[7]=n,e[8]=B):B=e[8];const{handleSubmit:k,register:E,formState:O}=U(B),{errors:V,isSubmitting:$}=O;if(!t){let l;return e[9]===Symbol.for("react.memo_cache_sentinel")?(l=s.jsxs(s.Fragment,{children:[s.jsx(H,{to:"/ec2/describe-volumes",children:"Back to volumes"}),s.jsx("p",{className:"text-muted-foreground",children:"Volume not found."})]}),e[9]=l):l=e[9],l}let F;e[10]!==r||e[11]!==o||e[12]!==t?(F=async l=>{await r.mutateAsync({...l,volumeId:t}),o({to:"/ec2/describe-volumes/$id",params:{id:t}})},e[10]=r,e[11]=o,e[12]=t,e[13]=F):F=e[13];const w=F;let m,c;e[14]!==t?(m=s.jsx(H,{params:{id:t},to:"/ec2/describe-volumes/$id",children:"Back to volume"}),c=s.jsx(Y,{subtitle:t,title:"Resize Volume"}),e[14]=t,e[15]=m,e[16]=c):(m=e[15],c=e[16]);let u;e[17]!==r.error?(u=r.error&&s.jsx(X,{error:r.error,msg:"Failed to resize volume"}),e[17]=r.error,e[18]=u):u=e[18];let d;e[19]!==k||e[20]!==w?(d=k(w),e[19]=k,e[20]=w,e[21]=d):d=e[21];let f;e[22]!==i?(f=s.jsx("div",{className:"space-y-2",children:s.jsxs("p",{className:"text-muted-foreground text-sm",children:["Current size: ",i," GiB"]})}),e[22]=i,e[23]=f):f=e[23];let N;e[24]===Symbol.for("react.memo_cache_sentinel")?(N=s.jsx(ee,{children:s.jsx("label",{htmlFor:"size",children:"New size (GiB)"})}),e[24]=N):N=e[24];const I=!!V.size,_=i+1;let p;e[25]!==E?(p=E("size",{valueAsNumber:!0}),e[25]=E,e[26]=p):p=e[26];let x;e[27]!==I||e[28]!==_||e[29]!==p?(x=s.jsx(te,{"aria-invalid":I,id:"size",min:_,type:"number",...p}),e[27]=I,e[28]=_,e[29]=p,e[30]=x):x=e[30];let h;e[31]!==V.size?(h=s.jsx(Z,{errors:[V.size]}),e[31]=V.size,e[32]=h):h=e[32];let b;e[33]!==x||e[34]!==h?(b=s.jsxs(se,{children:[N,x,h]}),e[33]=x,e[34]=h,e[35]=b):b=e[35];const C=$||r.isPending;let j;e[36]!==o||e[37]!==t?(j=()=>o({to:"/ec2/describe-volumes/$id",params:{id:t}}),e[36]=o,e[37]=t,e[38]=j):j=e[38];let z;e[39]!==C||e[40]!==j?(z=s.jsx(Q,{disabled:C,onClick:j,type:"button",variant:"outline",children:"Cancel"}),e[39]=C,e[40]=j,e[41]=z):z=e[41];const G=$||r.isPending,M=$||r.isPending?"Resizing…":"Resize Volume";let g;e[42]!==G||e[43]!==M?(g=s.jsx(Q,{disabled:G,type:"submit",children:M}),e[42]=G,e[43]=M,e[44]=g):g=e[44];let v;e[45]!==z||e[46]!==g?(v=s.jsxs("div",{className:"flex gap-2",children:[z,g]}),e[45]=z,e[46]=g,e[47]=v):v=e[47];let y;e[48]!==f||e[49]!==b||e[50]!==v||e[51]!==d?(y=s.jsxs("form",{className:"max-w-4xl space-y-6",onSubmit:d,children:[f,b,v]}),e[48]=f,e[49]=b,e[50]=v,e[51]=d,e[52]=y):y=e[52];let P;return e[53]!==y||e[54]!==m||e[55]!==c||e[56]!==u?(P=s.jsxs(s.Fragment,{children:[m,c,u,y]}),e[53]=y,e[54]=m,e[55]=c,e[56]=u,e[57]=P):P=e[57],P}export{xe as component};
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{c as g,c8 as u,j as s,bP as p}from"./index.js";import{u as x}from"./useSuspenseQuery.js";import{B as d}from"./back-link.js";import{P as I}from"./page-heading.js";import{B as j}from"./badge.js";import{A as h}from"./ami-details.js";import"./detail-row.js";function E(){const e=g.c(14),{id:c}=u.useParams();let o;e[0]!==c?(o=p(c),e[0]=c,e[1]=o):o=e[1];const{data:f}=x(o),t=f?.Images?.[0];if(!t?.ImageId){let n;return e[2]===Symbol.for("react.memo_cache_sentinel")?(n=s.jsxs(s.Fragment,{children:[s.jsx(d,{to:"/ec2/describe-images",children:"Back to images"}),s.jsx("p",{className:"text-muted-foreground",children:"Image not found."})]}),e[2]=n):n=e[2],n}let r;e[3]===Symbol.for("react.memo_cache_sentinel")?(r=s.jsx(d,{to:"/ec2/describe-images",children:"Back to images"}),e[3]=r):r=e[3];let a;e[4]!==t.State?(a=t.State?s.jsx(j,{variant:t.State==="available"?"default":"outline",children:t.State}):void 0,e[4]=t.State,e[5]=a):a=e[5];let i;e[6]!==t.ImageId||e[7]!==a?(i=s.jsx(I,{actions:a,subtitle:"EC2 Image Details",title:t.ImageId}),e[6]=t.ImageId,e[7]=a,e[8]=i):i=e[8];let m;e[9]!==t?(m=s.jsx(h,{image:t,showExtendedDetails:!0}),e[9]=t,e[10]=m):m=e[10];let l;return e[11]!==i||e[12]!==m?(l=s.jsxs(s.Fragment,{children:[r,s.jsxs("div",{className:"space-y-6",children:[i,m]})]}),e[11]=i,e[12]=m,e[13]=l):l=e[13],l}export{E as component};
|
||||
import{k as z,aB as E,u as V,aC as O,e as L,r as l,j as e,ay as H,B as u,L as M}from"./index.js";import{u as m}from"./useSuspenseQuery.js";import{B as x}from"./back-link.js";import{D as c,a as t}from"./detail-row.js";import{E as j}from"./error-banner.js";import{P as Q}from"./page-heading.js";import{S as _}from"./state-badge.js";import{g,f as y,b as D,a as S,c as f,d as C,e as b,h as v}from"./alert-dialog.js";import{b as $,a as G}from"./field.js";import{I as Y}from"./input.js";import{m as q,n as J}from"./ec2.js";import{T as K}from"./trash-2.js";const U=[["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2",key:"17jyea"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",key:"zix9uf"}]],W=z("copy",U);function de(){const{id:n}=E.useParams(),I=V(),{data:k}=m(O(n)),{data:A}=m(L),s=k.Snapshots?.[0],a=q(),o=J(),[w,i]=l.useState(!1),[B,r]=l.useState(!1),[d,p]=l.useState(""),N=A.Regions?.[0]?.RegionName??"ap-southeast-2",h=s?.State==="completed",P=async()=>{try{await a.mutateAsync(n),I({to:"/ec2/describe-snapshots"})}finally{i(!1)}},T=async()=>{try{await o.mutateAsync({sourceSnapshotId:n,sourceRegion:N,description:d||void 0}),r(!1),p("")}catch{}};if(!s?.SnapshotId)return e.jsxs(e.Fragment,{children:[e.jsx(x,{to:"/ec2/describe-snapshots",children:"Back to snapshots"}),e.jsx("p",{className:"text-muted-foreground",children:"Snapshot not found."})]});const F=H(s.StartTime);return e.jsxs(e.Fragment,{children:[e.jsx(x,{to:"/ec2/describe-snapshots",children:"Back to snapshots"}),a.error&&e.jsx(j,{error:a.error,msg:"Failed to delete snapshot"}),o.error&&e.jsx(j,{error:o.error,msg:"Failed to copy snapshot"}),e.jsxs("div",{className:"space-y-6",children:[e.jsx(Q,{actions:e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsxs(u,{disabled:!h,onClick:()=>i(!0),size:"sm",variant:"destructive",children:[e.jsx(K,{className:"size-4"}),"Delete"]}),e.jsxs(u,{disabled:!h,onClick:()=>r(!0),size:"sm",variant:"outline",children:[e.jsx(W,{className:"size-4"}),"Copy Snapshot"]}),e.jsx(_,{state:s.State})]}),subtitle:"EC2 Snapshot Details",title:s.SnapshotId}),e.jsxs(c,{children:[e.jsx(c.Header,{children:"Snapshot Information"}),e.jsxs(c.Content,{children:[e.jsx(t,{label:"Snapshot ID",value:s.SnapshotId}),e.jsx(t,{label:"Volume ID",value:s.VolumeId?e.jsx(M,{className:"text-primary hover:underline",params:{id:s.VolumeId},to:"/ec2/describe-volumes/$id",children:s.VolumeId}):void 0}),e.jsx(t,{label:"Volume Size",value:`${s.VolumeSize} GiB`}),e.jsx(t,{label:"State",value:s.State}),e.jsx(t,{label:"Description",value:s.Description}),e.jsx(t,{label:"Progress",value:s.Progress}),e.jsx(t,{label:"Start Time",value:F}),e.jsx(t,{label:"Owner ID",value:s.OwnerId}),e.jsx(t,{label:"Encrypted",value:s.Encrypted?"Yes":"No"})]})]})]}),e.jsx(g,{onOpenChange:i,open:w,children:e.jsxs(y,{children:[e.jsxs(D,{children:[e.jsx(S,{children:"Delete Snapshot"}),e.jsxs(f,{children:['Are you sure you want to delete the snapshot "',s.SnapshotId,'"? This action cannot be undone.']})]}),e.jsxs(C,{children:[e.jsx(b,{children:"Cancel"}),e.jsx(v,{disabled:a.isPending,onClick:P,children:a.isPending?"Deleting…":"Delete"})]})]})}),e.jsx(g,{onOpenChange:r,open:B,children:e.jsxs(y,{children:[e.jsxs(D,{children:[e.jsx(S,{children:"Copy Snapshot"}),e.jsx(f,{children:"Create a copy of this snapshot in the current region."})]}),e.jsxs($,{children:[e.jsx(G,{children:e.jsx("label",{htmlFor:"copyDescription",children:"Description (optional)"})}),e.jsx(Y,{id:"copyDescription",onChange:R=>p(R.target.value),placeholder:"Description for the snapshot copy",value:d})]}),e.jsxs(C,{children:[e.jsx(b,{children:"Cancel"}),e.jsx(v,{disabled:o.isPending,onClick:T,children:o.isPending?"Copying…":"Copy Snapshot"})]})]})})]})}export{de as component};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
import{aD as x,u as p,aE as h,r as j,j as e,ay as g,B as D}from"./index.js";import{u as K}from"./useSuspenseQuery.js";import{B as o}from"./back-link.js";import{D as s,a as r}from"./detail-row.js";import{E as f}from"./error-banner.js";import{P as b}from"./page-heading.js";import{g as P,f as k,b as T,a as A,c as C,d as v,e as N,h as w}from"./alert-dialog.js";import{o as B}from"./ec2.js";import{T as E}from"./trash-2.js";function M(){const{id:l}=x.useParams(),c=p(),{data:d}=K(h(l)),a=d.KeyPairs?.[0],i=B(),[y,t]=j.useState(!1),m=async()=>{try{await i.mutateAsync(l),c({to:"/ec2/describe-key-pairs"})}finally{t(!1)}};if(!a?.KeyPairId)return e.jsxs(e.Fragment,{children:[e.jsx(o,{to:"/ec2/describe-key-pairs",children:"Back to key pairs"}),e.jsx("p",{className:"text-muted-foreground",children:"Key pair not found."})]});const u=g(a.CreateTime);return e.jsxs(e.Fragment,{children:[e.jsx(o,{to:"/ec2/describe-key-pairs",children:"Back to key pairs"}),i.error&&e.jsx(f,{error:i.error,msg:"Failed to delete key pair"}),e.jsxs("div",{className:"space-y-6",children:[e.jsx(b,{actions:e.jsxs(D,{onClick:()=>t(!0),size:"sm",variant:"destructive",children:[e.jsx(E,{className:"size-4"}),"Delete"]}),subtitle:"EC2 Key Pair Details",title:a.KeyName??""}),e.jsxs(s,{children:[e.jsx(s.Header,{children:"Key Pair Information"}),e.jsxs(s.Content,{children:[e.jsx(r,{label:"Key Name",value:a.KeyName}),e.jsx(r,{label:"Key Pair ID",value:a.KeyPairId}),e.jsx(r,{label:"Key Type",value:a.KeyType}),e.jsx(r,{label:"Key Fingerprint",value:a.KeyFingerprint}),e.jsx(r,{label:"Creation Date",value:u})]})]}),a.Tags&&a.Tags.length>0&&e.jsxs(s,{children:[e.jsx(s.Header,{children:"Tags"}),e.jsx(s.Content,{children:a.Tags.map(n=>e.jsx(r,{label:n.Key||"",value:n.Value},n.Key))})]})]}),e.jsx(P,{onOpenChange:t,open:y,children:e.jsxs(k,{children:[e.jsxs(T,{children:[e.jsx(A,{children:"Delete Key Pair"}),e.jsxs(C,{children:['Are you sure you want to delete the key pair "',a.KeyName,'"? This action cannot be undone. Any instances using this key pair will no longer be able to be accessed with it.']})]}),e.jsxs(v,{children:[e.jsx(N,{children:"Cancel"}),e.jsx(w,{disabled:i.isPending,onClick:m,children:i.isPending?"Deleting…":"Delete"})]})]})})]})}export{M as component};
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import{c as f,cz as u,j as t,aH as p}from"./index.js";import{u as x}from"./useSuspenseQuery.js";import{B as d}from"./back-link.js";import{P as I}from"./page-heading.js";import{S as j}from"./state-badge.js";import{A as h}from"./ami-details.js";import"./detail-row.js";function _(){const e=f.c(14),{id:n}=u.useParams();let o;e[0]!==n?(o=p(n),e[0]=n,e[1]=o):o=e[1];const{data:g}=x(o),s=g?.Images?.[0];if(!s?.ImageId){let l;return e[2]===Symbol.for("react.memo_cache_sentinel")?(l=t.jsxs(t.Fragment,{children:[t.jsx(d,{to:"/ec2/describe-images",children:"Back to images"}),t.jsx("p",{className:"text-muted-foreground",children:"Image not found."})]}),e[2]=l):l=e[2],l}let r;e[3]===Symbol.for("react.memo_cache_sentinel")?(r=t.jsx(d,{to:"/ec2/describe-images",children:"Back to images"}),e[3]=r):r=e[3];let a;e[4]!==s.State?(a=t.jsx(j,{state:s.State}),e[4]=s.State,e[5]=a):a=e[5];let i;e[6]!==s.ImageId||e[7]!==a?(i=t.jsx(I,{actions:a,subtitle:"EC2 Image Details",title:s.ImageId}),e[6]=s.ImageId,e[7]=a,e[8]=i):i=e[8];let m;e[9]!==s?(m=t.jsx(h,{image:s,showExtendedDetails:!0}),e[9]=s,e[10]=m):m=e[10];let c;return e[11]!==i||e[12]!==m?(c=t.jsxs(t.Fragment,{children:[r,t.jsxs("div",{className:"space-y-6",children:[i,m]})]}),e[11]=i,e[12]=m,e[13]=c):c=e[13],c}export{_ as component};
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{c as S,bz as M,j as t}from"./index.js";import{D as T,a}from"./detail-row.js";function C(i){const e=S.c(55),{image:l,showExtendedDetails:P}=i,s=P===void 0?!1:P;let y;e[0]!==l.CreationDate?(y=M(l.CreationDate),e[0]=l.CreationDate,e[1]=y):y=e[1];const V=y,R=s?"AMI Information":"AMI Details";let o;e[2]!==R?(o=t.jsx(T.Header,{children:R}),e[2]=R,e[3]=o):o=e[3];let r;e[4]!==l.ImageId?(r=t.jsx(a,{label:"AMI ID",value:l.ImageId}),e[4]=l.ImageId,e[5]=r):r=e[5];let c;e[6]!==l.Name?(c=t.jsx(a,{label:"AMI Name",value:l.Name}),e[6]=l.Name,e[7]=c):c=e[7];let n;e[8]!==l.Description?(n=t.jsx(a,{label:"Description",value:l.Description}),e[8]=l.Description,e[9]=n):n=e[9];let u;e[10]!==l.Architecture?(u=t.jsx(a,{label:"Architecture",value:l.Architecture}),e[10]=l.Architecture,e[11]=u):u=e[11];let D;e[12]!==l.VirtualizationType?(D=t.jsx(a,{label:"Virtualization Type",value:l.VirtualizationType}),e[12]=l.VirtualizationType,e[13]=D):D=e[13];const A=l.Platform||"Linux";let v;e[14]!==A?(v=t.jsx(a,{label:"Platform",value:A}),e[14]=A,e[15]=v):v=e[15];let m;e[16]!==l.PlatformDetails?(m=t.jsx(a,{label:"Platform Details",value:l.PlatformDetails}),e[16]=l.PlatformDetails,e[17]=m):m=e[17];let x;e[18]!==l.ImageType?(x=t.jsx(a,{label:"Image Type",value:l.ImageType}),e[18]=l.ImageType,e[19]=x):x=e[19];let b;e[20]!==l.RootDeviceType?(b=t.jsx(a,{label:"Root Device Type",value:l.RootDeviceType}),e[20]=l.RootDeviceType,e[21]=b):b=e[21];let f;e[22]!==l.RootDeviceName?(f=t.jsx(a,{label:"Root Device Name",value:l.RootDeviceName}),e[22]=l.RootDeviceName,e[23]=f):f=e[23];let j;e[24]!==l.OwnerId?(j=t.jsx(a,{label:"Owner",value:l.OwnerId}),e[24]=l.OwnerId,e[25]=j):j=e[25];let d;e[26]!==l.State?(d=t.jsx(a,{label:"State",value:l.State}),e[26]=l.State,e[27]=d):d=e[27];let I;e[28]!==V||e[29]!==l.Public||e[30]!==s?(I=s&&t.jsxs(t.Fragment,{children:[t.jsx(a,{label:"Creation Date",value:V}),t.jsx(a,{label:"Public",value:l.Public?"Yes":"No"})]}),e[28]=V,e[29]=l.Public,e[30]=s,e[31]=I):I=e[31];let h;e[32]!==v||e[33]!==m||e[34]!==x||e[35]!==b||e[36]!==f||e[37]!==j||e[38]!==d||e[39]!==I||e[40]!==r||e[41]!==c||e[42]!==n||e[43]!==u||e[44]!==D?(h=t.jsxs(T.Content,{children:[r,c,n,u,D,v,m,x,b,f,j,d,I]}),e[32]=v,e[33]=m,e[34]=x,e[35]=b,e[36]=f,e[37]=j,e[38]=d,e[39]=I,e[40]=r,e[41]=c,e[42]=n,e[43]=u,e[44]=D,e[45]=h):h=e[45];let N;e[46]!==h||e[47]!==o?(N=t.jsxs(T,{children:[o,h]}),e[46]=h,e[47]=o,e[48]=N):N=e[48];let p;e[49]!==l.BlockDeviceMappings||e[50]!==s?(p=s&&l.BlockDeviceMappings&&l.BlockDeviceMappings.length>0&&l.BlockDeviceMappings.map(z),e[49]=l.BlockDeviceMappings,e[50]=s,e[51]=p):p=e[51];let E;return e[52]!==N||e[53]!==p?(E=t.jsxs(t.Fragment,{children:[N,p]}),e[52]=N,e[53]=p,e[54]=E):E=e[54],E}function z(i){return t.jsxs(T,{children:[t.jsxs(T.Header,{children:["Block Device: ",i.DeviceName]}),t.jsxs(T.Content,{children:[t.jsx(a,{label:"Device Name",value:i.DeviceName}),i.Ebs&&t.jsxs(t.Fragment,{children:[t.jsx(a,{label:"Volume Size",value:i.Ebs.VolumeSize?`${i.Ebs.VolumeSize} GB`:void 0}),t.jsx(a,{label:"Volume Type",value:i.Ebs.VolumeType}),t.jsx(a,{label:"Delete On Termination",value:i.Ebs.DeleteOnTermination?"Yes":"No"}),t.jsx(a,{label:"Encrypted",value:i.Ebs.Encrypted?"Yes":"No"}),i.Ebs.SnapshotId&&t.jsx(a,{label:"Snapshot ID",value:i.Ebs.SnapshotId})]}),i.VirtualName&&t.jsx(a,{label:"Virtual Name",value:i.VirtualName})]})]},i.DeviceName)}export{C as A};
|
||||
import{c as g,ay as k,j as t,L as M}from"./index.js";import{D as T,a}from"./detail-row.js";function C(s){const e=g.c(58),{image:l,showExtendedDetails:P}=s,i=P===void 0?!1:P;let E;e[0]!==l.CreationDate?(E=k(l.CreationDate),e[0]=l.CreationDate,e[1]=E):E=e[1];const R=E,S=i?"AMI Information":"AMI Details";let o;e[2]!==S?(o=t.jsx(T.Header,{children:S}),e[2]=S,e[3]=o):o=e[3];let r;e[4]!==l.ImageId||e[5]!==i?(r=l.ImageId&&!i?t.jsx(M,{className:"text-primary hover:underline",params:{id:l.ImageId},to:"/ec2/describe-images/$id",children:l.ImageId}):l.ImageId,e[4]=l.ImageId,e[5]=i,e[6]=r):r=e[6];let c;e[7]!==r?(c=t.jsx(a,{label:"AMI ID",value:r}),e[7]=r,e[8]=c):c=e[8];let n;e[9]!==l.Name?(n=t.jsx(a,{label:"AMI Name",value:l.Name}),e[9]=l.Name,e[10]=n):n=e[10];let u;e[11]!==l.Description?(u=t.jsx(a,{label:"Description",value:l.Description}),e[11]=l.Description,e[12]=u):u=e[12];let m;e[13]!==l.Architecture?(m=t.jsx(a,{label:"Architecture",value:l.Architecture}),e[13]=l.Architecture,e[14]=m):m=e[14];let v;e[15]!==l.VirtualizationType?(v=t.jsx(a,{label:"Virtualization Type",value:l.VirtualizationType}),e[15]=l.VirtualizationType,e[16]=v):v=e[16];const A=l.Platform||"Linux";let D;e[17]!==A?(D=t.jsx(a,{label:"Platform",value:A}),e[17]=A,e[18]=D):D=e[18];let d;e[19]!==l.PlatformDetails?(d=t.jsx(a,{label:"Platform Details",value:l.PlatformDetails}),e[19]=l.PlatformDetails,e[20]=d):d=e[20];let x;e[21]!==l.ImageType?(x=t.jsx(a,{label:"Image Type",value:l.ImageType}),e[21]=l.ImageType,e[22]=x):x=e[22];let b;e[23]!==l.RootDeviceType?(b=t.jsx(a,{label:"Root Device Type",value:l.RootDeviceType}),e[23]=l.RootDeviceType,e[24]=b):b=e[24];let j;e[25]!==l.RootDeviceName?(j=t.jsx(a,{label:"Root Device Name",value:l.RootDeviceName}),e[25]=l.RootDeviceName,e[26]=j):j=e[26];let f;e[27]!==l.OwnerId?(f=t.jsx(a,{label:"Owner",value:l.OwnerId}),e[27]=l.OwnerId,e[28]=f):f=e[28];let I;e[29]!==l.State?(I=t.jsx(a,{label:"State",value:l.State}),e[29]=l.State,e[30]=I):I=e[30];let h;e[31]!==R||e[32]!==l.Public||e[33]!==i?(h=i&&t.jsxs(t.Fragment,{children:[t.jsx(a,{label:"Creation Date",value:R}),t.jsx(a,{label:"Public",value:l.Public?"Yes":"No"})]}),e[31]=R,e[32]=l.Public,e[33]=i,e[34]=h):h=e[34];let p;e[35]!==v||e[36]!==D||e[37]!==d||e[38]!==x||e[39]!==b||e[40]!==j||e[41]!==f||e[42]!==I||e[43]!==h||e[44]!==c||e[45]!==n||e[46]!==u||e[47]!==m?(p=t.jsxs(T.Content,{children:[c,n,u,m,v,D,d,x,b,j,f,I,h]}),e[35]=v,e[36]=D,e[37]=d,e[38]=x,e[39]=b,e[40]=j,e[41]=f,e[42]=I,e[43]=h,e[44]=c,e[45]=n,e[46]=u,e[47]=m,e[48]=p):p=e[48];let N;e[49]!==p||e[50]!==o?(N=t.jsxs(T,{children:[o,p]}),e[49]=p,e[50]=o,e[51]=N):N=e[51];let y;e[52]!==l.BlockDeviceMappings||e[53]!==i?(y=i&&l.BlockDeviceMappings&&l.BlockDeviceMappings.length>0&&l.BlockDeviceMappings.map(w),e[52]=l.BlockDeviceMappings,e[53]=i,e[54]=y):y=e[54];let V;return e[55]!==N||e[56]!==y?(V=t.jsxs(t.Fragment,{children:[N,y]}),e[55]=N,e[56]=y,e[57]=V):V=e[57],V}function w(s){return t.jsxs(T,{children:[t.jsxs(T.Header,{children:["Block Device: ",s.DeviceName]}),t.jsxs(T.Content,{children:[t.jsx(a,{label:"Device Name",value:s.DeviceName}),s.Ebs&&t.jsxs(t.Fragment,{children:[t.jsx(a,{label:"Volume Size",value:s.Ebs.VolumeSize?`${s.Ebs.VolumeSize} GB`:void 0}),t.jsx(a,{label:"Volume Type",value:s.Ebs.VolumeType}),t.jsx(a,{label:"Delete On Termination",value:s.Ebs.DeleteOnTermination?"Yes":"No"}),t.jsx(a,{label:"Encrypted",value:s.Ebs.Encrypted?"Yes":"No"}),s.Ebs.SnapshotId&&t.jsx(a,{label:"Snapshot ID",value:t.jsx(M,{className:"text-primary hover:underline",params:{id:s.Ebs.SnapshotId},to:"/ec2/describe-snapshots/$id",children:s.Ebs.SnapshotId})})]}),s.VirtualName&&t.jsx(a,{label:"Virtual Name",value:s.VirtualName})]})]},s.DeviceName)}export{C as A};
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{g as o,c as r,j as l,L as a}from"./index.js";const m=[["path",{d:"m12 19-7-7 7-7",key:"1l729n"}],["path",{d:"M19 12H5",key:"x3x0zl"}]],x=o("arrow-left",m);function p(i){const e=r.c(7);let s,t;e[0]!==i?({children:s,...t}=i,e[0]=i,e[1]=s,e[2]=t):(s=e[1],t=e[2]);let n;e[3]===Symbol.for("react.memo_cache_sentinel")?(n=l.jsx(x,{className:"size-4"}),e[3]=n):n=e[3];let c;return e[4]!==s||e[5]!==t?(c=l.jsxs(a,{className:"mb-6 inline-flex items-center gap-2 text-sm",...t,children:[n,s]}),e[4]=s,e[5]=t,e[6]=c):c=e[6],c}export{p as B};
|
||||
import{k as o,c as r,j as l,L as a}from"./index.js";const m=[["path",{d:"m12 19-7-7 7-7",key:"1l729n"}],["path",{d:"M19 12H5",key:"x3x0zl"}]],x=o("arrow-left",m);function p(i){const e=r.c(7);let s,t;e[0]!==i?({children:s,...t}=i,e[0]=i,e[1]=s,e[2]=t):(s=e[1],t=e[2]);let n;e[3]===Symbol.for("react.memo_cache_sentinel")?(n=l.jsx(x,{className:"size-4"}),e[3]=n):n=e[3];let c;return e[4]!==s||e[5]!==t?(c=l.jsxs(a,{className:"mb-6 inline-flex items-center gap-2 text-sm",...t,children:[n,s]}),e[4]=s,e[5]=t,e[6]=c):c=e[6],c}export{p as B};
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
import{c as g,a5 as f,a as b,bx as p,c9 as m}from"./index.js";const h=p("group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-full border border-transparent px-2 py-0.5 font-medium text-[0.625rem] transition-all transition-colors focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-2.5!",{variants:{variant:{default:"bg-primary text-primary-foreground [a]:hover:bg-primary/80",secondary:"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",destructive:"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",outline:"border-border bg-input/20 text-foreground dark:bg-input/30 [a]:hover:bg-muted [a]:hover:text-muted-foreground",ghost:"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",link:"text-primary underline-offset-4 hover:underline"}},defaultVariants:{variant:"default"}});function y(d){const e=g.c(13);let t,i,a,s,r;if(e[0]!==d){const{className:l,variant:u,render:v,...c}=d;t=v,r=u===void 0?"default":u,s=m,i="span",a=f({className:b(h({className:l,variant:r}))},c),e[0]=d,e[1]=t,e[2]=i,e[3]=a,e[4]=s,e[5]=r}else t=e[1],i=e[2],a=e[3],s=e[4],r=e[5];let n;e[6]!==r?(n={slot:"badge",variant:r},e[6]=r,e[7]=n):n=e[7];let o;return e[8]!==t||e[9]!==i||e[10]!==a||e[11]!==n?(o={defaultTagName:i,props:a,render:t,state:n},e[8]=t,e[9]=i,e[10]=a,e[11]=n,e[12]=o):o=e[12],s(o)}export{y as B};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{g as c}from"./index.js";const e=[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]],t=c("check",e);export{t as C};
|
||||
import{k as c}from"./index.js";const e=[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]],t=c("check",e);export{t as C};
|
||||
|
||||
@@ -1 +1 @@
|
||||
import{o as M,f as $,c as I,u as R,j as t,B as E}from"./index.js";import{a as A,u as H,I as L,F as O,b as T,c as q}from"./field.js";import{B as D}from"./back-link.js";import{E as G}from"./error-banner.js";import{P as J}from"./page-heading.js";import{u as K}from"./s3.js";const Q=M({bucketName:$().min(3,"Bucket name must be at least 3 characters").max(63,"Bucket name must be at most 63 characters").regex(/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/,"Must start and end with a lowercase letter or number").regex(/^[a-z0-9.-]+$/,"Only lowercase letters, numbers, hyphens, and periods allowed").refine(e=>!e.includes(".."),"Must not contain consecutive periods").refine(e=>!(e.includes(".-")||e.includes("-.")),"Must not contain adjacent period and hyphen")});function ee(){const e=I.c(40),a=R(),s=K();let x;e[0]===Symbol.for("react.memo_cache_sentinel")?(x={resolver:A(Q)},e[0]=x):x=e[0];const{handleSubmit:y,register:N,formState:P}=H(x),{errors:h,isSubmitting:S}=P;let k;e[1]!==s||e[2]!==a?(k=async z=>{await s.mutateAsync(z),a({to:"/s3/ls"})},e[1]=s,e[2]=a,e[3]=k):k=e[3];const F=k;let p,j;e[4]===Symbol.for("react.memo_cache_sentinel")?(p=t.jsx(D,{to:"/s3/ls",children:"Back to buckets"}),j=t.jsx(J,{title:"Create Bucket"}),e[4]=p,e[5]=j):(p=e[4],j=e[5]);let r;e[6]!==s.error?(r=s.error&&t.jsx(G,{error:s.error,msg:"Failed to create bucket"}),e[6]=s.error,e[7]=r):r=e[7];let i;e[8]!==y||e[9]!==F?(i=y(F),e[8]=y,e[9]=F,e[10]=i):i=e[10];let g;e[11]===Symbol.for("react.memo_cache_sentinel")?(g=t.jsx(T,{children:t.jsx("label",{htmlFor:"bucketName",children:"Bucket Name"})}),e[11]=g):g=e[11];const v=!!h.bucketName;let l;e[12]!==N?(l=N("bucketName"),e[12]=N,e[13]=l):l=e[13];let n;e[14]!==v||e[15]!==l?(n=t.jsx(L,{"aria-invalid":v,id:"bucketName",placeholder:"my-bucket",type:"text",...l}),e[14]=v,e[15]=l,e[16]=n):n=e[16];let c;e[17]!==h.bucketName?(c=t.jsx(O,{errors:[h.bucketName]}),e[17]=h.bucketName,e[18]=c):c=e[18];let o;e[19]!==n||e[20]!==c?(o=t.jsxs(q,{children:[g,n,c]}),e[19]=n,e[20]=c,e[21]=o):o=e[21];const C=S||s.isPending;let m;e[22]!==a?(m=()=>a({to:"/s3/ls"}),e[22]=a,e[23]=m):m=e[23];let u;e[24]!==C||e[25]!==m?(u=t.jsx(E,{disabled:C,onClick:m,type:"button",variant:"outline",children:"Cancel"}),e[24]=C,e[25]=m,e[26]=u):u=e[26];const w=S||s.isPending,_=S||s.isPending?"Creating…":"Create Bucket";let d;e[27]!==w||e[28]!==_?(d=t.jsx(E,{disabled:w,type:"submit",children:_}),e[27]=w,e[28]=_,e[29]=d):d=e[29];let f;e[30]!==u||e[31]!==d?(f=t.jsxs("div",{className:"flex gap-2",children:[u,d]}),e[30]=u,e[31]=d,e[32]=f):f=e[32];let b;e[33]!==o||e[34]!==f||e[35]!==i?(b=t.jsxs("form",{className:"max-w-4xl space-y-6",onSubmit:i,children:[o,f]}),e[33]=o,e[34]=f,e[35]=i,e[36]=b):b=e[36];let B;return e[37]!==b||e[38]!==r?(B=t.jsxs(t.Fragment,{children:[p,j,r,b]}),e[37]=b,e[38]=r,e[39]=B):B=e[39],B}export{ee as component};
|
||||
import{o as M,f as $,c as I,u as R,j as t,B as E}from"./index.js";import{a as A,u as H}from"./zod.js";import{B as L}from"./back-link.js";import{E as O}from"./error-banner.js";import{P as T}from"./page-heading.js";import{F as q,a as D,b as G}from"./field.js";import{I as J}from"./input.js";import{u as K}from"./s3.js";const Q=M({bucketName:$().min(3,"Bucket name must be at least 3 characters").max(63,"Bucket name must be at most 63 characters").regex(/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/,"Must start and end with a lowercase letter or number").regex(/^[a-z0-9.-]+$/,"Only lowercase letters, numbers, hyphens, and periods allowed").refine(e=>!e.includes(".."),"Must not contain consecutive periods").refine(e=>!(e.includes(".-")||e.includes("-.")),"Must not contain adjacent period and hyphen")});function se(){const e=I.c(40),r=R(),s=K();let p;e[0]===Symbol.for("react.memo_cache_sentinel")?(p={resolver:A(Q)},e[0]=p):p=e[0];const{handleSubmit:y,register:N,formState:P}=H(p),{errors:x,isSubmitting:S}=P;let h;e[1]!==s||e[2]!==r?(h=async z=>{await s.mutateAsync(z),r({to:"/s3/ls"})},e[1]=s,e[2]=r,e[3]=h):h=e[3];const F=h;let k,j;e[4]===Symbol.for("react.memo_cache_sentinel")?(k=t.jsx(L,{to:"/s3/ls",children:"Back to buckets"}),j=t.jsx(T,{title:"Create Bucket"}),e[4]=k,e[5]=j):(k=e[4],j=e[5]);let a;e[6]!==s.error?(a=s.error&&t.jsx(O,{error:s.error,msg:"Failed to create bucket"}),e[6]=s.error,e[7]=a):a=e[7];let i;e[8]!==y||e[9]!==F?(i=y(F),e[8]=y,e[9]=F,e[10]=i):i=e[10];let g;e[11]===Symbol.for("react.memo_cache_sentinel")?(g=t.jsx(D,{children:t.jsx("label",{htmlFor:"bucketName",children:"Bucket Name"})}),e[11]=g):g=e[11];const v=!!x.bucketName;let l;e[12]!==N?(l=N("bucketName"),e[12]=N,e[13]=l):l=e[13];let n;e[14]!==v||e[15]!==l?(n=t.jsx(J,{"aria-invalid":v,id:"bucketName",placeholder:"my-bucket",type:"text",...l}),e[14]=v,e[15]=l,e[16]=n):n=e[16];let o;e[17]!==x.bucketName?(o=t.jsx(q,{errors:[x.bucketName]}),e[17]=x.bucketName,e[18]=o):o=e[18];let c;e[19]!==n||e[20]!==o?(c=t.jsxs(G,{children:[g,n,o]}),e[19]=n,e[20]=o,e[21]=c):c=e[21];const C=S||s.isPending;let m;e[22]!==r?(m=()=>r({to:"/s3/ls"}),e[22]=r,e[23]=m):m=e[23];let u;e[24]!==C||e[25]!==m?(u=t.jsx(E,{disabled:C,onClick:m,type:"button",variant:"outline",children:"Cancel"}),e[24]=C,e[25]=m,e[26]=u):u=e[26];const w=S||s.isPending,_=S||s.isPending?"Creating…":"Create Bucket";let d;e[27]!==w||e[28]!==_?(d=t.jsx(E,{disabled:w,type:"submit",children:_}),e[27]=w,e[28]=_,e[29]=d):d=e[29];let f;e[30]!==u||e[31]!==d?(f=t.jsxs("div",{className:"flex gap-2",children:[u,d]}),e[30]=u,e[31]=d,e[32]=f):f=e[32];let b;e[33]!==c||e[34]!==f||e[35]!==i?(b=t.jsxs("form",{className:"max-w-4xl space-y-6",onSubmit:i,children:[c,f]}),e[33]=c,e[34]=f,e[35]=i,e[36]=b):b=e[36];let B;return e[37]!==b||e[38]!==a?(B=t.jsxs(t.Fragment,{children:[k,j,a,b]}),e[37]=b,e[38]=a,e[39]=B):B=e[39],B}export{se as component};
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import{c as q,u as J,i as K,R as U,j as t,B as G}from"./index.js";import{a as W,u as X,C as Y}from"./zod.js";import{u as Z}from"./useSuspenseQuery.js";import{B as ee}from"./back-link.js";import{E as te}from"./error-banner.js";import{P as se}from"./page-heading.js";import{F as le,b as H,a as L}from"./field.js";import{S as oe,a as re,b as ie,c as ae,d as ne}from"./select.js";import{T as ce}from"./textarea.js";import{c as me}from"./ec2.js";import{d as de}from"./ec22.js";import"./check.js";function ye(){const e=q.c(51),l=J(),{data:B}=Z(K),s=me();let j;e[0]!==B.Volumes?(j=B.Volumes??[],e[0]=B.Volumes,e[1]=j):j=e[1];const b=j,{volumeId:M}=U.useSearch(),E=M??b[0]?.VolumeId??"";let v;e[2]===Symbol.for("react.memo_cache_sentinel")?(v=W(de),e[2]=v):v=e[2];let I;e[3]!==E?(I={resolver:v,defaultValues:{volumeId:E,description:""}},e[3]=E,e[4]=I):I=e[4];const{control:P,handleSubmit:k,register:N,formState:O}=X(I),{errors:o,isSubmitting:R}=O;let g;e[5]!==s||e[6]!==l?(g=async z=>{await s.mutateAsync(z),l({to:"/ec2/describe-snapshots"})},e[5]=s,e[6]=l,e[7]=g):g=e[7];const T=g;let V,C;e[8]===Symbol.for("react.memo_cache_sentinel")?(V=t.jsx(ee,{to:"/ec2/describe-snapshots",children:"Back to snapshots"}),C=t.jsx(se,{title:"Create Snapshot"}),e[8]=V,e[9]=C):(V=e[8],C=e[9]);let r;e[10]!==s.error?(r=s.error&&t.jsx(te,{error:s.error,msg:"Failed to create snapshot"}),e[10]=s.error,e[11]=r):r=e[11];let i;e[12]!==k||e[13]!==T?(i=k(T),e[12]=k,e[13]=T,e[14]=i):i=e[14];let y;e[15]===Symbol.for("react.memo_cache_sentinel")?(y=t.jsx(L,{children:t.jsx("label",{htmlFor:"volumeId",children:"Volume"})}),e[15]=y):y=e[15];let a;e[16]!==o.volumeId||e[17]!==b?(a=z=>{const{field:A}=z;return t.jsxs(oe,{onValueChange:$=>A.onChange($),value:A.value??"",children:[t.jsx(re,{"aria-invalid":!!o.volumeId,className:"w-full",id:"volumeId",children:t.jsx(ie,{})}),t.jsx(ae,{children:b.map(ue)})]})},e[16]=o.volumeId,e[17]=b,e[18]=a):a=e[18];let n;e[19]!==P||e[20]!==a?(n=t.jsx(Y,{control:P,name:"volumeId",render:a}),e[19]=P,e[20]=a,e[21]=n):n=e[21];let c;e[22]!==o.volumeId?(c=t.jsx(le,{errors:[o.volumeId]}),e[22]=o.volumeId,e[23]=c):c=e[23];let m;e[24]!==n||e[25]!==c?(m=t.jsxs(H,{children:[y,n,c]}),e[24]=n,e[25]=c,e[26]=m):m=e[26];let F;e[27]===Symbol.for("react.memo_cache_sentinel")?(F=t.jsx(L,{children:t.jsx("label",{htmlFor:"description",children:"Description (optional)"})}),e[27]=F):F=e[27];let d;e[28]!==N?(d=N("description"),e[28]=N,e[29]=d):d=e[29];let u;e[30]!==d?(u=t.jsxs(H,{children:[F,t.jsx(ce,{id:"description",placeholder:"Snapshot description",...d})]}),e[30]=d,e[31]=u):u=e[31];const w=R||s.isPending;let f;e[32]!==l?(f=()=>l({to:"/ec2/describe-snapshots"}),e[32]=l,e[33]=f):f=e[33];let p;e[34]!==w||e[35]!==f?(p=t.jsx(G,{disabled:w,onClick:f,type:"button",variant:"outline",children:"Cancel"}),e[34]=w,e[35]=f,e[36]=p):p=e[36];const D=R||s.isPending,Q=R||s.isPending?"Creating…":"Create Snapshot";let h;e[37]!==D||e[38]!==Q?(h=t.jsx(G,{disabled:D,type:"submit",children:Q}),e[37]=D,e[38]=Q,e[39]=h):h=e[39];let x;e[40]!==p||e[41]!==h?(x=t.jsxs("div",{className:"flex gap-2",children:[p,h]}),e[40]=p,e[41]=h,e[42]=x):x=e[42];let S;e[43]!==m||e[44]!==u||e[45]!==x||e[46]!==i?(S=t.jsxs("form",{className:"max-w-4xl space-y-6",onSubmit:i,children:[m,u,x]}),e[43]=m,e[44]=u,e[45]=x,e[46]=i,e[47]=S):S=e[47];let _;return e[48]!==S||e[49]!==r?(_=t.jsxs(t.Fragment,{children:[V,C,r,S]}),e[48]=S,e[49]=r,e[50]=_):_=e[50],_}function ue(e){return t.jsxs(ne,{value:e.VolumeId??"",children:[e.VolumeId," (",e.Size," GiB)"]},e.VolumeId)}export{ye as component};
|
||||
@@ -0,0 +1 @@
|
||||
import{c as oe,u as ce,h as de,g as me,j as t,B as Y}from"./index.js";import{a as ue,u as fe,C as ee}from"./zod.js";import{u as te}from"./useSuspenseQuery.js";import{B as pe}from"./back-link.js";import{E as be}from"./error-banner.js";import{P as he}from"./page-heading.js";import{F as se,a as U,b as W}from"./field.js";import{I as xe}from"./input.js";import{S as le,a as ie,b as ne,c as re,d as X}from"./select.js";import{b as je}from"./ec2.js";import{b as ve}from"./ec22.js";import"./check.js";function Ee(){const e=oe.c(66),s=ce(),{data:Q}=te(de),{data:R}=te(me),l=je();let V;e[0]!==Q.Vpcs?(V=Q.Vpcs??[],e[0]=Q.Vpcs,e[1]=V):V=e[1];const k=V;let _;e[2]!==R.AvailabilityZones?(_=R.AvailabilityZones??[],e[2]=R.AvailabilityZones,e[3]=_):_=e[3];const T=_;let Z;e[4]===Symbol.for("react.memo_cache_sentinel")?(Z=ue(ve),e[4]=Z):Z=e[4];const O=k[0]?.VpcId??"";let F;e[5]!==O?(F={resolver:Z,defaultValues:{vpcId:O,cidrBlock:"10.0.1.0/24",availabilityZone:""}},e[5]=O,e[6]=F):F=e[6];const{control:r,handleSubmit:H,register:K,formState:ae}=fe(F),{errors:i,isSubmitting:L}=ae;let N;e[7]!==l||e[8]!==s?(N=async B=>{const n=(await l.mutateAsync(B)).Subnet?.SubnetId;s(n?{to:"/ec2/describe-subnets/$id",params:{id:n}}:{to:"/ec2/describe-subnets"})},e[7]=l,e[8]=s,e[9]=N):N=e[9];const M=N;let A,P;e[10]===Symbol.for("react.memo_cache_sentinel")?(A=t.jsx(pe,{to:"/ec2/describe-subnets",children:"Back to Subnets"}),P=t.jsx(he,{title:"Create Subnet"}),e[10]=A,e[11]=P):(A=e[10],P=e[11]);let a;e[12]!==l.error?(a=l.error&&t.jsx(be,{error:l.error,msg:"Failed to create subnet"}),e[12]=l.error,e[13]=a):a=e[13];let o;e[14]!==H||e[15]!==M?(o=H(M),e[14]=H,e[15]=M,e[16]=o):o=e[16];let $;e[17]===Symbol.for("react.memo_cache_sentinel")?($=t.jsx(U,{children:t.jsx("label",{htmlFor:"vpcId",children:"VPC"})}),e[17]=$):$=e[17];let c;e[18]!==i.vpcId||e[19]!==k?(c=B=>{const{field:C}=B;return t.jsxs(le,{onValueChange:n=>C.onChange(n),value:C.value??"",children:[t.jsx(ie,{"aria-invalid":!!i.vpcId,className:"w-full",id:"vpcId",children:t.jsx(ne,{})}),t.jsx(re,{children:k.map(ye)})]})},e[18]=i.vpcId,e[19]=k,e[20]=c):c=e[20];let d;e[21]!==r||e[22]!==c?(d=t.jsx(ee,{control:r,name:"vpcId",render:c}),e[21]=r,e[22]=c,e[23]=d):d=e[23];let m;e[24]!==i.vpcId?(m=t.jsx(se,{errors:[i.vpcId]}),e[24]=i.vpcId,e[25]=m):m=e[25];let u;e[26]!==d||e[27]!==m?(u=t.jsxs(W,{children:[$,d,m]}),e[26]=d,e[27]=m,e[28]=u):u=e[28];let E;e[29]===Symbol.for("react.memo_cache_sentinel")?(E=t.jsx(U,{children:t.jsx("label",{htmlFor:"cidrBlock",children:"CIDR Block"})}),e[29]=E):E=e[29];const q=!!i.cidrBlock;let f;e[30]!==K?(f=K("cidrBlock"),e[30]=K,e[31]=f):f=e[31];let p;e[32]!==q||e[33]!==f?(p=t.jsx(xe,{"aria-invalid":q,id:"cidrBlock",placeholder:"10.0.1.0/24",...f}),e[32]=q,e[33]=f,e[34]=p):p=e[34];let b;e[35]!==i.cidrBlock?(b=t.jsx(se,{errors:[i.cidrBlock]}),e[35]=i.cidrBlock,e[36]=b):b=e[36];let h;e[37]!==p||e[38]!==b?(h=t.jsxs(W,{children:[E,p,b]}),e[37]=p,e[38]=b,e[39]=h):h=e[39];let w;e[40]===Symbol.for("react.memo_cache_sentinel")?(w=t.jsx(U,{children:t.jsx("label",{htmlFor:"availabilityZone",children:"Availability Zone (optional)"})}),e[40]=w):w=e[40];let x;e[41]!==T?(x=B=>{const{field:C}=B;return t.jsxs(le,{onValueChange:n=>C.onChange(n==="none"?"":n),value:C.value||"none",children:[t.jsx(ie,{className:"w-full",id:"availabilityZone",children:t.jsx(ne,{})}),t.jsxs(re,{children:[t.jsx(X,{value:"none",children:"none"}),T.map(Se)]})]})},e[41]=T,e[42]=x):x=e[42];let j;e[43]!==r||e[44]!==x?(j=t.jsxs(W,{children:[w,t.jsx(ee,{control:r,name:"availabilityZone",render:x})]}),e[43]=r,e[44]=x,e[45]=j):j=e[45];const z=L||l.isPending;let v;e[46]!==s?(v=()=>s({to:"/ec2/describe-subnets"}),e[46]=s,e[47]=v):v=e[47];let S;e[48]!==z||e[49]!==v?(S=t.jsx(Y,{disabled:z,onClick:v,type:"button",variant:"outline",children:"Cancel"}),e[48]=z,e[49]=v,e[50]=S):S=e[50];const G=L||l.isPending,J=L||l.isPending?"Creating…":"Create Subnet";let y;e[51]!==G||e[52]!==J?(y=t.jsx(Y,{disabled:G,type:"submit",children:J}),e[51]=G,e[52]=J,e[53]=y):y=e[53];let g;e[54]!==S||e[55]!==y?(g=t.jsxs("div",{className:"flex gap-2",children:[S,y]}),e[54]=S,e[55]=y,e[56]=g):g=e[56];let I;e[57]!==o||e[58]!==u||e[59]!==h||e[60]!==j||e[61]!==g?(I=t.jsxs("form",{className:"max-w-4xl space-y-6",onSubmit:o,children:[u,h,j,g]}),e[57]=o,e[58]=u,e[59]=h,e[60]=j,e[61]=g,e[62]=I):I=e[62];let D;return e[63]!==I||e[64]!==a?(D=t.jsxs(t.Fragment,{children:[A,P,a,I]}),e[63]=I,e[64]=a,e[65]=D):D=e[65],D}function Se(e){return t.jsx(X,{value:e.ZoneName??"",children:e.ZoneName},e.ZoneName)}function ye(e){const s=e.Tags?.find(ge)?.Value;return t.jsx(X,{value:e.VpcId??"",children:s?`${e.VpcId} (${s})`:`${e.VpcId} (${e.CidrBlock})`},e.VpcId)}function ge(e){return e.Key==="Name"}export{Ee as component};
|
||||
@@ -1 +1 @@
|
||||
import{c as $,u as q,j as t,B as I}from"./index.js";import{a as D,u as J,I as R,F as G,b as H,c as L}from"./field.js";import{B as K}from"./back-link.js";import{E as O}from"./error-banner.js";import{P as Q}from"./page-heading.js";import{u as U}from"./ec2.js";import{c as W}from"./ec22.js";function ae(){const e=$.c(52),s=q(),i=U();let g;e[0]===Symbol.for("react.memo_cache_sentinel")?(g={resolver:D(W),defaultValues:{size:1,availabilityZone:""}},e[0]=g):g=e[0];const{handleSubmit:C,register:a,formState:M}=J(g),{errors:l,isSubmitting:V}=M;let S;e[1]!==i||e[2]!==s?(S=async T=>{await i.mutateAsync(T),s({to:"/ec2/describe-volumes"})},e[1]=i,e[2]=s,e[3]=S):S=e[3];const E=S;let z,F;e[4]===Symbol.for("react.memo_cache_sentinel")?(z=t.jsx(K,{to:"/ec2/describe-volumes",children:"Back to volumes"}),F=t.jsx(Q,{title:"Create Volume"}),e[4]=z,e[5]=F):(z=e[4],F=e[5]);let r;e[6]!==i.error?(r=i.error&&t.jsx(O,{error:i.error,msg:"Failed to create volume"}),e[6]=i.error,e[7]=r):r=e[7];let o;e[8]!==C||e[9]!==E?(o=C(E),e[8]=C,e[9]=E,e[10]=o):o=e[10];let Z;e[11]===Symbol.for("react.memo_cache_sentinel")?(Z=t.jsx(H,{children:t.jsx("label",{htmlFor:"size",children:"Size (GiB)"})}),e[11]=Z):Z=e[11];const P=!!l.size;let n;e[12]!==a?(n=a("size",{valueAsNumber:!0}),e[12]=a,e[13]=n):n=e[13];let c;e[14]!==P||e[15]!==n?(c=t.jsx(R,{"aria-invalid":P,id:"size",max:16384,min:1,type:"number",...n}),e[14]=P,e[15]=n,e[16]=c):c=e[16];let m;e[17]!==l.size?(m=t.jsx(G,{errors:[l.size]}),e[17]=l.size,e[18]=m):m=e[18];let u;e[19]!==c||e[20]!==m?(u=t.jsxs(L,{children:[Z,c,m]}),e[19]=c,e[20]=m,e[21]=u):u=e[21];let _;e[22]===Symbol.for("react.memo_cache_sentinel")?(_=t.jsx(H,{children:t.jsx("label",{htmlFor:"availabilityZone",children:"Availability Zone"})}),e[22]=_):_=e[22];const k=!!l.availabilityZone;let f;e[23]!==a?(f=a("availabilityZone"),e[23]=a,e[24]=f):f=e[24];let d;e[25]!==k||e[26]!==f?(d=t.jsx(R,{"aria-invalid":k,id:"availabilityZone",placeholder:"ap-southeast-2a",type:"text",...f}),e[25]=k,e[26]=f,e[27]=d):d=e[27];let b;e[28]!==l.availabilityZone?(b=t.jsx(G,{errors:[l.availabilityZone]}),e[28]=l.availabilityZone,e[29]=b):b=e[29];let x;e[30]!==d||e[31]!==b?(x=t.jsxs(L,{children:[_,d,b]}),e[30]=d,e[31]=b,e[32]=x):x=e[32];const N=V||i.isPending;let h;e[33]!==s?(h=()=>s({to:"/ec2/describe-volumes"}),e[33]=s,e[34]=h):h=e[34];let v;e[35]!==N||e[36]!==h?(v=t.jsx(I,{disabled:N,onClick:h,type:"button",variant:"outline",children:"Cancel"}),e[35]=N,e[36]=h,e[37]=v):v=e[37];const A=V||i.isPending,w=V||i.isPending?"Creating…":"Create Volume";let p;e[38]!==A||e[39]!==w?(p=t.jsx(I,{disabled:A,type:"submit",children:w}),e[38]=A,e[39]=w,e[40]=p):p=e[40];let j;e[41]!==v||e[42]!==p?(j=t.jsxs("div",{className:"flex gap-2",children:[v,p]}),e[41]=v,e[42]=p,e[43]=j):j=e[43];let y;e[44]!==u||e[45]!==x||e[46]!==j||e[47]!==o?(y=t.jsxs("form",{className:"max-w-4xl space-y-6",onSubmit:o,children:[u,x,j]}),e[44]=u,e[45]=x,e[46]=j,e[47]=o,e[48]=y):y=e[48];let B;return e[49]!==y||e[50]!==r?(B=t.jsxs(t.Fragment,{children:[z,F,r,y]}),e[49]=y,e[50]=r,e[51]=B):B=e[51],B}export{ae as component};
|
||||
import{c as K,u as U,g as W,j as t,B as L}from"./index.js";import{a as X,u as Y,C as ee}from"./zod.js";import{u as te}from"./useSuspenseQuery.js";import{B as ie}from"./back-link.js";import{E as le}from"./error-banner.js";import{P as ae}from"./page-heading.js";import{F as M,a as O,b as $}from"./field.js";import{I as se}from"./input.js";import{S as re,a as oe,b as ne,c as me,d as ce}from"./select.js";import{a as ue}from"./ec2.js";import{a as fe}from"./ec22.js";import"./check.js";function Fe(){const e=K.c(57),a=U(),{data:V}=te(W),i=ue();let j;e[0]!==V.AvailabilityZones?(j=V.AvailabilityZones??[],e[0]=V.AvailabilityZones,e[1]=j):j=e[1];const Z=j,B=Z[0]?.ZoneName??"";let g;e[2]===Symbol.for("react.memo_cache_sentinel")?(g=X(fe),e[2]=g):g=e[2];let S;e[3]!==B?(S={resolver:g,defaultValues:{size:1,availabilityZone:B}},e[3]=B,e[4]=S):S=e[4];const{control:E,handleSubmit:P,register:k,formState:q}=Y(S),{errors:l,isSubmitting:w}=q;let C;e[5]!==i||e[6]!==a?(C=async G=>{await i.mutateAsync(G),a({to:"/ec2/describe-volumes"})},e[5]=i,e[6]=a,e[7]=C):C=e[7];const I=C;let z,F;e[8]===Symbol.for("react.memo_cache_sentinel")?(z=t.jsx(ie,{to:"/ec2/describe-volumes",children:"Back to volumes"}),F=t.jsx(ae,{title:"Create Volume"}),e[8]=z,e[9]=F):(z=e[8],F=e[9]);let s;e[10]!==i.error?(s=i.error&&t.jsx(le,{error:i.error,msg:"Failed to create volume"}),e[10]=i.error,e[11]=s):s=e[11];let r;e[12]!==P||e[13]!==I?(r=P(I),e[12]=P,e[13]=I,e[14]=r):r=e[14];let N;e[15]===Symbol.for("react.memo_cache_sentinel")?(N=t.jsx(O,{children:t.jsx("label",{htmlFor:"size",children:"Size (GiB)"})}),e[15]=N):N=e[15];const Q=!!l.size;let o;e[16]!==k?(o=k("size",{valueAsNumber:!0}),e[16]=k,e[17]=o):o=e[17];let n;e[18]!==Q||e[19]!==o?(n=t.jsx(se,{"aria-invalid":Q,id:"size",max:16384,min:1,type:"number",...o}),e[18]=Q,e[19]=o,e[20]=n):n=e[20];let m;e[21]!==l.size?(m=t.jsx(M,{errors:[l.size]}),e[21]=l.size,e[22]=m):m=e[22];let c;e[23]!==n||e[24]!==m?(c=t.jsxs($,{children:[N,n,m]}),e[23]=n,e[24]=m,e[25]=c):c=e[25];let _;e[26]===Symbol.for("react.memo_cache_sentinel")?(_=t.jsx(O,{children:t.jsx("label",{htmlFor:"availabilityZone",children:"Availability Zone"})}),e[26]=_):_=e[26];let u;e[27]!==Z||e[28]!==l.availabilityZone?(u=G=>{const{field:H}=G;return t.jsxs(re,{onValueChange:J=>H.onChange(J),value:H.value??"",children:[t.jsx(oe,{"aria-invalid":!!l.availabilityZone,className:"w-full",id:"availabilityZone",children:t.jsx(ne,{})}),t.jsx(me,{children:Z.map(de)})]})},e[27]=Z,e[28]=l.availabilityZone,e[29]=u):u=e[29];let f;e[30]!==E||e[31]!==u?(f=t.jsx(ee,{control:E,name:"availabilityZone",render:u}),e[30]=E,e[31]=u,e[32]=f):f=e[32];let d;e[33]!==l.availabilityZone?(d=t.jsx(M,{errors:[l.availabilityZone]}),e[33]=l.availabilityZone,e[34]=d):d=e[34];let b;e[35]!==f||e[36]!==d?(b=t.jsxs($,{children:[_,f,d]}),e[35]=f,e[36]=d,e[37]=b):b=e[37];const R=w||i.isPending;let v;e[38]!==a?(v=()=>a({to:"/ec2/describe-volumes"}),e[38]=a,e[39]=v):v=e[39];let x;e[40]!==R||e[41]!==v?(x=t.jsx(L,{disabled:R,onClick:v,type:"button",variant:"outline",children:"Cancel"}),e[40]=R,e[41]=v,e[42]=x):x=e[42];const T=w||i.isPending,D=w||i.isPending?"Creating…":"Create Volume";let y;e[43]!==T||e[44]!==D?(y=t.jsx(L,{disabled:T,type:"submit",children:D}),e[43]=T,e[44]=D,e[45]=y):y=e[45];let p;e[46]!==x||e[47]!==y?(p=t.jsxs("div",{className:"flex gap-2",children:[x,y]}),e[46]=x,e[47]=y,e[48]=p):p=e[48];let h;e[49]!==c||e[50]!==b||e[51]!==p||e[52]!==r?(h=t.jsxs("form",{className:"max-w-4xl space-y-6",onSubmit:r,children:[c,b,p]}),e[49]=c,e[50]=b,e[51]=p,e[52]=r,e[53]=h):h=e[53];let A;return e[54]!==h||e[55]!==s?(A=t.jsxs(t.Fragment,{children:[z,F,s,h]}),e[54]=h,e[55]=s,e[56]=A):A=e[56],A}function de(e){return t.jsx(ce,{value:e.ZoneName??"",children:e.ZoneName},e.ZoneName)}export{Fe as component};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
import{c as M,u as T,j as t,B as w}from"./index.js";import{a as q,u as z}from"./zod.js";import{B as G}from"./back-link.js";import{E as J}from"./error-banner.js";import{P as K}from"./page-heading.js";import{F as O,b as $,a as A}from"./field.js";import{I as D}from"./input.js";import{u as Q}from"./ec2.js";import{c as U}from"./ec22.js";function ce(){const e=M.c(46),r=T(),s=Q();let j;e[0]===Symbol.for("react.memo_cache_sentinel")?(j={resolver:q(U),defaultValues:{cidrBlock:"10.0.0.0/16",name:""}},e[0]=j):j=e[0];const{handleSubmit:V,register:l,formState:H}=z(j),{errors:B,isSubmitting:F}=H;let g;e[1]!==s||e[2]!==r?(g=async L=>{const R=(await s.mutateAsync(L)).Vpc?.VpcId;r(R?{to:"/ec2/describe-vpcs/$id",params:{id:R}}:{to:"/ec2/describe-vpcs"})},e[1]=s,e[2]=r,e[3]=g):g=e[3];const P=g;let k,v;e[4]===Symbol.for("react.memo_cache_sentinel")?(k=t.jsx(G,{to:"/ec2/describe-vpcs",children:"Back to VPCs"}),v=t.jsx(K,{title:"Create VPC"}),e[4]=k,e[5]=v):(k=e[4],v=e[5]);let i;e[6]!==s.error?(i=s.error&&t.jsx(J,{error:s.error,msg:"Failed to create VPC"}),e[6]=s.error,e[7]=i):i=e[7];let c;e[8]!==V||e[9]!==P?(c=V(P),e[8]=V,e[9]=P,e[10]=c):c=e[10];let C;e[11]===Symbol.for("react.memo_cache_sentinel")?(C=t.jsx(A,{children:t.jsx("label",{htmlFor:"cidrBlock",children:"CIDR Block"})}),e[11]=C):C=e[11];const _=!!B.cidrBlock;let o;e[12]!==l?(o=l("cidrBlock"),e[12]=l,e[13]=o):o=e[13];let a;e[14]!==_||e[15]!==o?(a=t.jsx(D,{"aria-invalid":_,id:"cidrBlock",placeholder:"10.0.0.0/16",...o}),e[14]=_,e[15]=o,e[16]=a):a=e[16];let n;e[17]!==B.cidrBlock?(n=t.jsx(O,{errors:[B.cidrBlock]}),e[17]=B.cidrBlock,e[18]=n):n=e[18];let m;e[19]!==a||e[20]!==n?(m=t.jsxs($,{children:[C,a,n]}),e[19]=a,e[20]=n,e[21]=m):m=e[21];let y;e[22]===Symbol.for("react.memo_cache_sentinel")?(y=t.jsx(A,{children:t.jsx("label",{htmlFor:"name",children:"Name (optional)"})}),e[22]=y):y=e[22];let d;e[23]!==l?(d=l("name"),e[23]=l,e[24]=d):d=e[24];let f;e[25]!==d?(f=t.jsxs($,{children:[y,t.jsx(D,{id:"name",placeholder:"my-vpc",...d})]}),e[25]=d,e[26]=f):f=e[26];const E=F||s.isPending;let p;e[27]!==r?(p=()=>r({to:"/ec2/describe-vpcs"}),e[27]=r,e[28]=p):p=e[28];let u;e[29]!==E||e[30]!==p?(u=t.jsx(w,{disabled:E,onClick:p,type:"button",variant:"outline",children:"Cancel"}),e[29]=E,e[30]=p,e[31]=u):u=e[31];const I=F||s.isPending,N=F||s.isPending?"Creating…":"Create VPC";let x;e[32]!==I||e[33]!==N?(x=t.jsx(w,{disabled:I,type:"submit",children:N}),e[32]=I,e[33]=N,e[34]=x):x=e[34];let h;e[35]!==u||e[36]!==x?(h=t.jsxs("div",{className:"flex gap-2",children:[u,x]}),e[35]=u,e[36]=x,e[37]=h):h=e[37];let b;e[38]!==m||e[39]!==f||e[40]!==h||e[41]!==c?(b=t.jsxs("form",{className:"max-w-4xl space-y-6",onSubmit:c,children:[m,f,h]}),e[38]=m,e[39]=f,e[40]=h,e[41]=c,e[42]=b):b=e[42];let S;return e[43]!==b||e[44]!==i?(S=t.jsxs(t.Fragment,{children:[k,v,i,b]}),e[43]=b,e[44]=i,e[45]=S):S=e[45],S}export{ce as component};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{g as o}from"./index.js";const a=[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]],e=o("download",a);export{e as D};
|
||||
import{k as o}from"./index.js";const a=[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]],e=o("download",a);export{e as D};
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{o as a,f as e,bo as t,bp as s}from"./index.js";const m=a({imageId:e("Please select an Image"),instanceType:e("Please select an instance type"),keyName:e("Please select a key pair"),count:s("Instance count must be a whole number").min(1,"Instance count must be at least 1")}),c=a({keyName:e().min(1,"Key name is required").max(255,"Key name must be 255 characters or less").regex(/^[\w\s._\-:/()#,@[\]+=&;{}!$*]+$/,"Key name contains invalid characters")}),r=a({keyName:e().min(1,"Key name is required").max(255,"Key name must be 255 characters or less").regex(/^[\w\s._\-:/()#,@[\]+=&;{}!$*]+$/,"Key name contains invalid characters"),publicKeyMaterial:e().min(1,"Public key is required").refine(i=>i.trim().length>0,"Public key cannot be empty")}),o=a({size:t().int("Size must be a whole number").min(1,"Size must be at least 1 GiB").max(16384,"Size must be at most 16384 GiB"),availabilityZone:e().min(1,"Availability zone is required")}),l=a({size:t().int("Size must be a whole number").min(1,"Size must be at least 1 GiB")});export{c as a,m as b,o as c,r as i,l as m};
|
||||
import{o as i,f as e,ao as a,ap as s,aq as t}from"./index.js";const r=i({imageId:e("Please select an Image"),instanceType:e("Please select an instance type"),keyName:e("Please select a key pair"),subnetId:e().optional(),count:s("Instance count must be a whole number").min(1,"Instance count must be at least 1")}),c=i({keyName:e().min(1,"Key name is required").max(255,"Key name must be 255 characters or less").regex(/^[\w\s._\-:/()#,@[\]+=&;{}!$*]+$/,"Key name contains invalid characters")}),m=i({keyName:e().min(1,"Key name is required").max(255,"Key name must be 255 characters or less").regex(/^[\w\s._\-:/()#,@[\]+=&;{}!$*]+$/,"Key name contains invalid characters"),publicKeyMaterial:e().min(1,"Public key is required").refine(n=>n.trim().length>0,"Public key cannot be empty")}),l=i({size:a().int("Size must be a whole number").min(1,"Size must be at least 1 GiB").max(16384,"Size must be at most 16384 GiB"),availabilityZone:e().min(1,"Availability zone is required")}),u=i({size:a().int("Size must be a whole number").min(1,"Size must be at least 1 GiB")}),d=i({volumeId:e().min(1,"Volume is required"),description:e().optional()});i({sourceSnapshotId:e().min(1,"Source snapshot is required"),sourceRegion:e().min(1,"Source region is required"),description:e().optional()});i({volumeId:e().min(1,"Volume is required"),instanceId:e().min(1,"Instance is required"),device:e().optional()});i({volumeId:e().min(1,"Volume is required"),instanceId:e().optional(),force:t().optional()});i({instanceType:e().min(1,"Instance type is required")});i({name:e().min(1,"Name is required"),description:e().optional()});const b=i({vpcId:e().min(1,"VPC is required"),cidrBlock:e().min(1,"CIDR block is required").regex(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/,"Must be a valid CIDR block (e.g. 10.0.1.0/24)"),availabilityZone:e().optional()}),p=i({cidrBlock:e().min(1,"CIDR block is required").regex(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/,"Must be a valid CIDR block (e.g. 10.0.0.0/16)"),name:e().optional()});export{l as a,b,p as c,d,c as e,r as f,m as i,u as m};
|
||||
|
||||
@@ -1 +1 @@
|
||||
import{c1 as d,c2 as m,c3 as u,c4 as p,c5 as c,bl as b,r as a,c6 as v,c7 as f,c as y,j as h}from"./index.js";var x=class extends d{#s;#i=void 0;#t;#e;constructor(s,t){super(),this.#s=s,this.setOptions(t),this.bindMethods(),this.#r()}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(s){const t=this.options;this.options=this.#s.defaultMutationOptions(s),m(this.options,t)||this.#s.getMutationCache().notify({type:"observerOptionsUpdated",mutation:this.#t,observer:this}),t?.mutationKey&&this.options.mutationKey&&u(t.mutationKey)!==u(this.options.mutationKey)?this.reset():this.#t?.state.status==="pending"&&this.#t.setOptions(this.options)}onUnsubscribe(){this.hasListeners()||this.#t?.removeObserver(this)}onMutationUpdate(s){this.#r(),this.#o(s)}getCurrentResult(){return this.#i}reset(){this.#t?.removeObserver(this),this.#t=void 0,this.#r(),this.#o()}mutate(s,t){return this.#e=t,this.#t?.removeObserver(this),this.#t=this.#s.getMutationCache().build(this.#s,this.options),this.#t.addObserver(this),this.#t.execute(s)}#r(){const s=this.#t?.state??p();this.#i={...s,isPending:s.status==="pending",isSuccess:s.status==="success",isError:s.status==="error",isIdle:s.status==="idle",mutate:this.mutate,reset:this.reset}}#o(s){c.batch(()=>{if(this.#e&&this.hasListeners()){const t=this.#i.variables,r=this.#i.context,i={client:this.#s,meta:this.options.meta,mutationKey:this.options.mutationKey};if(s?.type==="success"){try{this.#e.onSuccess?.(s.data,t,r,i)}catch(e){Promise.reject(e)}try{this.#e.onSettled?.(s.data,null,t,r,i)}catch(e){Promise.reject(e)}}else if(s?.type==="error"){try{this.#e.onError?.(s.error,t,r,i)}catch(e){Promise.reject(e)}try{this.#e.onSettled?.(void 0,s.error,t,r,i)}catch(e){Promise.reject(e)}}}this.listeners.forEach(t=>{t(this.#i)})})}};function g(s,t){const r=b(),[i]=a.useState(()=>new x(r,s));a.useEffect(()=>{i.setOptions(s)},[i,s]);const e=a.useSyncExternalStore(a.useCallback(n=>i.subscribe(c.batchCalls(n)),[i]),()=>i.getCurrentResult(),()=>i.getCurrentResult()),o=a.useCallback((n,l)=>{i.mutate(n,l).catch(v)},[i]);if(e.error&&f(i.options.throwOnError,[e.error]))throw e.error;return{...e,mutate:o,mutateAsync:e.mutate}}function E(s){const t=y.c(7),{msg:r,error:i}=s;let e;t[0]!==r?(e=r&&h.jsx("h2",{className:"text-destructive text-sm",children:r}),t[0]=r,t[1]=e):e=t[1];let o;t[2]!==i?(o=i&&h.jsx("p",{className:"text-destructive text-sm",children:i.message}),t[2]=i,t[3]=o):o=t[3];let n;return t[4]!==e||t[5]!==o?(n=h.jsxs("div",{className:"mb-6 max-w-4xl rounded-md border border-destructive bg-destructive/10 p-4",children:[e,o]}),t[4]=e,t[5]=o,t[6]=n):n=t[6],n}export{E,g as u};
|
||||
import{cs as d,ct as m,cu as c,cv as p,cw as l,aj as b,r as h,cx as v,cy as f,c as y,j as u}from"./index.js";var x=class extends d{#e;#i=void 0;#t;#s;constructor(e,t){super(),this.#e=e,this.setOptions(t),this.bindMethods(),this.#r()}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(e){const t=this.options;this.options=this.#e.defaultMutationOptions(e),m(this.options,t)||this.#e.getMutationCache().notify({type:"observerOptionsUpdated",mutation:this.#t,observer:this}),t?.mutationKey&&this.options.mutationKey&&c(t.mutationKey)!==c(this.options.mutationKey)?this.reset():this.#t?.state.status==="pending"&&this.#t.setOptions(this.options)}onUnsubscribe(){this.hasListeners()||this.#t?.removeObserver(this)}onMutationUpdate(e){this.#r(),this.#o(e)}getCurrentResult(){return this.#i}reset(){this.#t?.removeObserver(this),this.#t=void 0,this.#r(),this.#o()}mutate(e,t){return this.#s=t,this.#t?.removeObserver(this),this.#t=this.#e.getMutationCache().build(this.#e,this.options),this.#t.addObserver(this),this.#t.execute(e)}#r(){const e=this.#t?.state??p();this.#i={...e,isPending:e.status==="pending",isSuccess:e.status==="success",isError:e.status==="error",isIdle:e.status==="idle",mutate:this.mutate,reset:this.reset}}#o(e){l.batch(()=>{if(this.#s&&this.hasListeners()){const t=this.#i.variables,r=this.#i.context,s={client:this.#e,meta:this.options.meta,mutationKey:this.options.mutationKey};if(e?.type==="success"){try{this.#s.onSuccess?.(e.data,t,r,s)}catch(i){Promise.reject(i)}try{this.#s.onSettled?.(e.data,null,t,r,s)}catch(i){Promise.reject(i)}}else if(e?.type==="error"){try{this.#s.onError?.(e.error,t,r,s)}catch(i){Promise.reject(i)}try{this.#s.onSettled?.(void 0,e.error,t,r,s)}catch(i){Promise.reject(i)}}}this.listeners.forEach(t=>{t(this.#i)})})}};function E(e,t){const r=b(),[s]=h.useState(()=>new x(r,e));h.useEffect(()=>{s.setOptions(e)},[s,e]);const i=h.useSyncExternalStore(h.useCallback(o=>s.subscribe(l.batchCalls(o)),[s]),()=>s.getCurrentResult(),()=>s.getCurrentResult()),n=h.useCallback((o,a)=>{s.mutate(o,a).catch(v)},[s]);if(i.error&&f(s.options.throwOnError,[i.error]))throw i.error;return{...i,mutate:n,mutateAsync:i.mutate}}function g(e){const t=y.c(7),{msg:r,error:s}=e;let i;s&&(i=s.name!=="Error"?s.name:s.message);let n;t[0]!==r?(n=r&&u.jsx("h2",{className:"text-destructive text-sm",children:r}),t[0]=r,t[1]=n):n=t[1];let o;t[2]!==i?(o=i&&u.jsx("p",{className:"text-destructive text-sm",children:i}),t[2]=i,t[3]=o):o=t[3];let a;return t[4]!==n||t[5]!==o?(a=u.jsxs("div",{className:"mb-6 max-w-4xl rounded-md border border-destructive bg-destructive/10 p-4",children:[n,o]}),t[4]=n,t[5]=o,t[6]=a):a=t[6],a}export{g as E,E as u};
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{c as O,u as $,j as t,B as H}from"./index.js";import{a as q,u as G,I as J,F as Q,b as R,c as z}from"./field.js";import{B as U}from"./back-link.js";import{E as V}from"./error-banner.js";import{P as W}from"./page-heading.js";import{T as X}from"./textarea.js";import{a as Y}from"./ec2.js";import{i as Z}from"./ec22.js";function ce(){const e=O.c(53),s=$(),i=Y();let g;e[0]===Symbol.for("react.memo_cache_sentinel")?(g={resolver:q(Z)},e[0]=g):g=e[0];const{handleSubmit:_,register:l,formState:D}=G(g),{errors:r,isSubmitting:M}=D;let A;e[1]!==i||e[2]!==s?(A=async L=>{await i.mutateAsync(L),s({to:"/ec2/describe-key-pairs"})},e[1]=i,e[2]=s,e[3]=A):A=e[3];const v=A;let K,N;e[4]===Symbol.for("react.memo_cache_sentinel")?(K=t.jsx(U,{to:"/ec2/describe-key-pairs",children:"Back to key pairs"}),N=t.jsx(W,{title:"Import Key Pair"}),e[4]=K,e[5]=N):(K=e[4],N=e[5]);let a;e[6]!==i.error?(a=i.error&&t.jsx(V,{error:i.error,msg:"Failed to import key pair"}),e[6]=i.error,e[7]=a):a=e[7];let o;e[8]!==_||e[9]!==v?(o=_(v),e[8]=_,e[9]=v,e[10]=o):o=e[10];let P;e[11]===Symbol.for("react.memo_cache_sentinel")?(P=t.jsx(R,{children:t.jsx("label",{htmlFor:"keyName",children:"Key Pair Name"})}),e[11]=P):P=e[11];const I=!!r.keyName;let c;e[12]!==l?(c=l("keyName"),e[12]=l,e[13]=c):c=e[13];let n;e[14]!==I||e[15]!==c?(n=t.jsx(J,{"aria-invalid":I,id:"keyName",placeholder:"my-key-pair…",...c}),e[14]=I,e[15]=c,e[16]=n):n=e[16];let m;e[17]!==r.keyName?(m=t.jsx(Q,{errors:[r.keyName]}),e[17]=r.keyName,e[18]=m):m=e[18];let p;e[19]!==n||e[20]!==m?(p=t.jsxs(z,{children:[P,n,m]}),e[19]=n,e[20]=m,e[21]=p):p=e[21];let S,B;e[22]===Symbol.for("react.memo_cache_sentinel")?(S=t.jsx(R,{children:t.jsx("label",{htmlFor:"publicKeyMaterial",children:"Public Key"})}),B=t.jsx("p",{className:"text-muted-foreground text-xs",id:"publicKey-description",children:"Paste your OpenSSH public key (e.g. the contents of id_rsa.pub)"}),e[22]=S,e[23]=B):(S=e[22],B=e[23]);const E=!!r.publicKeyMaterial;let d;e[24]!==l?(d=l("publicKeyMaterial"),e[24]=l,e[25]=d):d=e[25];let y;e[26]!==E||e[27]!==d?(y=t.jsx(X,{"aria-invalid":E,id:"publicKeyMaterial",placeholder:"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC…",rows:8,...d}),e[26]=E,e[27]=d,e[28]=y):y=e[28];let f;e[29]!==r.publicKeyMaterial?(f=t.jsx(Q,{errors:[r.publicKeyMaterial]}),e[29]=r.publicKeyMaterial,e[30]=f):f=e[30];let u;e[31]!==y||e[32]!==f?(u=t.jsxs(z,{children:[S,B,y,f]}),e[31]=y,e[32]=f,e[33]=u):u=e[33];const C=M||i.isPending;let x;e[34]!==s?(x=()=>s({to:"/ec2/describe-key-pairs"}),e[34]=s,e[35]=x):x=e[35];let b;e[36]!==C||e[37]!==x?(b=t.jsx(H,{disabled:C,onClick:x,type:"button",variant:"outline",children:"Cancel"}),e[36]=C,e[37]=x,e[38]=b):b=e[38];const w=M||i.isPending,T=M||i.isPending?"Importing…":"Import Key Pair";let h;e[39]!==w||e[40]!==T?(h=t.jsx(H,{disabled:w,type:"submit",children:T}),e[39]=w,e[40]=T,e[41]=h):h=e[41];let j;e[42]!==b||e[43]!==h?(j=t.jsxs("div",{className:"flex gap-2",children:[b,h]}),e[42]=b,e[43]=h,e[44]=j):j=e[44];let k;e[45]!==p||e[46]!==u||e[47]!==j||e[48]!==o?(k=t.jsxs("form",{className:"max-w-4xl space-y-6",onSubmit:o,children:[p,u,j]}),e[45]=p,e[46]=u,e[47]=j,e[48]=o,e[49]=k):k=e[49];let F;return e[50]!==k||e[51]!==a?(F=t.jsxs(t.Fragment,{children:[K,N,a,k]}),e[50]=k,e[51]=a,e[52]=F):F=e[52],F}export{ce as component};
|
||||
import{c as O,u as $,j as t,B as H}from"./index.js";import{a as q,u as G}from"./zod.js";import{B as J}from"./back-link.js";import{E as U}from"./error-banner.js";import{P as V}from"./page-heading.js";import{F as Q,a as R,b as z}from"./field.js";import{I as W}from"./input.js";import{T as X}from"./textarea.js";import{d as Y}from"./ec2.js";import{i as Z}from"./ec22.js";function me(){const e=O.c(53),s=$(),i=Y();let g;e[0]===Symbol.for("react.memo_cache_sentinel")?(g={resolver:q(Z)},e[0]=g):g=e[0];const{handleSubmit:_,register:l,formState:D}=G(g),{errors:r,isSubmitting:M}=D;let A;e[1]!==i||e[2]!==s?(A=async L=>{await i.mutateAsync(L),s({to:"/ec2/describe-key-pairs"})},e[1]=i,e[2]=s,e[3]=A):A=e[3];const v=A;let K,N;e[4]===Symbol.for("react.memo_cache_sentinel")?(K=t.jsx(J,{to:"/ec2/describe-key-pairs",children:"Back to key pairs"}),N=t.jsx(V,{title:"Import Key Pair"}),e[4]=K,e[5]=N):(K=e[4],N=e[5]);let a;e[6]!==i.error?(a=i.error&&t.jsx(U,{error:i.error,msg:"Failed to import key pair"}),e[6]=i.error,e[7]=a):a=e[7];let o;e[8]!==_||e[9]!==v?(o=_(v),e[8]=_,e[9]=v,e[10]=o):o=e[10];let P;e[11]===Symbol.for("react.memo_cache_sentinel")?(P=t.jsx(R,{children:t.jsx("label",{htmlFor:"keyName",children:"Key Pair Name"})}),e[11]=P):P=e[11];const I=!!r.keyName;let c;e[12]!==l?(c=l("keyName"),e[12]=l,e[13]=c):c=e[13];let n;e[14]!==I||e[15]!==c?(n=t.jsx(W,{"aria-invalid":I,id:"keyName",placeholder:"my-key-pair…",...c}),e[14]=I,e[15]=c,e[16]=n):n=e[16];let m;e[17]!==r.keyName?(m=t.jsx(Q,{errors:[r.keyName]}),e[17]=r.keyName,e[18]=m):m=e[18];let p;e[19]!==n||e[20]!==m?(p=t.jsxs(z,{children:[P,n,m]}),e[19]=n,e[20]=m,e[21]=p):p=e[21];let S,B;e[22]===Symbol.for("react.memo_cache_sentinel")?(S=t.jsx(R,{children:t.jsx("label",{htmlFor:"publicKeyMaterial",children:"Public Key"})}),B=t.jsx("p",{className:"text-muted-foreground text-xs",id:"publicKey-description",children:"Paste your OpenSSH public key (e.g. the contents of id_rsa.pub)"}),e[22]=S,e[23]=B):(S=e[22],B=e[23]);const E=!!r.publicKeyMaterial;let d;e[24]!==l?(d=l("publicKeyMaterial"),e[24]=l,e[25]=d):d=e[25];let f;e[26]!==E||e[27]!==d?(f=t.jsx(X,{"aria-invalid":E,id:"publicKeyMaterial",placeholder:"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC…",rows:8,...d}),e[26]=E,e[27]=d,e[28]=f):f=e[28];let y;e[29]!==r.publicKeyMaterial?(y=t.jsx(Q,{errors:[r.publicKeyMaterial]}),e[29]=r.publicKeyMaterial,e[30]=y):y=e[30];let u;e[31]!==f||e[32]!==y?(u=t.jsxs(z,{children:[S,B,f,y]}),e[31]=f,e[32]=y,e[33]=u):u=e[33];const C=M||i.isPending;let x;e[34]!==s?(x=()=>s({to:"/ec2/describe-key-pairs"}),e[34]=s,e[35]=x):x=e[35];let b;e[36]!==C||e[37]!==x?(b=t.jsx(H,{disabled:C,onClick:x,type:"button",variant:"outline",children:"Cancel"}),e[36]=C,e[37]=x,e[38]=b):b=e[38];const w=M||i.isPending,T=M||i.isPending?"Importing…":"Import Key Pair";let h;e[39]!==w||e[40]!==T?(h=t.jsx(H,{disabled:w,type:"submit",children:T}),e[39]=w,e[40]=T,e[41]=h):h=e[41];let j;e[42]!==b||e[43]!==h?(j=t.jsxs("div",{className:"flex gap-2",children:[b,h]}),e[42]=b,e[43]=h,e[44]=j):j=e[44];let k;e[45]!==p||e[46]!==u||e[47]!==j||e[48]!==o?(k=t.jsxs("form",{className:"max-w-4xl space-y-6",onSubmit:o,children:[p,u,j]}),e[45]=p,e[46]=u,e[47]=j,e[48]=o,e[49]=k):k=e[49];let F;return e[50]!==k||e[51]!==a?(F=t.jsxs(t.Fragment,{children:[K,N,a,k]}),e[50]=k,e[51]=a,e[52]=F):F=e[52],F}export{me as component};
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+41
-41
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import{c as m,m as l,j as t,L as n,B as c}from"./index.js";import{u as p}from"./useSuspenseQuery.js";import{L as d}from"./list-card.js";import{P as y}from"./page-heading.js";function P(){const e=m.c(4),{data:s}=p(l);let r;if(e[0]!==s.KeyPairs){const a=(s.KeyPairs||[]).toSorted(f);let i;e[2]===Symbol.for("react.memo_cache_sentinel")?(i=t.jsx(n,{to:"/ec2/import-key-pair",children:t.jsx(c,{variant:"outline",children:"Import Key Pair"})}),e[2]=i):i=e[2];let o;e[3]===Symbol.for("react.memo_cache_sentinel")?(o=t.jsx(y,{actions:t.jsxs("div",{className:"flex gap-2",children:[i,t.jsx(n,{to:"/ec2/create-key-pair",children:t.jsx(c,{children:"Create Key Pair"})})]}),title:"EC2 Key Pairs"}),e[3]=o):o=e[3],r=t.jsxs(t.Fragment,{children:[o,a.length>0?t.jsx("div",{className:"space-y-4",children:a.map(u)}):t.jsx("p",{className:"text-muted-foreground",children:"No key pairs found."})]}),e[0]=s.KeyPairs,e[1]=r}else r=e[1];return r}function u(e){return e.KeyPairId?t.jsx(d,{params:{id:e.KeyPairId},subtitle:`${e.KeyPairId} • ${e.KeyType}`,title:e.KeyName??"",to:"/ec2/describe-key-pairs/$id"},e.KeyPairId):null}function f(e,s){const r=e.KeyName?.toLowerCase()??"",a=s.KeyName?.toLowerCase()??"";return r.localeCompare(a)}export{P as component};
|
||||
@@ -0,0 +1 @@
|
||||
import{c as u,x as f,j as t,L as I,B as x}from"./index.js";import{u as g}from"./useSuspenseQuery.js";import{L as h}from"./list-card.js";import{P as j}from"./page-heading.js";import{S}from"./state-badge.js";function L(){const e=u.c(5),{data:o}=g(f);let s;e[0]===Symbol.for("react.memo_cache_sentinel")?(s={running:0,pending:1,stopping:2,"shutting-down":3,stopped:4,terminated:5},e[0]=s):s=e[0];const c=s;let n;if(e[1]!==o.Reservations){let a;e[3]===Symbol.for("react.memo_cache_sentinel")?(a=(m,l)=>{const d=c[m.State?.Name??""]??6,p=c[l.State?.Name??""]??6;return d!==p?d-p:(m.InstanceId??"").localeCompare(l.InstanceId??"")},e[3]=a):a=e[3];const i=(o.Reservations?.flatMap(_)||[]).toSorted(a);let r;e[4]===Symbol.for("react.memo_cache_sentinel")?(r=t.jsx(j,{actions:t.jsx(I,{to:"/ec2/run-instances",children:t.jsx(x,{children:"Run Instances"})}),title:"EC2 Instances"}),e[4]=r):r=e[4],n=t.jsxs(t.Fragment,{children:[r,i.length>0?t.jsx("div",{className:"space-y-4",children:i.map(y)}):t.jsx("p",{className:"text-muted-foreground",children:"No instances found."})]}),e[1]=o.Reservations,e[2]=n}else n=e[2];return n}function y(e){return e.InstanceId?t.jsx(h,{badge:t.jsx(S,{state:e.State?.Name}),params:{id:e.InstanceId},subtitle:`${e.InstanceType} • ${e.ImageId}`,title:e.InstanceId,to:"/ec2/describe-instances/$id"},e.InstanceId):null}function _(e){return e.Instances||[]}export{L as component};
|
||||
@@ -0,0 +1 @@
|
||||
import{c as n,l as i,j as t}from"./index.js";import{u as c}from"./useSuspenseQuery.js";import{L as l}from"./list-card.js";import{P as d}from"./page-heading.js";import{S as p}from"./state-badge.js";function h(){const e=n.c(5),{data:o}=c(i);let s;e[0]!==o.Images?(s=o.Images||[],e[0]=o.Images,e[1]=s):s=e[1];const a=s;let m;e[2]===Symbol.for("react.memo_cache_sentinel")?(m=t.jsx(d,{title:"EC2 Images"}),e[2]=m):m=e[2];let r;return e[3]!==a?(r=t.jsxs(t.Fragment,{children:[m,a.length>0?t.jsx("div",{className:"space-y-4",children:a.map(u)}):t.jsx("p",{className:"text-muted-foreground",children:"No images found."})]}),e[3]=a,e[4]=r):r=e[4],r}function u(e){return e.ImageId?t.jsx(l,{badge:t.jsx(p,{state:e.State}),params:{id:e.ImageId},subtitle:e.Name,title:e.ImageId,to:"/ec2/describe-images/$id"},e.ImageId):null}export{h as component};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{ck as z,cl as x}from"./index.js";class T{marshaller;serializer;deserializer;serdeContext;defaultContentType;constructor({marshaller:i,serializer:n,deserializer:o,serdeContext:c,defaultContentType:y}){this.marshaller=i,this.serializer=n,this.deserializer=o,this.serdeContext=c,this.defaultContentType=y}async serializeEventStream({eventStream:i,requestSchema:n,initialRequest:o}){const c=this.marshaller,y=n.getEventStreamMember(),p=n.getMemberSchema(y),d=this.serializer,f=this.defaultContentType,h=Symbol("initialRequestMarker"),S={async*[Symbol.asyncIterator](){if(o){const r={":event-type":{type:"string",value:"initial-request"},":message-type":{type:"string",value:"event"},":content-type":{type:"string",value:f}};d.write(n,o);const t=d.flush();yield{[h]:!0,headers:r,body:t}}for await(const r of i)yield r}};return c.serialize(S,r=>{if(r[h])return{headers:r.headers,body:r.body};const t=Object.keys(r).find(s=>s!=="__type")??"",{additionalHeaders:e,body:a,eventType:l,explicitPayloadContentType:m}=this.writeEventBody(t,p,r);return{headers:{":event-type":{type:"string",value:l},":message-type":{type:"string",value:"event"},":content-type":{type:"string",value:m??f},...e},body:a}})}async deserializeEventStream({response:i,responseSchema:n,initialResponseContainer:o}){const c=this.marshaller,y=n.getEventStreamMember(),d=n.getMemberSchema(y).getMemberSchemas(),f=Symbol("initialResponseMarker"),h=c.deserialize(i.body,async t=>{const e=Object.keys(t).find(l=>l!=="__type")??"",a=t[e].body;if(e==="initial-response"){const l=await this.deserializer.read(n,a);return delete l[y],{[f]:!0,...l}}else if(e in d){const l=d[e];if(l.isStructSchema()){const m={};let u=!1;for(const[s,g]of l.structIterator()){const{eventHeader:v,eventPayload:w}=g.getMergedTraits();if(u=u||!!(v||w),w)g.isBlobSchema()?m[s]=a:g.isStringSchema()?m[s]=(this.serdeContext?.utf8Encoder??z)(a):g.isStructSchema()&&(m[s]=await this.deserializer.read(g,a));else if(v){const b=t[e].headers[s]?.value;b!=null&&(g.isNumericSchema()?b&&typeof b=="object"&&"bytes"in b?m[s]=BigInt(b.toString()):m[s]=Number(b):m[s]=b)}}if(u)return{[e]:m};if(a.byteLength===0)return{[e]:{}}}return{[e]:await this.deserializer.read(l,a)}}else return{$unknown:t}}),S=h[Symbol.asyncIterator](),r=await S.next();if(r.done)return h;if(r.value?.[f]){if(!n)throw new Error("@smithy::core/protocols - initial-response event encountered in event stream but no response schema given.");for(const[t,e]of Object.entries(r.value))o[t]=e}return{async*[Symbol.asyncIterator](){for(r?.value?.[f]||(yield r.value);;){const{done:t,value:e}=await S.next();if(t)break;yield e}}}}writeEventBody(i,n,o){const c=this.serializer;let y=i,p=null,d;const f=n.getSchema()[4].includes(i),h={};if(f){const t=n.getMemberSchema(i);if(t.isStructSchema()){for(const[e,a]of t.structIterator()){const{eventHeader:l,eventPayload:m}=a.getMergedTraits();if(m)p=e;else if(l){const u=o[i][e];let s="binary";a.isNumericSchema()?(-2)**31<=u&&u<=2**31-1?s="integer":s="long":a.isTimestampSchema()?s="timestamp":a.isStringSchema()?s="string":a.isBooleanSchema()&&(s="boolean"),u!=null&&(h[e]={type:s,value:u},delete o[i][e])}}if(p!==null){const e=t.getMemberSchema(p);e.isBlobSchema()?d="application/octet-stream":e.isStringSchema()&&(d="text/plain"),c.write(e,o[i][p])}else c.write(t,o[i])}else if(t.isUnitSchema())c.write(t,{});else throw new Error("@smithy/core/event-streams - non-struct member not supported in event stream union.")}else{const[t,e]=o[i];y=t,c.write(15,e)}const S=c.flush();return{body:typeof S=="string"?(this.serdeContext?.utf8Decoder??x)(S):S,eventType:y,explicitPayloadContentType:d,additionalHeaders:h}}}export{T as EventStreamSerde};
|
||||
import{cK as z,cL as x}from"./index.js";class T{marshaller;serializer;deserializer;serdeContext;defaultContentType;constructor({marshaller:i,serializer:n,deserializer:o,serdeContext:c,defaultContentType:y}){this.marshaller=i,this.serializer=n,this.deserializer=o,this.serdeContext=c,this.defaultContentType=y}async serializeEventStream({eventStream:i,requestSchema:n,initialRequest:o}){const c=this.marshaller,y=n.getEventStreamMember(),p=n.getMemberSchema(y),d=this.serializer,f=this.defaultContentType,h=Symbol("initialRequestMarker"),S={async*[Symbol.asyncIterator](){if(o){const r={":event-type":{type:"string",value:"initial-request"},":message-type":{type:"string",value:"event"},":content-type":{type:"string",value:f}};d.write(n,o);const t=d.flush();yield{[h]:!0,headers:r,body:t}}for await(const r of i)yield r}};return c.serialize(S,r=>{if(r[h])return{headers:r.headers,body:r.body};const t=Object.keys(r).find(s=>s!=="__type")??"",{additionalHeaders:e,body:a,eventType:l,explicitPayloadContentType:m}=this.writeEventBody(t,p,r);return{headers:{":event-type":{type:"string",value:l},":message-type":{type:"string",value:"event"},":content-type":{type:"string",value:m??f},...e},body:a}})}async deserializeEventStream({response:i,responseSchema:n,initialResponseContainer:o}){const c=this.marshaller,y=n.getEventStreamMember(),d=n.getMemberSchema(y).getMemberSchemas(),f=Symbol("initialResponseMarker"),h=c.deserialize(i.body,async t=>{const e=Object.keys(t).find(l=>l!=="__type")??"",a=t[e].body;if(e==="initial-response"){const l=await this.deserializer.read(n,a);return delete l[y],{[f]:!0,...l}}else if(e in d){const l=d[e];if(l.isStructSchema()){const m={};let u=!1;for(const[s,g]of l.structIterator()){const{eventHeader:v,eventPayload:w}=g.getMergedTraits();if(u=u||!!(v||w),w)g.isBlobSchema()?m[s]=a:g.isStringSchema()?m[s]=(this.serdeContext?.utf8Encoder??z)(a):g.isStructSchema()&&(m[s]=await this.deserializer.read(g,a));else if(v){const b=t[e].headers[s]?.value;b!=null&&(g.isNumericSchema()?b&&typeof b=="object"&&"bytes"in b?m[s]=BigInt(b.toString()):m[s]=Number(b):m[s]=b)}}if(u)return{[e]:m};if(a.byteLength===0)return{[e]:{}}}return{[e]:await this.deserializer.read(l,a)}}else return{$unknown:t}}),S=h[Symbol.asyncIterator](),r=await S.next();if(r.done)return h;if(r.value?.[f]){if(!n)throw new Error("@smithy::core/protocols - initial-response event encountered in event stream but no response schema given.");for(const[t,e]of Object.entries(r.value))o[t]=e}return{async*[Symbol.asyncIterator](){for(r?.value?.[f]||(yield r.value);;){const{done:t,value:e}=await S.next();if(t)break;yield e}}}}writeEventBody(i,n,o){const c=this.serializer;let y=i,p=null,d;const f=n.getSchema()[4].includes(i),h={};if(f){const t=n.getMemberSchema(i);if(t.isStructSchema()){for(const[e,a]of t.structIterator()){const{eventHeader:l,eventPayload:m}=a.getMergedTraits();if(m)p=e;else if(l){const u=o[i][e];let s="binary";a.isNumericSchema()?(-2)**31<=u&&u<=2**31-1?s="integer":s="long":a.isTimestampSchema()?s="timestamp":a.isStringSchema()?s="string":a.isBooleanSchema()&&(s="boolean"),u!=null&&(h[e]={type:s,value:u},delete o[i][e])}}if(p!==null){const e=t.getMemberSchema(p);e.isBlobSchema()?d="application/octet-stream":e.isStringSchema()&&(d="text/plain"),c.write(e,o[i][p])}else c.write(t,o[i])}else if(t.isUnitSchema())c.write(t,{});else throw new Error("@smithy/core/event-streams - non-struct member not supported in event stream union.")}else{const[t,e]=o[i];y=t,c.write(15,e)}const S=c.flush();return{body:typeof S=="string"?(this.serdeContext?.utf8Decoder??x)(S):S,eventType:y,explicitPayloadContentType:d,additionalHeaders:h}}}export{T as EventStreamSerde};
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{c as b,az as y,j as s,aA as k,aB as N}from"./index.js";import{u as B}from"./useSuspenseQuery.js";import{B as O}from"./back-link.js";import{E as C}from"./error-banner.js";import{P as F}from"./page-heading.js";import{a as K}from"./s3.js";import{U as v,F as A,O as E}from"./upload-button.js";import"./alert-dialog.js";import"./download.js";import"./trash-2.js";function T(){const e=b.c(31),{bucket:t}=y.useParams();let x;e[0]!==t?(x=N(t),e[0]=t,e[1]=x):x=e[1];const{data:i}=B(x),r=K();let p;e[2]!==i.Contents?(p=i.Contents||[],e[2]=i.Contents,e[3]=p):p=e[3];const o=p;let h;e[4]!==i.CommonPrefixes?(h=i.CommonPrefixes||[],e[4]=i.CommonPrefixes,e[5]=h):h=e[5];const l=h;let g;e[6]===Symbol.for("react.memo_cache_sentinel")?(g=s.jsx(O,{to:"/s3/ls",children:"Back to buckets"}),e[6]=g):g=e[6];let a;e[7]!==t||e[8]!==r.isPending||e[9]!==r.mutateAsync?(a=s.jsx(v,{bucket:t,isPending:r.isPending,onUpload:r.mutateAsync}),e[7]=t,e[8]=r.isPending,e[9]=r.mutateAsync,e[10]=a):a=e[10];let m;e[11]!==t||e[12]!==a?(m=s.jsx(F,{actions:a,title:t}),e[11]=t,e[12]=a,e[13]=m):m=e[13];let c;e[14]!==r.error?(c=r.error&&s.jsx(C,{error:r.error,msg:"Failed to upload object"}),e[14]=r.error,e[15]=c):c=e[15];let u;e[16]!==t||e[17]!==l?(u=l.length>0&&s.jsxs("div",{className:"mb-6",children:[s.jsx("h2",{className:"mb-3 font-semibold text-lg",children:"Folders"}),s.jsx("div",{className:"space-y-2",children:l.map(n=>{if(!n.Prefix)return null;const P=k(n.Prefix);return s.jsx(A,{bucketName:t,folderName:P,fullPrefix:n.Prefix},n.Prefix)})})]}),e[16]=t,e[17]=l,e[18]=u):u=e[18];let f;e[19]!==t||e[20]!==o?(f=o.length>0&&s.jsxs("div",{className:"space-y-2",children:[s.jsx("h2",{className:"mb-3 font-semibold text-lg",children:"Objects"}),o.map(n=>n.Key?s.jsx(E,{bucketName:t,displayName:n.Key,fullKey:n.Key,object:n},n.Key):null)]}),e[19]=t,e[20]=o,e[21]=f):f=e[21];let d;e[22]!==l.length||e[23]!==o.length?(d=o.length===0&&l.length===0&&s.jsx("p",{className:"text-muted-foreground",children:"No objects found in this bucket."}),e[22]=l.length,e[23]=o.length,e[24]=d):d=e[24];let j;return e[25]!==m||e[26]!==c||e[27]!==u||e[28]!==f||e[29]!==d?(j=s.jsxs(s.Fragment,{children:[g,m,c,u,f,d]}),e[25]=m,e[26]=c,e[27]=u,e[28]=f,e[29]=d,e[30]=j):j=e[30],j}export{T as component};
|
||||
import{c as b,q as y,j as s,t as k,v as N}from"./index.js";import{u as B}from"./useSuspenseQuery.js";import{B as O}from"./back-link.js";import{E as C}from"./error-banner.js";import{P as v}from"./page-heading.js";import{a as F}from"./s3.js";import{U as K,F as E,O as U}from"./upload-button.js";import"./alert-dialog.js";import"./download.js";import"./trash-2.js";function T(){const e=b.c(31),{bucket:t}=y.useParams();let x;e[0]!==t?(x=N(t),e[0]=t,e[1]=x):x=e[1];const{data:i}=B(x),r=F();let p;e[2]!==i.Contents?(p=i.Contents||[],e[2]=i.Contents,e[3]=p):p=e[3];const o=p;let h;e[4]!==i.CommonPrefixes?(h=i.CommonPrefixes||[],e[4]=i.CommonPrefixes,e[5]=h):h=e[5];const l=h;let g;e[6]===Symbol.for("react.memo_cache_sentinel")?(g=s.jsx(O,{to:"/s3/ls",children:"Back to buckets"}),e[6]=g):g=e[6];let a;e[7]!==t||e[8]!==r.isPending||e[9]!==r.mutateAsync?(a=s.jsx(K,{bucket:t,isPending:r.isPending,onUpload:r.mutateAsync}),e[7]=t,e[8]=r.isPending,e[9]=r.mutateAsync,e[10]=a):a=e[10];let m;e[11]!==t||e[12]!==a?(m=s.jsx(v,{actions:a,title:t}),e[11]=t,e[12]=a,e[13]=m):m=e[13];let c;e[14]!==r.error?(c=r.error&&s.jsx(C,{error:r.error,msg:"Failed to upload object"}),e[14]=r.error,e[15]=c):c=e[15];let u;e[16]!==t||e[17]!==l?(u=l.length>0&&s.jsxs("div",{className:"mb-6",children:[s.jsx("h2",{className:"mb-3 font-semibold text-lg",children:"Folders"}),s.jsx("div",{className:"space-y-2",children:l.map(n=>{if(!n.Prefix)return null;const P=k(n.Prefix);return s.jsx(E,{bucketName:t,folderName:P,fullPrefix:n.Prefix},n.Prefix)})})]}),e[16]=t,e[17]=l,e[18]=u):u=e[18];let f;e[19]!==t||e[20]!==o?(f=o.length>0&&s.jsxs("div",{className:"space-y-2",children:[s.jsx("h2",{className:"mb-3 font-semibold text-lg",children:"Objects"}),o.map(n=>n.Key?s.jsx(U,{bucketName:t,displayName:n.Key,fullKey:n.Key,object:n},n.Key):null)]}),e[19]=t,e[20]=o,e[21]=f):f=e[21];let d;e[22]!==l.length||e[23]!==o.length?(d=o.length===0&&l.length===0&&s.jsx("p",{className:"text-muted-foreground",children:"No objects found in this bucket."}),e[22]=l.length,e[23]=o.length,e[24]=d):d=e[24];let j;return e[25]!==m||e[26]!==c||e[27]!==u||e[28]!==f||e[29]!==d?(j=s.jsxs(s.Fragment,{children:[g,m,c,u,f,d]}),e[25]=m,e[26]=c,e[27]=u,e[28]=f,e[29]=d,e[30]=j):j=e[30],j}export{T as component};
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{c as l,aC as n,j as t,L as m,B as u}from"./index.js";import{u as c}from"./useSuspenseQuery.js";import{L as d}from"./list-card.js";import{P as p}from"./page-heading.js";import{B as f}from"./badge.js";function b(){const e=l.c(5),{data:r}=c(n);let s;e[0]!==r.Volumes?(s=r.Volumes||[],e[0]=r.Volumes,e[1]=s):s=e[1];const o=s;let a;e[2]===Symbol.for("react.memo_cache_sentinel")?(a=t.jsx(p,{actions:t.jsx(m,{to:"/ec2/create-volume",children:t.jsx(u,{children:"Create Volume"})}),title:"EC2 Volumes"}),e[2]=a):a=e[2];let i;return e[3]!==o?(i=t.jsxs(t.Fragment,{children:[a,o.length>0?t.jsx("div",{className:"space-y-4",children:o.map(x)}):t.jsx("p",{className:"text-muted-foreground",children:"No volumes found."})]}),e[3]=o,e[4]=i):i=e[4],i}function x(e){return e.VolumeId?t.jsx(d,{badge:e.State?t.jsx(f,{variant:e.State==="available"?"default":"outline",children:e.State}):void 0,params:{id:e.VolumeId},subtitle:`${e.Size} GiB • ${e.VolumeType}`,title:e.VolumeId,to:"/ec2/describe-volumes/$id"},e.VolumeId):null}export{b as component};
|
||||
import{c,h as i,j as t,L as m,B as d}from"./index.js";import{u as l}from"./useSuspenseQuery.js";import{L as p}from"./list-card.js";import{P as u}from"./page-heading.js";import{S as f}from"./state-badge.js";function I(){const e=c.c(5),{data:s}=l(i);let r;e[0]!==s.Vpcs?(r=s.Vpcs||[],e[0]=s.Vpcs,e[1]=r):r=e[1];const a=r;let n;e[2]===Symbol.for("react.memo_cache_sentinel")?(n=t.jsx(u,{actions:t.jsx(m,{to:"/ec2/create-vpc",children:t.jsx(d,{children:"Create VPC"})}),title:"VPCs"}),e[2]=n):n=e[2];let o;return e[3]!==a?(o=t.jsxs(t.Fragment,{children:[n,a.length>0?t.jsx("div",{className:"space-y-4",children:a.map(V)}):t.jsx("p",{className:"text-muted-foreground",children:"No VPCs found."})]}),e[3]=a,e[4]=o):o=e[4],o}function V(e){if(!e.VpcId)return null;const s=e.Tags?.find(x)?.Value;return t.jsx(p,{badge:t.jsx(f,{state:e.State}),params:{id:e.VpcId},subtitle:e.CidrBlock??"",title:s?`${e.VpcId} (${s})`:e.VpcId,to:"/ec2/describe-vpcs/$id"},e.VpcId)}function x(e){return e.Key==="Name"}export{I as component};
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{c as m,ax as l,j as t,L as n,B as c}from"./index.js";import{u as p}from"./useSuspenseQuery.js";import{L as d}from"./list-card.js";import{P as y}from"./page-heading.js";function P(){const e=m.c(4),{data:r}=p(l);let s;if(e[0]!==r.KeyPairs){const a=[...r.KeyPairs||[]].sort(x);let i;e[2]===Symbol.for("react.memo_cache_sentinel")?(i=t.jsx(n,{to:"/ec2/import-key-pair",children:t.jsx(c,{variant:"outline",children:"Import Key Pair"})}),e[2]=i):i=e[2];let o;e[3]===Symbol.for("react.memo_cache_sentinel")?(o=t.jsx(y,{actions:t.jsxs("div",{className:"flex gap-2",children:[i,t.jsx(n,{to:"/ec2/create-key-pair",children:t.jsx(c,{children:"Create Key Pair"})})]}),title:"EC2 Key Pairs"}),e[3]=o):o=e[3],s=t.jsxs(t.Fragment,{children:[o,a.length>0?t.jsx("div",{className:"space-y-4",children:a.map(u)}):t.jsx("p",{className:"text-muted-foreground",children:"No key pairs found."})]}),e[0]=r.KeyPairs,e[1]=s}else s=e[1];return s}function u(e){return e.KeyPairId?t.jsx(d,{params:{id:e.KeyPairId},subtitle:`${e.KeyPairId} • ${e.KeyType}`,title:e.KeyName??"",to:"/ec2/describe-key-pairs/$id"},e.KeyPairId):null}function x(e,r){const s=e.KeyName?.toLowerCase()??"",a=r.KeyName?.toLowerCase()??"";return s.localeCompare(a)}export{P as component};
|
||||
import{c as m,i as l,j as t,L as n,B as u}from"./index.js";import{u as c}from"./useSuspenseQuery.js";import{L as d}from"./list-card.js";import{P as p}from"./page-heading.js";import{S as f}from"./state-badge.js";function y(){const e=m.c(5),{data:a}=c(l);let s;e[0]!==a.Volumes?(s=a.Volumes||[],e[0]=a.Volumes,e[1]=s):s=e[1];const o=s;let r;e[2]===Symbol.for("react.memo_cache_sentinel")?(r=t.jsx(p,{actions:t.jsx(n,{to:"/ec2/create-volume",children:t.jsx(u,{children:"Create Volume"})}),title:"EC2 Volumes"}),e[2]=r):r=e[2];let i;return e[3]!==o?(i=t.jsxs(t.Fragment,{children:[r,o.length>0?t.jsx("div",{className:"space-y-4",children:o.map(x)}):t.jsx("p",{className:"text-muted-foreground",children:"No volumes found."})]}),e[3]=o,e[4]=i):i=e[4],i}function x(e){return e.VolumeId?t.jsx(d,{badge:t.jsx(f,{state:e.State}),params:{id:e.VolumeId},subtitle:`${e.Size} GiB • ${e.VolumeType}`,title:e.VolumeId,to:"/ec2/describe-volumes/$id"},e.VolumeId):null}export{y as component};
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{c as o,aD as c,j as t,L as i,B as m}from"./index.js";import{u as d}from"./useSuspenseQuery.js";import{L as u}from"./list-card.js";import{P as l}from"./page-heading.js";import{S as p}from"./state-badge.js";function y(){const e=o.c(3),{data:a}=d(c);let s;if(e[0]!==a.Reservations){const r=a.Reservations?.flatMap(I)||[];let n;e[2]===Symbol.for("react.memo_cache_sentinel")?(n=t.jsx(l,{actions:t.jsx(i,{to:"/ec2/run-instances",children:t.jsx(m,{children:"Run Instances"})}),title:"EC2 Instances"}),e[2]=n):n=e[2],s=t.jsxs(t.Fragment,{children:[n,r.length>0?t.jsx("div",{className:"space-y-4",children:r.map(f)}):t.jsx("p",{className:"text-muted-foreground",children:"No instances found."})]}),e[0]=a.Reservations,e[1]=s}else s=e[1];return s}function f(e){return e.InstanceId?t.jsx(u,{badge:t.jsx(p,{state:e.State?.Name}),params:{id:e.InstanceId},subtitle:`${e.InstanceType} • ${e.ImageId}`,title:e.InstanceId,to:"/ec2/describe-instances/$id"},e.InstanceId):null}function I(e){return e.Instances||[]}export{y as component};
|
||||
import{c as o,p as c,j as t,L as u,B as m}from"./index.js";import{u as l}from"./useSuspenseQuery.js";import{L as d}from"./list-card.js";import{P as p}from"./page-heading.js";import{S}from"./state-badge.js";function $(){const e=o.c(5),{data:s}=l(c);let n;e[0]!==s.Subnets?(n=s.Subnets||[],e[0]=s.Subnets,e[1]=n):n=e[1];const a=n;let r;e[2]===Symbol.for("react.memo_cache_sentinel")?(r=t.jsx(p,{actions:t.jsx(u,{to:"/ec2/create-subnet",children:t.jsx(m,{children:"Create Subnet"})}),title:"Subnets"}),e[2]=r):r=e[2];let i;return e[3]!==a?(i=t.jsxs(t.Fragment,{children:[r,a.length>0?t.jsx("div",{className:"space-y-4",children:a.map(f)}):t.jsx("p",{className:"text-muted-foreground",children:"No subnets found."})]}),e[3]=a,e[4]=i):i=e[4],i}function f(e){if(!e.SubnetId)return null;const s=e.Tags?.find(b)?.Value;return t.jsx(d,{badge:t.jsx(S,{state:e.State}),params:{id:e.SubnetId},subtitle:`${e.CidrBlock??""} • ${e.AvailabilityZone??""}`,title:s?`${e.SubnetId} (${s})`:e.SubnetId,to:"/ec2/describe-subnets/$id"},e.SubnetId)}function b(e){return e.Key==="Name"}export{$ as component};
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{c as n,aw as i,j as t}from"./index.js";import{u as l}from"./useSuspenseQuery.js";import{L as c}from"./list-card.js";import{P as d}from"./page-heading.js";import{B as u}from"./badge.js";function h(){const e=n.c(5),{data:m}=l(i);let s;e[0]!==m.Images?(s=m.Images||[],e[0]=m.Images,e[1]=s):s=e[1];const a=s;let r;e[2]===Symbol.for("react.memo_cache_sentinel")?(r=t.jsx(d,{title:"EC2 Images"}),e[2]=r):r=e[2];let o;return e[3]!==a?(o=t.jsxs(t.Fragment,{children:[r,a.length>0?t.jsx("div",{className:"space-y-4",children:a.map(p)}):t.jsx("p",{className:"text-muted-foreground",children:"No images found."})]}),e[3]=a,e[4]=o):o=e[4],o}function p(e){return e.ImageId?t.jsx(c,{badge:e.State?t.jsx(u,{variant:e.State==="available"?"default":"outline",children:e.State}):void 0,params:{id:e.ImageId},subtitle:e.Name,title:e.ImageId,to:"/ec2/describe-images/$id"},e.ImageId):null}export{h as component};
|
||||
import{c as i,w as c,j as e,L as m,B as p}from"./index.js";import{u as l}from"./useSuspenseQuery.js";import{L as d}from"./list-card.js";import{P as u}from"./page-heading.js";import{S}from"./state-badge.js";function b(){const t=i.c(5),{data:r}=l(c);let s;t[0]!==r.Snapshots?(s=r.Snapshots||[],t[0]=r.Snapshots,t[1]=s):s=t[1];const o=s;let a;t[2]===Symbol.for("react.memo_cache_sentinel")?(a=e.jsx(u,{actions:e.jsx(m,{to:"/ec2/create-snapshot",children:e.jsx(p,{children:"Create Snapshot"})}),title:"EC2 Snapshots"}),t[2]=a):a=t[2];let n;return t[3]!==o?(n=e.jsxs(e.Fragment,{children:[a,o.length>0?e.jsx("div",{className:"space-y-4",children:o.map(h)}):e.jsx("p",{className:"text-muted-foreground",children:"No snapshots found."})]}),t[3]=o,t[4]=n):n=t[4],n}function h(t){return t.SnapshotId?e.jsx(d,{badge:e.jsx(S,{state:t.State}),params:{id:t.SnapshotId},subtitle:`${t.VolumeSize} GiB • ${t.VolumeId}`,title:t.SnapshotId,to:"/ec2/describe-snapshots/$id"},t.SnapshotId):null}export{b as component};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
import{r as b,a_ as h,b2 as U,b3 as $,bv as A,aT as K,bt as S,j as F,c as _,a as q}from"./index.js";import{d as z,h as G,e as H,f as J,g as O,i as Q}from"./field.js";const W=b.forwardRef(function(e,a){const{render:r,className:l,id:i,name:o,value:s,disabled:I=!1,onValueChange:w,defaultValue:v,autoFocus:f=!1,...V}=e,{state:T,name:D,disabled:N,setTouched:g,setDirty:P,validityData:j,setFocused:d,setFilled:c,validationMode:k,validation:n}=z(),x=N||I,R=D??o,L={...T,disabled:x},{labelId:M}=G(),y=H({id:i});h(()=>{const t=s!=null;n.inputRef.current?.value||t&&s!==""?c(!0):t&&s===""&&c(!1)},[n.inputRef,c,s]);const p=b.useRef(null);h(()=>{f&&p.current===U($(p.current))&&d(!0)},[f,d]);const[B]=J({controlled:s,default:v,name:"FieldControl",state:"value"}),C=s!==void 0,E=C?B:void 0;return O({id:y,name:R,commit:n.commit,value:E,getValue:()=>n.inputRef.current?.value,controlRef:n.inputRef}),A("input",e,{ref:[a,p],state:L,props:[{id:y,disabled:x,name:R,ref:n.inputRef,"aria-labelledby":M,autoFocus:f,...C?{value:E}:{defaultValue:v},onChange(t){const m=t.currentTarget.value;w?.(m,K(S,t.nativeEvent)),P(m!==j.initialValue),c(m!=="")},onFocus(){d(!0)},onBlur(t){g(!0),d(!1),k==="onBlur"&&n.commit(t.currentTarget.value)},onKeyDown(t){t.currentTarget.tagName==="INPUT"&&t.key==="Enter"&&(g(!0),n.commit(t.currentTarget.value))}},n.getInputValidationProps(),V],stateAttributesMapping:Q})}),X=b.forwardRef(function(e,a){return F.jsx(W,{ref:a,...e})});function te(u){const e=_.c(10);let a,r,l;e[0]!==u?({className:a,type:l,...r}=u,e[0]=u,e[1]=a,e[2]=r,e[3]=l):(a=e[1],r=e[2],l=e[3]);let i;e[4]!==a?(i=q("h-7 w-full min-w-0 rounded-md border border-input bg-input/20 px-2 py-0.5 text-sm outline-none transition-colors file:inline-flex file:h-6 file:border-0 file:bg-transparent file:font-medium file:text-foreground file:text-xs/relaxed placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 md:text-xs/relaxed dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",a),e[4]=a,e[5]=i):i=e[5];let o;return e[6]!==r||e[7]!==i||e[8]!==l?(o=F.jsx(X,{className:i,"data-slot":"input",type:l,...r}),e[6]=r,e[7]=i,e[8]=l,e[9]=o):o=e[9],o}export{te as I};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{c as b,a as K,j as c,u as P,b as R,s as L,B as X}from"./index.js";import{a as B,u as G,I as F,F as z,b as $,c as D,d as O}from"./field.js";function T(e){const s=b.c(10);let l,t,r;s[0]!==e?({className:l,size:r,...t}=e,s[0]=e,s[1]=l,s[2]=t,s[3]=r):(l=s[1],t=s[2],r=s[3]);const a=r===void 0?"default":r;let i;s[4]!==l?(i=K("group/card flex flex-col gap-4 overflow-hidden rounded-lg bg-card py-4 text-card-foreground text-xs/relaxed ring-1 ring-foreground/10 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg",l),s[4]=l,s[5]=i):i=s[5];let o;return s[6]!==t||s[7]!==a||s[8]!==i?(o=c.jsx("div",{className:i,"data-size":a,"data-slot":"card",...t}),s[6]=t,s[7]=a,s[8]=i,s[9]=o):o=s[9],o}function Y(e){const s=b.c(8);let l,t;s[0]!==e?({className:l,...t}=e,s[0]=e,s[1]=l,s[2]=t):(l=s[1],t=s[2]);let r;s[3]!==l?(r=K("group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-lg px-4 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",l),s[3]=l,s[4]=r):r=s[4];let a;return s[5]!==t||s[6]!==r?(a=c.jsx("div",{className:r,"data-slot":"card-header",...t}),s[5]=t,s[6]=r,s[7]=a):a=s[7],a}function H(e){const s=b.c(8);let l,t;s[0]!==e?({className:l,...t}=e,s[0]=e,s[1]=l,s[2]=t):(l=s[1],t=s[2]);let r;s[3]!==l?(r=K("font-medium text-sm",l),s[3]=l,s[4]=r):r=s[4];let a;return s[5]!==t||s[6]!==r?(a=c.jsx("div",{className:r,"data-slot":"card-title",...t}),s[5]=t,s[6]=r,s[7]=a):a=s[7],a}function J(e){const s=b.c(8);let l,t;s[0]!==e?({className:l,...t}=e,s[0]=e,s[1]=l,s[2]=t):(l=s[1],t=s[2]);let r;s[3]!==l?(r=K("text-muted-foreground text-xs/relaxed",l),s[3]=l,s[4]=r):r=s[4];let a;return s[5]!==t||s[6]!==r?(a=c.jsx("div",{className:r,"data-slot":"card-description",...t}),s[5]=t,s[6]=r,s[7]=a):a=s[7],a}function U(e){const s=b.c(8);let l,t;s[0]!==e?({className:l,...t}=e,s[0]=e,s[1]=l,s[2]=t):(l=s[1],t=s[2]);let r;s[3]!==l?(r=K("px-4 group-data-[size=sm]/card:px-3",l),s[3]=l,s[4]=r):r=s[4];let a;return s[5]!==t||s[6]!==r?(a=c.jsx("div",{className:r,"data-slot":"card-content",...t}),s[5]=t,s[6]=r,s[7]=a):a=s[7],a}function Q(){const e=b.c(39),s=P();let l;e[0]===Symbol.for("react.memo_cache_sentinel")?(l={resolver:B(R)},e[0]=l):l=e[0];const{register:t,handleSubmit:r,formState:a}=G(l),{errors:i,isSubmitting:o}=a;let N;e[1]!==s?(N=function(M){L(M),s({to:"/"})},e[1]=s,e[2]=N):N=e[2];const _=N;let S;e[3]===Symbol.for("react.memo_cache_sentinel")?(S=c.jsx(H,{children:"AWS Credentials"}),e[3]=S):S=e[3];let v;e[4]===Symbol.for("react.memo_cache_sentinel")?(v=c.jsxs(Y,{children:[S,c.jsxs(J,{children:["Run ",c.jsx("code",{children:"cat ~/hive/config/hive.toml"})," to get your credentials"]})]}),e[4]=v):v=e[4];let d;e[5]!==r||e[6]!==_?(d=r(_),e[5]=r,e[6]=_,e[7]=d):d=e[7];let A;e[8]===Symbol.for("react.memo_cache_sentinel")?(A=c.jsx($,{children:c.jsx("label",{htmlFor:"accessKeyId",children:"Access Key ID"})}),e[8]=A):A=e[8];const w=!!i.accessKeyId;let n;e[9]!==t?(n=t("accessKeyId"),e[9]=t,e[10]=n):n=e[10];let m;e[11]!==w||e[12]!==n?(m=c.jsx(F,{"aria-invalid":w,autoComplete:"username",id:"accessKeyId",placeholder:"AKIAIOSFODNN7EXAMPLE",...n}),e[11]=w,e[12]=n,e[13]=m):m=e[13];let f;e[14]!==i.accessKeyId?(f=c.jsx(z,{errors:[i.accessKeyId]}),e[14]=i.accessKeyId,e[15]=f):f=e[15];let u;e[16]!==f||e[17]!==m?(u=c.jsxs(D,{children:[A,m,f]}),e[16]=f,e[17]=m,e[18]=u):u=e[18];let I;e[19]===Symbol.for("react.memo_cache_sentinel")?(I=c.jsx($,{children:c.jsx("label",{htmlFor:"secretAccessKey",children:"Secret Access Key"})}),e[19]=I):I=e[19];const E=!!i.secretAccessKey;let x;e[20]!==t?(x=t("secretAccessKey"),e[20]=t,e[21]=x):x=e[21];let p;e[22]!==E||e[23]!==x?(p=c.jsx(F,{"aria-invalid":E,autoComplete:"current-password",id:"secretAccessKey",placeholder:"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",type:"password",...x}),e[22]=E,e[23]=x,e[24]=p):p=e[24];let h;e[25]!==i.secretAccessKey?(h=c.jsx(z,{errors:[i.secretAccessKey]}),e[25]=i.secretAccessKey,e[26]=h):h=e[26];let g;e[27]!==p||e[28]!==h?(g=c.jsxs(D,{children:[I,p,h]}),e[27]=p,e[28]=h,e[29]=g):g=e[29];let j;e[30]!==o?(j=c.jsx(X,{className:"w-full",disabled:o,type:"submit",children:"Sign In"}),e[30]=o,e[31]=j):j=e[31];let y;e[32]!==u||e[33]!==g||e[34]!==j?(y=c.jsxs(O,{children:[u,g,j]}),e[32]=u,e[33]=g,e[34]=j,e[35]=y):y=e[35];let C;return e[36]!==y||e[37]!==d?(C=c.jsx("div",{className:"flex flex-1 items-center justify-center",children:c.jsxs(T,{className:"w-full max-w-sm",children:[v,c.jsx(U,{children:c.jsx("form",{onSubmit:d,children:y})})]})}),e[36]=y,e[37]=d,e[38]=C):C=e[38],C}export{Q as component};
|
||||
import{c as b,a as K,j as c,u as P,b as R,s as L,B as X}from"./index.js";import{a as B,u as G}from"./zod.js";import{F,a as z,b as $,c as O}from"./field.js";import{I as D}from"./input.js";function T(e){const s=b.c(10);let l,t,r;s[0]!==e?({className:l,size:r,...t}=e,s[0]=e,s[1]=l,s[2]=t,s[3]=r):(l=s[1],t=s[2],r=s[3]);const a=r===void 0?"default":r;let i;s[4]!==l?(i=K("group/card flex flex-col gap-4 overflow-hidden rounded-lg bg-card py-4 text-card-foreground text-xs/relaxed ring-1 ring-foreground/10 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg",l),s[4]=l,s[5]=i):i=s[5];let o;return s[6]!==t||s[7]!==a||s[8]!==i?(o=c.jsx("div",{className:i,"data-size":a,"data-slot":"card",...t}),s[6]=t,s[7]=a,s[8]=i,s[9]=o):o=s[9],o}function Y(e){const s=b.c(8);let l,t;s[0]!==e?({className:l,...t}=e,s[0]=e,s[1]=l,s[2]=t):(l=s[1],t=s[2]);let r;s[3]!==l?(r=K("group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-lg px-4 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",l),s[3]=l,s[4]=r):r=s[4];let a;return s[5]!==t||s[6]!==r?(a=c.jsx("div",{className:r,"data-slot":"card-header",...t}),s[5]=t,s[6]=r,s[7]=a):a=s[7],a}function H(e){const s=b.c(8);let l,t;s[0]!==e?({className:l,...t}=e,s[0]=e,s[1]=l,s[2]=t):(l=s[1],t=s[2]);let r;s[3]!==l?(r=K("font-medium text-sm",l),s[3]=l,s[4]=r):r=s[4];let a;return s[5]!==t||s[6]!==r?(a=c.jsx("div",{className:r,"data-slot":"card-title",...t}),s[5]=t,s[6]=r,s[7]=a):a=s[7],a}function J(e){const s=b.c(8);let l,t;s[0]!==e?({className:l,...t}=e,s[0]=e,s[1]=l,s[2]=t):(l=s[1],t=s[2]);let r;s[3]!==l?(r=K("text-muted-foreground text-xs/relaxed",l),s[3]=l,s[4]=r):r=s[4];let a;return s[5]!==t||s[6]!==r?(a=c.jsx("div",{className:r,"data-slot":"card-description",...t}),s[5]=t,s[6]=r,s[7]=a):a=s[7],a}function U(e){const s=b.c(8);let l,t;s[0]!==e?({className:l,...t}=e,s[0]=e,s[1]=l,s[2]=t):(l=s[1],t=s[2]);let r;s[3]!==l?(r=K("px-4 group-data-[size=sm]/card:px-3",l),s[3]=l,s[4]=r):r=s[4];let a;return s[5]!==t||s[6]!==r?(a=c.jsx("div",{className:r,"data-slot":"card-content",...t}),s[5]=t,s[6]=r,s[7]=a):a=s[7],a}function Z(){const e=b.c(39),s=P();let l;e[0]===Symbol.for("react.memo_cache_sentinel")?(l={resolver:B(R)},e[0]=l):l=e[0];const{register:t,handleSubmit:r,formState:a}=G(l),{errors:i,isSubmitting:o}=a;let N;e[1]!==s?(N=function(M){L(M),s({to:"/"})},e[1]=s,e[2]=N):N=e[2];const _=N;let S;e[3]===Symbol.for("react.memo_cache_sentinel")?(S=c.jsx(H,{children:"AWS Credentials"}),e[3]=S):S=e[3];let v;e[4]===Symbol.for("react.memo_cache_sentinel")?(v=c.jsxs(Y,{children:[S,c.jsxs(J,{children:["Run ",c.jsx("code",{children:"cat ~/hive/config/hive.toml"})," to get your credentials"]})]}),e[4]=v):v=e[4];let d;e[5]!==r||e[6]!==_?(d=r(_),e[5]=r,e[6]=_,e[7]=d):d=e[7];let A;e[8]===Symbol.for("react.memo_cache_sentinel")?(A=c.jsx(z,{children:c.jsx("label",{htmlFor:"accessKeyId",children:"Access Key ID"})}),e[8]=A):A=e[8];const w=!!i.accessKeyId;let n;e[9]!==t?(n=t("accessKeyId"),e[9]=t,e[10]=n):n=e[10];let m;e[11]!==w||e[12]!==n?(m=c.jsx(D,{"aria-invalid":w,autoComplete:"username",id:"accessKeyId",placeholder:"AKIAIOSFODNN7EXAMPLE",...n}),e[11]=w,e[12]=n,e[13]=m):m=e[13];let f;e[14]!==i.accessKeyId?(f=c.jsx(F,{errors:[i.accessKeyId]}),e[14]=i.accessKeyId,e[15]=f):f=e[15];let u;e[16]!==f||e[17]!==m?(u=c.jsxs($,{children:[A,m,f]}),e[16]=f,e[17]=m,e[18]=u):u=e[18];let I;e[19]===Symbol.for("react.memo_cache_sentinel")?(I=c.jsx(z,{children:c.jsx("label",{htmlFor:"secretAccessKey",children:"Secret Access Key"})}),e[19]=I):I=e[19];const E=!!i.secretAccessKey;let p;e[20]!==t?(p=t("secretAccessKey"),e[20]=t,e[21]=p):p=e[21];let x;e[22]!==E||e[23]!==p?(x=c.jsx(D,{"aria-invalid":E,autoComplete:"current-password",id:"secretAccessKey",placeholder:"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",type:"password",...p}),e[22]=E,e[23]=p,e[24]=x):x=e[24];let h;e[25]!==i.secretAccessKey?(h=c.jsx(F,{errors:[i.secretAccessKey]}),e[25]=i.secretAccessKey,e[26]=h):h=e[26];let g;e[27]!==x||e[28]!==h?(g=c.jsxs($,{children:[I,x,h]}),e[27]=x,e[28]=h,e[29]=g):g=e[29];let j;e[30]!==o?(j=c.jsx(X,{className:"w-full",disabled:o,type:"submit",children:"Sign In"}),e[30]=o,e[31]=j):j=e[31];let y;e[32]!==u||e[33]!==g||e[34]!==j?(y=c.jsxs(O,{children:[u,g,j]}),e[32]=u,e[33]=g,e[34]=j,e[35]=y):y=e[35];let C;return e[36]!==y||e[37]!==d?(C=c.jsx("div",{className:"flex flex-1 items-center justify-center",children:c.jsxs(T,{className:"w-full max-w-sm",children:[v,c.jsx(U,{children:c.jsx("form",{onSubmit:d,children:y})})]})}),e[36]=y,e[37]=d,e[38]=C):C=e[38],C}export{Z as component};
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{c as d,j as g}from"./index.js";function o(n){const e=d.c(3),{state:t}=n;let a="bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-100";t==="running"?a="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100":t==="stopped"&&(a="bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-100");const s=`rounded-full px-2 py-1 text-xs ${a}`;let r;return e[0]!==t||e[1]!==s?(r=g.jsx("div",{className:s,children:t}),e[0]=t,e[1]=s,e[2]=r):r=e[2],r}export{o as S};
|
||||
import{c as g,j as l}from"./index.js";function i(d){const t=g.c(3),{state:e}=d;let r="bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-100";e==="running"||e==="available"||e==="completed"?r="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100":e==="stopped"||e==="error"?r="bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-100":e==="pending"||e==="shutting-down"||e==="stopping"?r="bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100":e==="terminated"&&(r="bg-zinc-100 text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400");const n=`rounded-full px-2 py-1 text-xs ${r}`;let a;return t[0]!==e||t[1]!==n?(a=l.jsx("div",{className:n,children:e}),t[0]=e,t[1]=n,t[2]=a):a=t[2],a}export{i as S};
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{g as a}from"./index.js";const t=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],o=a("trash-2",t);export{o as T};
|
||||
import{k as a}from"./index.js";const t=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],o=a("trash-2",t);export{o as T};
|
||||
|
||||
@@ -1 +1 @@
|
||||
import{g as u,aI as g,aJ as j,aK as b,aL as y,aM as k,aN as C,c as w,j as e,L as v,r as f,aO as N,aP as D,B as A}from"./index.js";import{E as M}from"./error-banner.js";import{g as S,f as L,b as R,a as E,c as O,d as z,e as B,h as $}from"./alert-dialog.js";import{g as F,b as P,c as U}from"./s3.js";import{D as _}from"./download.js";import{T}from"./trash-2.js";const I=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",key:"1oefj6"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5",key:"wfsgrz"}]],H=u("file",I);const K=[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z",key:"1kt360"}]],q=u("folder",K);const G=[["path",{d:"M12 3v12",key:"1x0j5s"}],["path",{d:"m17 8-5-5-5 5",key:"7q97r8"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}]],V=u("upload",G),W=l=>(t,a)=>async r=>{const n=await t(r),{response:s}=n;if(g.isInstance(s)&&s.headers.expires){s.headers.expiresstring=s.headers.expires;try{j(s.headers.expires)}catch(o){a.logger?.warn(`AWS SDK Warning for ${a.clientName}::${a.commandName} response parsing (${s.headers.expires}): ${o}`),delete s.headers.expires}}return n},J={tags:["S3"],name:"s3ExpiresMiddleware",override:!0,relation:"after",toMiddleware:"deserializerMiddleware"},Z=l=>({applyToStack:t=>{t.addRelativeTo(W(),J)}});class Q extends b.classBuilder().ep({...y,Bucket:{type:"contextParams",name:"Bucket"},Key:{type:"contextParams",name:"Key"}}).m(function(t,a,r,n){return[k(r,t.getEndpointParameterInstructions()),F(r,{requestChecksumRequired:!1,requestValidationModeMember:"ChecksumMode",responseAlgorithms:["CRC64NVME","CRC32","CRC32C","SHA256","SHA1"]}),P(r),Z()]}).s("AmazonS3","GetObject",{}).n("S3Client","GetObjectCommand").sc(C).build(){}function re(l){const t=w.c(9),{folderName:a,fullPrefix:r,bucketName:n}=l;let s;t[0]!==n||t[1]!==r?(s={bucket:n,_splat:r},t[0]=n,t[1]=r,t[2]=s):s=t[2];let o;t[3]===Symbol.for("react.memo_cache_sentinel")?(o=e.jsx(q,{className:"size-5 text-muted-foreground"}),t[3]=o):o=t[3];let i;t[4]!==a?(i=e.jsx("span",{className:"font-medium",children:a}),t[4]=a,t[5]=i):i=t[5];let c;return t[6]!==s||t[7]!==i?(c=e.jsxs(v,{className:"flex items-center gap-3 rounded-lg border bg-card p-4 transition-colors hover:bg-accent",params:s,to:"/s3/ls/$bucket/$",children:[o,i]}),t[6]=s,t[7]=i,t[8]=c):c=t[8],c}function ne({object:l,bucketName:t,displayName:a,fullKey:r}){const[n,s]=f.useState(!1),o=U();async function i(){try{const m=new Q({Bucket:t,Key:r}),p=await D().send(m);if(p.Body){const x=await p.Body.transformToByteArray(),h=URL.createObjectURL(new Blob([x])),d=document.createElement("a");d.href=h,d.download=a,document.body.appendChild(d),d.click(),document.body.removeChild(d),URL.revokeObjectURL(h)}}catch{throw new Error("Failed to download object")}}async function c(){try{await o.mutateAsync({bucket:t,key:r})}finally{s(!1)}}return e.jsxs("div",{children:[o.error&&e.jsx(M,{error:o.error,msg:"Failed to delete object"}),e.jsxs("div",{className:"flex items-center justify-between rounded-lg border bg-card p-4",children:[e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsx(H,{className:"size-5 text-muted-foreground"}),e.jsxs("div",{children:[e.jsx("h3",{className:"font-medium",children:a}),l.LastModified&&e.jsxs("p",{className:"text-muted-foreground text-sm",children:["Last Modified: ",l.LastModified.toLocaleString()]})]})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsx("div",{className:"text-muted-foreground text-sm",children:N(l.Size||0)}),e.jsx("button",{className:"rounded-md p-2 transition-colors hover:bg-accent",onClick:i,type:"button",children:e.jsx(_,{className:"size-4"})}),e.jsx("button",{className:"rounded-md p-2 text-destructive transition-colors hover:bg-accent",onClick:()=>s(!0),type:"button",children:e.jsx(T,{className:"size-4"})})]}),e.jsx(S,{onOpenChange:s,open:n,children:e.jsxs(L,{children:[e.jsxs(R,{children:[e.jsx(E,{children:"Delete Object"}),e.jsxs(O,{children:["Are you sure you want to delete ",e.jsx("strong",{children:a}),"? This action cannot be undone."]})]}),e.jsxs(z,{children:[e.jsx(B,{children:"Cancel"}),e.jsx($,{className:"bg-destructive hover:bg-destructive/90",onClick:c,children:o.isPending?"Deleting…":"Delete"})]})]})})]})]},l.Key)}function oe({bucket:l,prefix:t="",isPending:a,onUpload:r}){const n=f.useRef(null);async function s(o){const i=o.target.files;if(!(!i||i.length===0))try{await Promise.all(Array.from(i).map(c=>{const m=`${t}${c.name}`;return r({bucket:l,key:m,file:c})}))}finally{n.current&&(n.current.value="")}}return e.jsxs(e.Fragment,{children:[e.jsx("input",{accept:"*/*","aria-label":"Choose files to upload",className:"hidden",multiple:!0,onChange:s,ref:n,type:"file"}),e.jsxs(A,{disabled:a,onClick:()=>n.current?.click(),size:"sm",children:[e.jsx(V,{className:"mr-2 size-4"}),a?"Uploading…":"Upload Files"]})]})}export{re as F,ne as O,oe as U};
|
||||
import{k as u,H as g,D as j,E as b,F as y,G as k,I as C,c as w,j as e,L as v,r as f,J as D,K as N,B as A}from"./index.js";import{E as M}from"./error-banner.js";import{g as S,f as E,b as R,a as L,c as O,d as z,e as B,h as F}from"./alert-dialog.js";import{g as $,b as P,c as U}from"./s3.js";import{D as _}from"./download.js";import{T}from"./trash-2.js";const H=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",key:"1oefj6"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5",key:"wfsgrz"}]],I=u("file",H);const G=[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z",key:"1kt360"}]],K=u("folder",G);const q=[["path",{d:"M12 3v12",key:"1x0j5s"}],["path",{d:"m17 8-5-5-5 5",key:"7q97r8"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}]],V=u("upload",q),W=l=>(t,a)=>async r=>{const n=await t(r),{response:s}=n;if(g.isInstance(s)&&s.headers.expires){s.headers.expiresstring=s.headers.expires;try{j(s.headers.expires)}catch(o){a.logger?.warn(`AWS SDK Warning for ${a.clientName}::${a.commandName} response parsing (${s.headers.expires}): ${o}`),delete s.headers.expires}}return n},J={tags:["S3"],name:"s3ExpiresMiddleware",override:!0,relation:"after",toMiddleware:"deserializerMiddleware"},Z=l=>({applyToStack:t=>{t.addRelativeTo(W(),J)}});class Q extends b.classBuilder().ep({...y,Bucket:{type:"contextParams",name:"Bucket"},Key:{type:"contextParams",name:"Key"}}).m(function(t,a,r,n){return[k(r,t.getEndpointParameterInstructions()),$(r,{requestChecksumRequired:!1,requestValidationModeMember:"ChecksumMode",responseAlgorithms:["CRC64NVME","CRC32","CRC32C","SHA256","SHA1"]}),P(r),Z()]}).s("AmazonS3","GetObject",{}).n("S3Client","GetObjectCommand").sc(C).build(){}function re(l){const t=w.c(9),{folderName:a,fullPrefix:r,bucketName:n}=l;let s;t[0]!==n||t[1]!==r?(s={bucket:n,_splat:r},t[0]=n,t[1]=r,t[2]=s):s=t[2];let o;t[3]===Symbol.for("react.memo_cache_sentinel")?(o=e.jsx(K,{className:"size-5 text-muted-foreground"}),t[3]=o):o=t[3];let i;t[4]!==a?(i=e.jsx("span",{className:"font-medium",children:a}),t[4]=a,t[5]=i):i=t[5];let c;return t[6]!==s||t[7]!==i?(c=e.jsxs(v,{className:"flex items-center gap-3 rounded-lg border bg-card p-4 transition-colors hover:bg-accent",params:s,to:"/s3/ls/$bucket/$",children:[o,i]}),t[6]=s,t[7]=i,t[8]=c):c=t[8],c}function ne({object:l,bucketName:t,displayName:a,fullKey:r}){const[n,s]=f.useState(!1),o=U();async function i(){try{const m=new Q({Bucket:t,Key:r}),p=await N().send(m);if(p.Body){const x=await p.Body.transformToByteArray(),h=URL.createObjectURL(new Blob([x])),d=document.createElement("a");d.href=h,d.download=a,document.body.appendChild(d),d.click(),document.body.removeChild(d),URL.revokeObjectURL(h)}}catch{throw new Error("Failed to download object")}}async function c(){try{await o.mutateAsync({bucket:t,key:r})}finally{s(!1)}}return e.jsxs("div",{children:[o.error&&e.jsx(M,{error:o.error,msg:"Failed to delete object"}),e.jsxs("div",{className:"flex items-center justify-between rounded-lg border bg-card p-4",children:[e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsx(I,{className:"size-5 text-muted-foreground"}),e.jsxs("div",{children:[e.jsx("h3",{className:"font-medium",children:a}),l.LastModified&&e.jsxs("p",{className:"text-muted-foreground text-sm",children:["Last Modified: ",l.LastModified.toLocaleString()]})]})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsx("div",{className:"text-muted-foreground text-sm",children:D(l.Size||0)}),e.jsx("button",{className:"rounded-md p-2 transition-colors hover:bg-accent",onClick:i,type:"button",children:e.jsx(_,{className:"size-4"})}),e.jsx("button",{className:"rounded-md p-2 text-destructive transition-colors hover:bg-accent",onClick:()=>s(!0),type:"button",children:e.jsx(T,{className:"size-4"})})]}),e.jsx(S,{onOpenChange:s,open:n,children:e.jsxs(E,{children:[e.jsxs(R,{children:[e.jsx(L,{children:"Delete Object"}),e.jsxs(O,{children:["Are you sure you want to delete ",e.jsx("strong",{children:a}),"? This action cannot be undone."]})]}),e.jsxs(z,{children:[e.jsx(B,{children:"Cancel"}),e.jsx(F,{className:"bg-destructive hover:bg-destructive/90",onClick:c,children:o.isPending?"Deleting…":"Delete"})]})]})})]})]},l.Key)}function oe({bucket:l,prefix:t="",isPending:a,onUpload:r}){const n=f.useRef(null);async function s(o){const i=o.target.files;if(!(!i||i.length===0))try{await Promise.all(Array.from(i).map(c=>{const m=`${t}${c.name}`;return r({bucket:l,key:m,file:c})}))}finally{n.current&&(n.current.value="")}}return e.jsxs(e.Fragment,{children:[e.jsx("input",{accept:"*/*","aria-label":"Choose files to upload",className:"hidden",multiple:!0,onChange:s,ref:n,type:"file"}),e.jsxs(A,{disabled:a,onClick:()=>n.current?.click(),size:"sm",children:[e.jsx(V,{className:"mr-2 size-4"}),a?"Uploading…":"Upload Files"]})]})}export{re as F,ne as O,oe as U};
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -13,35 +13,35 @@
|
||||
"@aws-sdk/client-ec2": "^3.995.0",
|
||||
"@aws-sdk/client-s3": "^3.995.0",
|
||||
"@aws-sdk/lib-storage": "^3.995.0",
|
||||
"@base-ui/react": "^1.1.0",
|
||||
"@base-ui/react": "^1.2.0",
|
||||
"@fontsource-variable/geist": "^5.2.8",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@tanstack/react-query": "^5.90.20",
|
||||
"@tanstack/react-router": "^1.157.5",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@tanstack/react-router": "^1.162.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.563.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-hook-form": "^7.71.1",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"lucide-react": "^0.575.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-hook-form": "^7.71.2",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss": "^4.2.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.12",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/router-plugin": "^1.157.5",
|
||||
"@types/node": "^24.10.9",
|
||||
"@types/react": "^19.2.9",
|
||||
"@biomejs/biome": "^2.4.4",
|
||||
"@tailwindcss/vite": "^4.2.0",
|
||||
"@tanstack/router-plugin": "^1.162.1",
|
||||
"@types/node": "^24.10.13",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-basic-ssl": "^2.1.4",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"@vitejs/plugin-react": "^5.1.4",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"typescript": "^5.9.3",
|
||||
"ultracite": "^7.2.3",
|
||||
"vite": "^7.3.1"
|
||||
},
|
||||
"packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48"
|
||||
"packageManager": "pnpm@10.30.0+sha512.2b5753de015d480eeb88f5b5b61e0051f05b4301808a82ec8b840c9d2adf7748eb352c83f5c1593ca703ff1017295bc3fdd3119abb9686efc96b9fcb18200937"
|
||||
}
|
||||
|
||||
+167
-444
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,12 @@
|
||||
export function DetailRow({ label, value }: { label: string; value?: string }) {
|
||||
import type { ReactNode } from "react"
|
||||
|
||||
export function DetailRow({
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
label: string
|
||||
value?: ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<dt className="text-muted-foreground text-sm">{label}</dt>
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
export function ErrorBanner({ msg, error }: { msg?: string; error?: Error }) {
|
||||
let errorText: string | undefined
|
||||
if (error) {
|
||||
errorText = error.name !== "Error" ? error.name : error.message
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-6 max-w-4xl rounded-md border border-destructive bg-destructive/10 p-4">
|
||||
{msg && <h2 className="text-destructive text-sm">{msg}</h2>}
|
||||
{error && <p className="text-destructive text-sm">{error.message}</p>}
|
||||
{errorText && <p className="text-destructive text-sm">{errorText}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,11 +2,20 @@ export function StateBadge({ state }: { state: string | undefined }) {
|
||||
let className =
|
||||
"bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-100"
|
||||
|
||||
if (state === "running") {
|
||||
if (state === "running" || state === "available" || state === "completed") {
|
||||
className =
|
||||
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100"
|
||||
} else if (state === "stopped") {
|
||||
} else if (state === "stopped" || state === "error") {
|
||||
className = "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-100"
|
||||
} else if (
|
||||
state === "pending" ||
|
||||
state === "shutting-down" ||
|
||||
state === "stopping"
|
||||
) {
|
||||
className =
|
||||
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100"
|
||||
} else if (state === "terminated") {
|
||||
className = "bg-zinc-100 text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400"
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { useQueryClient } from "@tanstack/react-query"
|
||||
import { Link, useLocation, useNavigate } from "@tanstack/react-router"
|
||||
import { HardDrive, Home, Image, Key, LogOut, Server } from "lucide-react"
|
||||
import {
|
||||
Camera,
|
||||
HardDrive,
|
||||
Home,
|
||||
Image,
|
||||
Key,
|
||||
LayoutGrid,
|
||||
LogOut,
|
||||
Network,
|
||||
Server,
|
||||
} from "lucide-react"
|
||||
|
||||
// import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
// import {
|
||||
@@ -106,7 +116,11 @@ export function SidebarLayout() {
|
||||
<SidebarMenuItem>
|
||||
<Link to="/ec2/describe-volumes">
|
||||
<SidebarMenuButton
|
||||
isActive={pathname.startsWith("/ec2/describe-volumes")}
|
||||
isActive={
|
||||
pathname.startsWith("/ec2/describe-volumes") ||
|
||||
pathname.startsWith("/ec2/create-volume") ||
|
||||
pathname.startsWith("/ec2/modify-volume")
|
||||
}
|
||||
tooltip="Volumes"
|
||||
>
|
||||
<HardDrive className="size-4" />
|
||||
@@ -114,6 +128,56 @@ export function SidebarLayout() {
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
</SidebarMenuItem>
|
||||
|
||||
<SidebarMenuItem>
|
||||
<Link to="/ec2/describe-snapshots">
|
||||
<SidebarMenuButton
|
||||
isActive={
|
||||
pathname.startsWith("/ec2/describe-snapshots") ||
|
||||
pathname.startsWith("/ec2/create-snapshot")
|
||||
}
|
||||
tooltip="Snapshots"
|
||||
>
|
||||
<Camera className="size-4" />
|
||||
<span>Snapshots</span>
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Networking</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<Link to="/ec2/describe-vpcs">
|
||||
<SidebarMenuButton
|
||||
isActive={
|
||||
pathname.startsWith("/ec2/describe-vpcs") ||
|
||||
pathname.startsWith("/ec2/create-vpc")
|
||||
}
|
||||
tooltip="VPCs"
|
||||
>
|
||||
<Network className="size-4" />
|
||||
<span>VPCs</span>
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
</SidebarMenuItem>
|
||||
|
||||
<SidebarMenuItem>
|
||||
<Link to="/ec2/describe-subnets">
|
||||
<SidebarMenuButton
|
||||
isActive={
|
||||
pathname.startsWith("/ec2/describe-subnets") ||
|
||||
pathname.startsWith("/ec2/create-subnet")
|
||||
}
|
||||
tooltip="Subnets"
|
||||
>
|
||||
<LayoutGrid className="size-4" />
|
||||
<span>Subnets</span>
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
import {
|
||||
type _InstanceType,
|
||||
AttachVolumeCommand,
|
||||
CopySnapshotCommand,
|
||||
CreateImageCommand,
|
||||
CreateKeyPairCommand,
|
||||
CreateSnapshotCommand,
|
||||
CreateSubnetCommand,
|
||||
CreateVolumeCommand,
|
||||
CreateVpcCommand,
|
||||
DeleteKeyPairCommand,
|
||||
DeleteSnapshotCommand,
|
||||
DeleteSubnetCommand,
|
||||
DeleteVolumeCommand,
|
||||
DeleteVpcCommand,
|
||||
DetachVolumeCommand,
|
||||
GetConsoleOutputCommand,
|
||||
ImportKeyPairCommand,
|
||||
ModifyInstanceAttributeCommand,
|
||||
ModifyVolumeCommand,
|
||||
RebootInstancesCommand,
|
||||
RunInstancesCommand,
|
||||
@@ -15,15 +27,20 @@ import {
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
|
||||
import { getEc2Client } from "@/lib/awsClient"
|
||||
import {
|
||||
ec2InstancesQueryOptions,
|
||||
ec2KeyPairsQueryOptions,
|
||||
} from "@/queries/ec2"
|
||||
import { ec2KeyPairsQueryOptions } from "@/queries/ec2"
|
||||
import type {
|
||||
AttachVolumeFormData,
|
||||
CopySnapshotFormData,
|
||||
CreateImageParams,
|
||||
CreateInstanceParams,
|
||||
CreateKeyPairData,
|
||||
CreateSnapshotFormData,
|
||||
CreateSubnetFormData,
|
||||
CreateVolumeFormData,
|
||||
CreateVpcFormData,
|
||||
DetachVolumeFormData,
|
||||
ImportKeyPairData,
|
||||
ModifyInstanceTypeFormData,
|
||||
ModifyVolumeParams,
|
||||
} from "@/types/ec2"
|
||||
|
||||
@@ -32,15 +49,11 @@ const WHITESPACE_REGEX = /\s+/
|
||||
export function useStartInstance() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (instanceId: string) => {
|
||||
try {
|
||||
const command = new StartInstancesCommand({
|
||||
InstanceIds: [instanceId],
|
||||
})
|
||||
return await getEc2Client().send(command)
|
||||
} catch {
|
||||
throw new Error("Failed to start instance")
|
||||
}
|
||||
mutationFn: (instanceId: string) => {
|
||||
const command = new StartInstancesCommand({
|
||||
InstanceIds: [instanceId],
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "instances"] })
|
||||
@@ -51,15 +64,11 @@ export function useStartInstance() {
|
||||
export function useStopInstance() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (instanceId: string) => {
|
||||
try {
|
||||
const command = new StopInstancesCommand({
|
||||
InstanceIds: [instanceId],
|
||||
})
|
||||
return await getEc2Client().send(command)
|
||||
} catch {
|
||||
throw new Error("Failed to stop instance")
|
||||
}
|
||||
mutationFn: (instanceId: string) => {
|
||||
const command = new StopInstancesCommand({
|
||||
InstanceIds: [instanceId],
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "instances"] })
|
||||
@@ -70,15 +79,11 @@ export function useStopInstance() {
|
||||
export function useTerminateInstance() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (instanceId: string) => {
|
||||
try {
|
||||
const command = new TerminateInstancesCommand({
|
||||
InstanceIds: [instanceId],
|
||||
})
|
||||
return await getEc2Client().send(command)
|
||||
} catch {
|
||||
throw new Error("Failed to terminate instance")
|
||||
}
|
||||
mutationFn: (instanceId: string) => {
|
||||
const command = new TerminateInstancesCommand({
|
||||
InstanceIds: [instanceId],
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "instances"] })
|
||||
@@ -89,15 +94,11 @@ export function useTerminateInstance() {
|
||||
export function useRebootInstance() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (instanceId: string) => {
|
||||
try {
|
||||
const command = new RebootInstancesCommand({
|
||||
InstanceIds: [instanceId],
|
||||
})
|
||||
return await getEc2Client().send(command)
|
||||
} catch {
|
||||
throw new Error("Failed to reboot instance")
|
||||
}
|
||||
mutationFn: (instanceId: string) => {
|
||||
const command = new RebootInstancesCommand({
|
||||
InstanceIds: [instanceId],
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "instances"] })
|
||||
@@ -108,44 +109,18 @@ export function useRebootInstance() {
|
||||
export function useCreateInstance() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (params: CreateInstanceParams) => {
|
||||
try {
|
||||
const command = new RunInstancesCommand({
|
||||
ImageId: params.imageId,
|
||||
InstanceType: params.instanceType as _InstanceType,
|
||||
KeyName: params.keyName,
|
||||
MinCount: params.count,
|
||||
MaxCount: params.count,
|
||||
SecurityGroupIds: params.securityGroupIds,
|
||||
SubnetId: params.subnetId,
|
||||
})
|
||||
return await getEc2Client().send(command)
|
||||
} catch {
|
||||
throw new Error("Failed to create instance")
|
||||
}
|
||||
mutationFn: (params: CreateInstanceParams) => {
|
||||
const command = new RunInstancesCommand({
|
||||
ImageId: params.imageId,
|
||||
InstanceType: params.instanceType as _InstanceType,
|
||||
KeyName: params.keyName,
|
||||
MinCount: params.count,
|
||||
MaxCount: params.count,
|
||||
SubnetId: params.subnetId || undefined,
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: async (data) => {
|
||||
await queryClient.cancelQueries(ec2InstancesQueryOptions)
|
||||
|
||||
const previousData = queryClient.getQueryData(
|
||||
ec2InstancesQueryOptions.queryKey,
|
||||
)
|
||||
|
||||
if (previousData && data.Instances) {
|
||||
const newReservation = {
|
||||
Instances: data.Instances,
|
||||
OwnerId: data.OwnerId,
|
||||
RequesterId: data.RequesterId,
|
||||
ReservationId: data.ReservationId,
|
||||
Groups: [],
|
||||
}
|
||||
|
||||
queryClient.setQueryData(ec2InstancesQueryOptions.queryKey, {
|
||||
...previousData,
|
||||
Reservations: [...(previousData.Reservations || []), newReservation],
|
||||
})
|
||||
}
|
||||
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "instances"] })
|
||||
},
|
||||
})
|
||||
@@ -154,16 +129,12 @@ export function useCreateInstance() {
|
||||
export function useCreateKeyPair() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (params: CreateKeyPairData) => {
|
||||
try {
|
||||
const command = new CreateKeyPairCommand({
|
||||
KeyName: params.keyName,
|
||||
KeyType: "rsa",
|
||||
})
|
||||
return await getEc2Client().send(command)
|
||||
} catch {
|
||||
throw new Error("Failed to create key pair")
|
||||
}
|
||||
mutationFn: (params: CreateKeyPairData) => {
|
||||
const command = new CreateKeyPairCommand({
|
||||
KeyName: params.keyName,
|
||||
KeyType: "rsa",
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(ec2KeyPairsQueryOptions)
|
||||
@@ -174,20 +145,16 @@ export function useCreateKeyPair() {
|
||||
export function useImportKeyPair() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (params: ImportKeyPairData) => {
|
||||
try {
|
||||
// remove optional comment from ssh key as it breaks the import
|
||||
const keyParts = params.publicKeyMaterial.trim().split(WHITESPACE_REGEX)
|
||||
const cleanedKey = keyParts.slice(0, 2).join(" ")
|
||||
mutationFn: (params: ImportKeyPairData) => {
|
||||
// remove optional comment from ssh key as it breaks the import
|
||||
const keyParts = params.publicKeyMaterial.trim().split(WHITESPACE_REGEX)
|
||||
const cleanedKey = keyParts.slice(0, 2).join(" ")
|
||||
|
||||
const command = new ImportKeyPairCommand({
|
||||
KeyName: params.keyName,
|
||||
PublicKeyMaterial: new TextEncoder().encode(cleanedKey),
|
||||
})
|
||||
return await getEc2Client().send(command)
|
||||
} catch {
|
||||
throw new Error("Failed to import key pair")
|
||||
}
|
||||
const command = new ImportKeyPairCommand({
|
||||
KeyName: params.keyName,
|
||||
PublicKeyMaterial: new TextEncoder().encode(cleanedKey),
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(ec2KeyPairsQueryOptions)
|
||||
@@ -198,15 +165,11 @@ export function useImportKeyPair() {
|
||||
export function useDeleteKeyPair() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (keyPairId: string) => {
|
||||
try {
|
||||
const command = new DeleteKeyPairCommand({
|
||||
KeyPairId: keyPairId,
|
||||
})
|
||||
return await getEc2Client().send(command)
|
||||
} catch {
|
||||
throw new Error("Failed to delete key pair")
|
||||
}
|
||||
mutationFn: (keyPairId: string) => {
|
||||
const command = new DeleteKeyPairCommand({
|
||||
KeyPairId: keyPairId,
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(ec2KeyPairsQueryOptions)
|
||||
@@ -217,17 +180,13 @@ export function useDeleteKeyPair() {
|
||||
export function useCreateVolume() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (params: CreateVolumeFormData) => {
|
||||
try {
|
||||
const command = new CreateVolumeCommand({
|
||||
Size: params.size,
|
||||
AvailabilityZone: params.availabilityZone,
|
||||
VolumeType: "gp3",
|
||||
})
|
||||
return await getEc2Client().send(command)
|
||||
} catch {
|
||||
throw new Error("Failed to create volume")
|
||||
}
|
||||
mutationFn: (params: CreateVolumeFormData) => {
|
||||
const command = new CreateVolumeCommand({
|
||||
Size: params.size,
|
||||
AvailabilityZone: params.availabilityZone,
|
||||
VolumeType: "gp3",
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "volumes"] })
|
||||
@@ -238,16 +197,12 @@ export function useCreateVolume() {
|
||||
export function useModifyVolume() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (params: ModifyVolumeParams) => {
|
||||
try {
|
||||
const command = new ModifyVolumeCommand({
|
||||
VolumeId: params.volumeId,
|
||||
Size: params.size,
|
||||
})
|
||||
return await getEc2Client().send(command)
|
||||
} catch {
|
||||
throw new Error("Failed to modify volume")
|
||||
}
|
||||
mutationFn: (params: ModifyVolumeParams) => {
|
||||
const command = new ModifyVolumeCommand({
|
||||
VolumeId: params.volumeId,
|
||||
Size: params.size,
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "volumes"] })
|
||||
@@ -258,18 +213,215 @@ export function useModifyVolume() {
|
||||
export function useDeleteVolume() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (volumeId: string) => {
|
||||
try {
|
||||
const command = new DeleteVolumeCommand({
|
||||
VolumeId: volumeId,
|
||||
})
|
||||
return await getEc2Client().send(command)
|
||||
} catch {
|
||||
throw new Error("Failed to delete volume")
|
||||
}
|
||||
mutationFn: (volumeId: string) => {
|
||||
const command = new DeleteVolumeCommand({
|
||||
VolumeId: volumeId,
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "volumes"] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useCreateSnapshot() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (params: CreateSnapshotFormData) => {
|
||||
const command = new CreateSnapshotCommand({
|
||||
VolumeId: params.volumeId,
|
||||
Description: params.description || undefined,
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "snapshots"] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useDeleteSnapshot() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (snapshotId: string) => {
|
||||
const command = new DeleteSnapshotCommand({
|
||||
SnapshotId: snapshotId,
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "snapshots"] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useCopySnapshot() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (params: CopySnapshotFormData) => {
|
||||
const command = new CopySnapshotCommand({
|
||||
SourceSnapshotId: params.sourceSnapshotId,
|
||||
SourceRegion: params.sourceRegion,
|
||||
Description: params.description || undefined,
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "snapshots"] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useAttachVolume() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (params: AttachVolumeFormData) => {
|
||||
const command = new AttachVolumeCommand({
|
||||
VolumeId: params.volumeId,
|
||||
InstanceId: params.instanceId,
|
||||
Device: params.device || undefined,
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "volumes"] })
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "instances"] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useDetachVolume() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (params: DetachVolumeFormData) => {
|
||||
const command = new DetachVolumeCommand({
|
||||
VolumeId: params.volumeId,
|
||||
InstanceId: params.instanceId || undefined,
|
||||
Force: params.force,
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "volumes"] })
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "instances"] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useModifyInstanceAttribute() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
instanceId,
|
||||
...params
|
||||
}: ModifyInstanceTypeFormData & { instanceId: string }) => {
|
||||
const command = new ModifyInstanceAttributeCommand({
|
||||
InstanceId: instanceId,
|
||||
InstanceType: { Value: params.instanceType },
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "instances"] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useGetConsoleOutput() {
|
||||
return useMutation({
|
||||
mutationFn: (instanceId: string) => {
|
||||
const command = new GetConsoleOutputCommand({
|
||||
InstanceId: instanceId,
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useCreateImage() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (params: CreateImageParams) => {
|
||||
const command = new CreateImageCommand({
|
||||
InstanceId: params.instanceId,
|
||||
Name: params.name,
|
||||
Description: params.description || undefined,
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "images"] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useCreateVpc() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (params: CreateVpcFormData) => {
|
||||
const command = new CreateVpcCommand({
|
||||
CidrBlock: params.cidrBlock,
|
||||
TagSpecifications: params.name
|
||||
? [
|
||||
{
|
||||
ResourceType: "vpc",
|
||||
Tags: [{ Key: "Name", Value: params.name }],
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "vpcs"] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useDeleteVpc() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (vpcId: string) => {
|
||||
const command = new DeleteVpcCommand({
|
||||
VpcId: vpcId,
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "vpcs"] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useCreateSubnet() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (params: CreateSubnetFormData) => {
|
||||
const command = new CreateSubnetCommand({
|
||||
VpcId: params.vpcId,
|
||||
CidrBlock: params.cidrBlock,
|
||||
AvailabilityZone: params.availabilityZone || undefined,
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "subnets"] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useDeleteSubnet() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (subnetId: string) => {
|
||||
const command = new DeleteSubnetCommand({
|
||||
SubnetId: subnetId,
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ec2", "subnets"] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,154 +1,184 @@
|
||||
import {
|
||||
DescribeAvailabilityZonesCommand,
|
||||
DescribeImagesCommand,
|
||||
DescribeInstancesCommand,
|
||||
DescribeInstanceTypesCommand,
|
||||
DescribeKeyPairsCommand,
|
||||
DescribeRegionsCommand,
|
||||
DescribeSnapshotsCommand,
|
||||
DescribeSubnetsCommand,
|
||||
DescribeVolumesCommand,
|
||||
DescribeVpcsCommand,
|
||||
} from "@aws-sdk/client-ec2"
|
||||
import { queryOptions } from "@tanstack/react-query"
|
||||
|
||||
import { getEc2Client } from "@/lib/awsClient"
|
||||
|
||||
export const ec2AvailabilityZonesQueryOptions = queryOptions({
|
||||
queryKey: ["ec2", "availabilityZones"],
|
||||
queryFn: () => {
|
||||
const command = new DescribeAvailabilityZonesCommand({})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
staleTime: 300_000,
|
||||
})
|
||||
|
||||
export const ec2InstancesQueryOptions = queryOptions({
|
||||
queryKey: ["ec2", "instances"],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const command = new DescribeInstancesCommand({})
|
||||
return await getEc2Client().send(command)
|
||||
} catch {
|
||||
throw new Error("Failed to fetch EC2 instances")
|
||||
}
|
||||
queryFn: () => {
|
||||
const command = new DescribeInstancesCommand({})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
})
|
||||
|
||||
export const ec2InstanceQueryOptions = (instanceId: string) =>
|
||||
queryOptions({
|
||||
queryKey: ["ec2", "instances", instanceId],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const command = new DescribeInstancesCommand({
|
||||
InstanceIds: [instanceId],
|
||||
})
|
||||
return await getEc2Client().send(command)
|
||||
} catch {
|
||||
throw new Error("Failed to fetch EC2 instance")
|
||||
}
|
||||
queryFn: () => {
|
||||
const command = new DescribeInstancesCommand({
|
||||
InstanceIds: [instanceId],
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
})
|
||||
|
||||
export const ec2InstanceTypesQueryOptions = queryOptions({
|
||||
queryKey: ["ec2", "instances", "types"],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const command = new DescribeInstanceTypesCommand({
|
||||
Filters: [
|
||||
{
|
||||
Name: "capacity",
|
||||
Values: ["true"],
|
||||
},
|
||||
],
|
||||
})
|
||||
return await getEc2Client().send(command)
|
||||
} catch {
|
||||
throw new Error("Failed to fetch EC2 instance types")
|
||||
}
|
||||
queryFn: () => {
|
||||
const command = new DescribeInstanceTypesCommand({
|
||||
Filters: [
|
||||
{
|
||||
Name: "capacity",
|
||||
Values: ["true"],
|
||||
},
|
||||
],
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
})
|
||||
|
||||
export const ec2ImagesQueryOptions = queryOptions({
|
||||
queryKey: ["ec2", "images"],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const command = new DescribeImagesCommand({})
|
||||
return await getEc2Client().send(command)
|
||||
} catch {
|
||||
throw new Error("Failed to fetch EC2 images")
|
||||
}
|
||||
queryFn: () => {
|
||||
const command = new DescribeImagesCommand({})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
})
|
||||
|
||||
export const ec2ImageQueryOptions = (imageId: string | undefined) =>
|
||||
queryOptions({
|
||||
queryKey: ["ec2", "images", imageId ?? "none"],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
if (!imageId) {
|
||||
return { Images: [], $metadata: {} }
|
||||
}
|
||||
const command = new DescribeImagesCommand({
|
||||
ImageIds: [imageId],
|
||||
})
|
||||
return await getEc2Client().send(command)
|
||||
} catch {
|
||||
throw new Error("Failed to fetch EC2 image")
|
||||
queryFn: () => {
|
||||
if (!imageId) {
|
||||
return { Images: [], $metadata: {} }
|
||||
}
|
||||
const command = new DescribeImagesCommand({
|
||||
ImageIds: [imageId],
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
})
|
||||
|
||||
export const ec2KeyPairsQueryOptions = queryOptions({
|
||||
queryKey: ["ec2", "keypairs"],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const command = new DescribeKeyPairsCommand({})
|
||||
return await getEc2Client().send(command)
|
||||
} catch {
|
||||
throw new Error("Failed to fetch key pairs")
|
||||
}
|
||||
queryFn: () => {
|
||||
const command = new DescribeKeyPairsCommand({})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
})
|
||||
|
||||
export const ec2KeyPairQueryOptions = (keyPairId: string) =>
|
||||
queryOptions({
|
||||
queryKey: ["ec2", "keypairs", keyPairId],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const command = new DescribeKeyPairsCommand({
|
||||
KeyPairIds: [keyPairId],
|
||||
})
|
||||
return await getEc2Client().send(command)
|
||||
} catch {
|
||||
throw new Error("Failed to fetch key pair")
|
||||
}
|
||||
queryFn: () => {
|
||||
const command = new DescribeKeyPairsCommand({
|
||||
KeyPairIds: [keyPairId],
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
})
|
||||
|
||||
export const ec2RegionsQueryOptions = queryOptions({
|
||||
queryKey: ["ec2", "regions"],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const command = new DescribeRegionsCommand({})
|
||||
return await getEc2Client().send(command)
|
||||
} catch {
|
||||
throw new Error("Failed to fetch regions")
|
||||
}
|
||||
queryFn: () => {
|
||||
const command = new DescribeRegionsCommand({})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
staleTime: 300_000,
|
||||
})
|
||||
|
||||
export const ec2SubnetsQueryOptions = queryOptions({
|
||||
queryKey: ["ec2", "subnets"],
|
||||
queryFn: () => {
|
||||
const command = new DescribeSubnetsCommand({})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
})
|
||||
|
||||
export const ec2SubnetQueryOptions = (subnetId: string) =>
|
||||
queryOptions({
|
||||
queryKey: ["ec2", "subnets", subnetId],
|
||||
queryFn: () => {
|
||||
const command = new DescribeSubnetsCommand({
|
||||
SubnetIds: [subnetId],
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
})
|
||||
|
||||
export const ec2SnapshotsQueryOptions = queryOptions({
|
||||
queryKey: ["ec2", "snapshots"],
|
||||
queryFn: () => {
|
||||
const command = new DescribeSnapshotsCommand({})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
})
|
||||
|
||||
export const ec2SnapshotQueryOptions = (snapshotId: string) =>
|
||||
queryOptions({
|
||||
queryKey: ["ec2", "snapshots", snapshotId],
|
||||
queryFn: () => {
|
||||
const command = new DescribeSnapshotsCommand({
|
||||
SnapshotIds: [snapshotId],
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
})
|
||||
|
||||
export const ec2VpcsQueryOptions = queryOptions({
|
||||
queryKey: ["ec2", "vpcs"],
|
||||
queryFn: () => {
|
||||
const command = new DescribeVpcsCommand({})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
})
|
||||
|
||||
export const ec2VpcQueryOptions = (vpcId: string) =>
|
||||
queryOptions({
|
||||
queryKey: ["ec2", "vpcs", vpcId],
|
||||
queryFn: () => {
|
||||
const command = new DescribeVpcsCommand({
|
||||
VpcIds: [vpcId],
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
})
|
||||
|
||||
export const ec2VolumesQueryOptions = queryOptions({
|
||||
queryKey: ["ec2", "volumes"],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const command = new DescribeVolumesCommand({})
|
||||
return await getEc2Client().send(command)
|
||||
} catch {
|
||||
throw new Error("Failed to fetch volumes")
|
||||
}
|
||||
queryFn: () => {
|
||||
const command = new DescribeVolumesCommand({})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
})
|
||||
|
||||
export const ec2VolumeQueryOptions = (volumeId: string) =>
|
||||
queryOptions({
|
||||
queryKey: ["ec2", "volumes", volumeId],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const command = new DescribeVolumesCommand({
|
||||
VolumeIds: [volumeId],
|
||||
})
|
||||
return await getEc2Client().send(command)
|
||||
} catch {
|
||||
throw new Error("Failed to fetch volume")
|
||||
}
|
||||
queryFn: () => {
|
||||
const command = new DescribeVolumesCommand({
|
||||
VolumeIds: [volumeId],
|
||||
})
|
||||
return getEc2Client().send(command)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -14,19 +14,28 @@ import { Route as AuthRouteRouteImport } from './routes/_auth/route'
|
||||
import { Route as AuthIndexRouteImport } from './routes/_auth/index'
|
||||
import { Route as AuthS3LsIndexRouteImport } from './routes/_auth/s3/ls/index'
|
||||
import { Route as AuthS3bucketsCreateBucketRouteImport } from './routes/_auth/s3/(buckets)/create-bucket'
|
||||
import { Route as AuthEc2vpcCreateVpcRouteImport } from './routes/_auth/ec2/(vpc)/create-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 AuthEc2keyImportKeyPairRouteImport } from './routes/_auth/ec2/(key)/import-key-pair'
|
||||
import { Route as AuthEc2keyCreateKeyPairRouteImport } from './routes/_auth/ec2/(key)/create-key-pair'
|
||||
import { Route as AuthEc2instancesRunInstancesRouteImport } from './routes/_auth/ec2/(instances)/run-instances'
|
||||
import { Route as AuthS3LsBucketRouteRouteImport } from './routes/_auth/s3/ls/$bucket/route'
|
||||
import { Route as AuthS3LsBucketIndexRouteImport } from './routes/_auth/s3/ls/$bucket/index'
|
||||
import { Route as AuthEc2vpcDescribeVpcsIndexRouteImport } from './routes/_auth/ec2/(vpc)/describe-vpcs/index'
|
||||
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 AuthEc2keyDescribeKeyPairsIndexRouteImport } from './routes/_auth/ec2/(key)/describe-key-pairs/index'
|
||||
import { Route as AuthEc2instancesDescribeInstancesIndexRouteImport } from './routes/_auth/ec2/(instances)/describe-instances/index'
|
||||
import { Route as AuthEc2imagesDescribeImagesIndexRouteImport } from './routes/_auth/ec2/(images)/describe-images/index'
|
||||
import { Route as AuthS3LsBucketSplatRouteImport } from './routes/_auth/s3/ls/$bucket/$'
|
||||
import { Route as AuthEc2vpcDescribeVpcsIdRouteImport } from './routes/_auth/ec2/(vpc)/describe-vpcs/$id'
|
||||
import { Route as AuthEc2volumesModifyVolumeIdRouteImport } from './routes/_auth/ec2/(volumes)/modify-volume/$id'
|
||||
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 AuthEc2keyDescribeKeyPairsIdRouteImport } from './routes/_auth/ec2/(key)/describe-key-pairs/$id'
|
||||
import { Route as AuthEc2instancesDescribeInstancesIdRouteImport } from './routes/_auth/ec2/(instances)/describe-instances/$id'
|
||||
import { Route as AuthEc2imagesDescribeImagesIdRouteImport } from './routes/_auth/ec2/(images)/describe-images/$id'
|
||||
@@ -56,12 +65,29 @@ const AuthS3bucketsCreateBucketRoute =
|
||||
path: '/s3/create-bucket',
|
||||
getParentRoute: () => AuthRouteRoute,
|
||||
} as any)
|
||||
const AuthEc2vpcCreateVpcRoute = AuthEc2vpcCreateVpcRouteImport.update({
|
||||
id: '/ec2/(vpc)/create-vpc',
|
||||
path: '/ec2/create-vpc',
|
||||
getParentRoute: () => AuthRouteRoute,
|
||||
} as any)
|
||||
const AuthEc2volumesCreateVolumeRoute =
|
||||
AuthEc2volumesCreateVolumeRouteImport.update({
|
||||
id: '/ec2/(volumes)/create-volume',
|
||||
path: '/ec2/create-volume',
|
||||
getParentRoute: () => AuthRouteRoute,
|
||||
} as any)
|
||||
const AuthEc2subnetCreateSubnetRoute =
|
||||
AuthEc2subnetCreateSubnetRouteImport.update({
|
||||
id: '/ec2/(subnet)/create-subnet',
|
||||
path: '/ec2/create-subnet',
|
||||
getParentRoute: () => AuthRouteRoute,
|
||||
} as any)
|
||||
const AuthEc2snapshotsCreateSnapshotRoute =
|
||||
AuthEc2snapshotsCreateSnapshotRouteImport.update({
|
||||
id: '/ec2/(snapshots)/create-snapshot',
|
||||
path: '/ec2/create-snapshot',
|
||||
getParentRoute: () => AuthRouteRoute,
|
||||
} as any)
|
||||
const AuthEc2keyImportKeyPairRoute = AuthEc2keyImportKeyPairRouteImport.update({
|
||||
id: '/ec2/(key)/import-key-pair',
|
||||
path: '/ec2/import-key-pair',
|
||||
@@ -88,12 +114,30 @@ const AuthS3LsBucketIndexRoute = AuthS3LsBucketIndexRouteImport.update({
|
||||
path: '/',
|
||||
getParentRoute: () => AuthS3LsBucketRouteRoute,
|
||||
} as any)
|
||||
const AuthEc2vpcDescribeVpcsIndexRoute =
|
||||
AuthEc2vpcDescribeVpcsIndexRouteImport.update({
|
||||
id: '/ec2/(vpc)/describe-vpcs/',
|
||||
path: '/ec2/describe-vpcs/',
|
||||
getParentRoute: () => AuthRouteRoute,
|
||||
} as any)
|
||||
const AuthEc2volumesDescribeVolumesIndexRoute =
|
||||
AuthEc2volumesDescribeVolumesIndexRouteImport.update({
|
||||
id: '/ec2/(volumes)/describe-volumes/',
|
||||
path: '/ec2/describe-volumes/',
|
||||
getParentRoute: () => AuthRouteRoute,
|
||||
} as any)
|
||||
const AuthEc2subnetDescribeSubnetsIndexRoute =
|
||||
AuthEc2subnetDescribeSubnetsIndexRouteImport.update({
|
||||
id: '/ec2/(subnet)/describe-subnets/',
|
||||
path: '/ec2/describe-subnets/',
|
||||
getParentRoute: () => AuthRouteRoute,
|
||||
} as any)
|
||||
const AuthEc2snapshotsDescribeSnapshotsIndexRoute =
|
||||
AuthEc2snapshotsDescribeSnapshotsIndexRouteImport.update({
|
||||
id: '/ec2/(snapshots)/describe-snapshots/',
|
||||
path: '/ec2/describe-snapshots/',
|
||||
getParentRoute: () => AuthRouteRoute,
|
||||
} as any)
|
||||
const AuthEc2keyDescribeKeyPairsIndexRoute =
|
||||
AuthEc2keyDescribeKeyPairsIndexRouteImport.update({
|
||||
id: '/ec2/(key)/describe-key-pairs/',
|
||||
@@ -117,6 +161,12 @@ const AuthS3LsBucketSplatRoute = AuthS3LsBucketSplatRouteImport.update({
|
||||
path: '/$',
|
||||
getParentRoute: () => AuthS3LsBucketRouteRoute,
|
||||
} as any)
|
||||
const AuthEc2vpcDescribeVpcsIdRoute =
|
||||
AuthEc2vpcDescribeVpcsIdRouteImport.update({
|
||||
id: '/ec2/(vpc)/describe-vpcs/$id',
|
||||
path: '/ec2/describe-vpcs/$id',
|
||||
getParentRoute: () => AuthRouteRoute,
|
||||
} as any)
|
||||
const AuthEc2volumesModifyVolumeIdRoute =
|
||||
AuthEc2volumesModifyVolumeIdRouteImport.update({
|
||||
id: '/ec2/(volumes)/modify-volume/$id',
|
||||
@@ -129,6 +179,18 @@ const AuthEc2volumesDescribeVolumesIdRoute =
|
||||
path: '/ec2/describe-volumes/$id',
|
||||
getParentRoute: () => AuthRouteRoute,
|
||||
} as any)
|
||||
const AuthEc2subnetDescribeSubnetsIdRoute =
|
||||
AuthEc2subnetDescribeSubnetsIdRouteImport.update({
|
||||
id: '/ec2/(subnet)/describe-subnets/$id',
|
||||
path: '/ec2/describe-subnets/$id',
|
||||
getParentRoute: () => AuthRouteRoute,
|
||||
} as any)
|
||||
const AuthEc2snapshotsDescribeSnapshotsIdRoute =
|
||||
AuthEc2snapshotsDescribeSnapshotsIdRouteImport.update({
|
||||
id: '/ec2/(snapshots)/describe-snapshots/$id',
|
||||
path: '/ec2/describe-snapshots/$id',
|
||||
getParentRoute: () => AuthRouteRoute,
|
||||
} as any)
|
||||
const AuthEc2keyDescribeKeyPairsIdRoute =
|
||||
AuthEc2keyDescribeKeyPairsIdRouteImport.update({
|
||||
id: '/ec2/(key)/describe-key-pairs/$id',
|
||||
@@ -155,19 +217,28 @@ export interface FileRoutesByFullPath {
|
||||
'/ec2/run-instances': typeof AuthEc2instancesRunInstancesRoute
|
||||
'/ec2/create-key-pair': typeof AuthEc2keyCreateKeyPairRoute
|
||||
'/ec2/import-key-pair': typeof AuthEc2keyImportKeyPairRoute
|
||||
'/ec2/create-snapshot': typeof AuthEc2snapshotsCreateSnapshotRoute
|
||||
'/ec2/create-subnet': typeof AuthEc2subnetCreateSubnetRoute
|
||||
'/ec2/create-volume': typeof AuthEc2volumesCreateVolumeRoute
|
||||
'/ec2/create-vpc': typeof AuthEc2vpcCreateVpcRoute
|
||||
'/s3/create-bucket': typeof AuthS3bucketsCreateBucketRoute
|
||||
'/s3/ls/': typeof AuthS3LsIndexRoute
|
||||
'/ec2/describe-images/$id': typeof AuthEc2imagesDescribeImagesIdRoute
|
||||
'/ec2/describe-instances/$id': typeof AuthEc2instancesDescribeInstancesIdRoute
|
||||
'/ec2/describe-key-pairs/$id': typeof AuthEc2keyDescribeKeyPairsIdRoute
|
||||
'/ec2/describe-snapshots/$id': typeof AuthEc2snapshotsDescribeSnapshotsIdRoute
|
||||
'/ec2/describe-subnets/$id': typeof AuthEc2subnetDescribeSubnetsIdRoute
|
||||
'/ec2/describe-volumes/$id': typeof AuthEc2volumesDescribeVolumesIdRoute
|
||||
'/ec2/modify-volume/$id': typeof AuthEc2volumesModifyVolumeIdRoute
|
||||
'/ec2/describe-vpcs/$id': typeof AuthEc2vpcDescribeVpcsIdRoute
|
||||
'/s3/ls/$bucket/$': typeof AuthS3LsBucketSplatRoute
|
||||
'/ec2/describe-images/': typeof AuthEc2imagesDescribeImagesIndexRoute
|
||||
'/ec2/describe-instances/': typeof AuthEc2instancesDescribeInstancesIndexRoute
|
||||
'/ec2/describe-key-pairs/': typeof AuthEc2keyDescribeKeyPairsIndexRoute
|
||||
'/ec2/describe-snapshots/': typeof AuthEc2snapshotsDescribeSnapshotsIndexRoute
|
||||
'/ec2/describe-subnets/': typeof AuthEc2subnetDescribeSubnetsIndexRoute
|
||||
'/ec2/describe-volumes/': typeof AuthEc2volumesDescribeVolumesIndexRoute
|
||||
'/ec2/describe-vpcs/': typeof AuthEc2vpcDescribeVpcsIndexRoute
|
||||
'/s3/ls/$bucket/': typeof AuthS3LsBucketIndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
@@ -176,19 +247,28 @@ export interface FileRoutesByTo {
|
||||
'/ec2/run-instances': typeof AuthEc2instancesRunInstancesRoute
|
||||
'/ec2/create-key-pair': typeof AuthEc2keyCreateKeyPairRoute
|
||||
'/ec2/import-key-pair': typeof AuthEc2keyImportKeyPairRoute
|
||||
'/ec2/create-snapshot': typeof AuthEc2snapshotsCreateSnapshotRoute
|
||||
'/ec2/create-subnet': typeof AuthEc2subnetCreateSubnetRoute
|
||||
'/ec2/create-volume': typeof AuthEc2volumesCreateVolumeRoute
|
||||
'/ec2/create-vpc': typeof AuthEc2vpcCreateVpcRoute
|
||||
'/s3/create-bucket': typeof AuthS3bucketsCreateBucketRoute
|
||||
'/s3/ls': typeof AuthS3LsIndexRoute
|
||||
'/ec2/describe-images/$id': typeof AuthEc2imagesDescribeImagesIdRoute
|
||||
'/ec2/describe-instances/$id': typeof AuthEc2instancesDescribeInstancesIdRoute
|
||||
'/ec2/describe-key-pairs/$id': typeof AuthEc2keyDescribeKeyPairsIdRoute
|
||||
'/ec2/describe-snapshots/$id': typeof AuthEc2snapshotsDescribeSnapshotsIdRoute
|
||||
'/ec2/describe-subnets/$id': typeof AuthEc2subnetDescribeSubnetsIdRoute
|
||||
'/ec2/describe-volumes/$id': typeof AuthEc2volumesDescribeVolumesIdRoute
|
||||
'/ec2/modify-volume/$id': typeof AuthEc2volumesModifyVolumeIdRoute
|
||||
'/ec2/describe-vpcs/$id': typeof AuthEc2vpcDescribeVpcsIdRoute
|
||||
'/s3/ls/$bucket/$': typeof AuthS3LsBucketSplatRoute
|
||||
'/ec2/describe-images': typeof AuthEc2imagesDescribeImagesIndexRoute
|
||||
'/ec2/describe-instances': typeof AuthEc2instancesDescribeInstancesIndexRoute
|
||||
'/ec2/describe-key-pairs': typeof AuthEc2keyDescribeKeyPairsIndexRoute
|
||||
'/ec2/describe-snapshots': typeof AuthEc2snapshotsDescribeSnapshotsIndexRoute
|
||||
'/ec2/describe-subnets': typeof AuthEc2subnetDescribeSubnetsIndexRoute
|
||||
'/ec2/describe-volumes': typeof AuthEc2volumesDescribeVolumesIndexRoute
|
||||
'/ec2/describe-vpcs': typeof AuthEc2vpcDescribeVpcsIndexRoute
|
||||
'/s3/ls/$bucket': typeof AuthS3LsBucketIndexRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
@@ -200,19 +280,28 @@ export interface FileRoutesById {
|
||||
'/_auth/ec2/(instances)/run-instances': typeof AuthEc2instancesRunInstancesRoute
|
||||
'/_auth/ec2/(key)/create-key-pair': typeof AuthEc2keyCreateKeyPairRoute
|
||||
'/_auth/ec2/(key)/import-key-pair': typeof AuthEc2keyImportKeyPairRoute
|
||||
'/_auth/ec2/(snapshots)/create-snapshot': typeof AuthEc2snapshotsCreateSnapshotRoute
|
||||
'/_auth/ec2/(subnet)/create-subnet': typeof AuthEc2subnetCreateSubnetRoute
|
||||
'/_auth/ec2/(volumes)/create-volume': typeof AuthEc2volumesCreateVolumeRoute
|
||||
'/_auth/ec2/(vpc)/create-vpc': typeof AuthEc2vpcCreateVpcRoute
|
||||
'/_auth/s3/(buckets)/create-bucket': typeof AuthS3bucketsCreateBucketRoute
|
||||
'/_auth/s3/ls/': typeof AuthS3LsIndexRoute
|
||||
'/_auth/ec2/(images)/describe-images/$id': typeof AuthEc2imagesDescribeImagesIdRoute
|
||||
'/_auth/ec2/(instances)/describe-instances/$id': typeof AuthEc2instancesDescribeInstancesIdRoute
|
||||
'/_auth/ec2/(key)/describe-key-pairs/$id': typeof AuthEc2keyDescribeKeyPairsIdRoute
|
||||
'/_auth/ec2/(snapshots)/describe-snapshots/$id': typeof AuthEc2snapshotsDescribeSnapshotsIdRoute
|
||||
'/_auth/ec2/(subnet)/describe-subnets/$id': typeof AuthEc2subnetDescribeSubnetsIdRoute
|
||||
'/_auth/ec2/(volumes)/describe-volumes/$id': typeof AuthEc2volumesDescribeVolumesIdRoute
|
||||
'/_auth/ec2/(volumes)/modify-volume/$id': typeof AuthEc2volumesModifyVolumeIdRoute
|
||||
'/_auth/ec2/(vpc)/describe-vpcs/$id': typeof AuthEc2vpcDescribeVpcsIdRoute
|
||||
'/_auth/s3/ls/$bucket/$': typeof AuthS3LsBucketSplatRoute
|
||||
'/_auth/ec2/(images)/describe-images/': typeof AuthEc2imagesDescribeImagesIndexRoute
|
||||
'/_auth/ec2/(instances)/describe-instances/': typeof AuthEc2instancesDescribeInstancesIndexRoute
|
||||
'/_auth/ec2/(key)/describe-key-pairs/': typeof AuthEc2keyDescribeKeyPairsIndexRoute
|
||||
'/_auth/ec2/(snapshots)/describe-snapshots/': typeof AuthEc2snapshotsDescribeSnapshotsIndexRoute
|
||||
'/_auth/ec2/(subnet)/describe-subnets/': typeof AuthEc2subnetDescribeSubnetsIndexRoute
|
||||
'/_auth/ec2/(volumes)/describe-volumes/': typeof AuthEc2volumesDescribeVolumesIndexRoute
|
||||
'/_auth/ec2/(vpc)/describe-vpcs/': typeof AuthEc2vpcDescribeVpcsIndexRoute
|
||||
'/_auth/s3/ls/$bucket/': typeof AuthS3LsBucketIndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
@@ -224,19 +313,28 @@ export interface FileRouteTypes {
|
||||
| '/ec2/run-instances'
|
||||
| '/ec2/create-key-pair'
|
||||
| '/ec2/import-key-pair'
|
||||
| '/ec2/create-snapshot'
|
||||
| '/ec2/create-subnet'
|
||||
| '/ec2/create-volume'
|
||||
| '/ec2/create-vpc'
|
||||
| '/s3/create-bucket'
|
||||
| '/s3/ls/'
|
||||
| '/ec2/describe-images/$id'
|
||||
| '/ec2/describe-instances/$id'
|
||||
| '/ec2/describe-key-pairs/$id'
|
||||
| '/ec2/describe-snapshots/$id'
|
||||
| '/ec2/describe-subnets/$id'
|
||||
| '/ec2/describe-volumes/$id'
|
||||
| '/ec2/modify-volume/$id'
|
||||
| '/ec2/describe-vpcs/$id'
|
||||
| '/s3/ls/$bucket/$'
|
||||
| '/ec2/describe-images/'
|
||||
| '/ec2/describe-instances/'
|
||||
| '/ec2/describe-key-pairs/'
|
||||
| '/ec2/describe-snapshots/'
|
||||
| '/ec2/describe-subnets/'
|
||||
| '/ec2/describe-volumes/'
|
||||
| '/ec2/describe-vpcs/'
|
||||
| '/s3/ls/$bucket/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
@@ -245,19 +343,28 @@ export interface FileRouteTypes {
|
||||
| '/ec2/run-instances'
|
||||
| '/ec2/create-key-pair'
|
||||
| '/ec2/import-key-pair'
|
||||
| '/ec2/create-snapshot'
|
||||
| '/ec2/create-subnet'
|
||||
| '/ec2/create-volume'
|
||||
| '/ec2/create-vpc'
|
||||
| '/s3/create-bucket'
|
||||
| '/s3/ls'
|
||||
| '/ec2/describe-images/$id'
|
||||
| '/ec2/describe-instances/$id'
|
||||
| '/ec2/describe-key-pairs/$id'
|
||||
| '/ec2/describe-snapshots/$id'
|
||||
| '/ec2/describe-subnets/$id'
|
||||
| '/ec2/describe-volumes/$id'
|
||||
| '/ec2/modify-volume/$id'
|
||||
| '/ec2/describe-vpcs/$id'
|
||||
| '/s3/ls/$bucket/$'
|
||||
| '/ec2/describe-images'
|
||||
| '/ec2/describe-instances'
|
||||
| '/ec2/describe-key-pairs'
|
||||
| '/ec2/describe-snapshots'
|
||||
| '/ec2/describe-subnets'
|
||||
| '/ec2/describe-volumes'
|
||||
| '/ec2/describe-vpcs'
|
||||
| '/s3/ls/$bucket'
|
||||
id:
|
||||
| '__root__'
|
||||
@@ -268,19 +375,28 @@ export interface FileRouteTypes {
|
||||
| '/_auth/ec2/(instances)/run-instances'
|
||||
| '/_auth/ec2/(key)/create-key-pair'
|
||||
| '/_auth/ec2/(key)/import-key-pair'
|
||||
| '/_auth/ec2/(snapshots)/create-snapshot'
|
||||
| '/_auth/ec2/(subnet)/create-subnet'
|
||||
| '/_auth/ec2/(volumes)/create-volume'
|
||||
| '/_auth/ec2/(vpc)/create-vpc'
|
||||
| '/_auth/s3/(buckets)/create-bucket'
|
||||
| '/_auth/s3/ls/'
|
||||
| '/_auth/ec2/(images)/describe-images/$id'
|
||||
| '/_auth/ec2/(instances)/describe-instances/$id'
|
||||
| '/_auth/ec2/(key)/describe-key-pairs/$id'
|
||||
| '/_auth/ec2/(snapshots)/describe-snapshots/$id'
|
||||
| '/_auth/ec2/(subnet)/describe-subnets/$id'
|
||||
| '/_auth/ec2/(volumes)/describe-volumes/$id'
|
||||
| '/_auth/ec2/(volumes)/modify-volume/$id'
|
||||
| '/_auth/ec2/(vpc)/describe-vpcs/$id'
|
||||
| '/_auth/s3/ls/$bucket/$'
|
||||
| '/_auth/ec2/(images)/describe-images/'
|
||||
| '/_auth/ec2/(instances)/describe-instances/'
|
||||
| '/_auth/ec2/(key)/describe-key-pairs/'
|
||||
| '/_auth/ec2/(snapshots)/describe-snapshots/'
|
||||
| '/_auth/ec2/(subnet)/describe-subnets/'
|
||||
| '/_auth/ec2/(volumes)/describe-volumes/'
|
||||
| '/_auth/ec2/(vpc)/describe-vpcs/'
|
||||
| '/_auth/s3/ls/$bucket/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
@@ -326,6 +442,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthS3bucketsCreateBucketRouteImport
|
||||
parentRoute: typeof AuthRouteRoute
|
||||
}
|
||||
'/_auth/ec2/(vpc)/create-vpc': {
|
||||
id: '/_auth/ec2/(vpc)/create-vpc'
|
||||
path: '/ec2/create-vpc'
|
||||
fullPath: '/ec2/create-vpc'
|
||||
preLoaderRoute: typeof AuthEc2vpcCreateVpcRouteImport
|
||||
parentRoute: typeof AuthRouteRoute
|
||||
}
|
||||
'/_auth/ec2/(volumes)/create-volume': {
|
||||
id: '/_auth/ec2/(volumes)/create-volume'
|
||||
path: '/ec2/create-volume'
|
||||
@@ -333,6 +456,20 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthEc2volumesCreateVolumeRouteImport
|
||||
parentRoute: typeof AuthRouteRoute
|
||||
}
|
||||
'/_auth/ec2/(subnet)/create-subnet': {
|
||||
id: '/_auth/ec2/(subnet)/create-subnet'
|
||||
path: '/ec2/create-subnet'
|
||||
fullPath: '/ec2/create-subnet'
|
||||
preLoaderRoute: typeof AuthEc2subnetCreateSubnetRouteImport
|
||||
parentRoute: typeof AuthRouteRoute
|
||||
}
|
||||
'/_auth/ec2/(snapshots)/create-snapshot': {
|
||||
id: '/_auth/ec2/(snapshots)/create-snapshot'
|
||||
path: '/ec2/create-snapshot'
|
||||
fullPath: '/ec2/create-snapshot'
|
||||
preLoaderRoute: typeof AuthEc2snapshotsCreateSnapshotRouteImport
|
||||
parentRoute: typeof AuthRouteRoute
|
||||
}
|
||||
'/_auth/ec2/(key)/import-key-pair': {
|
||||
id: '/_auth/ec2/(key)/import-key-pair'
|
||||
path: '/ec2/import-key-pair'
|
||||
@@ -368,6 +505,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthS3LsBucketIndexRouteImport
|
||||
parentRoute: typeof AuthS3LsBucketRouteRoute
|
||||
}
|
||||
'/_auth/ec2/(vpc)/describe-vpcs/': {
|
||||
id: '/_auth/ec2/(vpc)/describe-vpcs/'
|
||||
path: '/ec2/describe-vpcs'
|
||||
fullPath: '/ec2/describe-vpcs/'
|
||||
preLoaderRoute: typeof AuthEc2vpcDescribeVpcsIndexRouteImport
|
||||
parentRoute: typeof AuthRouteRoute
|
||||
}
|
||||
'/_auth/ec2/(volumes)/describe-volumes/': {
|
||||
id: '/_auth/ec2/(volumes)/describe-volumes/'
|
||||
path: '/ec2/describe-volumes'
|
||||
@@ -375,6 +519,20 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthEc2volumesDescribeVolumesIndexRouteImport
|
||||
parentRoute: typeof AuthRouteRoute
|
||||
}
|
||||
'/_auth/ec2/(subnet)/describe-subnets/': {
|
||||
id: '/_auth/ec2/(subnet)/describe-subnets/'
|
||||
path: '/ec2/describe-subnets'
|
||||
fullPath: '/ec2/describe-subnets/'
|
||||
preLoaderRoute: typeof AuthEc2subnetDescribeSubnetsIndexRouteImport
|
||||
parentRoute: typeof AuthRouteRoute
|
||||
}
|
||||
'/_auth/ec2/(snapshots)/describe-snapshots/': {
|
||||
id: '/_auth/ec2/(snapshots)/describe-snapshots/'
|
||||
path: '/ec2/describe-snapshots'
|
||||
fullPath: '/ec2/describe-snapshots/'
|
||||
preLoaderRoute: typeof AuthEc2snapshotsDescribeSnapshotsIndexRouteImport
|
||||
parentRoute: typeof AuthRouteRoute
|
||||
}
|
||||
'/_auth/ec2/(key)/describe-key-pairs/': {
|
||||
id: '/_auth/ec2/(key)/describe-key-pairs/'
|
||||
path: '/ec2/describe-key-pairs'
|
||||
@@ -403,6 +561,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthS3LsBucketSplatRouteImport
|
||||
parentRoute: typeof AuthS3LsBucketRouteRoute
|
||||
}
|
||||
'/_auth/ec2/(vpc)/describe-vpcs/$id': {
|
||||
id: '/_auth/ec2/(vpc)/describe-vpcs/$id'
|
||||
path: '/ec2/describe-vpcs/$id'
|
||||
fullPath: '/ec2/describe-vpcs/$id'
|
||||
preLoaderRoute: typeof AuthEc2vpcDescribeVpcsIdRouteImport
|
||||
parentRoute: typeof AuthRouteRoute
|
||||
}
|
||||
'/_auth/ec2/(volumes)/modify-volume/$id': {
|
||||
id: '/_auth/ec2/(volumes)/modify-volume/$id'
|
||||
path: '/ec2/modify-volume/$id'
|
||||
@@ -417,6 +582,20 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthEc2volumesDescribeVolumesIdRouteImport
|
||||
parentRoute: typeof AuthRouteRoute
|
||||
}
|
||||
'/_auth/ec2/(subnet)/describe-subnets/$id': {
|
||||
id: '/_auth/ec2/(subnet)/describe-subnets/$id'
|
||||
path: '/ec2/describe-subnets/$id'
|
||||
fullPath: '/ec2/describe-subnets/$id'
|
||||
preLoaderRoute: typeof AuthEc2subnetDescribeSubnetsIdRouteImport
|
||||
parentRoute: typeof AuthRouteRoute
|
||||
}
|
||||
'/_auth/ec2/(snapshots)/describe-snapshots/$id': {
|
||||
id: '/_auth/ec2/(snapshots)/describe-snapshots/$id'
|
||||
path: '/ec2/describe-snapshots/$id'
|
||||
fullPath: '/ec2/describe-snapshots/$id'
|
||||
preLoaderRoute: typeof AuthEc2snapshotsDescribeSnapshotsIdRouteImport
|
||||
parentRoute: typeof AuthRouteRoute
|
||||
}
|
||||
'/_auth/ec2/(key)/describe-key-pairs/$id': {
|
||||
id: '/_auth/ec2/(key)/describe-key-pairs/$id'
|
||||
path: '/ec2/describe-key-pairs/$id'
|
||||
@@ -460,18 +639,27 @@ interface AuthRouteRouteChildren {
|
||||
AuthEc2instancesRunInstancesRoute: typeof AuthEc2instancesRunInstancesRoute
|
||||
AuthEc2keyCreateKeyPairRoute: typeof AuthEc2keyCreateKeyPairRoute
|
||||
AuthEc2keyImportKeyPairRoute: typeof AuthEc2keyImportKeyPairRoute
|
||||
AuthEc2snapshotsCreateSnapshotRoute: typeof AuthEc2snapshotsCreateSnapshotRoute
|
||||
AuthEc2subnetCreateSubnetRoute: typeof AuthEc2subnetCreateSubnetRoute
|
||||
AuthEc2volumesCreateVolumeRoute: typeof AuthEc2volumesCreateVolumeRoute
|
||||
AuthEc2vpcCreateVpcRoute: typeof AuthEc2vpcCreateVpcRoute
|
||||
AuthS3bucketsCreateBucketRoute: typeof AuthS3bucketsCreateBucketRoute
|
||||
AuthS3LsIndexRoute: typeof AuthS3LsIndexRoute
|
||||
AuthEc2imagesDescribeImagesIdRoute: typeof AuthEc2imagesDescribeImagesIdRoute
|
||||
AuthEc2instancesDescribeInstancesIdRoute: typeof AuthEc2instancesDescribeInstancesIdRoute
|
||||
AuthEc2keyDescribeKeyPairsIdRoute: typeof AuthEc2keyDescribeKeyPairsIdRoute
|
||||
AuthEc2snapshotsDescribeSnapshotsIdRoute: typeof AuthEc2snapshotsDescribeSnapshotsIdRoute
|
||||
AuthEc2subnetDescribeSubnetsIdRoute: typeof AuthEc2subnetDescribeSubnetsIdRoute
|
||||
AuthEc2volumesDescribeVolumesIdRoute: typeof AuthEc2volumesDescribeVolumesIdRoute
|
||||
AuthEc2volumesModifyVolumeIdRoute: typeof AuthEc2volumesModifyVolumeIdRoute
|
||||
AuthEc2vpcDescribeVpcsIdRoute: typeof AuthEc2vpcDescribeVpcsIdRoute
|
||||
AuthEc2imagesDescribeImagesIndexRoute: typeof AuthEc2imagesDescribeImagesIndexRoute
|
||||
AuthEc2instancesDescribeInstancesIndexRoute: typeof AuthEc2instancesDescribeInstancesIndexRoute
|
||||
AuthEc2keyDescribeKeyPairsIndexRoute: typeof AuthEc2keyDescribeKeyPairsIndexRoute
|
||||
AuthEc2snapshotsDescribeSnapshotsIndexRoute: typeof AuthEc2snapshotsDescribeSnapshotsIndexRoute
|
||||
AuthEc2subnetDescribeSubnetsIndexRoute: typeof AuthEc2subnetDescribeSubnetsIndexRoute
|
||||
AuthEc2volumesDescribeVolumesIndexRoute: typeof AuthEc2volumesDescribeVolumesIndexRoute
|
||||
AuthEc2vpcDescribeVpcsIndexRoute: typeof AuthEc2vpcDescribeVpcsIndexRoute
|
||||
}
|
||||
|
||||
const AuthRouteRouteChildren: AuthRouteRouteChildren = {
|
||||
@@ -480,21 +668,33 @@ const AuthRouteRouteChildren: AuthRouteRouteChildren = {
|
||||
AuthEc2instancesRunInstancesRoute: AuthEc2instancesRunInstancesRoute,
|
||||
AuthEc2keyCreateKeyPairRoute: AuthEc2keyCreateKeyPairRoute,
|
||||
AuthEc2keyImportKeyPairRoute: AuthEc2keyImportKeyPairRoute,
|
||||
AuthEc2snapshotsCreateSnapshotRoute: AuthEc2snapshotsCreateSnapshotRoute,
|
||||
AuthEc2subnetCreateSubnetRoute: AuthEc2subnetCreateSubnetRoute,
|
||||
AuthEc2volumesCreateVolumeRoute: AuthEc2volumesCreateVolumeRoute,
|
||||
AuthEc2vpcCreateVpcRoute: AuthEc2vpcCreateVpcRoute,
|
||||
AuthS3bucketsCreateBucketRoute: AuthS3bucketsCreateBucketRoute,
|
||||
AuthS3LsIndexRoute: AuthS3LsIndexRoute,
|
||||
AuthEc2imagesDescribeImagesIdRoute: AuthEc2imagesDescribeImagesIdRoute,
|
||||
AuthEc2instancesDescribeInstancesIdRoute:
|
||||
AuthEc2instancesDescribeInstancesIdRoute,
|
||||
AuthEc2keyDescribeKeyPairsIdRoute: AuthEc2keyDescribeKeyPairsIdRoute,
|
||||
AuthEc2snapshotsDescribeSnapshotsIdRoute:
|
||||
AuthEc2snapshotsDescribeSnapshotsIdRoute,
|
||||
AuthEc2subnetDescribeSubnetsIdRoute: AuthEc2subnetDescribeSubnetsIdRoute,
|
||||
AuthEc2volumesDescribeVolumesIdRoute: AuthEc2volumesDescribeVolumesIdRoute,
|
||||
AuthEc2volumesModifyVolumeIdRoute: AuthEc2volumesModifyVolumeIdRoute,
|
||||
AuthEc2vpcDescribeVpcsIdRoute: AuthEc2vpcDescribeVpcsIdRoute,
|
||||
AuthEc2imagesDescribeImagesIndexRoute: AuthEc2imagesDescribeImagesIndexRoute,
|
||||
AuthEc2instancesDescribeInstancesIndexRoute:
|
||||
AuthEc2instancesDescribeInstancesIndexRoute,
|
||||
AuthEc2keyDescribeKeyPairsIndexRoute: AuthEc2keyDescribeKeyPairsIndexRoute,
|
||||
AuthEc2snapshotsDescribeSnapshotsIndexRoute:
|
||||
AuthEc2snapshotsDescribeSnapshotsIndexRoute,
|
||||
AuthEc2subnetDescribeSubnetsIndexRoute:
|
||||
AuthEc2subnetDescribeSubnetsIndexRoute,
|
||||
AuthEc2volumesDescribeVolumesIndexRoute:
|
||||
AuthEc2volumesDescribeVolumesIndexRoute,
|
||||
AuthEc2vpcDescribeVpcsIndexRoute: AuthEc2vpcDescribeVpcsIndexRoute,
|
||||
}
|
||||
|
||||
const AuthRouteRouteWithChildren = AuthRouteRoute._addFileChildren(
|
||||
|
||||
+2
-10
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"
|
||||
|
||||
import { BackLink } from "@/components/back-link"
|
||||
import { PageHeading } from "@/components/page-heading"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { StateBadge } from "@/components/state-badge"
|
||||
import { ec2ImageQueryOptions } from "@/queries/ec2"
|
||||
import { AmiDetails } from "../../-components/ami-details"
|
||||
|
||||
@@ -45,15 +45,7 @@ function ImageDetail() {
|
||||
|
||||
<div className="space-y-6">
|
||||
<PageHeading
|
||||
actions={
|
||||
image.State ? (
|
||||
<Badge
|
||||
variant={image.State === "available" ? "default" : "outline"}
|
||||
>
|
||||
{image.State}
|
||||
</Badge>
|
||||
) : undefined
|
||||
}
|
||||
actions={<StateBadge state={image.State} />}
|
||||
subtitle="EC2 Image Details"
|
||||
title={image.ImageId}
|
||||
/>
|
||||
|
||||
+2
-12
@@ -4,7 +4,7 @@ import { createFileRoute } from "@tanstack/react-router"
|
||||
|
||||
import { ListCard } from "@/components/list-card"
|
||||
import { PageHeading } from "@/components/page-heading"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { StateBadge } from "@/components/state-badge"
|
||||
import { ec2ImagesQueryOptions } from "@/queries/ec2"
|
||||
|
||||
export const Route = createFileRoute("/_auth/ec2/(images)/describe-images/")({
|
||||
@@ -38,17 +38,7 @@ function Images() {
|
||||
}
|
||||
return (
|
||||
<ListCard
|
||||
badge={
|
||||
image.State ? (
|
||||
<Badge
|
||||
variant={
|
||||
image.State === "available" ? "default" : "outline"
|
||||
}
|
||||
>
|
||||
{image.State}
|
||||
</Badge>
|
||||
) : undefined
|
||||
}
|
||||
badge={<StateBadge state={image.State} />}
|
||||
key={image.ImageId}
|
||||
params={{ id: image.ImageId }}
|
||||
subtitle={image.Name}
|
||||
|
||||
+306
-56
@@ -1,14 +1,45 @@
|
||||
import { useSuspenseQuery } from "@tanstack/react-query"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { createFileRoute, Link } from "@tanstack/react-router"
|
||||
import { ImagePlus, Settings2, Terminal } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
|
||||
import { BackLink } from "@/components/back-link"
|
||||
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 { StateBadge } from "@/components/state-badge"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Field, FieldTitle } from "@/components/ui/field"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { formatDateTime } from "@/lib/utils"
|
||||
import { ec2ImageQueryOptions, ec2InstanceQueryOptions } from "@/queries/ec2"
|
||||
import {
|
||||
useGetConsoleOutput,
|
||||
useModifyInstanceAttribute,
|
||||
} from "@/mutations/ec2"
|
||||
import {
|
||||
ec2ImageQueryOptions,
|
||||
ec2InstanceQueryOptions,
|
||||
ec2InstanceTypesQueryOptions,
|
||||
} from "@/queries/ec2"
|
||||
import { AmiDetails } from "../../-components/ami-details"
|
||||
import { CreateImageDialog } from "../../-components/create-image-dialog"
|
||||
import { InstanceActions } from "../../-components/instance-actions"
|
||||
|
||||
export const Route = createFileRoute(
|
||||
@@ -19,7 +50,10 @@ export const Route = createFileRoute(
|
||||
ec2InstanceQueryOptions(params.id),
|
||||
)
|
||||
const imageId = instanceData.Reservations?.[0]?.Instances?.[0]?.ImageId
|
||||
await context.queryClient.ensureQueryData(ec2ImageQueryOptions(imageId))
|
||||
await Promise.all([
|
||||
context.queryClient.ensureQueryData(ec2ImageQueryOptions(imageId)),
|
||||
context.queryClient.ensureQueryData(ec2InstanceTypesQueryOptions),
|
||||
])
|
||||
return instanceData
|
||||
},
|
||||
head: ({ loaderData }) => ({
|
||||
@@ -42,6 +76,26 @@ function InstanceDetail() {
|
||||
)
|
||||
const image = imageData?.Images?.[0]
|
||||
|
||||
const { data: instanceTypesData } = useSuspenseQuery(
|
||||
ec2InstanceTypesQueryOptions,
|
||||
)
|
||||
const modifyMutation = useModifyInstanceAttribute()
|
||||
const consoleMutation = useGetConsoleOutput()
|
||||
|
||||
const [showChangeType, setShowChangeType] = useState(false)
|
||||
const [selectedType, setSelectedType] = useState("")
|
||||
const [showConsole, setShowConsole] = useState(false)
|
||||
const [consoleOutput, setConsoleOutput] = useState<string | null>(null)
|
||||
const [showCreateImage, setShowCreateImage] = useState(false)
|
||||
|
||||
const instanceTypes = [
|
||||
...new Set(
|
||||
instanceTypesData.InstanceTypes?.flatMap((t) =>
|
||||
t.InstanceType ? [t.InstanceType] : [],
|
||||
),
|
||||
),
|
||||
].toSorted()
|
||||
|
||||
if (!instance?.InstanceId) {
|
||||
return (
|
||||
<>
|
||||
@@ -52,14 +106,93 @@ function InstanceDetail() {
|
||||
}
|
||||
|
||||
const launchTime = formatDateTime(instance.LaunchTime)
|
||||
const isRunning = instance.State?.Name === "running"
|
||||
const isStopped = instance.State?.Name === "stopped"
|
||||
|
||||
async function handleChangeType() {
|
||||
try {
|
||||
await modifyMutation.mutateAsync({
|
||||
instanceId: id,
|
||||
instanceType: selectedType,
|
||||
})
|
||||
setShowChangeType(false)
|
||||
setSelectedType("")
|
||||
} catch {
|
||||
// error shown via modifyMutation.error
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGetConsoleOutput() {
|
||||
try {
|
||||
const result = await consoleMutation.mutateAsync(id)
|
||||
const decoded = result.Output ? atob(result.Output) : "(no output)"
|
||||
setConsoleOutput(decoded)
|
||||
setShowConsole(true)
|
||||
} catch {
|
||||
// error shown via consoleMutation.error
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<BackLink to="/ec2/describe-instances">Back to instances</BackLink>
|
||||
|
||||
{modifyMutation.error && (
|
||||
<ErrorBanner
|
||||
error={modifyMutation.error}
|
||||
msg="Failed to modify instance"
|
||||
/>
|
||||
)}
|
||||
{consoleMutation.error && (
|
||||
<ErrorBanner
|
||||
error={consoleMutation.error}
|
||||
msg="Failed to get console output"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="space-y-6">
|
||||
<PageHeading
|
||||
actions={<StateBadge state={instance.State?.Name} />}
|
||||
actions={
|
||||
<div className="flex items-center gap-2">
|
||||
{isRunning && (
|
||||
<Button
|
||||
disabled={consoleMutation.isPending}
|
||||
onClick={handleGetConsoleOutput}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<Terminal className="size-4" />
|
||||
{consoleMutation.isPending
|
||||
? "Fetching\u2026"
|
||||
: "View Console"}
|
||||
</Button>
|
||||
)}
|
||||
{(isRunning || isStopped) && (
|
||||
<Button
|
||||
onClick={() => setShowCreateImage(true)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<ImagePlus className="size-4" />
|
||||
Create Image
|
||||
</Button>
|
||||
)}
|
||||
{isStopped && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setSelectedType(instance.InstanceType ?? "")
|
||||
setShowChangeType(true)
|
||||
}}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<Settings2 className="size-4" />
|
||||
Modify Instance
|
||||
</Button>
|
||||
)}
|
||||
<StateBadge state={instance.State?.Name} />
|
||||
</div>
|
||||
}
|
||||
subtitle="EC2 Instance Details"
|
||||
title={instance.InstanceId}
|
||||
/>
|
||||
@@ -69,6 +202,32 @@ function InstanceDetail() {
|
||||
state={instance.State?.Name}
|
||||
/>
|
||||
|
||||
{/* Console Output */}
|
||||
{showConsole && consoleOutput !== null && (
|
||||
<DetailCard>
|
||||
<DetailCard.Header>
|
||||
<div className="flex items-center justify-between">
|
||||
Console Output
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowConsole(false)
|
||||
setConsoleOutput(null)
|
||||
}}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
>
|
||||
Hide
|
||||
</Button>
|
||||
</div>
|
||||
</DetailCard.Header>
|
||||
<div className="p-4">
|
||||
<pre className="max-h-96 overflow-y-auto whitespace-pre-wrap rounded bg-muted p-3 font-mono text-xs">
|
||||
{consoleOutput}
|
||||
</pre>
|
||||
</div>
|
||||
</DetailCard>
|
||||
)}
|
||||
|
||||
{/* Instance Details */}
|
||||
<DetailCard>
|
||||
<DetailCard.Header>Instance Information</DetailCard.Header>
|
||||
@@ -89,70 +248,161 @@ function InstanceDetail() {
|
||||
</DetailCard>
|
||||
|
||||
{/* Network & Security */}
|
||||
{/* <DetailCard>
|
||||
<DetailCard.Header>Network & Security</DetailCard.Header>
|
||||
<DetailCard.Content>
|
||||
<DetailRow
|
||||
label="Private IP Address"
|
||||
value={instance.PrivateIpAddress}
|
||||
/>
|
||||
<DetailRow
|
||||
label="Public IP Address"
|
||||
value={instance.PublicIpAddress}
|
||||
/>
|
||||
<DetailRow label="Private DNS" value={instance.PrivateDnsName} />
|
||||
<DetailRow label="Public DNS" value={instance.PublicDnsName} />
|
||||
<DetailRow label="VPC ID" value={instance.VpcId} />
|
||||
<DetailRow label="Subnet ID" value={instance.SubnetId} />
|
||||
<DetailRow label="Key Name" value={instance.KeyName} />
|
||||
<DetailRow
|
||||
label="Security Groups"
|
||||
value={instance.SecurityGroups?.map((sg) => sg.GroupName).join(
|
||||
", ",
|
||||
)}
|
||||
/>
|
||||
</DetailCard.Content>
|
||||
</DetailCard> */}
|
||||
{(instance.PrivateIpAddress ||
|
||||
instance.PublicIpAddress ||
|
||||
instance.VpcId ||
|
||||
instance.SubnetId ||
|
||||
(instance.SecurityGroups && instance.SecurityGroups.length > 0)) && (
|
||||
<DetailCard>
|
||||
<DetailCard.Header>Network & Security</DetailCard.Header>
|
||||
<DetailCard.Content>
|
||||
<DetailRow
|
||||
label="Private IP Address"
|
||||
value={instance.PrivateIpAddress}
|
||||
/>
|
||||
<DetailRow
|
||||
label="Public IP Address"
|
||||
value={instance.PublicIpAddress}
|
||||
/>
|
||||
<DetailRow label="Private DNS" value={instance.PrivateDnsName} />
|
||||
<DetailRow label="Public DNS" value={instance.PublicDnsName} />
|
||||
<DetailRow
|
||||
label="VPC ID"
|
||||
value={
|
||||
instance.VpcId ? (
|
||||
<Link
|
||||
className="text-primary hover:underline"
|
||||
params={{ id: instance.VpcId }}
|
||||
to="/ec2/describe-vpcs/$id"
|
||||
>
|
||||
{instance.VpcId}
|
||||
</Link>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
<DetailRow
|
||||
label="Subnet ID"
|
||||
value={
|
||||
instance.SubnetId ? (
|
||||
<Link
|
||||
className="text-primary hover:underline"
|
||||
params={{ id: instance.SubnetId }}
|
||||
to="/ec2/describe-subnets/$id"
|
||||
>
|
||||
{instance.SubnetId}
|
||||
</Link>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
<DetailRow label="Key Name" value={instance.KeyName} />
|
||||
<DetailRow
|
||||
label="Security Groups"
|
||||
value={instance.SecurityGroups?.map((sg) => sg.GroupName).join(
|
||||
", ",
|
||||
)}
|
||||
/>
|
||||
</DetailCard.Content>
|
||||
</DetailCard>
|
||||
)}
|
||||
|
||||
{/* Block Device Mappings */}
|
||||
{instance.BlockDeviceMappings &&
|
||||
instance.BlockDeviceMappings.length > 0 && (
|
||||
<DetailCard>
|
||||
<DetailCard.Header>Block Device Mappings</DetailCard.Header>
|
||||
<DetailCard.Content>
|
||||
<DetailRow
|
||||
label="Device Name"
|
||||
value={instance.BlockDeviceMappings[0]?.DeviceName}
|
||||
/>
|
||||
<DetailRow
|
||||
label="Volume ID"
|
||||
value={instance.BlockDeviceMappings[0]?.Ebs?.VolumeId}
|
||||
/>
|
||||
<DetailRow
|
||||
label="Status"
|
||||
value={instance.BlockDeviceMappings[0]?.Ebs?.Status}
|
||||
/>
|
||||
<DetailRow
|
||||
label="Attach Time"
|
||||
value={formatDateTime(
|
||||
instance.BlockDeviceMappings[0]?.Ebs?.AttachTime,
|
||||
)}
|
||||
/>
|
||||
<DetailRow
|
||||
label="Delete on Termination"
|
||||
value={
|
||||
instance.BlockDeviceMappings[0]?.Ebs?.DeleteOnTermination
|
||||
? "Yes"
|
||||
: "No"
|
||||
}
|
||||
/>
|
||||
</DetailCard.Content>
|
||||
{instance.BlockDeviceMappings.map((bdm) => (
|
||||
<DetailCard.Content key={bdm.DeviceName}>
|
||||
<DetailRow label="Device Name" value={bdm.DeviceName} />
|
||||
<DetailRow
|
||||
label="Volume ID"
|
||||
value={
|
||||
bdm.Ebs?.VolumeId ? (
|
||||
<Link
|
||||
className="text-primary hover:underline"
|
||||
params={{ id: bdm.Ebs.VolumeId }}
|
||||
to="/ec2/describe-volumes/$id"
|
||||
>
|
||||
{bdm.Ebs.VolumeId}
|
||||
</Link>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
<DetailRow label="Status" value={bdm.Ebs?.Status} />
|
||||
<DetailRow
|
||||
label="Attach Time"
|
||||
value={formatDateTime(bdm.Ebs?.AttachTime)}
|
||||
/>
|
||||
<DetailRow
|
||||
label="Delete on Termination"
|
||||
value={bdm.Ebs?.DeleteOnTermination ? "Yes" : "No"}
|
||||
/>
|
||||
</DetailCard.Content>
|
||||
))}
|
||||
</DetailCard>
|
||||
)}
|
||||
|
||||
{/* AMI Details */}
|
||||
{image && <AmiDetails image={image} />}
|
||||
</div>
|
||||
|
||||
<AlertDialog
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setShowChangeType(false)
|
||||
setSelectedType("")
|
||||
}
|
||||
}}
|
||||
open={showChangeType}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Modify Instance</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Select a new instance type for "{instance.InstanceId}". The
|
||||
instance must be stopped to modify its attributes.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<Field>
|
||||
<FieldTitle>
|
||||
<label htmlFor="instanceType">Instance Type</label>
|
||||
</FieldTitle>
|
||||
<Select
|
||||
onValueChange={(value) => setSelectedType(value ?? "")}
|
||||
value={selectedType}
|
||||
>
|
||||
<SelectTrigger className="w-full" id="instanceType">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{instanceTypes.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{type}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={
|
||||
modifyMutation.isPending ||
|
||||
!selectedType ||
|
||||
selectedType === instance.InstanceType
|
||||
}
|
||||
onClick={handleChangeType}
|
||||
>
|
||||
{modifyMutation.isPending ? "Saving\u2026" : "Save Changes"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<CreateImageDialog
|
||||
instanceId={instance.InstanceId}
|
||||
onOpenChange={setShowCreateImage}
|
||||
open={showCreateImage}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
+18
-1
@@ -28,10 +28,27 @@ function Ec2() {
|
||||
// useInitialSidebar(false)
|
||||
const { data } = useSuspenseQuery(ec2InstancesQueryOptions)
|
||||
|
||||
const instances =
|
||||
const statePriority: Record<string, number> = {
|
||||
running: 0,
|
||||
pending: 1,
|
||||
stopping: 2,
|
||||
"shutting-down": 3,
|
||||
stopped: 4,
|
||||
terminated: 5,
|
||||
}
|
||||
|
||||
const instances = (
|
||||
data.Reservations?.flatMap(
|
||||
(reservation: Reservation) => reservation.Instances || [],
|
||||
) || []
|
||||
).toSorted((a, b) => {
|
||||
const pa = statePriority[a.State?.Name ?? ""] ?? 6
|
||||
const pb = statePriority[b.State?.Name ?? ""] ?? 6
|
||||
if (pa !== pb) {
|
||||
return pa - pb
|
||||
}
|
||||
return (a.InstanceId ?? "").localeCompare(b.InstanceId ?? "")
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -21,19 +21,17 @@ import {
|
||||
ec2ImagesQueryOptions,
|
||||
ec2InstanceTypesQueryOptions,
|
||||
ec2KeyPairsQueryOptions,
|
||||
ec2SubnetsQueryOptions,
|
||||
} from "@/queries/ec2"
|
||||
import { type CreateInstanceFormData, createInstanceSchema } from "@/types/ec2"
|
||||
|
||||
// Hardcoded values - will be replaced when IAM is implemented
|
||||
const SECURITY_GROUP_IDS = ["sg-placeholder"]
|
||||
const SUBNET_ID = "subnet-placeholder"
|
||||
|
||||
export const Route = createFileRoute("/_auth/ec2/(instances)/run-instances")({
|
||||
loader: async ({ context }) => {
|
||||
await Promise.all([
|
||||
context.queryClient.ensureQueryData(ec2ImagesQueryOptions),
|
||||
context.queryClient.ensureQueryData(ec2KeyPairsQueryOptions),
|
||||
context.queryClient.ensureQueryData(ec2InstanceTypesQueryOptions),
|
||||
context.queryClient.ensureQueryData(ec2SubnetsQueryOptions),
|
||||
])
|
||||
},
|
||||
head: () => ({
|
||||
@@ -53,14 +51,11 @@ function CreateInstance() {
|
||||
const { data: instanceTypesData } = useSuspenseQuery(
|
||||
ec2InstanceTypesQueryOptions,
|
||||
)
|
||||
const { data: subnetsData } = useSuspenseQuery(ec2SubnetsQueryOptions)
|
||||
const createMutation = useCreateInstance()
|
||||
const images = imagesData.Images ?? []
|
||||
const keyPairs = keyPairsData.KeyPairs ?? []
|
||||
const instanceTypes =
|
||||
instanceTypesData.InstanceTypes?.flatMap((type) =>
|
||||
type.InstanceType ? [type.InstanceType] : [],
|
||||
) ?? []
|
||||
|
||||
const subnets = subnetsData.Subnets ?? []
|
||||
const instanceTypeCounts =
|
||||
instanceTypesData.InstanceTypes?.reduce(
|
||||
(acc, type) => {
|
||||
@@ -112,11 +107,7 @@ function CreateInstance() {
|
||||
: 1
|
||||
|
||||
const onSubmit = async (data: CreateInstanceFormData) => {
|
||||
await createMutation.mutateAsync({
|
||||
...data,
|
||||
securityGroupIds: SECURITY_GROUP_IDS,
|
||||
subnetId: SUBNET_ID,
|
||||
})
|
||||
await createMutation.mutateAsync(data)
|
||||
|
||||
navigate({ to: "/ec2/describe-instances" })
|
||||
}
|
||||
@@ -127,7 +118,7 @@ function CreateInstance() {
|
||||
<PageHeading title="Run EC2 Instances" />
|
||||
|
||||
{/* Handle error when no instance types available */}
|
||||
{instanceTypes.length === 0 && (
|
||||
{uniqueInstanceTypes.length === 0 && (
|
||||
<ErrorBanner msg="No compute available. No new instances can be created until compute is available." />
|
||||
)}
|
||||
|
||||
@@ -254,6 +245,40 @@ function CreateInstance() {
|
||||
<FieldError errors={[errors.keyName]} />
|
||||
</Field>
|
||||
|
||||
{/* Subnet */}
|
||||
<Field>
|
||||
<FieldTitle>
|
||||
<label htmlFor="subnetId">Subnet</label>
|
||||
</FieldTitle>
|
||||
<Controller
|
||||
control={control}
|
||||
name="subnetId"
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
onValueChange={(value) =>
|
||||
field.onChange(value === "none" ? undefined : value)
|
||||
}
|
||||
value={field.value ?? "none"}
|
||||
>
|
||||
<SelectTrigger className="w-full" id="subnetId">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">none</SelectItem>
|
||||
{subnets.map((subnet) => (
|
||||
<SelectItem
|
||||
key={subnet.SubnetId}
|
||||
value={subnet.SubnetId ?? ""}
|
||||
>
|
||||
{subnet.SubnetId}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{/* Instance Count */}
|
||||
<Field>
|
||||
<FieldTitle>
|
||||
@@ -287,7 +312,7 @@ function CreateInstance() {
|
||||
disabled={
|
||||
isSubmitting ||
|
||||
createMutation.isPending ||
|
||||
instanceTypes.length === 0
|
||||
uniqueInstanceTypes.length === 0
|
||||
}
|
||||
type="submit"
|
||||
>
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ export const Route = createFileRoute("/_auth/ec2/(key)/describe-key-pairs/")({
|
||||
function KeyPairs() {
|
||||
const { data } = useSuspenseQuery(ec2KeyPairsQueryOptions)
|
||||
|
||||
const keyPairs = [...(data.KeyPairs || [])].sort((a, b) => {
|
||||
const keyPairs = (data.KeyPairs || []).toSorted((a, b) => {
|
||||
const nameA = a.KeyName?.toLowerCase() ?? ""
|
||||
const nameB = b.KeyName?.toLowerCase() ?? ""
|
||||
return nameA.localeCompare(nameB)
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useSuspenseQuery } from "@tanstack/react-query"
|
||||
import {
|
||||
createFileRoute,
|
||||
type SearchSchemaInput,
|
||||
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 { PageHeading } from "@/components/page-heading"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Field, FieldError, FieldTitle } from "@/components/ui/field"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { useCreateSnapshot } from "@/mutations/ec2"
|
||||
import { ec2VolumesQueryOptions } from "@/queries/ec2"
|
||||
import { type CreateSnapshotFormData, createSnapshotSchema } from "@/types/ec2"
|
||||
|
||||
export const Route = createFileRoute("/_auth/ec2/(snapshots)/create-snapshot")({
|
||||
validateSearch: (search: { volumeId?: string } & SearchSchemaInput) => ({
|
||||
volumeId: typeof search.volumeId === "string" ? search.volumeId : undefined,
|
||||
}),
|
||||
loader: async ({ context }) => {
|
||||
await context.queryClient.ensureQueryData(ec2VolumesQueryOptions)
|
||||
},
|
||||
head: () => ({
|
||||
meta: [
|
||||
{
|
||||
title: "Create Snapshot | EC2 | Mulga",
|
||||
},
|
||||
],
|
||||
}),
|
||||
component: CreateSnapshot,
|
||||
})
|
||||
|
||||
function CreateSnapshot() {
|
||||
const navigate = useNavigate()
|
||||
const { data: volumesData } = useSuspenseQuery(ec2VolumesQueryOptions)
|
||||
const createMutation = useCreateSnapshot()
|
||||
const volumes = volumesData.Volumes ?? []
|
||||
|
||||
const { volumeId: searchVolumeId } = Route.useSearch()
|
||||
const defaultVolumeId = searchVolumeId ?? volumes[0]?.VolumeId ?? ""
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<CreateSnapshotFormData>({
|
||||
resolver: zodResolver(createSnapshotSchema),
|
||||
defaultValues: {
|
||||
volumeId: defaultVolumeId,
|
||||
description: "",
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit = async (data: CreateSnapshotFormData) => {
|
||||
await createMutation.mutateAsync(data)
|
||||
navigate({ to: "/ec2/describe-snapshots" })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<BackLink to="/ec2/describe-snapshots">Back to snapshots</BackLink>
|
||||
|
||||
<PageHeading title="Create Snapshot" />
|
||||
|
||||
{createMutation.error && (
|
||||
<ErrorBanner
|
||||
error={createMutation.error}
|
||||
msg="Failed to create snapshot"
|
||||
/>
|
||||
)}
|
||||
|
||||
<form className="max-w-4xl space-y-6" onSubmit={handleSubmit(onSubmit)}>
|
||||
<Field>
|
||||
<FieldTitle>
|
||||
<label htmlFor="volumeId">Volume</label>
|
||||
</FieldTitle>
|
||||
<Controller
|
||||
control={control}
|
||||
name="volumeId"
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
onValueChange={(value) => field.onChange(value)}
|
||||
value={field.value ?? ""}
|
||||
>
|
||||
<SelectTrigger
|
||||
aria-invalid={!!errors.volumeId}
|
||||
className="w-full"
|
||||
id="volumeId"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{volumes.map((volume) => (
|
||||
<SelectItem
|
||||
key={volume.VolumeId}
|
||||
value={volume.VolumeId ?? ""}
|
||||
>
|
||||
{volume.VolumeId} ({volume.Size} GiB)
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
<FieldError errors={[errors.volumeId]} />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldTitle>
|
||||
<label htmlFor="description">Description (optional)</label>
|
||||
</FieldTitle>
|
||||
<Textarea
|
||||
id="description"
|
||||
placeholder="Snapshot description"
|
||||
{...register("description")}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
disabled={isSubmitting || createMutation.isPending}
|
||||
onClick={() => navigate({ to: "/ec2/describe-snapshots" })}
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isSubmitting || createMutation.isPending}
|
||||
type="submit"
|
||||
>
|
||||
{isSubmitting || createMutation.isPending
|
||||
? "Creating\u2026"
|
||||
: "Create Snapshot"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
import { useSuspenseQuery } from "@tanstack/react-query"
|
||||
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"
|
||||
import { Copy, Trash2 } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
|
||||
import { BackLink } from "@/components/back-link"
|
||||
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 { StateBadge } from "@/components/state-badge"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Field, FieldTitle } from "@/components/ui/field"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { formatDateTime } from "@/lib/utils"
|
||||
import { useCopySnapshot, useDeleteSnapshot } from "@/mutations/ec2"
|
||||
import { ec2RegionsQueryOptions, ec2SnapshotQueryOptions } from "@/queries/ec2"
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_auth/ec2/(snapshots)/describe-snapshots/$id",
|
||||
)({
|
||||
loader: async ({ context, params }) => {
|
||||
await Promise.all([
|
||||
context.queryClient.ensureQueryData(ec2SnapshotQueryOptions(params.id)),
|
||||
context.queryClient.ensureQueryData(ec2RegionsQueryOptions),
|
||||
])
|
||||
},
|
||||
head: ({ params }) => ({
|
||||
meta: [
|
||||
{
|
||||
title: `${params.id} | EC2 | Mulga`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
component: SnapshotDetail,
|
||||
})
|
||||
|
||||
function SnapshotDetail() {
|
||||
const { id } = Route.useParams()
|
||||
const navigate = useNavigate()
|
||||
const { data } = useSuspenseQuery(ec2SnapshotQueryOptions(id))
|
||||
const { data: regionsData } = useSuspenseQuery(ec2RegionsQueryOptions)
|
||||
const snapshot = data.Snapshots?.[0]
|
||||
const deleteMutation = useDeleteSnapshot()
|
||||
const copyMutation = useCopySnapshot()
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
const [showCopyDialog, setShowCopyDialog] = useState(false)
|
||||
const [copyDescription, setCopyDescription] = useState("")
|
||||
|
||||
const currentRegion = regionsData.Regions?.[0]?.RegionName ?? "ap-southeast-2"
|
||||
|
||||
const canDelete = snapshot?.State === "completed"
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await deleteMutation.mutateAsync(id)
|
||||
navigate({ to: "/ec2/describe-snapshots" })
|
||||
} finally {
|
||||
setShowDeleteDialog(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await copyMutation.mutateAsync({
|
||||
sourceSnapshotId: id,
|
||||
sourceRegion: currentRegion,
|
||||
description: copyDescription || undefined,
|
||||
})
|
||||
setShowCopyDialog(false)
|
||||
setCopyDescription("")
|
||||
} catch {
|
||||
// error is shown via copyMutation.error
|
||||
}
|
||||
}
|
||||
|
||||
if (!snapshot?.SnapshotId) {
|
||||
return (
|
||||
<>
|
||||
<BackLink to="/ec2/describe-snapshots">Back to snapshots</BackLink>
|
||||
<p className="text-muted-foreground">Snapshot not found.</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const startTime = formatDateTime(snapshot.StartTime)
|
||||
|
||||
return (
|
||||
<>
|
||||
<BackLink to="/ec2/describe-snapshots">Back to snapshots</BackLink>
|
||||
|
||||
{deleteMutation.error && (
|
||||
<ErrorBanner
|
||||
error={deleteMutation.error}
|
||||
msg="Failed to delete snapshot"
|
||||
/>
|
||||
)}
|
||||
|
||||
{copyMutation.error && (
|
||||
<ErrorBanner error={copyMutation.error} msg="Failed to copy snapshot" />
|
||||
)}
|
||||
|
||||
<div className="space-y-6">
|
||||
<PageHeading
|
||||
actions={
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
disabled={!canDelete}
|
||||
onClick={() => setShowDeleteDialog(true)}
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
Delete
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!canDelete}
|
||||
onClick={() => setShowCopyDialog(true)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<Copy className="size-4" />
|
||||
Copy Snapshot
|
||||
</Button>
|
||||
<StateBadge state={snapshot.State} />
|
||||
</div>
|
||||
}
|
||||
subtitle="EC2 Snapshot Details"
|
||||
title={snapshot.SnapshotId}
|
||||
/>
|
||||
|
||||
<DetailCard>
|
||||
<DetailCard.Header>Snapshot Information</DetailCard.Header>
|
||||
<DetailCard.Content>
|
||||
<DetailRow label="Snapshot ID" value={snapshot.SnapshotId} />
|
||||
<DetailRow
|
||||
label="Volume ID"
|
||||
value={
|
||||
snapshot.VolumeId ? (
|
||||
<Link
|
||||
className="text-primary hover:underline"
|
||||
params={{ id: snapshot.VolumeId }}
|
||||
to="/ec2/describe-volumes/$id"
|
||||
>
|
||||
{snapshot.VolumeId}
|
||||
</Link>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
<DetailRow
|
||||
label="Volume Size"
|
||||
value={`${snapshot.VolumeSize} GiB`}
|
||||
/>
|
||||
<DetailRow label="State" value={snapshot.State} />
|
||||
<DetailRow label="Description" value={snapshot.Description} />
|
||||
<DetailRow label="Progress" value={snapshot.Progress} />
|
||||
<DetailRow label="Start Time" value={startTime} />
|
||||
<DetailRow label="Owner ID" value={snapshot.OwnerId} />
|
||||
<DetailRow
|
||||
label="Encrypted"
|
||||
value={snapshot.Encrypted ? "Yes" : "No"}
|
||||
/>
|
||||
</DetailCard.Content>
|
||||
</DetailCard>
|
||||
</div>
|
||||
|
||||
<AlertDialog onOpenChange={setShowDeleteDialog} open={showDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Snapshot</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete the snapshot "
|
||||
{snapshot.SnapshotId}"? This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={deleteMutation.isPending}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{deleteMutation.isPending ? "Deleting\u2026" : "Delete"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<AlertDialog onOpenChange={setShowCopyDialog} open={showCopyDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Copy Snapshot</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Create a copy of this snapshot in the current region.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<Field>
|
||||
<FieldTitle>
|
||||
<label htmlFor="copyDescription">Description (optional)</label>
|
||||
</FieldTitle>
|
||||
<Input
|
||||
id="copyDescription"
|
||||
onChange={(e) => setCopyDescription(e.target.value)}
|
||||
placeholder="Description for the snapshot copy"
|
||||
value={copyDescription}
|
||||
/>
|
||||
</Field>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={copyMutation.isPending}
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{copyMutation.isPending ? "Copying\u2026" : "Copy Snapshot"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import type { Snapshot } 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 { StateBadge } from "@/components/state-badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ec2SnapshotsQueryOptions } from "@/queries/ec2"
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_auth/ec2/(snapshots)/describe-snapshots/",
|
||||
)({
|
||||
loader: async ({ context }) => {
|
||||
await context.queryClient.ensureQueryData(ec2SnapshotsQueryOptions)
|
||||
},
|
||||
head: () => ({
|
||||
meta: [
|
||||
{
|
||||
title: "Snapshots | EC2 | Mulga",
|
||||
},
|
||||
],
|
||||
}),
|
||||
component: Snapshots,
|
||||
})
|
||||
|
||||
function Snapshots() {
|
||||
const { data } = useSuspenseQuery(ec2SnapshotsQueryOptions)
|
||||
|
||||
const snapshots = data.Snapshots || []
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeading
|
||||
actions={
|
||||
<Link to="/ec2/create-snapshot">
|
||||
<Button>Create Snapshot</Button>
|
||||
</Link>
|
||||
}
|
||||
title="EC2 Snapshots"
|
||||
/>
|
||||
|
||||
{snapshots.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{snapshots.map((snapshot: Snapshot) => {
|
||||
if (!snapshot.SnapshotId) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<ListCard
|
||||
badge={<StateBadge state={snapshot.State} />}
|
||||
key={snapshot.SnapshotId}
|
||||
params={{ id: snapshot.SnapshotId }}
|
||||
subtitle={`${snapshot.VolumeSize} GiB \u2022 ${snapshot.VolumeId}`}
|
||||
title={snapshot.SnapshotId}
|
||||
to="/ec2/describe-snapshots/$id"
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground">No snapshots found.</p>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
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 { PageHeading } from "@/components/page-heading"
|
||||
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 { useCreateSubnet } from "@/mutations/ec2"
|
||||
import {
|
||||
ec2AvailabilityZonesQueryOptions,
|
||||
ec2VpcsQueryOptions,
|
||||
} from "@/queries/ec2"
|
||||
import { type CreateSubnetFormData, createSubnetSchema } from "@/types/ec2"
|
||||
|
||||
export const Route = createFileRoute("/_auth/ec2/(subnet)/create-subnet")({
|
||||
loader: async ({ context }) => {
|
||||
await Promise.all([
|
||||
context.queryClient.ensureQueryData(ec2VpcsQueryOptions),
|
||||
context.queryClient.ensureQueryData(ec2AvailabilityZonesQueryOptions),
|
||||
])
|
||||
},
|
||||
head: () => ({
|
||||
meta: [
|
||||
{
|
||||
title: "Create Subnet | EC2 | Mulga",
|
||||
},
|
||||
],
|
||||
}),
|
||||
component: CreateSubnet,
|
||||
})
|
||||
|
||||
function CreateSubnet() {
|
||||
const navigate = useNavigate()
|
||||
const { data: vpcsData } = useSuspenseQuery(ec2VpcsQueryOptions)
|
||||
const { data: azsData } = useSuspenseQuery(ec2AvailabilityZonesQueryOptions)
|
||||
const createMutation = useCreateSubnet()
|
||||
|
||||
const vpcs = vpcsData.Vpcs ?? []
|
||||
const azs = azsData.AvailabilityZones ?? []
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<CreateSubnetFormData>({
|
||||
resolver: zodResolver(createSubnetSchema),
|
||||
defaultValues: {
|
||||
vpcId: vpcs[0]?.VpcId ?? "",
|
||||
cidrBlock: "10.0.1.0/24",
|
||||
availabilityZone: "",
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit = async (data: CreateSubnetFormData) => {
|
||||
const result = await createMutation.mutateAsync(data)
|
||||
const subnetId = result.Subnet?.SubnetId
|
||||
if (subnetId) {
|
||||
navigate({ to: "/ec2/describe-subnets/$id", params: { id: subnetId } })
|
||||
} else {
|
||||
navigate({ to: "/ec2/describe-subnets" })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<BackLink to="/ec2/describe-subnets">Back to Subnets</BackLink>
|
||||
|
||||
<PageHeading title="Create Subnet" />
|
||||
|
||||
{createMutation.error && (
|
||||
<ErrorBanner
|
||||
error={createMutation.error}
|
||||
msg="Failed to create subnet"
|
||||
/>
|
||||
)}
|
||||
|
||||
<form className="max-w-4xl space-y-6" onSubmit={handleSubmit(onSubmit)}>
|
||||
<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 = vpc.Tags?.find((t) => t.Key === "Name")?.Value
|
||||
return (
|
||||
<SelectItem key={vpc.VpcId} value={vpc.VpcId ?? ""}>
|
||||
{name
|
||||
? `${vpc.VpcId} (${name})`
|
||||
: `${vpc.VpcId} (${vpc.CidrBlock})`}
|
||||
</SelectItem>
|
||||
)
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
<FieldError errors={[errors.vpcId]} />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldTitle>
|
||||
<label htmlFor="cidrBlock">CIDR Block</label>
|
||||
</FieldTitle>
|
||||
<Input
|
||||
aria-invalid={!!errors.cidrBlock}
|
||||
id="cidrBlock"
|
||||
placeholder="10.0.1.0/24"
|
||||
{...register("cidrBlock")}
|
||||
/>
|
||||
<FieldError errors={[errors.cidrBlock]} />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldTitle>
|
||||
<label htmlFor="availabilityZone">
|
||||
Availability Zone (optional)
|
||||
</label>
|
||||
</FieldTitle>
|
||||
<Controller
|
||||
control={control}
|
||||
name="availabilityZone"
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
onValueChange={(value) =>
|
||||
field.onChange(value === "none" ? "" : value)
|
||||
}
|
||||
value={field.value || "none"}
|
||||
>
|
||||
<SelectTrigger className="w-full" id="availabilityZone">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">none</SelectItem>
|
||||
{azs.map((az) => (
|
||||
<SelectItem key={az.ZoneName} value={az.ZoneName ?? ""}>
|
||||
{az.ZoneName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
disabled={isSubmitting || createMutation.isPending}
|
||||
onClick={() => navigate({ to: "/ec2/describe-subnets" })}
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isSubmitting || createMutation.isPending}
|
||||
type="submit"
|
||||
>
|
||||
{isSubmitting || createMutation.isPending
|
||||
? "Creating\u2026"
|
||||
: "Create Subnet"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { useSuspenseQuery } from "@tanstack/react-query"
|
||||
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"
|
||||
import { Trash2 } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
|
||||
import { BackLink } from "@/components/back-link"
|
||||
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 { StateBadge } from "@/components/state-badge"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useDeleteSubnet } from "@/mutations/ec2"
|
||||
import { ec2SubnetQueryOptions } from "@/queries/ec2"
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_auth/ec2/(subnet)/describe-subnets/$id",
|
||||
)({
|
||||
loader: async ({ context, params }) => {
|
||||
await context.queryClient.ensureQueryData(ec2SubnetQueryOptions(params.id))
|
||||
},
|
||||
head: ({ params }) => ({
|
||||
meta: [
|
||||
{
|
||||
title: `${params.id} | Subnet | Mulga`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
component: SubnetDetail,
|
||||
})
|
||||
|
||||
function SubnetDetail() {
|
||||
const { id } = Route.useParams()
|
||||
const navigate = useNavigate()
|
||||
const { data } = useSuspenseQuery(ec2SubnetQueryOptions(id))
|
||||
const subnet = data.Subnets?.[0]
|
||||
const deleteMutation = useDeleteSubnet()
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await deleteMutation.mutateAsync(id)
|
||||
navigate({ to: "/ec2/describe-subnets" })
|
||||
} finally {
|
||||
setShowDeleteDialog(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!subnet?.SubnetId) {
|
||||
return (
|
||||
<>
|
||||
<BackLink to="/ec2/describe-subnets">Back to Subnets</BackLink>
|
||||
<p className="text-muted-foreground">Subnet not found.</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const name = subnet.Tags?.find((t) => t.Key === "Name")?.Value
|
||||
|
||||
return (
|
||||
<>
|
||||
<BackLink to="/ec2/describe-subnets">Back to Subnets</BackLink>
|
||||
|
||||
{deleteMutation.error && (
|
||||
<ErrorBanner
|
||||
error={deleteMutation.error}
|
||||
msg="Failed to delete subnet"
|
||||
/>
|
||||
)}
|
||||
|
||||
<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>
|
||||
<StateBadge state={subnet.State} />
|
||||
</div>
|
||||
}
|
||||
subtitle="Subnet Details"
|
||||
title={name ? `${subnet.SubnetId} (${name})` : subnet.SubnetId}
|
||||
/>
|
||||
|
||||
<DetailCard>
|
||||
<DetailCard.Header>Subnet Information</DetailCard.Header>
|
||||
<DetailCard.Content>
|
||||
<DetailRow label="Subnet ID" value={subnet.SubnetId} />
|
||||
<DetailRow
|
||||
label="VPC ID"
|
||||
value={
|
||||
subnet.VpcId ? (
|
||||
<Link
|
||||
className="text-primary hover:underline"
|
||||
params={{ id: subnet.VpcId }}
|
||||
to="/ec2/describe-vpcs/$id"
|
||||
>
|
||||
{subnet.VpcId}
|
||||
</Link>
|
||||
) : (
|
||||
"\u2014"
|
||||
)
|
||||
}
|
||||
/>
|
||||
<DetailRow label="State" value={subnet.State} />
|
||||
<DetailRow label="CIDR Block" value={subnet.CidrBlock} />
|
||||
<DetailRow
|
||||
label="Availability Zone"
|
||||
value={subnet.AvailabilityZone}
|
||||
/>
|
||||
<DetailRow
|
||||
label="Available IP Count"
|
||||
value={subnet.AvailableIpAddressCount?.toString()}
|
||||
/>
|
||||
<DetailRow
|
||||
label="Map Public IP on Launch"
|
||||
value={subnet.MapPublicIpOnLaunch ? "Yes" : "No"}
|
||||
/>
|
||||
<DetailRow
|
||||
label="Default for AZ"
|
||||
value={subnet.DefaultForAz ? "Yes" : "No"}
|
||||
/>
|
||||
</DetailCard.Content>
|
||||
</DetailCard>
|
||||
</div>
|
||||
|
||||
<AlertDialog onOpenChange={setShowDeleteDialog} open={showDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Subnet</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete the subnet "{subnet.SubnetId}"?
|
||||
This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={deleteMutation.isPending}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{deleteMutation.isPending ? "Deleting\u2026" : "Delete"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import type { Subnet } 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 { StateBadge } from "@/components/state-badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ec2SubnetsQueryOptions } from "@/queries/ec2"
|
||||
|
||||
export const Route = createFileRoute("/_auth/ec2/(subnet)/describe-subnets/")({
|
||||
loader: async ({ context }) => {
|
||||
await context.queryClient.ensureQueryData(ec2SubnetsQueryOptions)
|
||||
},
|
||||
head: () => ({
|
||||
meta: [
|
||||
{
|
||||
title: "Subnets | EC2 | Mulga",
|
||||
},
|
||||
],
|
||||
}),
|
||||
component: Subnets,
|
||||
})
|
||||
|
||||
function Subnets() {
|
||||
const { data } = useSuspenseQuery(ec2SubnetsQueryOptions)
|
||||
|
||||
const subnets = data.Subnets || []
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeading
|
||||
actions={
|
||||
<Link to="/ec2/create-subnet">
|
||||
<Button>Create Subnet</Button>
|
||||
</Link>
|
||||
}
|
||||
title="Subnets"
|
||||
/>
|
||||
|
||||
{subnets.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{subnets.map((subnet: Subnet) => {
|
||||
if (!subnet.SubnetId) {
|
||||
return null
|
||||
}
|
||||
const name = subnet.Tags?.find((t) => t.Key === "Name")?.Value
|
||||
return (
|
||||
<ListCard
|
||||
badge={<StateBadge state={subnet.State} />}
|
||||
key={subnet.SubnetId}
|
||||
params={{ id: subnet.SubnetId }}
|
||||
subtitle={`${subnet.CidrBlock ?? ""} \u2022 ${subnet.AvailabilityZone ?? ""}`}
|
||||
title={name ? `${subnet.SubnetId} (${name})` : subnet.SubnetId}
|
||||
to="/ec2/describe-subnets/$id"
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground">No subnets found.</p>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useSuspenseQuery } from "@tanstack/react-query"
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { Controller, useForm } from "react-hook-form"
|
||||
|
||||
import { BackLink } from "@/components/back-link"
|
||||
import { ErrorBanner } from "@/components/error-banner"
|
||||
@@ -8,10 +9,21 @@ import { PageHeading } from "@/components/page-heading"
|
||||
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 { useCreateVolume } from "@/mutations/ec2"
|
||||
import { ec2AvailabilityZonesQueryOptions } from "@/queries/ec2"
|
||||
import { type CreateVolumeFormData, createVolumeSchema } from "@/types/ec2"
|
||||
|
||||
export const Route = createFileRoute("/_auth/ec2/(volumes)/create-volume")({
|
||||
loader: async ({ context }) => {
|
||||
await context.queryClient.ensureQueryData(ec2AvailabilityZonesQueryOptions)
|
||||
},
|
||||
head: () => ({
|
||||
meta: [
|
||||
{
|
||||
@@ -24,9 +36,14 @@ export const Route = createFileRoute("/_auth/ec2/(volumes)/create-volume")({
|
||||
|
||||
function CreateVolume() {
|
||||
const navigate = useNavigate()
|
||||
const { data: azData } = useSuspenseQuery(ec2AvailabilityZonesQueryOptions)
|
||||
const createMutation = useCreateVolume()
|
||||
const availabilityZones = azData.AvailabilityZones ?? []
|
||||
|
||||
const defaultAz = availabilityZones[0]?.ZoneName ?? ""
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
@@ -34,7 +51,7 @@ function CreateVolume() {
|
||||
resolver: zodResolver(createVolumeSchema),
|
||||
defaultValues: {
|
||||
size: 1,
|
||||
availabilityZone: "",
|
||||
availabilityZone: defaultAz,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -76,12 +93,30 @@ function CreateVolume() {
|
||||
<FieldTitle>
|
||||
<label htmlFor="availabilityZone">Availability Zone</label>
|
||||
</FieldTitle>
|
||||
<Input
|
||||
aria-invalid={!!errors.availabilityZone}
|
||||
id="availabilityZone"
|
||||
placeholder="ap-southeast-2a"
|
||||
type="text"
|
||||
{...register("availabilityZone")}
|
||||
<Controller
|
||||
control={control}
|
||||
name="availabilityZone"
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
onValueChange={(value) => field.onChange(value)}
|
||||
value={field.value ?? ""}
|
||||
>
|
||||
<SelectTrigger
|
||||
aria-invalid={!!errors.availabilityZone}
|
||||
className="w-full"
|
||||
id="availabilityZone"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availabilityZones.map((az) => (
|
||||
<SelectItem key={az.ZoneName} value={az.ZoneName ?? ""}>
|
||||
{az.ZoneName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
<FieldError errors={[errors.availabilityZone]} />
|
||||
</Field>
|
||||
|
||||
+247
-23
@@ -1,6 +1,6 @@
|
||||
import { useSuspenseQuery } from "@tanstack/react-query"
|
||||
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"
|
||||
import { Trash2 } from "lucide-react"
|
||||
import { Camera, Link2, Link2Off, Trash2 } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
|
||||
import { BackLink } from "@/components/back-link"
|
||||
@@ -8,6 +8,7 @@ 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 { StateBadge } from "@/components/state-badge"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -18,17 +19,34 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { Field, FieldTitle } from "@/components/ui/field"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { formatDateTime } from "@/lib/utils"
|
||||
import { useDeleteVolume } from "@/mutations/ec2"
|
||||
import { ec2VolumeQueryOptions } from "@/queries/ec2"
|
||||
import {
|
||||
useAttachVolume,
|
||||
useDeleteVolume,
|
||||
useDetachVolume,
|
||||
} from "@/mutations/ec2"
|
||||
import { ec2InstancesQueryOptions, ec2VolumeQueryOptions } from "@/queries/ec2"
|
||||
|
||||
type DialogType = "delete" | "attach" | "detach" | null
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_auth/ec2/(volumes)/describe-volumes/$id",
|
||||
)({
|
||||
loader: async ({ context, params }) => {
|
||||
await context.queryClient.ensureQueryData(ec2VolumeQueryOptions(params.id))
|
||||
await Promise.all([
|
||||
context.queryClient.ensureQueryData(ec2VolumeQueryOptions(params.id)),
|
||||
context.queryClient.ensureQueryData(ec2InstancesQueryOptions),
|
||||
])
|
||||
},
|
||||
head: ({ params }) => ({
|
||||
meta: [
|
||||
@@ -44,20 +62,63 @@ function VolumeDetail() {
|
||||
const { id } = Route.useParams()
|
||||
const navigate = useNavigate()
|
||||
const { data } = useSuspenseQuery(ec2VolumeQueryOptions(id))
|
||||
const { data: instancesData } = useSuspenseQuery(ec2InstancesQueryOptions)
|
||||
const volume = data.Volumes?.[0]
|
||||
const deleteMutation = useDeleteVolume()
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
const attachMutation = useAttachVolume()
|
||||
const detachMutation = useDetachVolume()
|
||||
const [activeDialog, setActiveDialog] = useState<DialogType>(null)
|
||||
const [attachInstanceId, setAttachInstanceId] = useState("")
|
||||
const [attachDevice, setAttachDevice] = useState("")
|
||||
const [detachForce, setDetachForce] = useState(false)
|
||||
|
||||
const canDelete =
|
||||
const runningInstances =
|
||||
instancesData.Reservations?.flatMap(
|
||||
(r) => r.Instances?.filter((i) => i.State?.Name === "running") ?? [],
|
||||
) ?? []
|
||||
|
||||
const isAvailable =
|
||||
volume?.State === "available" &&
|
||||
(!volume.Attachments || volume.Attachments.length === 0)
|
||||
|
||||
const closeDialog = () => setActiveDialog(null)
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await deleteMutation.mutateAsync(id)
|
||||
navigate({ to: "/ec2/describe-volumes" })
|
||||
} finally {
|
||||
setShowDeleteDialog(false)
|
||||
closeDialog()
|
||||
}
|
||||
}
|
||||
|
||||
const handleAttach = async () => {
|
||||
try {
|
||||
await attachMutation.mutateAsync({
|
||||
volumeId: id,
|
||||
instanceId: attachInstanceId,
|
||||
device: attachDevice || undefined,
|
||||
})
|
||||
closeDialog()
|
||||
setAttachInstanceId("")
|
||||
setAttachDevice("")
|
||||
} catch {
|
||||
// error shown via attachMutation.error
|
||||
}
|
||||
}
|
||||
|
||||
const handleDetach = async () => {
|
||||
const instanceId = volume?.Attachments?.[0]?.InstanceId
|
||||
try {
|
||||
await detachMutation.mutateAsync({
|
||||
volumeId: id,
|
||||
instanceId: instanceId || undefined,
|
||||
force: detachForce || undefined,
|
||||
})
|
||||
closeDialog()
|
||||
setDetachForce(false)
|
||||
} catch {
|
||||
// error shown via detachMutation.error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,8 +131,6 @@ function VolumeDetail() {
|
||||
)
|
||||
}
|
||||
|
||||
const createTime = formatDateTime(volume.CreateTime)
|
||||
|
||||
return (
|
||||
<>
|
||||
<BackLink to="/ec2/describe-volumes">Back to volumes</BackLink>
|
||||
@@ -83,20 +142,34 @@ function VolumeDetail() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{attachMutation.error && (
|
||||
<ErrorBanner
|
||||
error={attachMutation.error}
|
||||
msg="Failed to attach volume"
|
||||
/>
|
||||
)}
|
||||
|
||||
{detachMutation.error && (
|
||||
<ErrorBanner
|
||||
error={detachMutation.error}
|
||||
msg="Failed to detach volume"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="space-y-6">
|
||||
<PageHeading
|
||||
actions={
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
disabled={!canDelete}
|
||||
onClick={() => setShowDeleteDialog(true)}
|
||||
disabled={!isAvailable}
|
||||
onClick={() => setActiveDialog("delete")}
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
Delete
|
||||
</Button>
|
||||
{volume.State === "available" ? (
|
||||
{isAvailable ? (
|
||||
<Link
|
||||
className={buttonVariants({ variant: "outline" })}
|
||||
params={{ id: volume.VolumeId }}
|
||||
@@ -109,13 +182,35 @@ function VolumeDetail() {
|
||||
Resize Volume
|
||||
</Button>
|
||||
)}
|
||||
{volume.State && (
|
||||
<Badge
|
||||
variant={volume.State === "available" ? "default" : "outline"}
|
||||
{isAvailable ? (
|
||||
<Button
|
||||
onClick={() => setActiveDialog("attach")}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
{volume.State}
|
||||
</Badge>
|
||||
)}
|
||||
<Link2 className="size-4" />
|
||||
Attach
|
||||
</Button>
|
||||
) : null}
|
||||
{volume.Attachments && volume.Attachments.length > 0 ? (
|
||||
<Button
|
||||
onClick={() => setActiveDialog("detach")}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<Link2Off className="size-4" />
|
||||
Detach
|
||||
</Button>
|
||||
) : null}
|
||||
<Link
|
||||
className={buttonVariants({ variant: "outline", size: "sm" })}
|
||||
search={{ volumeId: volume.VolumeId }}
|
||||
to="/ec2/create-snapshot"
|
||||
>
|
||||
<Camera className="size-4" />
|
||||
Create Snapshot
|
||||
</Link>
|
||||
<StateBadge state={volume.State} />
|
||||
</div>
|
||||
}
|
||||
subtitle="EC2 Volume Details"
|
||||
@@ -133,7 +228,10 @@ function VolumeDetail() {
|
||||
label="Availability Zone"
|
||||
value={volume.AvailabilityZone}
|
||||
/>
|
||||
<DetailRow label="Create Time" value={createTime} />
|
||||
<DetailRow
|
||||
label="Create Time"
|
||||
value={formatDateTime(volume.CreateTime)}
|
||||
/>
|
||||
<DetailRow
|
||||
label="Encrypted"
|
||||
value={volume.Encrypted ? "Yes" : "No"}
|
||||
@@ -147,7 +245,20 @@ function VolumeDetail() {
|
||||
<DetailCard.Header>Attachments</DetailCard.Header>
|
||||
{volume.Attachments.map((attachment) => (
|
||||
<DetailCard.Content key={attachment.InstanceId}>
|
||||
<DetailRow label="Instance ID" value={attachment.InstanceId} />
|
||||
<DetailRow
|
||||
label="Instance ID"
|
||||
value={
|
||||
attachment.InstanceId ? (
|
||||
<Link
|
||||
className="text-primary hover:underline"
|
||||
params={{ id: attachment.InstanceId }}
|
||||
to="/ec2/describe-instances/$id"
|
||||
>
|
||||
{attachment.InstanceId}
|
||||
</Link>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
<DetailRow label="Device" value={attachment.Device} />
|
||||
<DetailRow label="Status" value={attachment.State} />
|
||||
<DetailRow
|
||||
@@ -164,7 +275,14 @@ function VolumeDetail() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AlertDialog onOpenChange={setShowDeleteDialog} open={showDeleteDialog}>
|
||||
<AlertDialog
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
closeDialog()
|
||||
}
|
||||
}}
|
||||
open={activeDialog === "delete"}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Volume</AlertDialogTitle>
|
||||
@@ -179,7 +297,113 @@ function VolumeDetail() {
|
||||
disabled={deleteMutation.isPending}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{deleteMutation.isPending ? "Deleting…" : "Delete"}
|
||||
{deleteMutation.isPending ? "Deleting\u2026" : "Delete"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<AlertDialog
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
closeDialog()
|
||||
setAttachInstanceId("")
|
||||
setAttachDevice("")
|
||||
}
|
||||
}}
|
||||
open={activeDialog === "attach"}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Attach Volume</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Attach this volume to a running instance.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<Field>
|
||||
<FieldTitle>
|
||||
<label htmlFor="attachInstance">Instance</label>
|
||||
</FieldTitle>
|
||||
<Select
|
||||
onValueChange={(value) => setAttachInstanceId(value ?? "")}
|
||||
value={attachInstanceId}
|
||||
>
|
||||
<SelectTrigger className="w-full" id="attachInstance">
|
||||
<SelectValue placeholder="Select an instance" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{runningInstances.map((instance) => {
|
||||
const name = instance.Tags?.find(
|
||||
(t) => t.Key === "Name",
|
||||
)?.Value
|
||||
return (
|
||||
<SelectItem
|
||||
key={instance.InstanceId}
|
||||
value={instance.InstanceId ?? ""}
|
||||
>
|
||||
{instance.InstanceId}
|
||||
{name ? ` (${name})` : ""}
|
||||
</SelectItem>
|
||||
)
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldTitle>
|
||||
<label htmlFor="attachDevice">Device (optional)</label>
|
||||
</FieldTitle>
|
||||
<Input
|
||||
id="attachDevice"
|
||||
onChange={(e) => setAttachDevice(e.target.value)}
|
||||
placeholder="/dev/sdf"
|
||||
value={attachDevice}
|
||||
/>
|
||||
</Field>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={attachMutation.isPending || !attachInstanceId}
|
||||
onClick={handleAttach}
|
||||
>
|
||||
{attachMutation.isPending ? "Attaching\u2026" : "Attach"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<AlertDialog
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
closeDialog()
|
||||
setDetachForce(false)
|
||||
}
|
||||
}}
|
||||
open={activeDialog === "detach"}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Detach Volume</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to detach the volume "{volume.VolumeId}"
|
||||
from instance "{volume.Attachments?.[0]?.InstanceId}"?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
checked={detachForce}
|
||||
onChange={(e) => setDetachForce(e.target.checked)}
|
||||
type="checkbox"
|
||||
/>
|
||||
Force detach
|
||||
</label>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={detachMutation.isPending}
|
||||
onClick={handleDetach}
|
||||
>
|
||||
{detachMutation.isPending ? "Detaching\u2026" : "Detach"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
+2
-12
@@ -4,7 +4,7 @@ import { createFileRoute, Link } from "@tanstack/react-router"
|
||||
|
||||
import { ListCard } from "@/components/list-card"
|
||||
import { PageHeading } from "@/components/page-heading"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { StateBadge } from "@/components/state-badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ec2VolumesQueryOptions } from "@/queries/ec2"
|
||||
|
||||
@@ -46,17 +46,7 @@ function Volumes() {
|
||||
}
|
||||
return (
|
||||
<ListCard
|
||||
badge={
|
||||
volume.State ? (
|
||||
<Badge
|
||||
variant={
|
||||
volume.State === "available" ? "default" : "outline"
|
||||
}
|
||||
>
|
||||
{volume.State}
|
||||
</Badge>
|
||||
) : undefined
|
||||
}
|
||||
badge={<StateBadge state={volume.State} />}
|
||||
key={volume.VolumeId}
|
||||
params={{ id: volume.VolumeId }}
|
||||
subtitle={`${volume.Size} GiB • ${volume.VolumeType}`}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router"
|
||||
import { useForm } from "react-hook-form"
|
||||
|
||||
import { BackLink } from "@/components/back-link"
|
||||
import { ErrorBanner } from "@/components/error-banner"
|
||||
import { PageHeading } from "@/components/page-heading"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Field, FieldError, FieldTitle } from "@/components/ui/field"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { useCreateVpc } from "@/mutations/ec2"
|
||||
import { type CreateVpcFormData, createVpcSchema } from "@/types/ec2"
|
||||
|
||||
export const Route = createFileRoute("/_auth/ec2/(vpc)/create-vpc")({
|
||||
head: () => ({
|
||||
meta: [
|
||||
{
|
||||
title: "Create VPC | EC2 | Mulga",
|
||||
},
|
||||
],
|
||||
}),
|
||||
component: CreateVpc,
|
||||
})
|
||||
|
||||
function CreateVpc() {
|
||||
const navigate = useNavigate()
|
||||
const createMutation = useCreateVpc()
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<CreateVpcFormData>({
|
||||
resolver: zodResolver(createVpcSchema),
|
||||
defaultValues: {
|
||||
cidrBlock: "10.0.0.0/16",
|
||||
name: "",
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit = async (data: CreateVpcFormData) => {
|
||||
const result = await createMutation.mutateAsync(data)
|
||||
const vpcId = result.Vpc?.VpcId
|
||||
if (vpcId) {
|
||||
navigate({ to: "/ec2/describe-vpcs/$id", params: { id: vpcId } })
|
||||
} else {
|
||||
navigate({ to: "/ec2/describe-vpcs" })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<BackLink to="/ec2/describe-vpcs">Back to VPCs</BackLink>
|
||||
|
||||
<PageHeading title="Create VPC" />
|
||||
|
||||
{createMutation.error && (
|
||||
<ErrorBanner error={createMutation.error} msg="Failed to create VPC" />
|
||||
)}
|
||||
|
||||
<form className="max-w-4xl space-y-6" onSubmit={handleSubmit(onSubmit)}>
|
||||
<Field>
|
||||
<FieldTitle>
|
||||
<label htmlFor="cidrBlock">CIDR Block</label>
|
||||
</FieldTitle>
|
||||
<Input
|
||||
aria-invalid={!!errors.cidrBlock}
|
||||
id="cidrBlock"
|
||||
placeholder="10.0.0.0/16"
|
||||
{...register("cidrBlock")}
|
||||
/>
|
||||
<FieldError errors={[errors.cidrBlock]} />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldTitle>
|
||||
<label htmlFor="name">Name (optional)</label>
|
||||
</FieldTitle>
|
||||
<Input id="name" placeholder="my-vpc" {...register("name")} />
|
||||
</Field>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
disabled={isSubmitting || createMutation.isPending}
|
||||
onClick={() => navigate({ to: "/ec2/describe-vpcs" })}
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isSubmitting || createMutation.isPending}
|
||||
type="submit"
|
||||
>
|
||||
{isSubmitting || createMutation.isPending
|
||||
? "Creating\u2026"
|
||||
: "Create VPC"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import type { Subnet } from "@aws-sdk/client-ec2"
|
||||
import { useSuspenseQuery } from "@tanstack/react-query"
|
||||
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"
|
||||
import { Trash2 } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
|
||||
import { BackLink } from "@/components/back-link"
|
||||
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 { StateBadge } from "@/components/state-badge"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useDeleteVpc } from "@/mutations/ec2"
|
||||
import { ec2SubnetsQueryOptions, ec2VpcQueryOptions } from "@/queries/ec2"
|
||||
|
||||
export const Route = createFileRoute("/_auth/ec2/(vpc)/describe-vpcs/$id")({
|
||||
loader: async ({ context, params }) => {
|
||||
await Promise.all([
|
||||
context.queryClient.ensureQueryData(ec2VpcQueryOptions(params.id)),
|
||||
context.queryClient.ensureQueryData(ec2SubnetsQueryOptions),
|
||||
])
|
||||
},
|
||||
head: ({ params }) => ({
|
||||
meta: [
|
||||
{
|
||||
title: `${params.id} | VPC | Mulga`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
component: VpcDetail,
|
||||
})
|
||||
|
||||
function VpcDetail() {
|
||||
const { id } = Route.useParams()
|
||||
const navigate = useNavigate()
|
||||
const { data } = useSuspenseQuery(ec2VpcQueryOptions(id))
|
||||
const { data: subnetsData } = useSuspenseQuery(ec2SubnetsQueryOptions)
|
||||
const vpc = data.Vpcs?.[0]
|
||||
const deleteMutation = useDeleteVpc()
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
|
||||
const associatedSubnets =
|
||||
subnetsData.Subnets?.filter((s: Subnet) => s.VpcId === id) ?? []
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await deleteMutation.mutateAsync(id)
|
||||
navigate({ to: "/ec2/describe-vpcs" })
|
||||
} finally {
|
||||
setShowDeleteDialog(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!vpc?.VpcId) {
|
||||
return (
|
||||
<>
|
||||
<BackLink to="/ec2/describe-vpcs">Back to VPCs</BackLink>
|
||||
<p className="text-muted-foreground">VPC not found.</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const name = vpc.Tags?.find((t) => t.Key === "Name")?.Value
|
||||
|
||||
return (
|
||||
<>
|
||||
<BackLink to="/ec2/describe-vpcs">Back to VPCs</BackLink>
|
||||
|
||||
{deleteMutation.error && (
|
||||
<ErrorBanner error={deleteMutation.error} msg="Failed to delete VPC" />
|
||||
)}
|
||||
|
||||
<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>
|
||||
<StateBadge state={vpc.State} />
|
||||
</div>
|
||||
}
|
||||
subtitle="VPC Details"
|
||||
title={name ? `${vpc.VpcId} (${name})` : vpc.VpcId}
|
||||
/>
|
||||
|
||||
<DetailCard>
|
||||
<DetailCard.Header>VPC Information</DetailCard.Header>
|
||||
<DetailCard.Content>
|
||||
<DetailRow label="VPC ID" value={vpc.VpcId} />
|
||||
<DetailRow label="State" value={vpc.State} />
|
||||
<DetailRow label="CIDR Block" value={vpc.CidrBlock} />
|
||||
<DetailRow
|
||||
label="Is Default"
|
||||
value={vpc.IsDefault ? "Yes" : "No"}
|
||||
/>
|
||||
<DetailRow label="DHCP Options ID" value={vpc.DhcpOptionsId} />
|
||||
<DetailRow label="Instance Tenancy" value={vpc.InstanceTenancy} />
|
||||
<DetailRow label="Owner ID" value={vpc.OwnerId} />
|
||||
</DetailCard.Content>
|
||||
</DetailCard>
|
||||
|
||||
{associatedSubnets.length > 0 && (
|
||||
<DetailCard>
|
||||
<DetailCard.Header>
|
||||
Subnets ({associatedSubnets.length})
|
||||
</DetailCard.Header>
|
||||
{associatedSubnets.map((subnet: Subnet) => {
|
||||
if (!subnet.SubnetId) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<DetailCard.Content key={subnet.SubnetId}>
|
||||
<DetailRow
|
||||
label="Subnet ID"
|
||||
value={
|
||||
<Link
|
||||
className="text-primary hover:underline"
|
||||
params={{ id: subnet.SubnetId }}
|
||||
to="/ec2/describe-subnets/$id"
|
||||
>
|
||||
{subnet.SubnetId}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<DetailRow label="CIDR Block" value={subnet.CidrBlock} />
|
||||
<DetailRow
|
||||
label="Availability Zone"
|
||||
value={subnet.AvailabilityZone}
|
||||
/>
|
||||
<DetailRow label="State" value={subnet.State} />
|
||||
</DetailCard.Content>
|
||||
)
|
||||
})}
|
||||
</DetailCard>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AlertDialog onOpenChange={setShowDeleteDialog} open={showDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete VPC</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete the VPC "{vpc.VpcId}"? This action
|
||||
cannot be undone. The VPC must have no associated subnets or
|
||||
internet gateways.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={deleteMutation.isPending}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{deleteMutation.isPending ? "Deleting\u2026" : "Delete"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { Vpc } 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 { StateBadge } from "@/components/state-badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ec2VpcsQueryOptions } from "@/queries/ec2"
|
||||
|
||||
export const Route = createFileRoute("/_auth/ec2/(vpc)/describe-vpcs/")({
|
||||
loader: async ({ context }) => {
|
||||
await context.queryClient.ensureQueryData(ec2VpcsQueryOptions)
|
||||
},
|
||||
head: () => ({
|
||||
meta: [
|
||||
{
|
||||
title: "VPCs | EC2 | Mulga",
|
||||
},
|
||||
],
|
||||
}),
|
||||
component: Vpcs,
|
||||
})
|
||||
|
||||
function Vpcs() {
|
||||
const { data } = useSuspenseQuery(ec2VpcsQueryOptions)
|
||||
|
||||
const vpcs = data.Vpcs || []
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeading
|
||||
actions={
|
||||
<Link to="/ec2/create-vpc">
|
||||
<Button>Create VPC</Button>
|
||||
</Link>
|
||||
}
|
||||
title="VPCs"
|
||||
/>
|
||||
|
||||
{vpcs.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{vpcs.map((vpc: Vpc) => {
|
||||
if (!vpc.VpcId) {
|
||||
return null
|
||||
}
|
||||
const name = vpc.Tags?.find((t) => t.Key === "Name")?.Value
|
||||
return (
|
||||
<ListCard
|
||||
badge={<StateBadge state={vpc.State} />}
|
||||
key={vpc.VpcId}
|
||||
params={{ id: vpc.VpcId }}
|
||||
subtitle={vpc.CidrBlock ?? ""}
|
||||
title={name ? `${vpc.VpcId} (${name})` : vpc.VpcId}
|
||||
to="/ec2/describe-vpcs/$id"
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground">No VPCs found.</p>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Image } from "@aws-sdk/client-ec2"
|
||||
import { Link } from "@tanstack/react-router"
|
||||
|
||||
import { DetailCard } from "@/components/detail-card"
|
||||
import { DetailRow } from "@/components/detail-row"
|
||||
@@ -23,7 +24,22 @@ export function AmiDetails({
|
||||
{showExtendedDetails ? "AMI Information" : "AMI Details"}
|
||||
</DetailCard.Header>
|
||||
<DetailCard.Content>
|
||||
<DetailRow label="AMI ID" value={image.ImageId} />
|
||||
<DetailRow
|
||||
label="AMI ID"
|
||||
value={
|
||||
image.ImageId && !showExtendedDetails ? (
|
||||
<Link
|
||||
className="text-primary hover:underline"
|
||||
params={{ id: image.ImageId }}
|
||||
to="/ec2/describe-images/$id"
|
||||
>
|
||||
{image.ImageId}
|
||||
</Link>
|
||||
) : (
|
||||
image.ImageId
|
||||
)
|
||||
}
|
||||
/>
|
||||
<DetailRow label="AMI Name" value={image.Name} />
|
||||
<DetailRow label="Description" value={image.Description} />
|
||||
<DetailRow label="Architecture" value={image.Architecture} />
|
||||
@@ -83,7 +99,15 @@ export function AmiDetails({
|
||||
{mapping.Ebs.SnapshotId && (
|
||||
<DetailRow
|
||||
label="Snapshot ID"
|
||||
value={mapping.Ebs.SnapshotId}
|
||||
value={
|
||||
<Link
|
||||
className="text-primary hover:underline"
|
||||
params={{ id: mapping.Ebs.SnapshotId }}
|
||||
to="/ec2/describe-snapshots/$id"
|
||||
>
|
||||
{mapping.Ebs.SnapshotId}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
import { useState } from "react"
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { Field, FieldTitle } from "@/components/ui/field"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { useCreateImage } from "@/mutations/ec2"
|
||||
|
||||
interface CreateImageDialogProps {
|
||||
instanceId: string
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function CreateImageDialog({
|
||||
instanceId,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: CreateImageDialogProps) {
|
||||
const createImageMutation = useCreateImage()
|
||||
const [imageName, setImageName] = useState("")
|
||||
const [imageDescription, setImageDescription] = useState("")
|
||||
const [createdImageId, setCreatedImageId] = useState<string | null>(null)
|
||||
|
||||
function handleClose(nextOpen: boolean) {
|
||||
if (!nextOpen) {
|
||||
setImageName("")
|
||||
setImageDescription("")
|
||||
setCreatedImageId(null)
|
||||
}
|
||||
onOpenChange(nextOpen)
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
try {
|
||||
const result = await createImageMutation.mutateAsync({
|
||||
instanceId,
|
||||
name: imageName,
|
||||
description: imageDescription || undefined,
|
||||
})
|
||||
setCreatedImageId(result.ImageId ?? null)
|
||||
setImageName("")
|
||||
setImageDescription("")
|
||||
} catch {
|
||||
// error shown via createImageMutation.error
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertDialog onOpenChange={handleClose} open={open}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{createdImageId ? "Image Created" : "Create Image (AMI)"}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{createdImageId
|
||||
? `Image "${createdImageId}" was created successfully.`
|
||||
: `Create an AMI from instance "${instanceId}".`}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
{!createdImageId && (
|
||||
<>
|
||||
<Field>
|
||||
<FieldTitle>
|
||||
<label htmlFor="imageName">Name</label>
|
||||
</FieldTitle>
|
||||
<Input
|
||||
id="imageName"
|
||||
onChange={(e) => setImageName(e.target.value)}
|
||||
placeholder="my-image"
|
||||
value={imageName}
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldTitle>
|
||||
<label htmlFor="imageDescription">Description</label>
|
||||
</FieldTitle>
|
||||
<Textarea
|
||||
id="imageDescription"
|
||||
onChange={(e) => setImageDescription(e.target.value)}
|
||||
placeholder="Optional description"
|
||||
rows={2}
|
||||
value={imageDescription}
|
||||
/>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
{createdImageId ? "Close" : "Cancel"}
|
||||
</AlertDialogCancel>
|
||||
{!createdImageId && (
|
||||
<AlertDialogAction
|
||||
disabled={createImageMutation.isPending || !imageName.trim()}
|
||||
onClick={handleCreate}
|
||||
>
|
||||
{createImageMutation.isPending
|
||||
? "Creating\u2026"
|
||||
: "Create Image"}
|
||||
</AlertDialogAction>
|
||||
)}
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ export const createInstanceSchema = z.object({
|
||||
imageId: z.string("Please select an Image"),
|
||||
instanceType: z.string("Please select an instance type"),
|
||||
keyName: z.string("Please select a key pair"),
|
||||
subnetId: z.string().optional(),
|
||||
count: z
|
||||
.int("Instance count must be a whole number")
|
||||
.min(1, "Instance count must be at least 1"),
|
||||
@@ -11,10 +12,7 @@ export const createInstanceSchema = z.object({
|
||||
|
||||
export type CreateInstanceFormData = z.infer<typeof createInstanceSchema>
|
||||
|
||||
export type CreateInstanceParams = CreateInstanceFormData & {
|
||||
securityGroupIds: string[]
|
||||
subnetId: string
|
||||
}
|
||||
export type CreateInstanceParams = CreateInstanceFormData
|
||||
|
||||
export const createKeyPairSchema = z.object({
|
||||
keyName: z
|
||||
@@ -67,3 +65,78 @@ export const modifyVolumeSchema = z.object({
|
||||
export type ModifyVolumeFormData = z.infer<typeof modifyVolumeSchema>
|
||||
|
||||
export type ModifyVolumeParams = ModifyVolumeFormData & { volumeId: string }
|
||||
|
||||
export const createSnapshotSchema = z.object({
|
||||
volumeId: z.string().min(1, "Volume is required"),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
export type CreateSnapshotFormData = z.infer<typeof createSnapshotSchema>
|
||||
|
||||
export const copySnapshotSchema = z.object({
|
||||
sourceSnapshotId: z.string().min(1, "Source snapshot is required"),
|
||||
sourceRegion: z.string().min(1, "Source region is required"),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
export type CopySnapshotFormData = z.infer<typeof copySnapshotSchema>
|
||||
|
||||
export const attachVolumeSchema = z.object({
|
||||
volumeId: z.string().min(1, "Volume is required"),
|
||||
instanceId: z.string().min(1, "Instance is required"),
|
||||
device: z.string().optional(),
|
||||
})
|
||||
|
||||
export type AttachVolumeFormData = z.infer<typeof attachVolumeSchema>
|
||||
|
||||
export const detachVolumeSchema = z.object({
|
||||
volumeId: z.string().min(1, "Volume is required"),
|
||||
instanceId: z.string().optional(),
|
||||
force: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export type DetachVolumeFormData = z.infer<typeof detachVolumeSchema>
|
||||
|
||||
export const modifyInstanceTypeSchema = z.object({
|
||||
instanceType: z.string().min(1, "Instance type is required"),
|
||||
})
|
||||
|
||||
export type ModifyInstanceTypeFormData = z.infer<
|
||||
typeof modifyInstanceTypeSchema
|
||||
>
|
||||
|
||||
export const createImageSchema = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
export type CreateImageFormData = z.infer<typeof createImageSchema>
|
||||
|
||||
export type CreateImageParams = CreateImageFormData & { instanceId: string }
|
||||
|
||||
export const createSubnetSchema = z.object({
|
||||
vpcId: z.string().min(1, "VPC is required"),
|
||||
cidrBlock: z
|
||||
.string()
|
||||
.min(1, "CIDR block is required")
|
||||
.regex(
|
||||
/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/,
|
||||
"Must be a valid CIDR block (e.g. 10.0.1.0/24)",
|
||||
),
|
||||
availabilityZone: z.string().optional(),
|
||||
})
|
||||
|
||||
export type CreateSubnetFormData = z.infer<typeof createSubnetSchema>
|
||||
|
||||
export const createVpcSchema = z.object({
|
||||
cidrBlock: z
|
||||
.string()
|
||||
.min(1, "CIDR block is required")
|
||||
.regex(
|
||||
/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/,
|
||||
"Must be a valid CIDR block (e.g. 10.0.0.0/16)",
|
||||
),
|
||||
name: z.string().optional(),
|
||||
})
|
||||
|
||||
export type CreateVpcFormData = z.infer<typeof createVpcSchema>
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"target": "ES2023",
|
||||
"jsx": "react-jsx",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"types": ["vite/client"],
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { defineConfig } from "vite"
|
||||
export default defineConfig({
|
||||
envDir: "../",
|
||||
build: {
|
||||
target: "es2023",
|
||||
chunkSizeWarningLimit: 1500,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
|
||||
Reference in New Issue
Block a user