Files
bruno/packages/bruno-app/src/components/OpenAPISyncTab/CollectionStatusSection/index.js
Abhishek S Lal 1877119b81 fix(openapi-sync): simplify IPC calls, fix state priorities, and improve stored spec missing UX (#7489)
* refactor(OpenAPISyncTab): remove unused props and streamline IPC calls

- Eliminated unnecessary sourceUrl prop from various components and hooks in the OpenAPISyncTab.
- Improved pretty-printing logic in OpenAPISpecTab to handle non-JSON content gracefully.
- Updated IPC calls to remove redundant parameters, enhancing code clarity and maintainability.

* feat(OpenAPISyncTab): enhance user interaction and visual feedback

- Added onTabSelect prop to OpenAPISyncTab for improved tab navigation.
- Updated color properties in StyledWrapper for better consistency with theme.
- Replaced IconClock with IconAlertTriangle in CollectionStatusSection for clearer status indication.
- Enhanced messaging in OverviewSection and SpecStatusSection to provide clearer user guidance.
- Introduced handleRestoreSpec function in useSyncFlow for better spec restoration handling.

* fix(OpenAPISyncTab): update button labels for clarity in OverviewSection

- Changed button label from 'restore' to 'spec-details' for better context.
- Updated the button text from 'View Details' to 'Go to Spec Updates' to enhance user understanding of navigation options.

* refactor(OpenAPISyncTab): remove unused props and streamline component logic

- Eliminated unnecessary props from OpenAPISyncTab, CollectionStatusSection, and SpecStatusSection for cleaner code.
- Removed commented-out code in OverviewSection and SpecStatusSection to enhance readability.
- Introduced posixifyPath utility function in filesystem.js to standardize path formatting.

* fix(OpenAPISyncTab): update openapi config handling to support array format

- Modified the logic in loadBrunoConfig to handle openapi as an array, ensuring consistent resolution of source URLs for all entries. This change improves the configuration handling for OpenAPI specifications.

* fix(OpenAPISyncTab): improve openapi config handling and merge logic

- Updated loadBrunoConfig to ensure openapi is treated as an array, enhancing source URL resolution.
- Modified mergeWithUserValues to handle cases where specItems may be undefined, improving robustness in merging user values with specifications.
2026-03-16 18:14:53 +05:30

258 lines
9.7 KiB
JavaScript

import { useMemo } from 'react';
import {
IconCheck,
IconPlus,
IconTrash,
IconArrowBackUp,
IconExternalLink,
IconAlertTriangle,
IconInfoCircle,
IconLoader2
} from '@tabler/icons';
import moment from 'moment';
import Button from 'ui/Button';
import StatusBadge from 'ui/StatusBadge';
import Modal from 'components/Modal';
import EndpointChangeSection from '../EndpointChangeSection';
import ExpandableEndpointRow from '../EndpointChangeSection/ExpandableEndpointRow';
import useEndpointActions from '../hooks/useEndpointActions';
const CollectionStatusSection = ({
collection,
collectionDrift,
reloadDrift,
specDrift,
storedSpec,
lastSyncDate,
onOpenEndpoint,
isLoading,
onTabSelect
}) => {
const {
pendingAction, setPendingAction,
confirmPendingAction,
handleResetEndpoint,
handleResetAllModified,
handleDeleteEndpoint,
handleDeleteAllLocalOnly,
handleRevertAllChanges,
handleAddMissingEndpoint,
handleAddAllMissing
} = useEndpointActions(collection, collectionDrift, reloadDrift);
const spec = storedSpec || specDrift?.newSpec;
const hasStoredSpec = collectionDrift && !collectionDrift.noStoredSpec;
const hasDrift = hasStoredSpec && (collectionDrift.modified?.length > 0
|| collectionDrift.missing?.length > 0
|| collectionDrift.localOnly?.length > 0);
const renderDriftRow = (endpoint, idx, actions) => (
<ExpandableEndpointRow
key={endpoint.id}
endpoint={endpoint}
collectionPath={collection.pathname}
newSpec={spec}
showDecisions={false}
diffLeftLabel="Last Synced Spec"
diffRightLabel="Current (in collection)"
swapDiffSides
collectionUid={collection.uid}
actions={actions}
/>
);
const modifiedCount = collectionDrift?.modified?.length || 0;
const missingCount = collectionDrift?.missing?.length || 0;
const localOnlyCount = collectionDrift?.localOnly?.length || 0;
const version = specDrift?.storedVersion || storedSpec?.info?.version;
const bannerState = useMemo(() => {
if (hasDrift) {
return {
variant: 'muted',
message: 'Collection has changes since last sync',
badges: { modifiedCount, missingCount, localOnlyCount },
actions: ['revert-all']
};
}
return null;
}, [hasDrift, modifiedCount, missingCount, localOnlyCount, version, lastSyncDate]);
return (
<div className="collection-status-section">
{bannerState && (
<div className={`spec-update-banner ${bannerState.variant}`}>
<div className="banner-left">
{bannerState.variant === 'success'
? <IconCheck size={16} className="status-check-icon" />
: <div className={`status-dot ${bannerState.variant}`} />}
<span className="banner-title">
{bannerState.message}
</span>
{bannerState.badges && (
<span className="banner-details">
{bannerState.badges.modifiedCount > 0 && <StatusBadge status="warning" radius="full">{bannerState.badges.modifiedCount} modified</StatusBadge>}
{bannerState.badges.missingCount > 0 && <StatusBadge status="danger" radius="full">{bannerState.badges.missingCount} deleted</StatusBadge>}
{bannerState.badges.localOnlyCount > 0 && <StatusBadge status="muted" radius="full">{bannerState.badges.localOnlyCount} added</StatusBadge>}
</span>
)}
</div>
{bannerState.actions.includes('revert-all') && (
<div className="banner-actions">
<Button size="sm" variant="ghost" color="danger" onClick={handleRevertAllChanges}>
Revert All to Spec
</Button>
</div>
)}
</div>
)}
{hasDrift && (
<div className="sync-info-notice mt-4">
<IconInfoCircle size={14} className="sync-info-icon" />
<span><span className="whats-updated-title">What's tracked:</span> Changes to parameters, headers, body and auth compared to the synced spec. Your variables, scripts, tests, assertions, settings etc. are not tracked here.</span>
</div>
)}
{hasDrift ? (
<div className="mt-5">
{/* Modified in Collection */}
<EndpointChangeSection
title="Modified in Collection"
type="modified"
endpoints={collectionDrift.modified || []}
expandableLayout
collectionUid={collection.uid}
sectionKey="drift-modified"
renderItem={(endpoint, idx) =>
renderDriftRow(endpoint, idx, (
<>
<Button size="xs" variant="ghost" onClick={() => onOpenEndpoint(endpoint.id)} title="Open in tab" icon={<IconExternalLink size={14} />}>
Open
</Button>
<Button size="xs" variant="ghost" onClick={() => handleResetEndpoint(endpoint)} title="Reset to spec" icon={<IconArrowBackUp size={14} />}>
Reset
</Button>
</>
))}
actions={(
<Button
size="xs"
variant="outline"
onClick={handleResetAllModified}
title="Reset all modified endpoints to match the spec"
icon={<IconArrowBackUp size={14} />}
>
Reset All
</Button>
)}
/>
{/* Deleted from Collection */}
<EndpointChangeSection
title="Deleted from Collection"
type="missing"
endpoints={collectionDrift.missing || []}
expandableLayout
collectionUid={collection.uid}
sectionKey="drift-missing"
renderItem={(endpoint, idx) =>
renderDriftRow(endpoint, idx, (
<Button size="xs" variant="ghost" onClick={() => handleAddMissingEndpoint(endpoint)} title="Restore to collection" icon={<IconPlus size={14} />}>
Restore
</Button>
))}
actions={(
<Button
size="xs"
variant="outline"
onClick={handleAddAllMissing}
title="Add all deleted endpoints back to collection"
icon={<IconPlus size={14} />}
>
Restore All
</Button>
)}
/>
{/* Added to Collection */}
<EndpointChangeSection
title="Added to Collection"
type="local-only"
endpoints={collectionDrift.localOnly || []}
expandableLayout
collectionUid={collection.uid}
sectionKey="drift-local-only"
renderItem={(endpoint, idx) =>
renderDriftRow(endpoint, idx, (
<>
<Button size="xs" variant="ghost" onClick={() => onOpenEndpoint(endpoint.id)} title="Open in tab" icon={<IconExternalLink size={14} />}>
Open
</Button>
<Button size="xs" variant="ghost" color="danger" onClick={() => handleDeleteEndpoint(endpoint)} title="Delete endpoint" icon={<IconTrash size={14} />}>
Delete
</Button>
</>
))}
actions={(
<Button
size="xs"
variant="outline"
color="danger"
onClick={handleDeleteAllLocalOnly}
title="Delete all locally added endpoints"
icon={<IconTrash size={14} />}
>
Delete All
</Button>
)}
/>
</div>
) : isLoading ? (
<div className="sync-review-empty-state mt-5">
<IconLoader2 size={40} className="empty-state-icon animate-spin" />
<h4>Checking for updates</h4>
<p>Comparing your collection with the last synced spec...</p>
</div>
) : !hasStoredSpec ? (
<div className="sync-review-empty-state mt-5">
<IconAlertTriangle size={40} className="empty-state-icon" />
<h4>{lastSyncDate ? 'Cannot track collection changes' : 'Waiting for initial sync'}</h4>
<p>{lastSyncDate
? 'The last synced spec is missing. Go to the \'Spec Updates\' tab to restore it, or sync the collection if updates are available to track future changes.'
: 'Once you sync your collection with the spec, local changes will appear here.'}
</p>
<Button variant="outline" size="sm" className="mt-4" onClick={() => onTabSelect('spec-updates')}>Go to Spec Updates</Button>
</div>
) : (
<div className="sync-review-empty-state mt-5">
<IconCheck size={40} className="empty-state-icon" />
<h4>No changes in collection</h4>
<p>The collection endpoints match the last synced spec. Nothing to review.</p>
</div>
)}
{/* Action confirmation modal */}
{pendingAction && (
<Modal size="sm" title={pendingAction.title} hideFooter={true} handleCancel={() => setPendingAction(null)}>
<div className="action-confirm-modal">
<p className="confirm-message">{pendingAction.message}</p>
<div className="confirm-actions">
<Button variant="ghost" onClick={() => setPendingAction(null)}>
Cancel
</Button>
<Button
color={pendingAction.type.includes('delete') ? 'danger' : 'primary'}
onClick={confirmPendingAction}
>
Confirm
</Button>
</div>
</div>
</Modal>
)}
</div>
);
};
export default CollectionStatusSection;