feat: MSK, MSK CONNECT and SES
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { CopyValueButton } from './CopyValueButton'
|
||||
import {
|
||||
AlertTriangle,
|
||||
HardDrive,
|
||||
@@ -32,10 +33,12 @@ import {
|
||||
startEc2Instance,
|
||||
stopEc2Instance,
|
||||
terminateEc2Instance,
|
||||
updateEc2InstanceSecurityGroups,
|
||||
updateEc2InstanceTags,
|
||||
type Ec2Image,
|
||||
type Ec2KeyPair,
|
||||
type Ec2SecurityGroup,
|
||||
type Ec2SecurityGroupRef,
|
||||
type Ec2Subnet,
|
||||
type Ec2Tag,
|
||||
type Ec2Vpc,
|
||||
@@ -143,6 +146,42 @@ function InlineTagEditor({ instanceId, initialTags }: { instanceId: string; init
|
||||
)
|
||||
}
|
||||
|
||||
function InlineSecurityGroupEditor({ instanceId, vpcId, initialGroups }: {
|
||||
instanceId: string; vpcId?: string; initialGroups: Ec2SecurityGroupRef[]
|
||||
}) {
|
||||
const qc = useQueryClient()
|
||||
const [selected, setSelected] = useState(() => new Set(initialGroups.map((group) => group.id).filter(Boolean) as string[]))
|
||||
const groups = useQuery({queryKey: ['ec2', 'security-groups', vpcId], queryFn: ({signal}) => listEc2SecurityGroups(vpcId, signal)})
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => updateEc2InstanceSecurityGroups(instanceId, [...selected]),
|
||||
onSuccess: () => {
|
||||
void qc.invalidateQueries({queryKey: ['ec2', 'instance', instanceId]})
|
||||
void qc.invalidateQueries({queryKey: ['ec2', 'instances']})
|
||||
void qc.invalidateQueries({queryKey: ['cloud-resources', 'aws', 'compute']})
|
||||
},
|
||||
})
|
||||
const compatibleGroups = (groups.data ?? []).filter((group) => !vpcId || group.vpcId === vpcId)
|
||||
|
||||
return <div style={{display: 'flex', flexDirection: 'column', gap: 8}}>
|
||||
{compatibleGroups.map((group) => <label key={group.groupId} style={{display: 'flex', gap: 6, alignItems: 'center'}}>
|
||||
<input type="checkbox" checked={selected.has(group.groupId)} onChange={(event) => {
|
||||
setSelected((previous) => {
|
||||
const next = new Set(previous)
|
||||
if (event.target.checked) next.add(group.groupId)
|
||||
else next.delete(group.groupId)
|
||||
return next
|
||||
})
|
||||
}}/>
|
||||
<span>{group.groupName} <code>{group.groupId}</code></span>
|
||||
</label>)}
|
||||
{groups.isError && <p className="error-text compact-text">Unable to load security groups.</p>}
|
||||
{mutation.isError && <p className="error-text compact-text">{mutation.error instanceof Error ? mutation.error.message : 'Save failed'}</p>}
|
||||
<button className="button primary compact" disabled={groups.isLoading || selected.size === 0 || mutation.isPending} onClick={() => mutation.mutate()}>
|
||||
<Save size={13}/>{mutation.isPending ? 'Saving' : 'Save security groups'}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
// ─── Create AMI modal (inline) ────────────────────────────────────────────────
|
||||
|
||||
function CreateAmiModal({ instanceId, instanceName, onClose, onCreated }: {
|
||||
@@ -341,7 +380,7 @@ function DeregisterAmiConfirm({ imageId, onConfirm, onCancel, isPending }: {
|
||||
|
||||
function InstancePanel({ instanceId }: { instanceId: string }) {
|
||||
const qc = useQueryClient()
|
||||
const [tab, setTab] = useState<'details' | 'tags' | 'console'>('details')
|
||||
const [tab, setTab] = useState<'details' | 'connection' | 'securityGroups' | 'tags' | 'console'>('details')
|
||||
const [showAmiModal, setShowAmiModal] = useState(false)
|
||||
const [showConsoleModal, setShowConsoleModal] = useState(false)
|
||||
const [terminateStep, setTerminateStep] = useState<'idle' | 'confirm'>('idle')
|
||||
@@ -510,6 +549,18 @@ function InstancePanel({ instanceId }: { instanceId: string }) {
|
||||
>
|
||||
Details
|
||||
</button>
|
||||
<button
|
||||
className={`sns-tab${tab === 'connection' ? ' active' : ''}`}
|
||||
onClick={() => setTab('connection')}
|
||||
>
|
||||
Connect
|
||||
</button>
|
||||
<button
|
||||
className={`sns-tab${tab === 'securityGroups' ? ' active' : ''}`}
|
||||
onClick={() => setTab('securityGroups')}
|
||||
>
|
||||
Security groups
|
||||
</button>
|
||||
<button
|
||||
className={`sns-tab${tab === 'tags' ? ' active' : ''}`}
|
||||
onClick={() => setTab('tags')}
|
||||
@@ -602,10 +653,38 @@ function InstancePanel({ instanceId }: { instanceId: string }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'connection' && (() => {
|
||||
const connection = inst.connection
|
||||
const command = connection?.sshHost && connection.sshPort && inst.keyName
|
||||
? `ssh -i ${inst.keyName}.pem -p ${connection.sshPort} root@${connection.sshHost}`
|
||||
: undefined
|
||||
if (state === 'stopped') {
|
||||
return <p style={{ fontSize: 12, color: 'var(--text-muted)', margin: 0 }}>Start this instance before connecting over SSH.</p>
|
||||
}
|
||||
if (state === 'terminated') {
|
||||
return <p style={{ fontSize: 12, color: 'var(--text-muted)', margin: 0 }}>Terminated instances cannot accept connections.</p>
|
||||
}
|
||||
if (state !== 'running') {
|
||||
return <p style={{ fontSize: 12, color: 'var(--text-muted)', margin: 0 }}>SSH is unavailable while this instance is {state}.</p>
|
||||
}
|
||||
if (!command || connection?.containerStatus !== 'running') {
|
||||
return <p style={{ fontSize: 12, color: 'var(--text-muted)', margin: 0 }}>SSH is unavailable. Backing container status: {connection?.containerStatus ?? 'unavailable'}.</p>
|
||||
}
|
||||
return <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
<div className="meta-row"><span className="meta-label">SSH port</span><span className="meta-value">{connection.sshPort}</span></div>
|
||||
<div className="meta-row"><span className="meta-label">Backing container</span><span className="meta-value">{connection.containerStatus}</span></div>
|
||||
<div className="meta-row" style={{ gridColumn: '1 / -1' }}><span className="meta-label">Command</span><code style={{ overflowWrap: 'anywhere' }}>{command}</code><CopyValueButton value={command} label="SSH command" /></div>
|
||||
</div>
|
||||
})()}
|
||||
|
||||
{tab === 'tags' && (
|
||||
<InlineTagEditor instanceId={instanceId} initialTags={inst.tags} />
|
||||
)}
|
||||
|
||||
{tab === 'securityGroups' && (
|
||||
<InlineSecurityGroupEditor instanceId={instanceId} vpcId={inst.vpcId} initialGroups={inst.securityGroups}/>
|
||||
)}
|
||||
|
||||
{tab === 'console' && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<p style={{ fontSize: 12, color: 'var(--text-muted)', margin: 0 }}>
|
||||
|
||||
Reference in New Issue
Block a user