Feat/openapi sync beta tag (#7461)

* feat: introduce OpenAPI Sync beta feedback feature

- Added a feedback section in the OpenAPISyncTab and ConnectSpecForm to encourage user input during the beta phase.
- Styled the feedback message and button for better visibility.
- Updated the beta features list to include OpenAPI Sync and adjusted related components to reflect its beta status with appropriate badges.
- Enhanced the StatusBadge component to support a new 'xs' size for better integration in various UI elements.

* feat: integrate OpenAPI Sync beta feature toggle

- Updated the ImportCollectionLocation component to conditionally enable the "Check for Spec Updates" option based on the OpenAPI Sync beta feature status.
- Modified default preferences to disable OpenAPI Sync by default, ensuring users are not prompted for updates unless explicitly enabled.

* feat: enhance beta features integration in Preferences

- Updated the BETA_FEATURES array to use constants from utils/beta-features for better maintainability.
- Improved the handling of beta preferences by merging new preferences with existing ones, ensuring a smoother user experience when toggling features.

* feat: enhance OpenAPI Sync polling with beta feature toggle

- Integrated a beta feature toggle for OpenAPI Sync polling, allowing conditional activation based on user settings.
- Updated the pollingEnabled logic to incorporate the new beta feature status, ensuring better control over sync behavior.
This commit is contained in:
Abhishek S Lal
2026-03-13 11:29:04 +05:30
committed by lohit-bruno
parent 4d17809562
commit 663ece708e
11 changed files with 88 additions and 22 deletions

View File

@@ -124,6 +124,17 @@ const ConnectSpecForm = ({ sourceUrl, setSourceUrl, isLoading, error, setError,
</div>
))}
</div>
<p className="beta-feedback-inline">
OpenAPI Sync is in Beta we'd love to hear your feedback and suggestions.{' '}
<button
type="button"
className="beta-feedback-link"
onClick={() => window?.ipcRenderer?.openExternal('https://github.com/usebruno/bruno/discussions/7401')}
>
Share feedback
</button>
</p>
</div>
);
};

View File

@@ -2278,6 +2278,26 @@ const StyledWrapper = styled.div`
gap: 0.5rem;
flex-shrink: 0;
}
.beta-feedback-inline {
margin-top: 2rem;
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.colors.text.muted};
.beta-feedback-link {
background: none;
border: none;
padding: 0;
color: ${(props) => props.theme.status.info.text};
cursor: pointer;
font-size: inherit;
text-decoration: underline;
&:hover {
opacity: 0.8;
}
}
}
`;
export default StyledWrapper;

View File

@@ -134,6 +134,16 @@ const OpenAPISyncTab = ({ collection }) => {
fileNotFound={fileNotFound}
onOpenSettings={() => setShowSettingsModal(true)}
/>
<p className="beta-feedback-inline">
OpenAPI Sync is in Beta we'd love to hear your feedback and suggestions.{' '}
<button
type="button"
className="beta-feedback-link"
onClick={() => window?.ipcRenderer?.openExternal('https://github.com/usebruno/bruno/discussions/7401')}
>
Share feedback
</button>
</p>
</div>
)}
@@ -195,6 +205,7 @@ const OpenAPISyncTab = ({ collection }) => {
)}
</>
)}
</div>
{showSettingsModal && (

View File

@@ -8,17 +8,19 @@ import debounce from 'lodash/debounce';
import toast from 'react-hot-toast';
import { IconFlask } from '@tabler/icons';
import get from 'lodash/get';
import { BETA_FEATURES as BETA_FEATURE_IDS } from 'utils/beta-features';
/**
* Add beta features here.
* Example:
* {
* id: 'nodevm',
* label: 'Node VM Runtime',
* description: 'Enable Node VM runtime for JavaScript execution in Developer Mode'
* }
* UI metadata for beta features rendered in Preferences.
* IDs must match keys from utils/beta-features.js BETA_FEATURES.
*/
const BETA_FEATURES = [];
const BETA_FEATURES = [
{
id: BETA_FEATURE_IDS.OPENAPI_SYNC,
label: 'OpenAPI Sync',
description: 'Synchronize your Bruno collection with an OpenAPI specification. Detect drift, review changes, and sync with a single click.'
}
];
const Beta = ({ close }) => {
const preferences = useSelector((state) => state.app.preferences);
@@ -45,6 +47,7 @@ const Beta = ({ close }) => {
const betaSchema = generateValidationSchema();
const formik = useFormik({
enableReinitialize: true,
initialValues: generateInitialValues(),
validationSchema: betaSchema,
onSubmit: async (values) => {
@@ -61,7 +64,10 @@ const Beta = ({ close }) => {
dispatch(
savePreferences({
...preferences,
beta: newBetaPreferences
beta: {
...preferences.beta,
...newBetaPreferences
}
})
)
.catch((err) => console.log(err) && toast.error('Failed to update beta preferences'));

View File

@@ -32,6 +32,8 @@ import { getRevealInFolderLabel } from 'utils/common/platform';
import classNames from 'classnames';
import StyledWrapper from './StyledWrapper';
import { useTheme } from 'providers/Theme';
import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features';
import StatusBadge from 'ui/StatusBadge/index';
const CollectionHeader = ({ collection, isScratchCollection }) => {
const dispatch = useDispatch();
@@ -42,6 +44,8 @@ const CollectionHeader = ({ collection, isScratchCollection }) => {
// Get the current active workspace
const currentWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);
const gitRootPath = collection?.git?.gitRootPath;
const isOpenAPISyncEnabled = useBetaFeature(BETA_FEATURES.OPENAPI_SYNC);
// Workspace rename state
const [isRenamingWorkspace, setIsRenamingWorkspace] = useState(false);
@@ -201,8 +205,8 @@ const CollectionHeader = ({ collection, isScratchCollection }) => {
// Build overflow menu items for the "..." dropdown
const overflowMenuItems = [
{ id: 'variables', label: 'Variables', leftSection: IconEye, onClick: viewVariables },
...(!hasOpenApiSyncConfigured
? [{ id: 'openapi-sync', label: 'OpenAPI', leftSection: OpenAPISyncIcon, onClick: viewOpenApiSync }]
...(isOpenAPISyncEnabled && !hasOpenApiSyncConfigured
? [{ id: 'openapi-sync', label: 'OpenAPI', leftSection: OpenAPISyncIcon, rightSection: <StatusBadge status="info" size="xs">Beta</StatusBadge>, onClick: viewOpenApiSync }]
: []),
{ id: 'collection-settings', label: 'Collection Settings', leftSection: IconSettings, onClick: viewCollectionSettings }
];
@@ -461,8 +465,8 @@ const CollectionHeader = ({ collection, isScratchCollection }) => {
{/* Right side: Actions (only for regular collections) */}
{!isScratchCollection && (
<div className="flex flex-grow gap-1.5 items-center justify-end">
{/* OpenAPI Sync - standalone only when configured */}
{hasOpenApiSyncConfigured && (
{/* OpenAPI Sync - standalone only when configured and beta enabled */}
{isOpenAPISyncEnabled && hasOpenApiSyncConfigured && (
<ToolHint
text={hasOpenApiError ? 'OpenAPI Error' : hasOpenApiUpdates ? 'OpenAPI Updates Available' : 'OpenAPI'}
toolhintId="OpenApiSyncToolhintId"

View File

@@ -2,6 +2,7 @@ import React from 'react';
import GradientCloseButton from './GradientCloseButton';
import { IconVariable, IconSettings, IconRun, IconFolder, IconDatabase, IconWorld, IconHome, IconFileCode } from '@tabler/icons';
import OpenAPISyncIcon from 'components/Icons/OpenAPISync';
import StatusBadge from 'ui/StatusBadge/index';
const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick, hasDraft }) => {
const getTabInfo = (type, tabName) => {
@@ -90,7 +91,8 @@ const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick, hasDra
return (
<>
<OpenAPISyncIcon size={14} className="special-tab-icon flex-shrink-0" />
<span className="ml-1 tab-name">OpenAPI</span>
<span className="ml-1 tab-name mr-1">OpenAPI</span>
<StatusBadge status="info" size="xs">Beta</StatusBadge>
</>
);
}

View File

@@ -48,6 +48,8 @@ import { getRevealInFolderLabel } from 'utils/common/platform';
import { openDevtoolsAndSwitchToTerminal } from 'utils/terminal';
import ActionIcon from 'ui/ActionIcon';
import MenuDropdown from 'ui/MenuDropdown';
import StatusBadge from 'ui/StatusBadge';
import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features';
import { useSidebarAccordion } from 'components/Sidebar/SidebarAccordionContext';
import { createEmptyStateMenuItems } from 'utils/collections/emptyStateRequest';
@@ -56,6 +58,7 @@ import { createEmptyStateMenuItems } from 'utils/collections/emptyStateRequest';
const EMPTY_STATE_DELAY_MS = 300;
const Collection = ({ collection, searchText }) => {
const isOpenAPISyncEnabled = useBetaFeature(BETA_FEATURES.OPENAPI_SYNC);
const { dropdownContainerRef } = useSidebarAccordion();
const [showNewFolderModal, setShowNewFolderModal] = useState(false);
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
@@ -382,12 +385,13 @@ const Collection = ({ collection, searchText }) => {
setShowCloneCollectionModalOpen(true);
}
},
{
...(isOpenAPISyncEnabled ? [{
id: 'sync-openapi',
leftSection: OpenAPISyncIcon,
label: 'OpenAPI',
rightSection: <StatusBadge status="info" size="xs">Beta</StatusBadge>,
onClick: openOpenAPISyncTab
},
}] : []),
...(hasCopiedItems
? [
{

View File

@@ -13,6 +13,7 @@ import { processBrunoCollection } from 'utils/importers/bruno-collection';
import { processOpenCollection } from 'utils/importers/opencollection';
import { wsdlToBruno } from '@usebruno/converters';
import { toastError } from 'utils/common/error';
import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features';
import Modal from 'components/Modal';
import Help from 'components/Help';
import Dropdown from 'components/Dropdown';
@@ -101,13 +102,14 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, rawData, format, sour
const dispatch = useDispatch();
const [groupingType, setGroupingType] = useState('tags');
const [collectionFormat, setCollectionFormat] = useState(DEFAULT_COLLECTION_FORMAT);
const [enableCheckForSpecUpdates, setEnableCheckForSpecUpdates] = useState(true);
const isOpenAPISyncEnabled = useBetaFeature(BETA_FEATURES.OPENAPI_SYNC);
const [enableCheckForSpecUpdates, setEnableCheckForSpecUpdates] = useState(isOpenAPISyncEnabled);
const dropdownTippyRef = useRef();
const isOpenApi = format === 'openapi';
const isZipImport = format === 'bruno-zip';
const isOpenApiFromUrl = isOpenApi && !!sourceUrl && !filePath;
const isOpenApiFromFile = isOpenApi && !!filePath && !sourceUrl;
const showCheckForSpecUpdatesOption = isOpenApiFromUrl || isOpenApiFromFile;
const showCheckForSpecUpdatesOption = isOpenAPISyncEnabled && (isOpenApiFromUrl || isOpenApiFromFile);
const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);
const preferences = useSelector((state) => state.app.preferences);

View File

@@ -2,14 +2,16 @@ import { useEffect, useMemo, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { checkActiveWorkspaceCollectionsForUpdates } from 'providers/ReduxStore/slices/openapi-sync';
import { normalizePath } from 'utils/common/path';
import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features';
const POLL_INTERVAL = 5 * 60 * 1000; // 5 minutes
const useOpenAPISyncPolling = () => {
const dispatch = useDispatch();
// Global toggle for pausing all OpenAPI sync polling (defaults to true, not yet wired to any UI)
const pollingEnabled = useSelector((state) => state.openapiSync?.pollingEnabled ?? true);
const isOpenAPISyncEnabled = useBetaFeature(BETA_FEATURES.OPENAPI_SYNC);
// Global toggle for pausing all OpenAPI sync polling
const pollingEnabled = useSelector((state) => state.openapiSync?.pollingEnabled ?? true) && isOpenAPISyncEnabled;
const collections = useSelector((state) => state.collections?.collections || []);
const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);
const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);

View File

@@ -5,7 +5,8 @@ import { useSelector } from 'react-redux';
* Contains all available beta feature keys
*/
export const BETA_FEATURES = Object.freeze({
NODE_VM: 'nodevm'
NODE_VM: 'nodevm',
OPENAPI_SYNC: 'openapi-sync'
});
/**

View File

@@ -45,7 +45,9 @@ const defaultPreferences = {
layout: {
responsePaneOrientation: 'horizontal'
},
beta: {},
beta: {
'openapi-sync': false
},
onboarding: {
hasLaunchedBefore: false,
hasSeenWelcomeModal: true
@@ -154,6 +156,7 @@ const preferencesSchema = Yup.object().shape({
responsePaneOrientation: Yup.string().oneOf(['horizontal', 'vertical'])
}),
beta: Yup.object({
'openapi-sync': Yup.boolean()
}),
onboarding: Yup.object({
hasLaunchedBefore: Yup.boolean(),