mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-01 16:44:16 +00:00
* Add drag-resize split pane for API Spec viewer Introduce a drag-to-resize split pane for the API Spec viewer and persist left pane width. Adds a new useDragResize hook to manage dragging state and clamping, plus UI: dragbar styles, a loading state for the Swagger preview (onComplete + loader), and memoization of the Swagger renderer. Wire up persisted widths via Redux: add updateApiSpecPanelLeftPaneWidth (apiSpec slice) and updateApiSpecTabLeftPaneWidth (tabs slice), and propagate leftPaneWidth / onLeftPaneWidthChange through ApiSpecPanel, OpenAPISpecTab, RequestTabPanel and SpecViewer. Misc: pass tab uid into OpenAPISpecTab and add .gstack/ to .gitignore. * Refactor SpecViewer and OpenAPISpecTab for improved loading and state management - Updated SpecViewer to enhance loading state handling for Swagger content, ensuring a smoother user experience by preventing flashes of unrendered content. - Refactored OpenAPISpecTab to streamline environment context management, optimizing the loading process for OpenAPI specifications. - Simplified the useDragResize hook by removing unnecessary references and improving the handling of drag events, ensuring better performance and responsiveness during resizing actions. * Enhance useDragResize hook to clamp width seed and improve test coverage - Updated the useDragResize hook to clamp the width seed value, ensuring it stays within defined bounds during drag events. - Added a new test case to verify that an out-of-bounds width seed is correctly clamped and persisted on immediate mouseup, enhancing the robustness of the drag-resize functionality. * Remove .gstack/ from .gitignore Delete the .gstack/ ignore entry and normalize the packages/bruno-converters/dist entry in .gitignore (deduplicated). No code changes; just tidy up ignore rules.
129 lines
4.2 KiB
JavaScript
129 lines
4.2 KiB
JavaScript
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
import find from 'lodash/find';
|
|
import { IconLoader2, IconCloud } from '@tabler/icons';
|
|
import fastJsonFormat from 'fast-json-format';
|
|
import SpecViewer from 'components/ApiSpecPanel/SpecViewer';
|
|
import StyledWrapper from 'components/ApiSpecPanel/StyledWrapper';
|
|
import { updateApiSpecTabLeftPaneWidth } from 'providers/ReduxStore/slices/tabs';
|
|
|
|
/**
|
|
* Pretty-print JSON content for readable display. YAML content is returned as-is.
|
|
*/
|
|
const prettyPrintSpec = (content) => {
|
|
if (!content) return content;
|
|
if (content.trimStart()[0] !== '{') return content;
|
|
try {
|
|
return fastJsonFormat(content);
|
|
} catch {
|
|
return content;
|
|
}
|
|
};
|
|
|
|
const OpenAPISpecTab = ({ collection, tabUid }) => {
|
|
const dispatch = useDispatch();
|
|
const leftPaneWidth = useSelector((state) => {
|
|
const tab = find(state.tabs.tabs, (t) => t.uid === tabUid);
|
|
return tab?.apiSpecLeftPaneWidth ?? null;
|
|
});
|
|
const handleLeftPaneWidthChange = useCallback(
|
|
(w) => dispatch(updateApiSpecTabLeftPaneWidth({ uid: tabUid, apiSpecLeftPaneWidth: w })),
|
|
[dispatch, tabUid]
|
|
);
|
|
|
|
const [specContent, setSpecContent] = useState(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState(null);
|
|
const [isRemote, setIsRemote] = useState(false);
|
|
|
|
const openApiSyncConfig = collection?.brunoConfig?.openapi?.[0];
|
|
const sourceUrl = openApiSyncConfig?.sourceUrl;
|
|
|
|
// Latest env context for loadSpec's remote-fetch fallback. Kept out of
|
|
// loadSpec's deps so toggling a variable doesn't refire the spec load.
|
|
const envContextRef = useRef({});
|
|
envContextRef.current = {
|
|
activeEnvironmentUid: collection?.activeEnvironmentUid,
|
|
environments: collection?.environments,
|
|
runtimeVariables: collection?.runtimeVariables,
|
|
globalEnvironmentVariables: collection?.globalEnvironmentVariables
|
|
};
|
|
|
|
const loadSpec = useCallback(async () => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
setIsRemote(false);
|
|
try {
|
|
const { ipcRenderer } = window;
|
|
const result = await ipcRenderer.invoke('renderer:read-openapi-spec', {
|
|
collectionPath: collection.pathname
|
|
});
|
|
if (result.error) {
|
|
// Local file not found — fall back to fetching from remote URL
|
|
if (sourceUrl) {
|
|
const fetchResult = await ipcRenderer.invoke('renderer:fetch-openapi-spec', {
|
|
collectionUid: collection.uid,
|
|
collectionPath: collection.pathname,
|
|
sourceUrl,
|
|
environmentContext: envContextRef.current
|
|
});
|
|
if (fetchResult.content) {
|
|
setSpecContent(prettyPrintSpec(fetchResult.content));
|
|
setIsRemote(true);
|
|
return;
|
|
}
|
|
}
|
|
setError(result.error);
|
|
} else {
|
|
setSpecContent(prettyPrintSpec(result.content));
|
|
}
|
|
} catch (err) {
|
|
setError(err.message || 'Failed to read spec file');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [collection?.pathname, collection?.uid, sourceUrl]);
|
|
|
|
useEffect(() => {
|
|
if (collection?.pathname) {
|
|
loadSpec();
|
|
}
|
|
}, [loadSpec]);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-full gap-2 opacity-50">
|
|
<IconLoader2 size={20} className="animate-spin" />
|
|
<span>Loading spec...</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error || !specContent) {
|
|
return (
|
|
<div className="flex items-center justify-center h-full opacity-50">
|
|
<span>{error || 'No spec file found. Sync your collection first.'}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<StyledWrapper className="flex flex-col flex-grow relative">
|
|
{isRemote && (
|
|
<div className="flex items-center gap-1.5 px-3 py-1.5 text-xs opacity-60" style={{ borderBottom: '1px solid var(--color-border)' }}>
|
|
<IconCloud size={14} />
|
|
<span>Showing spec file from {sourceUrl}.</span>
|
|
</div>
|
|
)}
|
|
<SpecViewer
|
|
content={specContent}
|
|
readOnly
|
|
leftPaneWidth={leftPaneWidth}
|
|
onLeftPaneWidthChange={handleLeftPaneWidthChange}
|
|
/>
|
|
</StyledWrapper>
|
|
);
|
|
};
|
|
|
|
export default OpenAPISpecTab;
|