mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-25 21:55:49 +00:00
add: change log tab (#8289)
This commit is contained in:
@@ -38,6 +38,9 @@ export default defineConfig({
|
||||
dynamicImportMode: "eager",
|
||||
},
|
||||
},
|
||||
rules: [
|
||||
{ test: /\.md$/, type: 'asset/source' }
|
||||
]
|
||||
},
|
||||
ignoreWarnings: [
|
||||
(warning) => warning.message.includes('Critical dependency: the request of a dependency is an expression') && warning?.moduleDescriptor?.name?.includes('flow-parser')
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# What's New in Bruno
|
||||
|
||||
- Various stability and performance improvements.
|
||||
|
||||
---
|
||||
|
||||
For the full release history, see the [Bruno releases page](https://github.com/usebruno/bruno/releases).
|
||||
@@ -0,0 +1,31 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.changelog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid ${(props) => props.theme.requestTabs?.border || props.theme.sidebar?.border || 'transparent'};
|
||||
color: ${(props) => props.theme.text};
|
||||
|
||||
.header-version {
|
||||
font-size: ${(props) => props.theme.font?.size?.sm || '0.85em'};
|
||||
color: ${(props) => props.theme.colors?.text?.muted || props.theme.text};
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.changelog-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem 1.5rem 2rem 1.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
23
packages/bruno-app/src/components/ChangelogTab/index.js
Normal file
23
packages/bruno-app/src/components/ChangelogTab/index.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { IconConfetti } from '@tabler/icons';
|
||||
import Markdown from 'components/MarkDown';
|
||||
import { version } from '../../../package.json';
|
||||
import changelogContent from './CHANGELOG.md';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ChangelogTab = () => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="changelog-header">
|
||||
<IconConfetti size={18} strokeWidth={1.5} />
|
||||
<span>What's New</span>
|
||||
<span className="header-version">v{version}</span>
|
||||
</div>
|
||||
<div className="changelog-body">
|
||||
<Markdown content={changelogContent} onDoubleClick={() => {}} />
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangelogTab;
|
||||
@@ -43,6 +43,7 @@ import EnvironmentSettings from 'components/Environments/EnvironmentSettings';
|
||||
import GlobalEnvironmentSettings from 'components/Environments/GlobalEnvironmentSettings';
|
||||
import OpenAPISyncTab from 'components/OpenAPISyncTab';
|
||||
import OpenAPISpecTab from 'components/OpenAPISpecTab';
|
||||
import ChangelogTab from 'components/ChangelogTab';
|
||||
import CollapsedPanelIndicator from './CollapsedPanelIndicator';
|
||||
import { clampRequestHeightForResponse } from './paneSize';
|
||||
import { IconLoader2 } from '@tabler/icons';
|
||||
@@ -335,6 +336,10 @@ const RequestTabPanel = () => {
|
||||
return <Preferences />;
|
||||
}
|
||||
|
||||
if (focusedTab.type === 'changelog') {
|
||||
return <ChangelogTab />;
|
||||
}
|
||||
|
||||
if (focusedTab.type === 'workspaceOverview') {
|
||||
return activeWorkspace ? <WorkspaceOverview workspace={activeWorkspace} /> : null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import GradientCloseButton from './GradientCloseButton';
|
||||
import { IconVariable, IconSettings, IconRun, IconFolder, IconDatabase, IconWorld, IconHome, IconFileCode } from '@tabler/icons';
|
||||
import { IconVariable, IconSettings, IconRun, IconFolder, IconDatabase, IconWorld, IconHome, IconFileCode, IconConfetti } from '@tabler/icons';
|
||||
import OpenAPISyncIcon from 'components/Icons/OpenAPISync';
|
||||
|
||||
const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick, hasDraft }) => {
|
||||
@@ -102,6 +102,14 @@ const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick, hasDra
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'changelog': {
|
||||
return (
|
||||
<>
|
||||
<IconConfetti size={14} strokeWidth={1.5} className="special-tab-icon flex-shrink-0" />
|
||||
<span className="ml-1 tab-name">What's New</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -193,7 +193,8 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
'workspaceOverview',
|
||||
'workspaceEnvironments',
|
||||
'openapi-sync',
|
||||
'openapi-spec'
|
||||
'openapi-spec',
|
||||
'changelog'
|
||||
];
|
||||
|
||||
const hasDraft = tab.type === 'collection-settings' && collection?.draft;
|
||||
|
||||
@@ -7,6 +7,7 @@ import useIpcEvents from './useIpcEvents';
|
||||
import useTelemetry from './useTelemetry';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import useOpenAPISyncPolling from './useOpenAPISyncPolling';
|
||||
import useChangelogOnUpdate from './useChangelogOnUpdate';
|
||||
import { version } from '../../../package.json';
|
||||
|
||||
export const AppContext = React.createContext();
|
||||
@@ -15,6 +16,7 @@ export const AppProvider = (props) => {
|
||||
useTelemetry({ version });
|
||||
useIpcEvents();
|
||||
useOpenAPISyncPolling();
|
||||
useChangelogOnUpdate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
58
packages/bruno-app/src/providers/App/useChangelogOnUpdate.js
Normal file
58
packages/bruno-app/src/providers/App/useChangelogOnUpdate.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import semver from 'semver';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { savePreferences } from 'providers/ReduxStore/slices/app';
|
||||
import { version as currentVersion } from '../../../package.json';
|
||||
|
||||
const useChangelogOnUpdate = () => {
|
||||
const dispatch = useDispatch();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const snapshotReady = useSelector((state) => state.app.snapshotReady);
|
||||
const activeWorkspace = useSelector((state) => {
|
||||
const { workspaces, activeWorkspaceUid } = state.workspaces;
|
||||
return workspaces.find((w) => w.uid === activeWorkspaceUid);
|
||||
});
|
||||
const activeTabCollectionUid = useSelector((state) => {
|
||||
const activeTab = state.tabs.tabs.find((t) => t.uid === state.tabs.activeTabUid);
|
||||
return activeTab?.collectionUid;
|
||||
});
|
||||
const hasRunRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasRunRef.current) return;
|
||||
|
||||
// hasLaunchedBefore is set by electron-side onboarding before the renderer
|
||||
// receives preferences via main:load-preferences. Until that flips true,
|
||||
// we're still on the renderer's default state and shouldn't act yet.
|
||||
const hasLaunchedBefore = preferences?.onboarding?.hasLaunchedBefore;
|
||||
if (!hasLaunchedBefore) return;
|
||||
|
||||
// Wait until snapshot hydration finishes, otherwise the workspace's
|
||||
// overview/restored tabs are added after ours and steal active focus.
|
||||
if (!snapshotReady) return;
|
||||
|
||||
// Need a collection context to dock the tab onto an existing tab strip.
|
||||
const collectionUid = activeTabCollectionUid || activeWorkspace?.scratchCollectionUid;
|
||||
if (!collectionUid) return;
|
||||
|
||||
hasRunRef.current = true;
|
||||
|
||||
const onboarding = preferences.onboarding || {};
|
||||
const { lastSeenVersion } = onboarding;
|
||||
if (lastSeenVersion && semver.valid(lastSeenVersion) && semver.gte(lastSeenVersion, currentVersion)) return;
|
||||
|
||||
dispatch(addTab({
|
||||
type: 'changelog',
|
||||
uid: `${collectionUid}-changelog`,
|
||||
collectionUid
|
||||
}));
|
||||
|
||||
dispatch(savePreferences({
|
||||
...preferences,
|
||||
onboarding: { ...onboarding, lastSeenVersion: currentVersion }
|
||||
})).catch(() => {});
|
||||
}, [preferences, snapshotReady, activeWorkspace, activeTabCollectionUid, dispatch]);
|
||||
};
|
||||
|
||||
export default useChangelogOnUpdate;
|
||||
@@ -49,7 +49,8 @@ const initialState = {
|
||||
},
|
||||
onboarding: {
|
||||
hasLaunchedBefore: false,
|
||||
hasSeenWelcomeModal: true
|
||||
hasSeenWelcomeModal: true,
|
||||
lastSeenVersion: null
|
||||
},
|
||||
autoSave: {
|
||||
enabled: false,
|
||||
|
||||
@@ -62,7 +62,8 @@ export const tabsSlice = createSlice({
|
||||
'workspaceOverview',
|
||||
'workspaceEnvironments',
|
||||
'openapi-sync',
|
||||
'openapi-spec'
|
||||
'openapi-spec',
|
||||
'changelog'
|
||||
];
|
||||
|
||||
const existingTab = find(state.tabs, (tab) => tab.uid === uid);
|
||||
|
||||
@@ -101,12 +101,16 @@ async function onboardUser(mainWindow, lastOpenedCollections) {
|
||||
pendingSampleCollection = { mainWindow, ...collectionInfo };
|
||||
}
|
||||
|
||||
// Mark as launched and explicitly enable the welcome modal for new users
|
||||
// Mark as launched and explicitly enable the welcome modal for new users.
|
||||
// lastSeenVersion is set here (not in the renderer) so it lands in the same
|
||||
// write as hasSeenWelcomeModal, avoids a race with the welcome-modal
|
||||
// dismissal save. New users only see future changelogs, not the current one.
|
||||
const preferences = getPreferences();
|
||||
preferences.onboarding = {
|
||||
...preferences.onboarding,
|
||||
hasLaunchedBefore: true,
|
||||
hasSeenWelcomeModal: false
|
||||
hasSeenWelcomeModal: false,
|
||||
lastSeenVersion: app.getVersion()
|
||||
};
|
||||
await savePreferences(preferences);
|
||||
} catch (error) {
|
||||
|
||||
@@ -51,7 +51,8 @@ const defaultPreferences = {
|
||||
},
|
||||
onboarding: {
|
||||
hasLaunchedBefore: false,
|
||||
hasSeenWelcomeModal: true
|
||||
hasSeenWelcomeModal: true,
|
||||
lastSeenVersion: null
|
||||
},
|
||||
general: {
|
||||
defaultLocation: '',
|
||||
@@ -127,7 +128,8 @@ const preferencesSchema = Yup.object().shape({
|
||||
}),
|
||||
onboarding: Yup.object({
|
||||
hasLaunchedBefore: Yup.boolean(),
|
||||
hasSeenWelcomeModal: Yup.boolean()
|
||||
hasSeenWelcomeModal: Yup.boolean(),
|
||||
lastSeenVersion: Yup.string().nullable()
|
||||
}),
|
||||
general: Yup.object({
|
||||
defaultLocation: Yup.string().max(1024).nullable(),
|
||||
|
||||
152
tests/changelog/changelog-tab.spec.ts
Normal file
152
tests/changelog/changelog-tab.spec.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { ElectronApplication } from '@playwright/test';
|
||||
import { test, expect, closeElectronApp } from '../../playwright';
|
||||
import { waitForReadyPage } from '../utils/page';
|
||||
import { buildCommonLocators } from '../utils/page/locators';
|
||||
|
||||
const initUserDataFresh = path.join(__dirname, 'init-user-data-fresh');
|
||||
const initUserDataExisting = path.join(__dirname, 'init-user-data-existing');
|
||||
const initUserDataCurrent = path.join(__dirname, 'init-user-data-current');
|
||||
|
||||
// app.getVersion() reads from bruno-electron's package.json — match that here
|
||||
const currentVersion = require('../../packages/bruno-electron/package.json').version;
|
||||
|
||||
test.describe('Changelog ("What\'s New") Tab', () => {
|
||||
test('should NOT show the changelog tab to brand-new users', async ({ launchElectronApp }) => {
|
||||
let app: ElectronApplication | undefined;
|
||||
|
||||
try {
|
||||
app = await launchElectronApp({ initUserDataPath: initUserDataFresh });
|
||||
const page = await waitForReadyPage(app);
|
||||
const locators = buildCommonLocators(page);
|
||||
|
||||
// New users see the welcome modal — that's the established flow.
|
||||
await expect(page.getByTestId('welcome-modal')).toBeVisible();
|
||||
|
||||
// The changelog tab must not appear alongside it.
|
||||
await expect(locators.tabs.requestTab('What\'s New')).toHaveCount(0);
|
||||
} finally {
|
||||
if (app) {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should show the changelog tab for existing users upgrading to a new version', async ({ launchElectronApp }) => {
|
||||
let app: ElectronApplication | undefined;
|
||||
|
||||
try {
|
||||
app = await launchElectronApp({ initUserDataPath: initUserDataExisting });
|
||||
const page = await waitForReadyPage(app);
|
||||
const locators = buildCommonLocators(page);
|
||||
|
||||
// Tab appears in the active workspace's tab strip and becomes active.
|
||||
await expect(locators.tabs.requestTab('What\'s New')).toHaveCount(1, { timeout: 15000 });
|
||||
await expect(locators.tabs.activeRequestTab()).toContainText('What\'s New');
|
||||
|
||||
// Welcome modal must NOT show — this user already onboarded.
|
||||
await expect(page.getByTestId('welcome-modal')).not.toBeVisible();
|
||||
|
||||
// Content sanity: header + a piece of the bundled markdown.
|
||||
await expect(page.getByText('What\'s New in Bruno')).toBeVisible();
|
||||
} finally {
|
||||
if (app) {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should NOT show the changelog tab when lastSeenVersion matches the current version', async ({ launchElectronApp }) => {
|
||||
let app: ElectronApplication | undefined;
|
||||
|
||||
try {
|
||||
app = await launchElectronApp({
|
||||
initUserDataPath: initUserDataCurrent,
|
||||
templateVars: { currentVersion }
|
||||
});
|
||||
const page = await waitForReadyPage(app);
|
||||
const locators = buildCommonLocators(page);
|
||||
|
||||
// Give the hook time to run — snapshotReady + activeWorkspace must both settle.
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await expect(locators.tabs.requestTab('What\'s New')).toHaveCount(0);
|
||||
} finally {
|
||||
if (app) {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should persist lastSeenVersion on open and not re-open on next launch', async ({ launchElectronApp, createTmpDir }) => {
|
||||
const userDataPath = await createTmpDir('changelog-persist');
|
||||
let app: ElectronApplication | undefined;
|
||||
|
||||
try {
|
||||
app = await launchElectronApp({ userDataPath, initUserDataPath: initUserDataExisting });
|
||||
let page = await waitForReadyPage(app);
|
||||
let locators = buildCommonLocators(page);
|
||||
|
||||
await expect(locators.tabs.requestTab('What\'s New')).toHaveCount(1, { timeout: 15000 });
|
||||
|
||||
// The hook saves lastSeenVersion right after addTab. Poll the prefs file
|
||||
// until the write lands — electron-store writes synchronously, but the
|
||||
// dispatch chain is async.
|
||||
await expect.poll(
|
||||
async () => {
|
||||
try {
|
||||
const prefs = JSON.parse(await fs.promises.readFile(path.join(userDataPath, 'preferences.json'), 'utf8'));
|
||||
return prefs.preferences?.onboarding?.lastSeenVersion;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
{ timeout: 10000 }
|
||||
).toBe(currentVersion);
|
||||
|
||||
await closeElectronApp(app);
|
||||
app = undefined;
|
||||
|
||||
// Restart against the same user data — tab must NOT reappear for this version.
|
||||
app = await launchElectronApp({ userDataPath });
|
||||
page = await waitForReadyPage(app);
|
||||
locators = buildCommonLocators(page);
|
||||
|
||||
// Settle window for snapshot hydration + hook evaluation.
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await expect(locators.tabs.requestTab('What\'s New')).toHaveCount(0);
|
||||
} finally {
|
||||
if (app) {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should close the changelog tab when the user closes it', async ({ launchElectronApp }) => {
|
||||
let app: ElectronApplication | undefined;
|
||||
|
||||
try {
|
||||
app = await launchElectronApp({ initUserDataPath: initUserDataExisting });
|
||||
const page = await waitForReadyPage(app);
|
||||
const locators = buildCommonLocators(page);
|
||||
|
||||
const changelogTab = locators.tabs.requestTab('What\'s New');
|
||||
await expect(changelogTab).toHaveCount(1, { timeout: 15000 });
|
||||
|
||||
// Hover to reveal the close button, then click it.
|
||||
await changelogTab.hover();
|
||||
await page.locator('.request-tab')
|
||||
.filter({ hasText: 'What\'s New' })
|
||||
.getByTestId('request-tab-close-icon')
|
||||
.click();
|
||||
|
||||
await expect(changelogTab).toHaveCount(0);
|
||||
} finally {
|
||||
if (app) {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
12
tests/changelog/init-user-data-current/preferences.json
Normal file
12
tests/changelog/init-user-data-current/preferences.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"lastOpenedCollections": [
|
||||
"{{projectRoot}}/packages/bruno-tests/collection"
|
||||
],
|
||||
"preferences": {
|
||||
"onboarding": {
|
||||
"hasLaunchedBefore": true,
|
||||
"hasSeenWelcomeModal": true,
|
||||
"lastSeenVersion": "{{currentVersion}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
11
tests/changelog/init-user-data-existing/preferences.json
Normal file
11
tests/changelog/init-user-data-existing/preferences.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"lastOpenedCollections": [
|
||||
"{{projectRoot}}/packages/bruno-tests/collection"
|
||||
],
|
||||
"preferences": {
|
||||
"onboarding": {
|
||||
"hasLaunchedBefore": true,
|
||||
"hasSeenWelcomeModal": true
|
||||
}
|
||||
}
|
||||
}
|
||||
8
tests/changelog/init-user-data-fresh/preferences.json
Normal file
8
tests/changelog/init-user-data-fresh/preferences.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"preferences": {
|
||||
"onboarding": {
|
||||
"hasLaunchedBefore": false,
|
||||
"hasSeenWelcomeModal": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user