From 39f8ce2a2fba4f50a277ccce12cd4aa77d7f7057 Mon Sep 17 00:00:00 2001 From: Abhishek S Lal Date: Thu, 5 Mar 2026 17:23:31 +0530 Subject: [PATCH] feat: enhance OpenAPI Sync tab with sync status indicators and improved styling (#7371) - Added sync status logic to determine if the collection is in sync with the spec, displaying appropriate indicators in the UI. - Updated OpenAPISyncHeader to include linked collection information and sync status icons. - Refined styling in StyledWrapper for better layout and visual feedback on sync status. - Removed unused styles and components to streamline the codebase. --- .../EnvironmentSelector/StyledWrapper.js | 5 +- .../src/components/Icons/OpenAPISync/index.js | 17 +- .../OpenAPISyncTab/OpenAPISyncHeader/index.js | 31 +- .../OpenAPISyncTab/StyledWrapper.js | 348 +++++------------- .../src/components/OpenAPISyncTab/index.js | 9 + .../RequestTabs/CollectionHeader/index.js | 60 +-- 6 files changed, 179 insertions(+), 291 deletions(-) diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSelector/StyledWrapper.js b/packages/bruno-app/src/components/Environments/EnvironmentSelector/StyledWrapper.js index 082c7a50f..d3591e6fb 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSelector/StyledWrapper.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSelector/StyledWrapper.js @@ -9,6 +9,7 @@ const Wrapper = styled.div` border: 1px solid ${(props) => props.theme.app.collection.toolbar.environmentSelector.border}; line-height: 1rem; transition: all 0.15s ease; + height: 24px; &:hover { border-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.hoverBorder}; @@ -73,7 +74,7 @@ const Wrapper = styled.div` border-top: 0.0625rem solid ${(props) => props.theme.dropdown.separator}; z-index: 10; margin: 0; - + &:hover { background-color: ${(props) => props.theme.dropdown.bg + ' !important'}; } @@ -119,7 +120,7 @@ const Wrapper = styled.div` .environment-list { flex: 1; overflow-y: auto; - max-height: calc(75vh - 8rem); + max-height: calc(75vh - 8rem); padding-bottom: 2.625rem; } diff --git a/packages/bruno-app/src/components/Icons/OpenAPISync/index.js b/packages/bruno-app/src/components/Icons/OpenAPISync/index.js index b27d38a3a..0f80e967a 100644 --- a/packages/bruno-app/src/components/Icons/OpenAPISync/index.js +++ b/packages/bruno-app/src/components/Icons/OpenAPISync/index.js @@ -2,19 +2,10 @@ import React from 'react'; const OpenAPISyncIcon = ({ size = 16, color = 'currentColor', ...props }) => { return ( - - - - - - - - - - - - - + + + + ); }; diff --git a/packages/bruno-app/src/components/OpenAPISyncTab/OpenAPISyncHeader/index.js b/packages/bruno-app/src/components/OpenAPISyncTab/OpenAPISyncHeader/index.js index 037964362..fc2158570 100644 --- a/packages/bruno-app/src/components/OpenAPISyncTab/OpenAPISyncHeader/index.js +++ b/packages/bruno-app/src/components/OpenAPISyncTab/OpenAPISyncHeader/index.js @@ -3,16 +3,19 @@ import { IconDotsVertical, IconUnlink, IconSettings, - IconRefresh + IconRefresh, + IconCircleCheck, + IconAlertTriangle } from '@tabler/icons'; import toast from 'react-hot-toast'; import Button from 'ui/Button'; import StatusBadge from 'ui/StatusBadge'; import ActionIcon from 'ui/ActionIcon/index'; import MenuDropdown from 'ui/MenuDropdown'; +import Help from 'components/Help'; const OpenAPISyncHeader = ({ - collection, spec, sourceUrl, onViewSpec, + collection, spec, sourceUrl, syncStatus, onViewSpec, onOpenSettings, onOpenDisconnect, onCheck, isLoading }) => { @@ -100,7 +103,7 @@ const OpenAPISyncHeader = ({
- {sourceIsLocal ? 'Source File' : 'Source URL'} + {sourceIsLocal ? 'Source File:' : 'Source URL:'} {sourceIsLocal ? (
+
+ Linked Collection: + {collection.name} + {syncStatus === 'in-sync' && ( + } + > + Collection is up to date with the spec + + )} + {syncStatus === 'not-in-sync' && ( + } + > + Collection is not up to date with the spec + + )} +
); }; diff --git a/packages/bruno-app/src/components/OpenAPISyncTab/StyledWrapper.js b/packages/bruno-app/src/components/OpenAPISyncTab/StyledWrapper.js index 2966c018a..3c73310fe 100644 --- a/packages/bruno-app/src/components/OpenAPISyncTab/StyledWrapper.js +++ b/packages/bruno-app/src/components/OpenAPISyncTab/StyledWrapper.js @@ -22,7 +22,6 @@ const StyledWrapper = styled.div` } .setup-form { - /* background: ${(props) => props.theme.background.surface0}; */ border: 1px solid ${(props) => props.theme.border.border1}; border-radius: ${(props) => props.theme.border.radius.md}; padding: 1rem; @@ -40,6 +39,7 @@ const StyledWrapper = styled.div` display: flex; gap: 0.5rem; align-items: center; + margin-top: 0.25rem; } .url-input { @@ -87,40 +87,6 @@ const StyledWrapper = styled.div` } } - .url-label { - display: block; - font-size: ${(props) => props.theme.font.size.xs}; - font-weight: 500; - color: ${(props) => props.theme.colors.text.muted}; - margin-bottom: 0.375rem; - } - - .url-row { - display: flex; - gap: 0.5rem; - align-items: center; - } - - .url-input { - flex: 1; - padding: 0.375rem 0.75rem; - font-size: ${(props) => props.theme.font.size.sm}; - font-family: monospace; - color: ${(props) => props.theme.text}; - background: ${(props) => props.theme.input.bg}; - border: 1px solid ${(props) => props.theme.input.border}; - border-radius: ${(props) => props.theme.border.radius.md}; - outline: none; - - &:focus { - border-color: ${(props) => props.theme.input.focusBorder}; - } - - &::placeholder { - color: ${(props) => props.theme.colors.text.muted}; - } - } - /* Spec Info Card — borderless header */ .spec-info-card { margin-bottom: 14px; @@ -161,20 +127,19 @@ const StyledWrapper = styled.div` flex-shrink: 0; } + .spec-url-label { + color: ${(props) => props.theme.colors.text.muted}; + flex-shrink: 0; + } + .spec-url-row { display: flex; align-items: center; gap: 6px; - - .spec-url-label { - font-size: 11px; - color: ${(props) => props.theme.colors.text.muted}; - flex-shrink: 0; - } + font-size: 11px; .spec-url-value { font-family: monospace; - font-size: 11px; color: ${(props) => props.theme.colors.text.subtext0}; overflow: hidden; text-overflow: ellipsis; @@ -203,6 +168,27 @@ const StyledWrapper = styled.div` } + .linked-collection-row { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + + .linked-collection-name { + color: ${(props) => props.theme.colors.text.subtext0}; + } + + .sync-status-icon { + &.in-sync { + color: ${(props) => props.theme.colors.text.green}; + } + + &.not-in-sync { + color: ${(props) => props.theme.colors.text.warning}; + } + } + } + .copy-btn { flex-shrink: 0; padding: 0 4px; @@ -279,59 +265,8 @@ const StyledWrapper = styled.div` gap: 8px; } - .status-dot { - position: relative; - width: 12px; - height: 12px; - display: inline-flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - - &::before { - content: ''; - position: absolute; - width: 7px; - height: 7px; - border-radius: 50%; - opacity: 0.35; - animation: radiate 1.6s ease-out infinite; - } - - &::after { - content: ''; - position: relative; - width: 7px; - height: 7px; - border-radius: 50%; - } - - &.success { - &::before, &::after { background: ${(props) => props.theme.colors.text.green}; } - &::before { animation: none; } - } - &.warning { - &::before, &::after { background: ${(props) => props.theme.colors.text.warning}; } - } - &.muted { - &::before, &::after { background: ${(props) => props.theme.colors.text.muted}; } - } - &.danger { - &::before, &::after { background: ${(props) => props.theme.colors.text.danger}; } - } - &.info { - &::before, &::after { background: ${(props) => props.theme.status.info.text}; } - } - } - - .status-check-icon { - flex-shrink: 0; - color: ${(props) => props.theme.colors.text.green}; - } - - .banner-title { - font-size: 12px; - font-weight: 500; + .status-dot.success::before { + animation: none; } .banner-subtitle { @@ -456,58 +391,7 @@ const StyledWrapper = styled.div` min-width: 0; } - .status-dot { - position: relative; - width: 12px; - height: 12px; - display: inline-flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - - &::before { - content: ''; - position: absolute; - width: 7px; - height: 7px; - border-radius: 50%; - opacity: 0.35; - animation: radiate 1.6s ease-out infinite; - } - - &::after { - content: ''; - position: relative; - width: 7px; - height: 7px; - border-radius: 50%; - } - - &.success { - &::before, &::after { background: ${(props) => props.theme.colors.text.green}; } - } - &.info { - &::before, &::after { background: ${(props) => props.theme.status.info.text}; } - } - &.warning { - &::before, &::after { background: ${(props) => props.theme.colors.text.warning}; } - } - &.danger { - &::before, &::after { background: ${(props) => props.theme.colors.text.danger}; } - } - &.muted { - &::before, &::after { background: ${(props) => props.theme.colors.text.muted}; } - } - } - - .status-check-icon { - flex-shrink: 0; - color: ${(props) => props.theme.colors.text.green}; - } - .banner-title { - font-size: 12px; - font-weight: 500; color: ${(props) => props.theme.text}; .version-code { @@ -548,6 +432,60 @@ const StyledWrapper = styled.div` 100% { transform: scale(2.8); opacity: 0; } } + .status-dot { + position: relative; + width: 12px; + height: 12px; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + &::before { + content: ''; + position: absolute; + width: 7px; + height: 7px; + border-radius: 50%; + opacity: 0.35; + animation: radiate 1.6s ease-out infinite; + } + + &::after { + content: ''; + position: relative; + width: 7px; + height: 7px; + border-radius: 50%; + } + + &.success { + &::before, &::after { background: ${(props) => props.theme.colors.text.green}; } + } + &.warning { + &::before, &::after { background: ${(props) => props.theme.colors.text.warning}; } + } + &.muted { + &::before, &::after { background: ${(props) => props.theme.colors.text.muted}; } + } + &.danger { + &::before, &::after { background: ${(props) => props.theme.colors.text.danger}; } + } + &.info { + &::before, &::after { background: ${(props) => props.theme.status.info.text}; } + } + } + + .status-check-icon { + flex-shrink: 0; + color: ${(props) => props.theme.colors.text.green}; + } + + .banner-title { + font-size: 12px; + font-weight: 500; + } + /* Summary Cards */ .sync-summary-title-row { @@ -803,8 +741,6 @@ const StyledWrapper = styled.div` } } - - /* State Messages */ .state-message { display: flex; @@ -859,54 +795,7 @@ const StyledWrapper = styled.div` } } - .collection-status-section { - margin-top: 20px; - - .change-section { - margin-top: 0.75rem; - - .section-body.expandable-mode { - border-radius: 0 0 8px 8px; - max-height: none; /* Override default max-height so all items remain visible */ - } - } - - /* Local Changes tab: override hover background */ - .endpoint-review-row .review-row-header:hover { - background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; - } - - .sync-review-empty-state { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 4rem 2rem; - text-align: center; - - .empty-state-icon { - color: var(--color-text-muted, #9ca3af); - margin-bottom: 1rem; - } - - h4 { - font-size: ${(props) => props.theme.font.size.base}; - font-weight: 500; - color: ${(props) => props.theme.text}; - margin: 0 0 0.375rem 0; - } - - p { - font-size: ${(props) => props.theme.font.size.xs}; - line-height: 1.5; - max-width: 400px; - margin: 0; - color: ${(props) => props.theme.colors.text.muted}; - } - } - } - - .sync-tab-content .sync-review-empty-state { + .sync-review-empty-state { display: flex; flex-direction: column; align-items: center; @@ -935,6 +824,20 @@ const StyledWrapper = styled.div` } } + .collection-status-section { + margin-top: 20px; + + .change-section { + margin-top: 0.75rem; + + .section-body.expandable-mode { + border-radius: 0 0 8px 8px; + max-height: none; /* Override default max-height so all items remain visible */ + } + } + + } + /* Expandable endpoint rows — shared base styles */ .endpoint-review-row { border-bottom: 1px solid ${(props) => props.theme.border.border1}; @@ -1183,10 +1086,6 @@ const StyledWrapper = styled.div` } } - - - - /* Endpoint Details */ .endpoint-details { padding: 0.75rem; @@ -1332,10 +1231,6 @@ const StyledWrapper = styled.div` } } - - - - /* Disconnect Modal */ .disconnect-modal { .disconnect-message { @@ -1470,8 +1365,6 @@ const StyledWrapper = styled.div` } } - - /* Sync Review Modal */ .sync-review-page { position: relative; @@ -1823,36 +1716,6 @@ const StyledWrapper = styled.div` margin-top: 0; } - .sync-review-empty-state { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 4rem 2rem; - text-align: center; - color: var(--color-text-muted, #6b7280); - - .empty-state-icon { - color: var(--color-text-muted, #9ca3af); - margin-bottom: 1rem; - } - - h4 { - font-size: ${(props) => props.theme.font.size.base}; - font-weight: 500; - color: ${(props) => props.theme.text}; - margin: 0 0 0.375rem 0; - } - - p { - font-size: ${(props) => props.theme.font.size.xs}; - line-height: 1.5; - max-width: 400px; - margin: 0; - color: ${(props) => props.theme.colors.text.muted}; - } - } - .endpoints-review-sections { display: flex; flex-direction: column; @@ -1890,7 +1753,6 @@ const StyledWrapper = styled.div` max-height: none; &.expandable-mode { - /* border: 1px solid ${(props) => props.theme.border.border1}; */ border-top: none; border-radius: 0 0 ${(props) => props.theme.border.radius.sm} ${(props) => props.theme.border.radius.sm}; } @@ -1974,10 +1836,6 @@ const StyledWrapper = styled.div` } } - .review-row-diff { - background: ${(props) => props.theme.background.mantle}; - } - .endpoint-diff-view { .diff-section { margin-bottom: 0.5rem; @@ -2108,12 +1966,10 @@ const StyledWrapper = styled.div` bottom: 0rem; background: ${(props) => props.theme.background.base}; margin-top: 1rem; - /* box-shadow: 0 -4px 12px -4px rgba(0, 0, 0, 0.3); */ z-index: 10; padding-top: 0.75rem; padding-bottom: 0.75rem; - .bar-stats { display: flex; align-items: center; @@ -2218,12 +2074,6 @@ const StyledWrapper = styled.div` cursor: pointer; user-select: none; - .chevron { - color: ${(props) => props.theme.colors.text.muted}; - transition: transform 0.15s ease; - flex-shrink: 0; - &.expanded { transform: rotate(90deg); } - } } .confirm-group-label { diff --git a/packages/bruno-app/src/components/OpenAPISyncTab/index.js b/packages/bruno-app/src/components/OpenAPISyncTab/index.js index 1c941d93e..21a2d7e6c 100644 --- a/packages/bruno-app/src/components/OpenAPISyncTab/index.js +++ b/packages/bruno-app/src/components/OpenAPISyncTab/index.js @@ -61,6 +61,14 @@ const OpenAPISyncTab = ({ collection }) => { ? (specDrift?.added?.length || 0) + (specDrift?.modified?.length || 0) + (specDrift?.removed?.length || 0) : (remoteDrift?.modified?.length || 0) + (remoteDrift?.missing?.length || 0); + const syncStatus = (() => { + if (isLoading) return 'loading'; + if (error) return 'not-in-sync'; + if (!hasDriftData) return null; + if (collectionChangesCount > 0 || specUpdatesCount > 0) return 'not-in-sync'; + return 'in-sync'; + })(); + const syncTabs = useMemo(() => [ { key: 'overview', label: 'Overview' }, { @@ -96,6 +104,7 @@ const OpenAPISyncTab = ({ collection }) => { collection={collection} spec={storedSpec || specDrift?.newSpec} sourceUrl={sourceUrl} + syncStatus={syncStatus} onViewSpec={handleViewSpec} onOpenSettings={() => setShowSettingsModal(true)} onOpenDisconnect={() => setShowDisconnectModal(true)} diff --git a/packages/bruno-app/src/components/RequestTabs/CollectionHeader/index.js b/packages/bruno-app/src/components/RequestTabs/CollectionHeader/index.js index 8f0e0f30f..6a7f88af8 100644 --- a/packages/bruno-app/src/components/RequestTabs/CollectionHeader/index.js +++ b/packages/bruno-app/src/components/RequestTabs/CollectionHeader/index.js @@ -22,6 +22,7 @@ import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs'; import { uuid } from 'utils/common'; import toast from 'react-hot-toast'; import Dropdown from 'components/Dropdown'; +import MenuDropdown from 'ui/MenuDropdown'; import CloseWorkspace from 'components/Sidebar/CloseWorkspace'; import EnvironmentSelector from 'components/Environments/EnvironmentSelector'; import ToolHint from 'components/ToolHint'; @@ -197,6 +198,15 @@ const CollectionHeader = ({ collection, isScratchCollection }) => { })); }; + // Build overflow menu items for the "..." dropdown + const overflowMenuItems = [ + { id: 'variables', label: 'Variables', leftSection: IconEye, onClick: viewVariables }, + { id: 'collection-settings', label: 'Collection Settings', leftSection: IconSettings, onClick: viewCollectionSettings }, + ...(!hasOpenApiSyncConfigured + ? [{ id: 'openapi-sync', label: 'OpenAPI Sync', leftSection: () => , onClick: viewOpenApiSync }] + : []) + ]; + // Workspace action handlers (only used when isScratchCollection is true) const handleRenameWorkspaceClick = () => { workspaceActionsRef.current?.hide(); @@ -450,36 +460,38 @@ const CollectionHeader = ({ collection, isScratchCollection }) => { {/* Right side: Actions (only for regular collections) */} {!isScratchCollection && ( -
- - - - {(hasOpenApiUpdates || hasOpenApiError) && ( - - )} - - +
+ {/* OpenAPI Sync - standalone only when configured */} + {hasOpenApiSyncConfigured && ( + + + + {(hasOpenApiUpdates || hasOpenApiError) && ( + + )} + + + )} + {/* Runner - always visible */} - - - - - - - - - - + {/* JS Sandbox Mode - always visible */} - + {/* Overflow menu */} + + + + + + {/* Environment Selector - always visible */} +