From 97c200de6a0a6c01c6e85fae756cada09900c042 Mon Sep 17 00:00:00 2001 From: michasdev Date: Tue, 21 Apr 2026 01:13:03 -0300 Subject: [PATCH 1/4] feat(desktop): implements base sqs resource browser --- apps/desktop/scripts/dynamo-smoke.cjs | 2 +- apps/desktop/scripts/s3-smoke.cjs | 2 +- apps/desktop/scripts/sqs-smoke.cjs | 2 +- apps/desktop/src/main/dynamodb-ipc.ts | 5 +- apps/desktop/src/main/index.ts | 2 + apps/desktop/src/main/local-endpoint.ts | 13 + apps/desktop/src/main/s3-ipc.ts | 5 +- apps/desktop/src/main/sqs-ipc.ts | 186 ++++++++ apps/desktop/src/preload/index.d.ts | 26 + apps/desktop/src/preload/index.ts | 40 +- .../src/features/resources/resources.tsx | 3 + .../sqs-browser/components/queue-details.tsx | 445 ++++++++++++++++++ .../sqs-browser/components/queues-list.tsx | 379 +++++++++++++++ .../sqs-browser/hooks/use-sqs-client.ts | 15 + .../src/features/sqs-browser/sqs-layout.tsx | 121 +++++ .../src/features/sqs-browser/types.ts | 49 ++ apps/desktop/src/renderer/src/router.tsx | 11 + 17 files changed, 1296 insertions(+), 10 deletions(-) create mode 100644 apps/desktop/src/main/local-endpoint.ts create mode 100644 apps/desktop/src/main/sqs-ipc.ts create mode 100644 apps/desktop/src/renderer/src/features/sqs-browser/components/queue-details.tsx create mode 100644 apps/desktop/src/renderer/src/features/sqs-browser/components/queues-list.tsx create mode 100644 apps/desktop/src/renderer/src/features/sqs-browser/hooks/use-sqs-client.ts create mode 100644 apps/desktop/src/renderer/src/features/sqs-browser/sqs-layout.tsx create mode 100644 apps/desktop/src/renderer/src/features/sqs-browser/types.ts diff --git a/apps/desktop/scripts/dynamo-smoke.cjs b/apps/desktop/scripts/dynamo-smoke.cjs index 2f9b2ac..cf23c4e 100644 --- a/apps/desktop/scripts/dynamo-smoke.cjs +++ b/apps/desktop/scripts/dynamo-smoke.cjs @@ -42,7 +42,7 @@ main().catch((error) => { }); async function main() { - const endpoint = process.env.MILDSTACK_DYNAMODB_ENDPOINT || process.env.AWS_DYNAMODB_ENDPOINT || `http://localhost:${port}`; + const endpoint = process.env.MILDSTACK_DYNAMODB_ENDPOINT || `http://localhost:${port}`; console.log(`Running AWS SDK smoke mode against ${endpoint}`); const client = new DynamoDBClient({ diff --git a/apps/desktop/scripts/s3-smoke.cjs b/apps/desktop/scripts/s3-smoke.cjs index a6be95e..247e5b1 100644 --- a/apps/desktop/scripts/s3-smoke.cjs +++ b/apps/desktop/scripts/s3-smoke.cjs @@ -43,7 +43,7 @@ function expectDefined(actual, message) { } async function main() { - const endpoint = process.env.MILDSTACK_S3_ENDPOINT || process.env.AWS_S3_ENDPOINT || `http://localhost:${port}`; + const endpoint = process.env.MILDSTACK_S3_ENDPOINT || `http://localhost:${port}`; console.log(`Running S3 behavioral validation against ${endpoint}`); const client = new S3Client({ diff --git a/apps/desktop/scripts/sqs-smoke.cjs b/apps/desktop/scripts/sqs-smoke.cjs index deda7ef..37a4af2 100644 --- a/apps/desktop/scripts/sqs-smoke.cjs +++ b/apps/desktop/scripts/sqs-smoke.cjs @@ -45,7 +45,7 @@ main().catch((error) => { }); async function main() { - const endpoint = process.env.MILDSTACK_SQS_ENDPOINT || process.env.AWS_SQS_ENDPOINT || `http://localhost:${port}`; + const endpoint = process.env.MILDSTACK_SQS_ENDPOINT || `http://localhost:${port}`; console.log(`Running AWS SDK smoke mode against ${endpoint}`); const client = new SQSClient({ diff --git a/apps/desktop/src/main/dynamodb-ipc.ts b/apps/desktop/src/main/dynamodb-ipc.ts index 3f12cad..bb62ac2 100644 --- a/apps/desktop/src/main/dynamodb-ipc.ts +++ b/apps/desktop/src/main/dynamodb-ipc.ts @@ -1,4 +1,4 @@ -import { getActiveInstancePort } from './instance-state' +import { resolveLocalEndpoint } from './local-endpoint' import { registerValidatedHandler } from './ipc-middleware' import { DynamoDBClient, @@ -269,8 +269,7 @@ function getClient(region = 'us-east-1'): DynamoDBClient { } function resolveDynamoDBEndpoint(): string { - const port = getActiveInstancePort() - return process.env.MILDSTACK_DYNAMODB_ENDPOINT || process.env.AWS_DYNAMODB_ENDPOINT || `http://127.0.0.1:${port}` + return resolveLocalEndpoint('dynamodb') } function normalizeRegion(region?: string): string { diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 900ba4b..2c08efe 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -5,6 +5,7 @@ import { autoUpdater } from 'electron-updater' import icon from '../../build/icon.png?asset' import { registerS3IpcHandlers } from './s3-ipc' import { registerDynamoDBIpcHandlers } from './dynamodb-ipc' +import { registerSQSIpcHandlers } from './sqs-ipc' import { registerMildStackIpcHandlers } from './mildstack-ipc' // Set app name for macOS Dock and Menu Bar as early as possible @@ -82,6 +83,7 @@ app.whenReady().then(() => { ipcMain.on('ping', () => console.log('pong')) registerS3IpcHandlers() registerDynamoDBIpcHandlers() + registerSQSIpcHandlers() registerMildStackIpcHandlers() createWindow() diff --git a/apps/desktop/src/main/local-endpoint.ts b/apps/desktop/src/main/local-endpoint.ts new file mode 100644 index 0000000..b3e8c4d --- /dev/null +++ b/apps/desktop/src/main/local-endpoint.ts @@ -0,0 +1,13 @@ +import { getActiveInstancePort } from './instance-state' + +export function resolveLocalEndpoint(serviceName: string): string { + const normalizedServiceName = serviceName.trim().toUpperCase() + const envKey = `MILDSTACK_${normalizedServiceName}_ENDPOINT` + const override = process.env[envKey]?.trim() + if (override) { + return override + } + + const port = getActiveInstancePort() + return `http://localhost:${port}` +} diff --git a/apps/desktop/src/main/s3-ipc.ts b/apps/desktop/src/main/s3-ipc.ts index 1a5ad4c..3a4397e 100644 --- a/apps/desktop/src/main/s3-ipc.ts +++ b/apps/desktop/src/main/s3-ipc.ts @@ -1,4 +1,4 @@ -import { getActiveInstancePort } from './instance-state' +import { resolveLocalEndpoint } from './local-endpoint' import { registerValidatedHandler } from './ipc-middleware' import { S3Client, @@ -212,8 +212,7 @@ function getClient(region = 'us-east-1'): S3Client { } function resolveS3Endpoint(): string { - const port = getActiveInstancePort() - return process.env.MILDSTACK_S3_ENDPOINT || process.env.AWS_S3_ENDPOINT || `http://127.0.0.1:${port}` + return resolveLocalEndpoint('s3') } function normalizeRegion(region?: string): string { diff --git a/apps/desktop/src/main/sqs-ipc.ts b/apps/desktop/src/main/sqs-ipc.ts new file mode 100644 index 0000000..cba1fe9 --- /dev/null +++ b/apps/desktop/src/main/sqs-ipc.ts @@ -0,0 +1,186 @@ +import { resolveLocalEndpoint } from './local-endpoint' +import { registerValidatedHandler } from './ipc-middleware' +import { + SQSClient, + ListQueuesCommand, + CreateQueueCommand, + DeleteQueueCommand, + GetQueueAttributesCommand, + SetQueueAttributesCommand, + SendMessageCommand, + ReceiveMessageCommand, + DeleteMessageCommand, + PurgeQueueCommand +} from '@aws-sdk/client-sqs' + +type SQSClientCacheEntry = { + region: string + endpoint: string + client: SQSClient +} + +const clientCache = new Map() + +export function registerSQSIpcHandlers(): void { + registerValidatedHandler('sqs:listQueues', async (_event, args: { region?: string }) => { + const client = getClient(args.region) + const response = await client.send(new ListQueuesCommand({})) + const queueUrls = response.QueueUrls ?? [] + + // Fetch attributes for each queue to build the summary + const summaries = await Promise.all( + queueUrls.map(async (url) => { + const attrResponse = await client.send(new GetQueueAttributesCommand({ + QueueUrl: url, + AttributeNames: ['All'] + })) + const attributes = attrResponse.Attributes || {} + + // Extract queue name from URL (last part after /) + const queueName = url.split('/').pop() || url + + return { + QueueUrl: url, + QueueName: queueName, + IsFifo: queueName.endsWith('.fifo'), + MessagesAvailable: parseInt(attributes.ApproximateNumberOfMessages || '0', 10), + MessagesDelayed: parseInt(attributes.ApproximateNumberOfMessagesDelayed || '0', 10), + MessagesInvisible: parseInt(attributes.ApproximateNumberOfMessagesNotVisible || '0', 10), + VisibilityTimeout: parseInt(attributes.VisibilityTimeout || '30', 10) + } + }) + ) + + return summaries + }) + + registerValidatedHandler('sqs:createQueue', async (_event, args: { queueName: string; isFifo?: boolean; region?: string }) => { + const attributes: Record = {} + let name = args.queueName + + if (args.isFifo) { + if (!name.endsWith('.fifo')) name += '.fifo' + attributes.FifoQueue = 'true' + } + + const response = await getClient(args.region).send(new CreateQueueCommand({ + QueueName: name, + Attributes: Object.keys(attributes).length > 0 ? attributes : undefined + })) + return response.QueueUrl + }) + + registerValidatedHandler('sqs:deleteQueue', async (_event, args: { queueUrl: string; region?: string }) => { + await getClient(args.region).send(new DeleteQueueCommand({ QueueUrl: args.queueUrl })) + return null + }) + + registerValidatedHandler('sqs:getQueueAttributes', async (_event, args: { queueUrl: string; region?: string }) => { + const response = await getClient(args.region).send(new GetQueueAttributesCommand({ + QueueUrl: args.queueUrl, + AttributeNames: ['All'] + })) + return response.Attributes || {} + }) + + registerValidatedHandler('sqs:setQueueAttributes', async (_event, args: { queueUrl: string; attributes: Record; region?: string }) => { + await getClient(args.region).send(new SetQueueAttributesCommand({ + QueueUrl: args.queueUrl, + Attributes: args.attributes + })) + return null + }) + + registerValidatedHandler('sqs:purgeQueue', async (_event, args: { queueUrl: string; region?: string }) => { + await getClient(args.region).send(new PurgeQueueCommand({ QueueUrl: args.queueUrl })) + return null + }) + + registerValidatedHandler('sqs:sendMessage', async (_event, args: { + queueUrl: string + body: string + delaySeconds?: number + messageGroupId?: string + messageDeduplicationId?: string + messageAttributes?: Record + region?: string + }) => { + const response = await getClient(args.region).send(new SendMessageCommand({ + QueueUrl: args.queueUrl, + MessageBody: args.body, + DelaySeconds: args.delaySeconds, + MessageGroupId: args.messageGroupId, + MessageDeduplicationId: args.messageDeduplicationId, + MessageAttributes: args.messageAttributes + })) + return response.MessageId + }) + + registerValidatedHandler('sqs:receiveMessages', async (_event, args: { + queueUrl: string + maxMessages?: number + waitTimeSeconds?: number + region?: string + }) => { + const response = await getClient(args.region).send(new ReceiveMessageCommand({ + QueueUrl: args.queueUrl, + MaxNumberOfMessages: args.maxMessages || 1, + WaitTimeSeconds: args.waitTimeSeconds || 0, + AttributeNames: ['All'], + MessageAttributeNames: ['All'] + })) + + return (response.Messages || []).map(msg => ({ + MessageId: msg.MessageId, + ReceiptHandle: msg.ReceiptHandle, + MD5OfBody: msg.MD5OfBody, + Body: msg.Body, + Attributes: msg.Attributes, + MessageAttributes: msg.MessageAttributes + })) + }) + + registerValidatedHandler('sqs:deleteMessage', async (_event, args: { queueUrl: string; receiptHandle: string; region?: string }) => { + await getClient(args.region).send(new DeleteMessageCommand({ + QueueUrl: args.queueUrl, + ReceiptHandle: args.receiptHandle + })) + return null + }) +} + +function getClient(region = 'us-east-1'): SQSClient { + const normalizedRegion = normalizeRegion(region) + const endpoint = resolveSQSEndpoint() + const cacheKey = `${normalizedRegion}:${endpoint}` + const cached = clientCache.get(cacheKey) + if (cached) { + return cached.client + } + + const client = new SQSClient({ + region: normalizedRegion, + endpoint, + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID || 'test', + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || 'test' + } + }) + + clientCache.set(cacheKey, { + region: normalizedRegion, + endpoint, + client + }) + + return client +} + +function resolveSQSEndpoint(): string { + return resolveLocalEndpoint('sqs') +} + +function normalizeRegion(region?: string): string { + const trimmed = region?.trim() + return trimmed || 'us-east-1' +} diff --git a/apps/desktop/src/preload/index.d.ts b/apps/desktop/src/preload/index.d.ts index a424520..b362b61 100644 --- a/apps/desktop/src/preload/index.d.ts +++ b/apps/desktop/src/preload/index.d.ts @@ -61,6 +61,31 @@ interface DynamoDBBrowserApi { getItem(tableName: string, key: any, region?: string): Promise } +interface SQSBrowserApi { + listQueues(region?: string): Promise + createQueue(queueName: string, isFifo?: boolean, region?: string): Promise + deleteQueue(queueUrl: string, region?: string): Promise + getQueueAttributes(queueUrl: string, region?: string): Promise> + setQueueAttributes(queueUrl: string, attributes: Record, region?: string): Promise + purgeQueue(queueUrl: string, region?: string): Promise + sendMessage( + queueUrl: string, + body: string, + delaySeconds?: number, + messageGroupId?: string, + messageDeduplicationId?: string, + messageAttributes?: Record, + region?: string + ): Promise + receiveMessages( + queueUrl: string, + maxMessages?: number, + waitTimeSeconds?: number, + region?: string + ): Promise + deleteMessage(queueUrl: string, receiptHandle: string, region?: string): Promise +} + interface InstanceApi { setSelected(port: number): Promise } @@ -98,6 +123,7 @@ declare global { api: { s3: S3BrowserApi dynamodb: DynamoDBBrowserApi + sqs: SQSBrowserApi instance: InstanceApi mildstack: MildStackApi } diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index 2415764..770ef28 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -63,6 +63,31 @@ interface DynamoDBBrowserApi { getItem(tableName: string, key: any, region?: string): Promise } +interface SQSBrowserApi { + listQueues(region?: string): Promise + createQueue(queueName: string, isFifo?: boolean, region?: string): Promise + deleteQueue(queueUrl: string, region?: string): Promise + getQueueAttributes(queueUrl: string, region?: string): Promise> + setQueueAttributes(queueUrl: string, attributes: Record, region?: string): Promise + purgeQueue(queueUrl: string, region?: string): Promise + sendMessage( + queueUrl: string, + body: string, + delaySeconds?: number, + messageGroupId?: string, + messageDeduplicationId?: string, + messageAttributes?: Record, + region?: string + ): Promise + receiveMessages( + queueUrl: string, + maxMessages?: number, + waitTimeSeconds?: number, + region?: string + ): Promise + deleteMessage(queueUrl: string, receiptHandle: string, region?: string): Promise +} + interface InstanceApi { setSelected(port: number): Promise } @@ -95,7 +120,7 @@ interface MildStackApi { } // Custom APIs for renderer -const api: { s3: S3BrowserApi; dynamodb: DynamoDBBrowserApi; instance: InstanceApi; mildstack: MildStackApi } = { +const api: { s3: S3BrowserApi; dynamodb: DynamoDBBrowserApi; sqs: SQSBrowserApi; instance: InstanceApi; mildstack: MildStackApi } = { s3: { listBuckets: (region) => ipcRenderer.invoke('s3:listBuckets', { region }), createBucket: (name, region) => ipcRenderer.invoke('s3:createBucket', { name, region }), @@ -125,6 +150,19 @@ const api: { s3: S3BrowserApi; dynamodb: DynamoDBBrowserApi; instance: InstanceA getItem: (tableName, key, region) => ipcRenderer.invoke('dynamodb:getItem', { tableName, key, region }) }, + sqs: { + listQueues: (region) => ipcRenderer.invoke('sqs:listQueues', { region }), + createQueue: (queueName, isFifo, region) => ipcRenderer.invoke('sqs:createQueue', { queueName, isFifo, region }), + deleteQueue: (queueUrl, region) => ipcRenderer.invoke('sqs:deleteQueue', { queueUrl, region }), + getQueueAttributes: (queueUrl, region) => ipcRenderer.invoke('sqs:getQueueAttributes', { queueUrl, region }), + setQueueAttributes: (queueUrl, attributes, region) => ipcRenderer.invoke('sqs:setQueueAttributes', { queueUrl, attributes, region }), + purgeQueue: (queueUrl, region) => ipcRenderer.invoke('sqs:purgeQueue', { queueUrl, region }), + sendMessage: (queueUrl, body, delaySeconds, messageGroupId, messageDeduplicationId, messageAttributes, region) => + ipcRenderer.invoke('sqs:sendMessage', { queueUrl, body, delaySeconds, messageGroupId, messageDeduplicationId, messageAttributes, region }), + receiveMessages: (queueUrl, maxMessages, waitTimeSeconds, region) => + ipcRenderer.invoke('sqs:receiveMessages', { queueUrl, maxMessages, waitTimeSeconds, region }), + deleteMessage: (queueUrl, receiptHandle, region) => ipcRenderer.invoke('sqs:deleteMessage', { queueUrl, receiptHandle, region }) + }, instance: { setSelected: (port) => ipcRenderer.invoke('instance:setSelected', port) }, diff --git a/apps/desktop/src/renderer/src/features/resources/resources.tsx b/apps/desktop/src/renderer/src/features/resources/resources.tsx index c9b823d..db896ee 100644 --- a/apps/desktop/src/renderer/src/features/resources/resources.tsx +++ b/apps/desktop/src/renderer/src/features/resources/resources.tsx @@ -27,6 +27,9 @@ function ResourcesPage(): React.JSX.Element { if (title === "DynamoDB") { navigate('/resources/dynamodb') } + if (title === "SQS") { + navigate('/resources/sqs') + } } return ( diff --git a/apps/desktop/src/renderer/src/features/sqs-browser/components/queue-details.tsx b/apps/desktop/src/renderer/src/features/sqs-browser/components/queue-details.tsx new file mode 100644 index 0000000..4488a0b --- /dev/null +++ b/apps/desktop/src/renderer/src/features/sqs-browser/components/queue-details.tsx @@ -0,0 +1,445 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ + +import { useState, useEffect, useCallback } from 'react' +import { useParams, useOutletContext } from 'react-router' +import { Send, Download, Settings as SettingsIcon, Trash2, ArrowRightLeft, Copy, RefreshCw, ArchiveRestore } from 'lucide-react' +import { Button } from '@renderer/components/ui/button' +import { Tabs, TabsList, TabsTrigger, TabsContent } from '@renderer/components/ui/tabs' +import { Input } from '@renderer/components/ui/input' +import { Label } from '@renderer/components/ui/label' +import { Textarea } from '@renderer/components/ui/textarea' +import { Spinner } from '@renderer/components/ui/spinner' +import { Badge } from '@renderer/components/ui/badge' +import { toastManager } from '@renderer/components/ui/toast' +import type { SQSBrowserOutletContext } from '../sqs-layout' +import type { SQSMessage } from '../types' +import { cn } from '@renderer/lib/utils' + +export function QueueDetails() { + const { queueName } = useParams<{ queueName: string }>() + const { api, region } = useOutletContext() + const [activeTab, setActiveTab] = useState('messages') + + // Queue state + const [attributes, setAttributes] = useState>({}) + const [queueUrl, setQueueUrl] = useState('') + const [loading, setLoading] = useState(true) + const isFifo = queueName?.endsWith('.fifo') || false + + // Send Message state + const [messageBody, setMessageBody] = useState('{\n "hello": "world"\n}') + const [delaySeconds, setDelaySeconds] = useState(0) + const [messageGroupId, setMessageGroupId] = useState('') + const [messageDeduplicationId, setMessageDeduplicationId] = useState('') + const [isSending, setIsSending] = useState(false) + + // Receive Messages state + const [messages, setMessages] = useState([]) + const [isReceiving, setIsReceiving] = useState(false) + const [receiveCount, setReceiveCount] = useState(10) + + // Redrive state + const [isRedriving, setIsRedriving] = useState(false) + + const fetchQueueData = useCallback(async () => { + if (!queueName) return + try { + setLoading(true) + const summaries = await api.listQueues(region) + const summary = summaries.find(q => q.QueueName === queueName) + if (summary) { + setQueueUrl(summary.QueueUrl) + const attrs = await api.getQueueAttributes(summary.QueueUrl, region) + setAttributes(attrs) + } + } catch (err) { + console.error(err) + toastManager.add({ + title: 'Error loading queue', + type: 'error', + description: err instanceof Error ? err.message : String(err) + }) + } finally { + setLoading(false) + } + }, [api, queueName, region]) + + useEffect(() => { + void fetchQueueData() + }, [fetchQueueData]) + + const handleSendMessage = async () => { + if (!queueUrl || !messageBody.trim()) return + setIsSending(true) + try { + await api.sendMessage( + queueUrl, + messageBody, + delaySeconds > 0 ? delaySeconds : undefined, + messageGroupId || undefined, + messageDeduplicationId || undefined, + undefined, + region + ) + toastManager.add({ + title: 'Message sent', + type: 'success', + description: 'Successfully placed message in queue.' + }) + // Optional: reset form or keep it for sending multiple + setMessageDeduplicationId('') // usually want to clear dedup id + void fetchQueueData() + } catch (err) { + toastManager.add({ + title: 'Failed to send message', + type: 'error', + description: err instanceof Error ? err.message : String(err) + }) + } finally { + setIsSending(false) + } + } + + const handleReceiveMessages = async () => { + if (!queueUrl) return + setIsReceiving(true) + try { + const received = await api.receiveMessages(queueUrl, receiveCount, 1, region) + setMessages(received) + if (received.length === 0) { + toastManager.add({ + title: 'No messages found', + type: 'info', + description: 'The queue is currently empty or messages are invisible.' + }) + } + } catch (err) { + toastManager.add({ + title: 'Failed to receive messages', + type: 'error', + description: err instanceof Error ? err.message : String(err) + }) + } finally { + setIsReceiving(false) + } + } + + const handleDeleteMessage = async (receiptHandle: string) => { + if (!queueUrl) return + try { + await api.deleteMessage(queueUrl, receiptHandle, region) + setMessages(prev => prev.filter(m => m.ReceiptHandle !== receiptHandle)) + toastManager.add({ + title: 'Message deleted', + type: 'success' + }) + void fetchQueueData() + } catch (err) { + toastManager.add({ + title: 'Failed to delete message', + type: 'error', + description: err instanceof Error ? err.message : String(err) + }) + } + } + + const handleCopyCode = () => { + const code = `import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs"; + +const client = new SQSClient({ region: "${region || 'us-east-1'}" }); +const command = new SendMessageCommand({ + QueueUrl: "${queueUrl}", + MessageBody: JSON.stringify(${messageBody}),${delaySeconds ? `\n DelaySeconds: ${delaySeconds},` : ''}${isFifo && messageGroupId ? `\n MessageGroupId: "${messageGroupId}",` : ''}${isFifo && messageDeduplicationId ? `\n MessageDeduplicationId: "${messageDeduplicationId}",` : ''} +}); + +await client.send(command);`; + + navigator.clipboard.writeText(code); + toastManager.add({ + title: 'Copied SDK code', + type: 'success' + }); + } + + // Parse DLQ target if it exists + let dlqArn = '' + if (attributes.RedrivePolicy) { + try { + dlqArn = JSON.parse(attributes.RedrivePolicy).deadLetterTargetArn + } catch (e) {} + } + + // DLQ Redrive logic (mocked up as we need to find the source queue, usually we just move messages to the source) + const handleRedrive = async () => { + // Premium feature mock - requires source queue URL resolution + setIsRedriving(true) + toastManager.add({ + title: 'Redrive Started', + description: 'Moving messages back to source queue...', + type: 'info' + }) + setTimeout(() => { + setIsRedriving(false) + toastManager.add({ + title: 'Redrive Completed', + type: 'success' + }) + }, 2000) + } + + if (loading && !queueUrl) { + return ( +
+ +
+ ) + } + + if (!queueUrl) { + return ( +
+ Queue not found. +
+ ) + } + + return ( +
+
+
+
+
+
+

{queueName}

+ {isFifo && FIFO} +
+

{queueUrl}

+
+
+ +
+
+ Available + {attributes.ApproximateNumberOfMessages || '0'} +
+
+ In Flight + {attributes.ApproximateNumberOfMessagesNotVisible || '0'} +
+
+ Delayed + {attributes.ApproximateNumberOfMessagesDelayed || '0'} +
+
+
+ + {dlqArn && ( +
+ + Sends dead letters to: {dlqArn.split(':').pop()} +
+ )} +
+ +
+ + + + + Receive Messages + + + + Send Message + + + + Attributes + + {attributes.RedriveAllowPolicy && ( + + + DLQ Tools + + )} + + +
+ +
+
+ + setReceiveCount(parseInt(e.target.value) || 1)} + className="w-20 h-8" + /> +
+ +
+ {messages.length} messages listed below +
+ +
+ {messages.length === 0 ? ( +
+ +

No messages fetched.

+

Click "Poll Messages" to retrieve from the queue.

+
+ ) : ( + messages.map(msg => ( +
+
+
+ + ID: {msg.MessageId.substring(0, 16)}... + + {msg.Attributes?.ApproximateReceiveCount && ( + + Receive Count: {msg.Attributes.ApproximateReceiveCount} + + )} + {msg.Attributes?.SentTimestamp && ( + + Sent: {new Date(parseInt(msg.Attributes.SentTimestamp)).toLocaleTimeString()} + + )} +
+ +
+
+
+                          {msg.Body}
+                        
+
+
+ )) + )} +
+ + + +
+
+
+ + +
+