586 lines
22 KiB
Vue

<template>
<div class="bg-white dark:bg-gray-800 flex flex-col">
<!-- Enhanced Response Header -->
<div class="flex items-center justify-between p-6 flex-shrink-0 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center gap-3">
<Icon name="ic:outline-api" size="20" class="text-gray-500" />
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Response</h3>
<div v-if="hasResponse" class="text-sm text-gray-500 dark:text-gray-400">
{{ new Date(response.timestamp || Date.now()).toLocaleTimeString() }}
</div>
</div>
<div v-if="hasResponse" class="flex items-center gap-3 flex-wrap">
<rs-badge :variant="getStatusVariant(response.status || 500)" class="font-medium">
{{ response.status || 'Error' }} {{ response.statusText || 'Request Failed' }}
</rs-badge>
<div class="flex items-center gap-4 text-sm text-gray-500">
<span v-if="response.time" class="flex items-center gap-1">
<Icon name="ic:outline-timer" size="14" />
{{ response.time }}ms
</span>
<span v-if="response.size" class="flex items-center gap-1">
<Icon name="ic:outline-data-usage" size="14" />
{{ formatBytes(response.size) }}
</span>
</div>
</div>
</div>
<!-- Response Content Area -->
<div v-if="hasResponse" class="p-6">
<div class="border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800">
<!-- Enhanced Response Tabs -->
<div class="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
<nav class="px-4 flex space-x-6 overflow-x-auto">
<button
v-for="tab in responseTabs"
:key="tab.id"
@click="responseActiveTab = tab.id"
:class="[
responseActiveTab === tab.id
? 'border-primary-500 text-primary-600 dark:text-primary-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300',
'whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm flex items-center gap-2 flex-shrink-0'
]"
>
<Icon :name="tab.icon" size="16" />
{{ tab.label }}
<rs-badge v-if="tab.count" variant="secondary" size="sm">{{ tab.count }}</rs-badge>
</button>
</nav>
</div>
<!-- Response Content -->
<div>
<!-- Body Tab -->
<div v-if="responseActiveTab === 'body'">
<!-- Enhanced Response Tools -->
<div v-if="isJsonResponse || response.data" class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center gap-2 flex-wrap">
<rs-button
v-if="isJsonResponse"
variant="secondary-outline"
size="sm"
@click="beautifyResponseJson"
class="flex items-center gap-1"
>
<Icon name="ic:outline-auto-fix-high" size="16" />
Beautify
</rs-button>
<rs-button
v-if="isJsonResponse"
variant="secondary-outline"
size="sm"
@click="minifyResponseJson"
class="flex items-center gap-1"
>
<Icon name="ic:outline-compress" size="16" />
Minify
</rs-button>
<rs-button
variant="secondary-outline"
size="sm"
@click="copyResponseToClipboard"
class="flex items-center gap-1"
>
<Icon name="ic:outline-content-copy" size="16" />
Copy
</rs-button>
<rs-button
variant="secondary-outline"
size="sm"
@click="downloadResponse"
class="flex items-center gap-1"
>
<Icon name="ic:outline-download" size="16" />
Download
</rs-button>
</div>
<div class="flex items-center gap-3 flex-wrap">
<div class="flex items-center gap-2">
<span class="text-xs text-gray-500">Format:</span>
<rs-badge variant="info" size="sm">{{ responseFormat }}</rs-badge>
</div>
<div class="flex items-center gap-2">
<span class="text-xs text-gray-500">Size:</span>
<rs-badge variant="secondary" size="sm">{{ formattedResponseData.length }} chars</rs-badge>
</div>
</div>
</div>
<!-- Response Body Content -->
<div class="p-6">
<pre
ref="responseBody"
class="text-sm bg-gray-50 dark:bg-gray-900 p-4 rounded-lg whitespace-pre-wrap w-full break-words border font-mono leading-relaxed"
:class="{
'language-json': isJsonResponse,
'language-xml': isXmlResponse,
'language-html': isHtmlResponse
}"
>{{ formattedResponseData }}</pre>
</div>
</div>
<!-- Headers Tab -->
<div v-if="responseActiveTab === 'headers'" class="p-6 space-y-4">
<!-- Copy Headers Button -->
<div class="flex justify-between items-center">
<h4 class="font-medium text-gray-900 dark:text-white">Response Headers</h4>
<rs-button variant="secondary-outline" size="sm" @click="copyHeadersToClipboard">
<Icon name="ic:outline-content-copy" size="16" class="mr-1" />
Copy All
</rs-button>
</div>
<!-- Headers List -->
<div class="space-y-2">
<div v-for="(value, key) in (response && response.headers ? response.headers : {})" :key="key" class="flex flex-wrap gap-2 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border">
<span class="font-medium text-gray-700 dark:text-gray-300 min-w-48 break-words">{{ key }}:</span>
<span class="text-gray-600 dark:text-gray-400 break-words flex-1 font-mono text-sm">{{ value }}</span>
<rs-button
variant="text"
size="sm"
@click="copyHeaderToClipboard(key, value)"
class="opacity-60 hover:opacity-100 flex-shrink-0"
>
<Icon name="ic:outline-content-copy" size="14" />
</rs-button>
</div>
<div v-if="Object.keys(response?.headers || {}).length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
<Icon name="ic:outline-web-asset-off" size="48" class="mx-auto mb-4 opacity-50" />
<p>No response headers available</p>
</div>
</div>
</div>
<!-- Cookies Tab -->
<div v-if="responseActiveTab === 'cookies'" class="p-6 space-y-4">
<div class="flex justify-between items-center">
<h4 class="font-medium text-gray-900 dark:text-white">Cookies</h4>
<rs-button variant="secondary-outline" size="sm" @click="clearCookies">
<Icon name="ic:outline-clear" size="16" class="mr-1" />
Clear All
</rs-button>
</div>
<div class="space-y-2">
<div v-for="cookie in parsedCookies" :key="cookie.name" class="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border">
<div class="flex items-center justify-between mb-2">
<span class="font-medium text-gray-900 dark:text-white">{{ cookie.name }}</span>
<rs-button variant="text" size="sm" @click="copyCookie(cookie)">
<Icon name="ic:outline-content-copy" size="14" />
</rs-button>
</div>
<div class="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<div><span class="font-medium">Value:</span> {{ cookie.value }}</div>
<div v-if="cookie.domain"><span class="font-medium">Domain:</span> {{ cookie.domain }}</div>
<div v-if="cookie.path"><span class="font-medium">Path:</span> {{ cookie.path }}</div>
<div v-if="cookie.expires"><span class="font-medium">Expires:</span> {{ cookie.expires }}</div>
<div v-if="cookie.httpOnly" class="flex items-center gap-1">
<Icon name="ic:outline-security" size="14" />
<span>HTTP Only</span>
</div>
<div v-if="cookie.secure" class="flex items-center gap-1">
<Icon name="ic:outline-lock" size="14" />
<span>Secure</span>
</div>
</div>
</div>
<div v-if="parsedCookies.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
<Icon name="ic:outline-cookie" size="48" class="mx-auto mb-4 opacity-50" />
<p>No cookies in response</p>
</div>
</div>
</div>
<!-- Timeline Tab -->
<div v-if="responseActiveTab === 'timeline'" class="p-6 space-y-4">
<h4 class="font-medium text-gray-900 dark:text-white">Request Timeline</h4>
<div class="space-y-3">
<div v-for="(stage, index) in timelineStages" :key="stage.name" class="flex items-center gap-4 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div class="flex-shrink-0">
<div class="w-3 h-3 rounded-full" :class="stage.color"></div>
</div>
<div class="flex-1">
<div class="font-medium text-gray-900 dark:text-white">{{ stage.name }}</div>
<div class="text-sm text-gray-600 dark:text-gray-400">{{ stage.description }}</div>
</div>
<div class="text-sm font-medium text-gray-900 dark:text-white">
{{ stage.time }}ms
</div>
</div>
</div>
<!-- Total Time -->
<div class="mt-6 p-4 bg-primary-50 dark:bg-primary-900/20 rounded-lg border border-primary-200 dark:border-primary-800">
<div class="flex items-center justify-between">
<span class="font-medium text-primary-900 dark:text-primary-100">Total Request Time</span>
<span class="text-xl font-bold text-primary-900 dark:text-primary-100">{{ response.time || 0 }}ms</span>
</div>
</div>
</div>
<!-- Test Results Tab -->
<div v-if="responseActiveTab === 'tests'" class="p-6 space-y-4">
<div class="flex justify-between items-center">
<h4 class="font-medium text-gray-900 dark:text-white">Test Results</h4>
<rs-button variant="primary" size="sm" @click="runBasicTests">
<Icon name="ic:outline-play-arrow" size="16" class="mr-1" />
Run Tests
</rs-button>
</div>
<div class="space-y-2">
<div v-for="test in testResults" :key="test.name" class="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<Icon
:name="test.passed ? 'ic:outline-check-circle' : 'ic:outline-error'"
size="20"
:class="test.passed ? 'text-green-500' : 'text-red-500'"
/>
<div class="flex-1">
<div class="font-medium text-gray-900 dark:text-white">{{ test.name }}</div>
<div class="text-sm text-gray-600 dark:text-gray-400">{{ test.description }}</div>
</div>
<rs-badge :variant="test.passed ? 'success' : 'danger'" size="sm">
{{ test.passed ? 'PASS' : 'FAIL' }}
</rs-badge>
</div>
<div v-if="testResults.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
<Icon name="ic:outline-quiz" size="48" class="mx-auto mb-4 opacity-50" />
<p>No test results yet</p>
<p class="text-xs mt-2">Click "Run Tests" to execute basic validation tests</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Empty State -->
<div v-else class="flex-1 flex items-center justify-center text-gray-500 dark:text-gray-400 p-6">
<div class="text-center">
<Icon name="ic:outline-send" size="64" class="mx-auto mb-6 opacity-50" />
<h3 class="text-lg font-medium mb-2">Ready to send your first request?</h3>
<p class="text-sm">Configure your request above and click Send to see the response here</p>
</div>
</div>
</div>
</template>
<script setup>
const { formatJson, getStatusVariant } = useApiRequest()
const { response, responseActiveTab, showNotification } = useApiPlatform()
// Enhanced tab configuration
const responseTabs = computed(() => [
{
id: 'body',
label: 'Body',
icon: 'ic:outline-description',
count: null
},
{
id: 'headers',
label: 'Headers',
icon: 'ic:outline-list',
count: response.value?.headers ? Object.keys(response.value.headers).length : 0
},
{
id: 'cookies',
label: 'Cookies',
icon: 'ic:outline-cookie',
count: parsedCookies.value.length
},
{
id: 'timeline',
label: 'Timeline',
icon: 'ic:outline-timeline',
count: null
},
{
id: 'tests',
label: 'Tests',
icon: 'ic:outline-quiz',
count: testResults.value.length
}
])
// Check if we have a response to show
const hasResponse = computed(() => {
return response.value && (response.value.status || response.value.data)
})
// Response format detection
const isJsonResponse = computed(() => {
if (!response.value?.data) return false
return typeof response.value.data === 'object' ||
(typeof response.value.data === 'string' && isValidJson(response.value.data))
})
const isXmlResponse = computed(() => {
if (!response.value?.data || typeof response.value.data !== 'string') return false
return response.value.data.trim().startsWith('<?xml') || response.value.data.trim().startsWith('<')
})
const isHtmlResponse = computed(() => {
if (!response.value?.data || typeof response.value.data !== 'string') return false
return response.value.data.trim().toLowerCase().includes('<html')
})
const responseFormat = computed(() => {
if (isJsonResponse.value) return 'JSON'
if (isXmlResponse.value) return 'XML'
if (isHtmlResponse.value) return 'HTML'
return 'TEXT'
})
// Cookie parsing
const parsedCookies = computed(() => {
if (!response.value?.headers) return []
const cookieHeaders = []
Object.entries(response.value.headers).forEach(([key, value]) => {
if (key.toLowerCase() === 'set-cookie') {
if (Array.isArray(value)) {
cookieHeaders.push(...value)
} else {
cookieHeaders.push(value)
}
}
})
return cookieHeaders.map(cookieString => {
const parts = cookieString.split(';').map(part => part.trim())
const [name, value] = parts[0].split('=')
const cookie = { name, value: value || '' }
parts.slice(1).forEach(part => {
const [key, val] = part.split('=')
const lowerKey = key.toLowerCase()
if (lowerKey === 'domain') cookie.domain = val
else if (lowerKey === 'path') cookie.path = val
else if (lowerKey === 'expires') cookie.expires = val
else if (lowerKey === 'httponly') cookie.httpOnly = true
else if (lowerKey === 'secure') cookie.secure = true
})
return cookie
})
})
// Timeline stages
const timelineStages = computed(() => {
const total = response.value?.time || 0
const stages = [
{ name: 'DNS Lookup', description: 'Resolving domain name', time: Math.round(total * 0.1), color: 'bg-blue-500' },
{ name: 'TCP Connect', description: 'Establishing connection', time: Math.round(total * 0.15), color: 'bg-green-500' },
{ name: 'TLS Handshake', description: 'SSL/TLS negotiation', time: Math.round(total * 0.2), color: 'bg-yellow-500' },
{ name: 'Request Sent', description: 'Sending request data', time: Math.round(total * 0.05), color: 'bg-purple-500' },
{ name: 'Server Processing', description: 'Server processing time', time: Math.round(total * 0.4), color: 'bg-red-500' },
{ name: 'Response Received', description: 'Downloading response', time: Math.round(total * 0.1), color: 'bg-indigo-500' }
]
return stages
})
// Test results
const testResults = ref([])
// Response body formatting
const formattedResponseData = ref('')
const formatResponseData = () => {
if (!response.value?.data) {
formattedResponseData.value = 'No response data'
return
}
if (typeof response.value.data === 'object') {
formattedResponseData.value = JSON.stringify(response.value.data, null, 2)
} else if (typeof response.value.data === 'string' && isValidJson(response.value.data)) {
try {
const parsed = JSON.parse(response.value.data)
formattedResponseData.value = JSON.stringify(parsed, null, 2)
} catch (e) {
formattedResponseData.value = response.value.data
}
} else {
formattedResponseData.value = String(response.value.data)
}
}
// Utility functions
const isValidJson = (str) => {
try {
JSON.parse(str)
return true
} catch (e) {
return false
}
}
const formatBytes = (bytes) => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
// Response JSON tools
const beautifyResponseJson = () => {
if (!isJsonResponse.value) return
try {
let data = response.value.data
if (typeof data === 'string') {
data = JSON.parse(data)
}
formattedResponseData.value = JSON.stringify(data, null, 2)
showNotification('JSON beautified', 'success', 2000)
} catch (error) {
showNotification('Failed to beautify JSON', 'error')
}
}
const minifyResponseJson = () => {
if (!isJsonResponse.value) return
try {
let data = response.value.data
if (typeof data === 'string') {
data = JSON.parse(data)
}
formattedResponseData.value = JSON.stringify(data)
showNotification('JSON minified', 'success', 2000)
} catch (error) {
showNotification('Failed to minify JSON', 'error')
}
}
const copyResponseToClipboard = async () => {
try {
await navigator.clipboard.writeText(formattedResponseData.value)
showNotification('Response copied to clipboard', 'success', 2000)
} catch (error) {
showNotification('Failed to copy response', 'error')
}
}
const downloadResponse = () => {
try {
const blob = new Blob([formattedResponseData.value], {
type: isJsonResponse.value ? 'application/json' : 'text/plain'
})
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
const extension = isJsonResponse.value ? 'json' : 'txt'
a.href = url
a.download = `response-${Date.now()}.${extension}`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
showNotification('Response downloaded', 'success', 2000)
} catch (error) {
showNotification('Failed to download response', 'error')
}
}
// Headers tools
const copyHeadersToClipboard = async () => {
try {
if (!response.value || !response.value.headers) {
showNotification('No headers to copy', 'warning')
return
}
const headersText = Object.entries(response.value.headers)
.map(([key, value]) => `${key}: ${value}`)
.join('\n')
await navigator.clipboard.writeText(headersText)
showNotification('Headers copied to clipboard', 'success', 2000)
} catch (error) {
showNotification('Failed to copy headers', 'error')
}
}
const copyHeaderToClipboard = async (key, value) => {
try {
await navigator.clipboard.writeText(`${key}: ${value}`)
showNotification(`Header "${key}" copied`, 'success', 2000)
} catch (error) {
showNotification('Failed to copy header', 'error')
}
}
// Cookie tools
const clearCookies = () => {
showNotification('Cookies cleared from display', 'info', 2000)
}
const copyCookie = async (cookie) => {
try {
await navigator.clipboard.writeText(`${cookie.name}=${cookie.value}`)
showNotification(`Cookie "${cookie.name}" copied`, 'success', 2000)
} catch (error) {
showNotification('Failed to copy cookie', 'error')
}
}
// Test functions
const runBasicTests = () => {
testResults.value = []
if (!response.value) return
// Status code tests
if (response.value.status) {
testResults.value.push({
name: 'Status Code is 2xx',
description: 'Response has successful status code',
passed: response.value.status >= 200 && response.value.status < 300
})
testResults.value.push({
name: 'Response Time < 1000ms',
description: 'Response was received quickly',
passed: (response.value.time || 0) < 1000
})
}
// Content tests
if (response.value.data) {
testResults.value.push({
name: 'Response Has Body',
description: 'Response contains data',
passed: !!response.value.data
})
if (isJsonResponse.value) {
testResults.value.push({
name: 'Valid JSON Response',
description: 'Response body is valid JSON',
passed: true
})
}
}
// Headers tests
if (response.value.headers) {
testResults.value.push({
name: 'Content-Type Header Present',
description: 'Response includes content type',
passed: !!response.value.headers['content-type'] || !!response.value.headers['Content-Type']
})
}
showNotification(`${testResults.value.length} tests completed`, 'info', 2000)
}
// Watch for response changes
watch(() => response.value, formatResponseData, { immediate: true, deep: true })
</script>