mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-27 22:54:07 +00:00
Test Runner UI Revamp (#6011)
* Moved collection results to runner title bar so they are move visible. Added breakdown of test results within collection. Added filtering based on passing/failing requests and tests by click on results text. * feat: revamp Test Runner UI with unified filter and improved layout - Add unified filter bar (All/Passed/Failed/Skipped) with counts and active indicator - Implement filtering that filters both requests and tests within requests - Move action buttons to top bar, prevent filter wrapping - Add close button and placeholder to response view - Update styling for light/dark modes with proper colors and typography * refactored the RunnerResults component to be more clear and readable * refactor: revert formatting changes while preserving new UI and filtering logic - Restore original function formatting with return statements and braces - Restore removed input attributes (autoCorrect, autoCapitalize, spellCheck) - Revert ternary operator changes to match original code style - Restore original variable names (savedConfiguration) and comments - Restore original test results rendering structure - Preserve new filter bar UI, filtering logic, and response view improvements * fix: implement smart auto-scroll behavior in test runner - Only auto-scroll when user is near the bottom (within 100px) - Preserve user's scroll position if they've scrolled up to view content - Move ref to actual scrollable container for proper scroll detection * Update RunnerResults component * chore: reformat --------- Co-authored-by: Morgan English <morgan.english@canterbury.ac.nz> Co-authored-by: Sid <siddharth@usebruno.com>
This commit is contained in:
committed by
GitHub
parent
45cfbc5c49
commit
44ed0b01d8
@@ -3,16 +3,13 @@ import path from 'utils/common/path';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { get, cloneDeep } from 'lodash';
|
||||
import { runCollectionFolder, cancelRunnerExecution, mountCollection, updateRunnerConfiguration } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { resetCollectionRunner } from 'providers/ReduxStore/slices/collections';
|
||||
import { findItemInCollection, getTotalRequestCountInCollection } from 'utils/collections';
|
||||
import { IconRefresh, IconCircleCheck, IconCircleX, IconCircleOff, IconCheck, IconX, IconRun, IconLoader2 } from '@tabler/icons';
|
||||
import { resetCollectionRunner, updateRunnerTagsDetails } from 'providers/ReduxStore/slices/collections';
|
||||
import { findItemInCollection, getTotalRequestCountInCollection, areItemsLoading, getRequestItemsForCollectionRun } from 'utils/collections';
|
||||
import { IconRefresh, IconCircleCheck, IconCircleX, IconCircleOff, IconCheck, IconX, IconRun, IconExternalLink } from '@tabler/icons';
|
||||
import ResponsePane from './ResponsePane';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { areItemsLoading } from 'utils/collections';
|
||||
import RunnerTags from './RunnerTags/index';
|
||||
import RunConfigurationPanel from './RunConfigurationPanel';
|
||||
import { getRequestItemsForCollectionRun } from 'utils/collections/index';
|
||||
import { updateRunnerTagsDetails } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const getDisplayName = (fullPath, pathname, name = '') => {
|
||||
let relativePath = path.relative(fullPath, pathname);
|
||||
@@ -42,67 +39,61 @@ const anyTestFailed = (item) => {
|
||||
item.postResponseTestStatus === 'fail';
|
||||
};
|
||||
|
||||
// === Centralized filters definition ===
|
||||
const FILTERS = {
|
||||
all: {
|
||||
label: 'All',
|
||||
predicate: () => true,
|
||||
resultFilter: (results) => results
|
||||
},
|
||||
passed: {
|
||||
label: 'Passed',
|
||||
predicate: (item) => allTestsPassed(item),
|
||||
resultFilter: (results) => results?.filter((r) => r.status === 'pass')
|
||||
},
|
||||
failed: {
|
||||
label: 'Failed',
|
||||
predicate: (item) => anyTestFailed(item),
|
||||
resultFilter: (results) => results?.filter((r) => ['fail', 'error'].includes(r.status))
|
||||
},
|
||||
skipped: {
|
||||
label: 'Skipped',
|
||||
predicate: (item) => item.status === 'skipped',
|
||||
resultFilter: (results) => results
|
||||
}
|
||||
};
|
||||
|
||||
// === Reusable filter button ===
|
||||
const FilterButton = ({ label, count, active, onClick }) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`font-medium transition-colors cursor-pointer flex items-center gap-1.5 border-b-2 pb-2 ${
|
||||
active
|
||||
? 'text-[#343434] dark:text-[#CCCCCC] border-[#F59E0B]'
|
||||
: 'text-[#989898] dark:text-[#CCCCCC80] border-transparent'
|
||||
}`}
|
||||
style={{ fontFamily: 'Inter', fontSize: '14px', fontWeight: 500, lineHeight: '100%', letterSpacing: '0%' }}
|
||||
>
|
||||
{label}
|
||||
<span
|
||||
className="px-[4.5px] py-[2px] rounded-[2px] bg-[#F7F7F7] dark:bg-[#242424] border border-[#EFEFEF] dark:border-[#92929233] text-[#989898] dark:text-inherit"
|
||||
style={{ borderWidth: '1px', fontFamily: 'Inter', fontSize: '10px', fontWeight: 500, lineHeight: '100%', letterSpacing: '0%' }}
|
||||
>
|
||||
{count}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
export default function RunnerResults({ collection }) {
|
||||
const dispatch = useDispatch();
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
const [delay, setDelay] = useState(null);
|
||||
const [activeFilter, setActiveFilter] = useState('all');
|
||||
|
||||
|
||||
const getActiveFilterPredicate = () => {
|
||||
switch (activeFilter) {
|
||||
case 'passing_requests':
|
||||
return (item) => item.status !== 'error' && item.testStatus === 'pass' && item.assertionStatus === 'pass';
|
||||
case 'failing_requests':
|
||||
return (item) => (item.status !== 'error' && item.testStatus === 'fail') || item.assertionStatus === 'fail';
|
||||
case 'passing_tests':
|
||||
return (item) => item.testResults?.some((result) => result.status === 'pass');
|
||||
case 'failing_tests':
|
||||
return (item) => item.testResults?.some((result) => result.status === 'fail' || result.status === 'error');
|
||||
default:
|
||||
return () => true
|
||||
}
|
||||
}
|
||||
|
||||
const [selectedRequestItems, setSelectedRequestItems] = useState([]);
|
||||
const [configureMode, setConfigureMode] = useState(false);
|
||||
|
||||
// ref for the runner output body
|
||||
const runnerBodyRef = useRef();
|
||||
|
||||
const autoScrollRunnerBody = () => {
|
||||
if (runnerBodyRef?.current) {
|
||||
// mimics the native terminal scroll style
|
||||
runnerBodyRef.current.scrollTo(0, 100000);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!collection.runnerResult) {
|
||||
setSelectedItem(null);
|
||||
}
|
||||
autoScrollRunnerBody();
|
||||
}, [collection, setSelectedItem]);
|
||||
|
||||
useEffect(() => {
|
||||
const runnerInfo = get(collection, 'runnerResult.info', {});
|
||||
if (runnerInfo.status === 'running') {
|
||||
setConfigureMode(false);
|
||||
}
|
||||
}, [collection.runnerResult]);
|
||||
|
||||
useEffect(() => {
|
||||
const savedConfiguration = get(collection, 'runnerConfiguration', null);
|
||||
if (savedConfiguration) {
|
||||
if (savedConfiguration.selectedRequestItems && configureMode) {
|
||||
setSelectedRequestItems(savedConfiguration.selectedRequestItems);
|
||||
}
|
||||
if (savedConfiguration.delay !== undefined && delay === null) {
|
||||
setDelay(savedConfiguration.delay);
|
||||
}
|
||||
}
|
||||
}, [collection.runnerConfiguration, configureMode, delay]);
|
||||
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
const runnerInfo = get(collection, 'runnerResult.info', {});
|
||||
|
||||
@@ -144,6 +135,63 @@ export default function RunnerResults({ collection }) {
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const activeFilterConfig = FILTERS[activeFilter];
|
||||
const filteredItems = items.filter(activeFilterConfig.predicate);
|
||||
|
||||
const filterTestResults = (results) => {
|
||||
if (!results || !Array.isArray(results)) return [];
|
||||
return activeFilterConfig.resultFilter(results);
|
||||
};
|
||||
|
||||
const autoScrollRunnerBody = () => {
|
||||
if (runnerBodyRef?.current) {
|
||||
const element = runnerBodyRef.current;
|
||||
const scrollThreshold = 100; // pixels from bottom to consider "at bottom"
|
||||
const isNearBottom
|
||||
= element.scrollHeight - element.scrollTop - element.clientHeight < scrollThreshold;
|
||||
|
||||
// Only auto-scroll if user is already near the bottom
|
||||
if (isNearBottom) {
|
||||
// mimics the native terminal scroll style
|
||||
element.scrollTo(0, 100000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!collection.runnerResult) {
|
||||
setSelectedItem(null);
|
||||
}
|
||||
autoScrollRunnerBody();
|
||||
}, [collection, setSelectedItem]);
|
||||
|
||||
useEffect(() => {
|
||||
// Auto-scroll when items are added or updated during execution
|
||||
// Only scrolls if user is already at/near the bottom
|
||||
if (filteredItems.length > 0) {
|
||||
autoScrollRunnerBody();
|
||||
}
|
||||
}, [filteredItems]);
|
||||
|
||||
useEffect(() => {
|
||||
const runnerInfo = get(collection, 'runnerResult.info', {});
|
||||
if (runnerInfo.status === 'running') {
|
||||
setConfigureMode(false);
|
||||
}
|
||||
}, [collection.runnerResult]);
|
||||
|
||||
useEffect(() => {
|
||||
const savedConfiguration = get(collection, 'runnerConfiguration', null);
|
||||
if (savedConfiguration) {
|
||||
if (savedConfiguration.selectedRequestItems && configureMode) {
|
||||
setSelectedRequestItems(savedConfiguration.selectedRequestItems);
|
||||
}
|
||||
if (savedConfiguration.delay !== undefined && delay === null) {
|
||||
setDelay(savedConfiguration.delay);
|
||||
}
|
||||
}
|
||||
}, [collection.runnerConfiguration, configureMode, delay]);
|
||||
|
||||
const ensureCollectionIsMounted = () => {
|
||||
if(collection.mountStatus === 'mounted'){
|
||||
return;
|
||||
@@ -210,52 +258,14 @@ export default function RunnerResults({ collection }) {
|
||||
}, [tagsEnabled]);
|
||||
|
||||
const totalRequestsInCollection = getTotalRequestCountInCollection(collectionCopy);
|
||||
const filterCounts = {
|
||||
all: items.length,
|
||||
passed: items.filter(allTestsPassed).length,
|
||||
failed: items.filter(anyTestFailed).length,
|
||||
skipped: items.filter((i) => i.status === 'skipped').length
|
||||
};
|
||||
|
||||
const displayCollectionResults = () => {
|
||||
let passedRequests = 0;
|
||||
let failedRequests = 0;
|
||||
let totalTestsInCollection = 0;
|
||||
let passedTests = 0;
|
||||
let failedTests = 0;
|
||||
items.forEach(item => {
|
||||
const isPassedRequest = item.status !== 'error' && item.testStatus === 'pass' && item.assertionStatus === 'pass';
|
||||
const isFailedRequest = (item.status !== 'error' && item.testStatus === 'fail') || item.assertionStatus === 'fail';
|
||||
|
||||
if (isPassedRequest) passedRequests++;
|
||||
if (isFailedRequest) failedRequests++;
|
||||
|
||||
const testResults = Array.isArray(item?.testResults) ? item.testResults : [];
|
||||
totalTestsInCollection += testResults.length;
|
||||
testResults.forEach(result => {
|
||||
if (result.status === 'pass') passedTests++;
|
||||
if (result.status === 'fail' || result.status === 'error') failedTests++;
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="pb-2 font-medium test-summary flex flex-col items-start justify-center mx-2">
|
||||
<div>
|
||||
<span onClick={() => setActiveFilter('all')} className={`cursor-pointer ${activeFilter === 'all' ? 'underline font-semibold' : ''} hover:font-semibold`}>Total Requests: {items.length}, </span>
|
||||
<span onClick={() => setActiveFilter('passing_requests')} className={`cursor-pointer ${activeFilter === 'passing_requests' ? 'underline font-semibold' : ''} hover:font-semibold`}>Passed: {passedRequests}, </span>
|
||||
<span onClick={() => setActiveFilter('failing_requests') } className={`cursor-pointer ${activeFilter === 'failing_requests' ? 'underline font-semibold' : ''} hover:font-semibold`}>Failed: {failedRequests}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span onClick={() => setActiveFilter('all')} className={`cursor-pointer ${activeFilter === 'all' ? 'underline font-semibold' : ''} hover:font-semibold`}>Total Tests: {totalTestsInCollection}, </span>
|
||||
<span onClick={() => setActiveFilter('passing_tests')} className={`cursor-pointer ${activeFilter === 'passing_tests' ? 'underline font-semibold' : ''} hover:font-semibold`}>Passed: {passedTests}, </span>
|
||||
<span onClick={() => setActiveFilter('failing_tests')} className={`cursor-pointer ${activeFilter === 'failing_tests' ? 'underline font-semibold' : ''} hover:font-semibold`}>Failed: {failedTests}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const passedRequests = items.filter(allTestsPassed);
|
||||
const failedRequests = items.filter(anyTestFailed);
|
||||
|
||||
const skippedRequests = items.filter((item) => {
|
||||
return item.status === 'skipped';
|
||||
});
|
||||
let isCollectionLoading = areItemsLoading(collection);
|
||||
|
||||
if (!items || !items.length) {
|
||||
return (
|
||||
<StyledWrapper className="pl-4 overflow-hidden h-full">
|
||||
@@ -341,35 +351,57 @@ export default function RunnerResults({ collection }) {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-4 pb-4 flex flex-grow flex-col relative overflow-auto">
|
||||
<div className="flex items-center my-6 flex-row">
|
||||
<div className="font-medium title flex items-center">
|
||||
Runner
|
||||
<IconRun size={20} strokeWidth={1.5} className="ml-2" />
|
||||
{/* Filter Bar and Actions */}
|
||||
<div className="flex items-center justify-between mb-4 pt-[14px] gap-4">
|
||||
<div className="flex items-stretch rounded-lg border border-[#EFEFEF] dark:border-[#92929233] max-h-[35px] flex-shrink-0" style={{ borderWidth: '1px' }}>
|
||||
<div className="flex items-center px-3 py-2 rounded-l-lg bg-[#F3F3F3] dark:bg-[#2B2D2F]">
|
||||
<span className="text-gray-600 dark:text-gray-400" style={{ fontFamily: 'Inter', fontSize: '14px', fontWeight: 400 }}>
|
||||
Filter by:
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-5 px-3 pt-2 pb-0 rounded-r-lg bg-transparent dark:bg-transparent">
|
||||
{Object.entries(FILTERS).map(([key, { label }]) => (
|
||||
<FilterButton
|
||||
key={key}
|
||||
label={label}
|
||||
count={filterCounts[key]}
|
||||
active={activeFilter === key}
|
||||
onClick={() => setActiveFilter(key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{displayCollectionResults()}
|
||||
{runnerInfo.status !== 'ended' && runnerInfo.cancelTokenUid && (
|
||||
<button className="btn btn-sm btn-danger" onClick={cancelExecution}>
|
||||
Cancel Execution
|
||||
</button>
|
||||
)}
|
||||
|
||||
{runnerInfo.status !== 'ended' && runnerInfo.cancelTokenUid ? (
|
||||
<div className="flex items-center flex-shrink-0">
|
||||
<button className="btn btn-sm btn-danger" onClick={cancelExecution}>Cancel Execution</button>
|
||||
</div>
|
||||
) : runnerInfo.status === 'ended' ? (
|
||||
<div className="flex items-center gap-3 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
className="px-3 py-1.5 rounded-md bg-transparent border border-[#989898] dark:border-[#444444] text-[#989898] hover:opacity-80 transition-colors"
|
||||
style={{ fontFamily: 'Inter', fontSize: '12px', fontWeight: 500 }}
|
||||
onClick={runAgain}
|
||||
>
|
||||
Run Again
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="px-3 py-1.5 rounded-md bg-transparent border border-[#989898] dark:border-[#444444] text-[#989898] hover:opacity-80 transition-colors"
|
||||
style={{ fontFamily: 'Inter', fontSize: '12px', fontWeight: 500 }}
|
||||
onClick={resetRunner}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 h-[calc(100vh_-_10rem)] overflow-hidden">
|
||||
<div
|
||||
className={`flex flex-col overflow-y-auto ${selectedItem || (configureMode && !selectedItem && !runnerInfo.status === 'running') ? 'w-1/2' : 'w-full'}`}
|
||||
ref={runnerBodyRef}
|
||||
className="flex flex-col w-1/2"
|
||||
>
|
||||
{runnerInfo?.statusText ?
|
||||
<div className="pb-2 font-medium danger">
|
||||
{runnerInfo?.statusText}
|
||||
</div>
|
||||
: null}
|
||||
|
||||
<div className="pb-2 font-medium test-summary">
|
||||
Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}, Skipped:{' '}
|
||||
{skippedRequests.length}
|
||||
</div>
|
||||
|
||||
{tagsEnabled && areTagsAdded && (
|
||||
<div className="pb-2 text-xs flex flex-row gap-1">
|
||||
Tags:
|
||||
@@ -383,9 +415,15 @@ export default function RunnerResults({ collection }) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{runnerInfo?.statusText ?
|
||||
<div className="pb-2 font-medium danger">
|
||||
{runnerInfo?.statusText}
|
||||
</div>
|
||||
: null}
|
||||
|
||||
{/* Items list */}
|
||||
<div className="overflow-y-auto flex-1">
|
||||
{items.filter(getActiveFilterPredicate()).map((item) => {
|
||||
<div className="overflow-y-auto flex-1 " ref={runnerBodyRef}>
|
||||
{filteredItems.map((item) => {
|
||||
return (
|
||||
<div key={item.uid}>
|
||||
<div className="item-path mt-2">
|
||||
@@ -429,7 +467,7 @@ export default function RunnerResults({ collection }) {
|
||||
|
||||
<ul className="pl-8">
|
||||
{item.preRequestTestResults
|
||||
? item.preRequestTestResults.map((result) => (
|
||||
? filterTestResults(item.preRequestTestResults).map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
@@ -449,7 +487,7 @@ export default function RunnerResults({ collection }) {
|
||||
))
|
||||
: null}
|
||||
{item.postResponseTestResults
|
||||
? item.postResponseTestResults.map((result) => (
|
||||
? filterTestResults(item.postResponseTestResults).map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
@@ -469,7 +507,7 @@ export default function RunnerResults({ collection }) {
|
||||
))
|
||||
: null}
|
||||
{item.testResults
|
||||
? item.testResults.map((result) => (
|
||||
? filterTestResults(item.testResults).map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
@@ -488,7 +526,7 @@ export default function RunnerResults({ collection }) {
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
{item.assertionResults?.map((result) => (
|
||||
{filterTestResults(item.assertionResults).map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
@@ -512,43 +550,51 @@ export default function RunnerResults({ collection }) {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{runnerInfo.status === 'ended' ? (
|
||||
<div className="mt-2 mb-4">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runAgain}>
|
||||
Run Again
|
||||
</button>
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6 ml-3" disabled={shouldDisableCollectionRun} onClick={runCollection}>
|
||||
Run Collection
|
||||
</button>
|
||||
<button className="btn btn-sm btn-close mt-6 ml-3" onClick={resetRunner}>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{selectedItem ? (
|
||||
<div className="flex flex-1 w-[50%] overflow-y-auto">
|
||||
<div className="flex flex-col w-full overflow-hidden">
|
||||
<div className="flex items-center mb-4 font-medium">
|
||||
<span className="mr-2">{selectedItem.displayName}</span>
|
||||
<span>
|
||||
{allTestsPassed(selectedItem) ?
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{anyTestFailed(selectedItem) ?
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{selectedItem.status === 'skipped' ?
|
||||
<IconCircleOff className="skipped-request" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
</span>
|
||||
<div className="flex items-center justify-between mb-4 font-medium">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2">{selectedItem.displayName}</span>
|
||||
<span>
|
||||
{allTestsPassed(selectedItem)
|
||||
? <IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{anyTestFailed(selectedItem)
|
||||
? <IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{selectedItem.status === 'skipped'
|
||||
? <IconCircleOff className="skipped-request" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSelectedItem(null)}
|
||||
className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors cursor-pointer flex items-center justify-center"
|
||||
title="Close"
|
||||
aria-label="Close response view"
|
||||
>
|
||||
<IconX size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
<ResponsePane item={selectedItem} collection={collection} />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
) : (
|
||||
<div className="flex flex-1 w-[50%] overflow-y-auto">
|
||||
<div className="flex flex-col w-full h-full items-center justify-center text-center">
|
||||
<div className="mb-4 text-gray-400 dark:text-gray-500">
|
||||
<IconExternalLink size={64} strokeWidth={1.5} />
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm">
|
||||
Click on the status code to view the response
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user