diff --git a/lib/api/src/index.tsx b/lib/api/src/index.tsx index e463c25a3d05..5bc52b6be306 100644 --- a/lib/api/src/index.tsx +++ b/lib/api/src/index.tsx @@ -29,6 +29,8 @@ import * as provider from './modules/provider'; import * as addons from './modules/addons'; import * as channel from './modules/channel'; import * as notifications from './modules/notifications'; +import * as settings from './modules/settings'; +import * as releaseNotes from './modules/release-notes'; import * as stories from './modules/stories'; import * as refs from './modules/refs'; import * as layout from './modules/layout'; @@ -187,6 +189,8 @@ class ManagerProvider extends Component { addons, layout, notifications, + settings, + releaseNotes, shortcuts, stories, refs, diff --git a/lib/api/src/modules/release-notes.ts b/lib/api/src/modules/release-notes.ts new file mode 100644 index 000000000000..3a2a631884a8 --- /dev/null +++ b/lib/api/src/modules/release-notes.ts @@ -0,0 +1,60 @@ +import { RELEASE_NOTES_DATA } from 'global'; +import memoize from 'memoizerific'; + +import { ModuleFn, State } from '../index'; + +export interface ReleaseNotes { + success?: boolean; + currentVersion?: string; + showOnFirstLaunch?: boolean; +} + +const getReleaseNotesData = memoize(1)( + (): ReleaseNotes => { + try { + return { ...(JSON.parse(RELEASE_NOTES_DATA) || {}) }; + } catch (e) { + return {}; + } + } +); + +export interface SubAPI { + releaseNotesVersion: () => string; + showReleaseNotesOnLaunch: () => Promise; +} + +export const init: ModuleFn = ({ fullAPI, store }) => { + let initStoreState: Promise; + const releaseNotesData = getReleaseNotesData(); + + const api: SubAPI = { + releaseNotesVersion: () => releaseNotesData.currentVersion, + showReleaseNotesOnLaunch: async () => { + // Make sure any consumers of this function's return value have waited for + // this module's state to have been setup. Otherwise, it may be called + // before the store state is set and therefore have inaccurate data. + await initStoreState; + const { showReleaseNotesOnLaunch } = store.getState(); + return showReleaseNotesOnLaunch; + }, + }; + + const initModule = () => { + const { releaseNotesViewed: persistedReleaseNotesViewed } = store.getState(); + let releaseNotesViewed = persistedReleaseNotesViewed || []; + const didViewReleaseNotes = releaseNotesViewed.includes(releaseNotesData.currentVersion); + const showReleaseNotesOnLaunch = releaseNotesData.showOnFirstLaunch && !didViewReleaseNotes; + + if (showReleaseNotesOnLaunch) { + releaseNotesViewed = [...releaseNotesViewed, releaseNotesData.currentVersion]; + } + + initStoreState = Promise.all([ + store.setState({ showReleaseNotesOnLaunch }), + store.setState({ releaseNotesViewed }, { persistence: 'permanent' }), + ]); + }; + + return { init: initModule, api }; +}; diff --git a/lib/api/src/modules/settings.ts b/lib/api/src/modules/settings.ts new file mode 100644 index 000000000000..072c1f4186ed --- /dev/null +++ b/lib/api/src/modules/settings.ts @@ -0,0 +1,49 @@ +import { ModuleFn } from '../index'; + +export interface SubAPI { + changeSettingsTab: (tab: string) => void; + closeSettings: () => void; + isSettingsScreenActive: () => boolean; + navigateToSettingsPage: (path: string) => Promise; +} + +export const init: ModuleFn = ({ store, navigate, fullAPI }) => { + const isSettingsScreenActive = () => { + const { path } = fullAPI.getUrlState(); + return !!(path || '').match(/^\/settings/); + }; + const api: SubAPI = { + closeSettings: () => { + const { + settings: { lastTrackedStoryId }, + } = store.getState(); + + if (lastTrackedStoryId) { + fullAPI.selectStory(lastTrackedStoryId); + } else { + fullAPI.selectFirstStory(); + } + }, + changeSettingsTab: (tab: string) => { + navigate(`/settings/${tab}`); + }, + isSettingsScreenActive, + navigateToSettingsPage: async (path) => { + if (!isSettingsScreenActive()) { + const { settings, storyId } = store.getState(); + + await store.setState({ + settings: { ...settings, lastTrackedStoryId: storyId }, + }); + } + + navigate(path); + }, + }; + + const initModule = async () => { + await store.setState({ settings: { lastTrackedStoryId: null } }); + }; + + return { init: initModule, api }; +}; diff --git a/lib/api/src/modules/stories.ts b/lib/api/src/modules/stories.ts index a6fa268e3b2e..ff2fb9eb0ad8 100644 --- a/lib/api/src/modules/stories.ts +++ b/lib/api/src/modules/stories.ts @@ -46,6 +46,7 @@ export interface SubState { export interface SubAPI { storyId: typeof toId; resolveStory: (storyId: StoryId, refsId?: string) => Story | Group | Root; + selectFirstStory: () => void; selectStory: ( kindOrId: string, story?: string, @@ -223,6 +224,20 @@ export const init: ModuleFn = ({ storiesFailed: error, }); }, + selectFirstStory: () => { + const { storiesHash } = store.getState(); + const lookupList = Object.keys(storiesHash).filter( + (k) => !(storiesHash[k].children || Array.isArray(storiesHash[k])) + ); + const firstStory = lookupList[0]; + + if (firstStory) { + api.selectStory(firstStory); + return; + } + + navigate('/story/*'); + }, selectStory: (kindOrId, story = undefined, options = {}) => { const { ref, viewMode: viewModeFromArgs } = options; const { @@ -301,10 +316,15 @@ export const init: ModuleFn = ({ // Later when we change story via the manager (or SELECT_STORY below), we'll already be at the // correct path before CURRENT_STORY_WAS_SET is emitted, so this is less important (the navigate is a no-op) // Note this is the case for refs also. - fullAPI.on(CURRENT_STORY_WAS_SET, function handleCurrentStoryWasSet({ storyId, viewMode }) { + fullAPI.on(CURRENT_STORY_WAS_SET, async function handleCurrentStoryWasSet({ + storyId, + viewMode, + }) { const { source }: { source: string } = this; const [sourceType] = getSourceType(source); + if (fullAPI.isSettingsScreenActive()) return; + if (sourceType === 'local' && storyId && viewMode) { navigate(`/${viewMode}/${storyId}`); } diff --git a/lib/api/src/modules/url.ts b/lib/api/src/modules/url.ts index 603b9c4bd542..305eb3519bbe 100644 --- a/lib/api/src/modules/url.ts +++ b/lib/api/src/modules/url.ts @@ -150,10 +150,14 @@ export const init: ModuleFn = ({ store, navigate, state, provider, fullAPI, ...r }, }; - const initModule = () => { + const initModule = async () => { fullAPI.on(NAVIGATE_URL, (url: string, options: { [k: string]: any }) => { fullAPI.navigateUrl(url, options); }); + + if (await fullAPI.showReleaseNotesOnLaunch()) { + navigate('/settings/release-notes'); + } }; return { diff --git a/lib/api/src/tests/stories.test.js b/lib/api/src/tests/stories.test.js index df5158cbcf21..7f6adb83bacc 100644 --- a/lib/api/src/tests/stories.test.js +++ b/lib/api/src/tests/stories.test.js @@ -338,7 +338,12 @@ describe('stories API', () => { describe('CURRENT_STORY_WAS_SET event', () => { it('navigates to the story', async () => { const navigate = jest.fn(); - const api = new LocalEventEmitter(); + class EventEmitterWithSettings extends LocalEventEmitter { + isSettingsScreenActive() { + return false; + } + } + const api = new EventEmitterWithSettings(); const store = createMockStore({}); const { init } = initStories({ store, navigate, provider, fullAPI: api }); @@ -350,7 +355,12 @@ describe('stories API', () => { it('DOES not navigate if a settings page was selected', async () => { const navigate = jest.fn(); - const api = new LocalEventEmitter(); + class EventEmitterWithSettings extends LocalEventEmitter { + isSettingsScreenActive() { + return true; + } + } + const api = new EventEmitterWithSettings(); const store = createMockStore({ viewMode: 'settings', storyId: 'about' }); initStories({ store, navigate, provider, fullAPI: api }); diff --git a/lib/core/src/server/build-dev.js b/lib/core/src/server/build-dev.js index ae54eeaba8a9..6180681f848b 100644 --- a/lib/core/src/server/build-dev.js +++ b/lib/core/src/server/build-dev.js @@ -124,16 +124,74 @@ const updateCheck = async (version) => { return result; }; -const getReleaseNotesHistory = async (version) => { +// We only expect to have release notes available for major and minor releases. +// For this reason, we convert the actual version of the build here so that +// every place that relies on this data can reference the version of the +// release notes that we expect to use. +const getReleaseNotesVersion = (version) => { + const { major, minor } = semver.parse(version); + const { version: releaseNotesVersion } = semver.coerce(`${major}.${minor}`); + return releaseNotesVersion; +}; + +const getReleaseNotesFailedState = (version) => { + return { + success: false, + currentVersion: getReleaseNotesVersion(version), + showOnFirstLaunch: false, + }; +}; + +export const RELEASE_NOTES_CACHE_KEY = 'releaseNotesData'; + +export const getReleaseNotesData = async (currentVersionToParse, fileSystemCache) => { let result; try { - const fromCache = await cache.get('releaseNotesHistory', []); - if (!fromCache.includes(version)) { - await cache.set('releaseNotesHistory', [...fromCache, version]); + const fromCache = await fileSystemCache.get('releaseNotesData', []); + const releaseNotesVersion = getReleaseNotesVersion(currentVersionToParse); + const versionHasNotBeenSeen = !fromCache.includes(releaseNotesVersion); + + if (versionHasNotBeenSeen) { + await fileSystemCache.set('releaseNotesData', [...fromCache, releaseNotesVersion]); } - result = { success: true, current: version, history: fromCache }; + + const sortedHistory = semver.sort(fromCache); + const highestVersionSeenInThePast = sortedHistory.slice(-1)[0]; + + let isUpgrading = false; + let isMajorOrMinorDiff = false; + + if (highestVersionSeenInThePast) { + isUpgrading = semver.gt(releaseNotesVersion, highestVersionSeenInThePast); + const versionDiff = semver.diff(releaseNotesVersion, highestVersionSeenInThePast); + isMajorOrMinorDiff = versionDiff === 'major' || versionDiff === 'minor'; + } + + result = { + success: true, + showOnFirstLaunch: + versionHasNotBeenSeen && + // Only show the release notes if this is not the first time Storybook + // has been built. + !!highestVersionSeenInThePast && + isUpgrading && + isMajorOrMinorDiff, + currentVersion: releaseNotesVersion, + }; } catch (error) { - result = { success: false }; + console.log(` + + + + + ERROR + + + + + `); + console.log(error); + result = getReleaseNotesFailedState(currentVersionToParse); } return result; }; @@ -270,17 +328,19 @@ export async function buildDevStandalone(options) { const { host, extendServer, packageJson, versionUpdates, releaseNotes } = options; const { version } = packageJson; - const [port, versionCheck, releaseNotesHistory] = await Promise.all([ + const [port, versionCheck, releaseNotesData] = await Promise.all([ getFreePort(options.port), versionUpdates ? updateCheck(version) : Promise.resolve({ success: false, data: {}, time: Date.now() }), - releaseNotes ? getReleaseNotesHistory(version) : Promise.resolve({ success: false }), + releaseNotes + ? getReleaseNotesData(version, cache) + : Promise.resolve(getReleaseNotesFailedState(version)), ]); /* eslint-disable no-param-reassign */ options.versionCheck = versionCheck; - options.releaseNotesHistory = releaseNotesHistory; + options.releaseNotesData = releaseNotesData; /* eslint-enable no-param-reassign */ if (!options.ci && !options.smokeTest && options.port != null && port !== options.port) { diff --git a/lib/core/src/server/build-dev.test.js b/lib/core/src/server/build-dev.test.js new file mode 100644 index 000000000000..ec850c0251a0 --- /dev/null +++ b/lib/core/src/server/build-dev.test.js @@ -0,0 +1,80 @@ +import { getReleaseNotesData, RELEASE_NOTES_CACHE_KEY } from './build-dev'; + +describe('getReleaseNotesData', () => { + it('handles errors gracefully', async () => { + const version = '4.0.0'; + // The cache is missing necessary functions. This will cause an error. + const cache = {}; + + expect(await getReleaseNotesData(version, cache)).toEqual({ + currentVersion: version, + showOnFirstLaunch: false, + success: false, + }); + }); + + it('does not show the release notes on first build', async () => { + const version = '4.0.0'; + const set = jest.fn(() => Promise.resolve()); + const cache = { get: () => Promise.resolve([]), set }; + + expect(await getReleaseNotesData(version, cache)).toEqual({ + currentVersion: version, + showOnFirstLaunch: false, + success: true, + }); + expect(set).toHaveBeenCalledWith(RELEASE_NOTES_CACHE_KEY, ['4.0.0']); + }); + + it('shows the release notes after upgrading a major version', async () => { + const version = '4.0.0'; + const set = jest.fn(() => Promise.resolve()); + const cache = { get: () => Promise.resolve(['3.0.0']), set }; + + expect(await getReleaseNotesData(version, cache)).toEqual({ + currentVersion: version, + showOnFirstLaunch: true, + success: true, + }); + expect(set).toHaveBeenCalledWith(RELEASE_NOTES_CACHE_KEY, ['3.0.0', '4.0.0']); + }); + + it('shows the release notes after upgrading a minor version', async () => { + const version = '4.1.0'; + const set = jest.fn(() => Promise.resolve()); + const cache = { get: () => Promise.resolve(['4.0.0']), set }; + + expect(await getReleaseNotesData(version, cache)).toEqual({ + currentVersion: version, + showOnFirstLaunch: true, + success: true, + }); + expect(set).toHaveBeenCalledWith(RELEASE_NOTES_CACHE_KEY, ['4.0.0', '4.1.0']); + }); + + it('transforms patch versions to the closest major.minor version', async () => { + const version = '4.0.1'; + const set = jest.fn(() => Promise.resolve()); + const cache = { get: () => Promise.resolve(['4.0.0']), set }; + + expect(await getReleaseNotesData(version, cache)).toEqual({ + currentVersion: '4.0.0', + showOnFirstLaunch: false, + success: true, + }); + expect(set).not.toHaveBeenCalled(); + }); + + it('does not show release notes when downgrading', async () => { + const version = '3.0.0'; + const set = jest.fn(() => Promise.resolve()); + const cache = { get: () => Promise.resolve(['4.0.0']), set }; + + expect(await getReleaseNotesData(version, cache)).toEqual({ + currentVersion: '3.0.0', + showOnFirstLaunch: false, + success: true, + }); + expect(set).toHaveBeenCalledWith(RELEASE_NOTES_CACHE_KEY, ['4.0.0', '3.0.0']); + }); +}); diff --git a/lib/core/src/server/cli/dev.js b/lib/core/src/server/cli/dev.js index ec3a55b2912e..224cffc6750b 100644 --- a/lib/core/src/server/cli/dev.js +++ b/lib/core/src/server/cli/dev.js @@ -28,7 +28,11 @@ async function getCLI(packageJson) { .option('--loglevel [level]', 'Control level of logging during build') .option('--quiet', 'Suppress verbose build output') .option('--no-version-updates', 'Suppress update check', true) - .option('--no-release-notes', 'Suppress release notes', true) + .option( + '--no-release-notes', + 'Suppress automatic redirects to the release notes after upgrading', + true + ) .option('--no-dll', 'Do not use dll reference') .option('--debug-webpack', 'Display final webpack configurations for debugging purposes') .option( diff --git a/lib/core/src/server/manager/manager-webpack.config.js b/lib/core/src/server/manager/manager-webpack.config.js index bc6cfeca0baa..f67841fd3ded 100644 --- a/lib/core/src/server/manager/manager-webpack.config.js +++ b/lib/core/src/server/manager/manager-webpack.config.js @@ -36,7 +36,7 @@ export default async ({ cache, previewUrl, versionCheck, - releaseNotesHistory, + releaseNotesData, presets, }) => { const { raw, stringified } = loadEnv(); @@ -87,7 +87,7 @@ export default async ({ globals: { LOGLEVEL: logLevel, VERSIONCHECK: JSON.stringify(versionCheck), - RELEASE_NOTES_HISTORY: JSON.stringify(releaseNotesHistory), + RELEASE_NOTES_DATA: JSON.stringify(releaseNotesData), DOCS_MODE: docsMode, // global docs mode PREVIEW_URL: previewUrl, // global preview URL }, diff --git a/lib/theming/src/animation.ts b/lib/theming/src/animation.ts index c92be3f7cc97..051f01a80ce2 100644 --- a/lib/theming/src/animation.ts +++ b/lib/theming/src/animation.ts @@ -4,7 +4,7 @@ export const easing = { rubber: 'cubic-bezier(0.175, 0.885, 0.335, 1.05)', }; -const rotate360 = keyframes` +export const rotate360 = keyframes` from { transform: rotate(0deg); } diff --git a/lib/theming/src/index.ts b/lib/theming/src/index.ts index 86d514f19ad7..e7cc8fd880bf 100644 --- a/lib/theming/src/index.ts +++ b/lib/theming/src/index.ts @@ -4,6 +4,7 @@ import { Theme } from './types'; export const styled = emotionStyled as CreateStyled; export * from './base'; +export * from './animation'; export * from './types'; export * from '@emotion/core'; diff --git a/lib/ui/src/containers/menu.tsx b/lib/ui/src/containers/menu.tsx index 11933047c154..eb775f841409 100644 --- a/lib/ui/src/containers/menu.tsx +++ b/lib/ui/src/containers/menu.tsx @@ -2,6 +2,7 @@ import React, { useMemo } from 'react'; import { Badge } from '@storybook/components'; import { API } from '@storybook/api'; +import { color } from '@storybook/theming'; import { shortcutToHumanString } from '@storybook/api/shortcut'; import { MenuItemIcon } from '../components/sidebar/Menu'; @@ -24,6 +25,39 @@ export const useMenu = ( ) => { const shortcutKeys = api.getShortcutKeys(); + const about = useMemo( + () => ({ + id: 'about', + title: 'About your Storybook', + onClick: () => api.navigateToSettingsPage('/settings/about'), + right: api.versionUpdateAvailable() && Update, + left: , + }), + [api, shortcutToHumanStringIfEnabled, enableShortcuts, shortcutKeys] + ); + + const releaseNotes = useMemo( + () => ({ + id: 'release-notes', + title: 'Release notes', + onClick: () => api.navigateToSettingsPage('/settings/release-notes'), + left: , + }), + [api, shortcutToHumanStringIfEnabled, enableShortcuts, shortcutKeys] + ); + + const shortcuts = useMemo( + () => ({ + id: 'shortcuts', + title: 'Keyboard shortcuts', + onClick: () => api.navigateToSettingsPage('/settings/shortcuts'), + right: shortcutToHumanStringIfEnabled(shortcutKeys.shortcutsPage, enableShortcuts), + left: , + style: { borderBottom: `4px solid ${color.mediumlight}` }, + }), + [api, shortcutToHumanStringIfEnabled, enableShortcuts, shortcutKeys] + ); + const sidebarToggle = useMemo( () => ({ id: 'S', @@ -123,28 +157,6 @@ export const useMenu = ( [api, shortcutToHumanStringIfEnabled, enableShortcuts, shortcutKeys] ); - const about = useMemo( - () => ({ - id: 'about', - title: 'About your Storybook', - onClick: () => api.navigate('/settings/about'), - right: api.versionUpdateAvailable() && Update, - left: , - }), - [api, shortcutToHumanStringIfEnabled, enableShortcuts, shortcutKeys] - ); - - const shortcuts = useMemo( - () => ({ - id: 'shortcuts', - title: 'Keyboard shortcuts', - onClick: () => api.navigate('/settings/shortcuts'), - right: shortcutToHumanStringIfEnabled(shortcutKeys.shortcutsPage, enableShortcuts), - left: , - }), - [api, shortcutToHumanStringIfEnabled, enableShortcuts, shortcutKeys] - ); - const collapse = useMemo( () => ({ id: 'collapse', @@ -158,6 +170,9 @@ export const useMenu = ( return useMemo( () => [ + about, + releaseNotes, + shortcuts, sidebarToggle, addonsToggle, addonsOrientationToggle, @@ -167,11 +182,12 @@ export const useMenu = ( down, prev, next, - about, - shortcuts, collapse, ], [ + about, + releaseNotes, + shortcuts, sidebarToggle, addonsToggle, addonsOrientationToggle, @@ -181,8 +197,6 @@ export const useMenu = ( down, prev, next, - about, - shortcuts, collapse, ] ); diff --git a/lib/ui/src/settings/about.tsx b/lib/ui/src/settings/about.tsx index ac3e7962958f..3a640d8f608b 100644 --- a/lib/ui/src/settings/about.tsx +++ b/lib/ui/src/settings/about.tsx @@ -123,89 +123,68 @@ const AboutScreen: FunctionComponent<{ return ( - {} }} - tools={ + +
+ + Storybook {current.version} +
+ + {updateMessage} + + {latest ? ( - { - e.preventDefault(); - return onClose(); - }} - title="close" - > - - + + {latest.version} Changelog + + Read full changelog + + + + {latest.info.plain} + - } - > -
- -
- - Storybook {current.version} -
- - {updateMessage} - - {latest ? ( - - - {latest.version} Changelog - - Read full changelog - - - - {latest.info.plain} - - - ) : ( - - - Check Storybook's release history - - - )} - - {canUpdate && ( - - -

- Upgrade all Storybook packages to latest: -

- - npx npm-check-updates '/storybook/' -u && npm install - -

- Alternatively, if you're using yarn run the following command, and check all - Storybook related packages: -

- - yarn upgrade-interactive --latest - -
-
- )} - - -
-
-
+ ) : ( + + + Check Storybook's release history + + + )} + + {canUpdate && ( + + +

+ Upgrade all Storybook packages to latest: +

+ + npx npm-check-updates '/storybook/' -u && npm install + +

+ Alternatively, if you're using yarn run the following command, and check all + Storybook related packages: +

+ + yarn upgrade-interactive --latest + +
+
+ )} + + +
); }; diff --git a/lib/ui/src/settings/about_page.tsx b/lib/ui/src/settings/about_page.tsx index 584567ce9655..24eaf1767942 100644 --- a/lib/ui/src/settings/about_page.tsx +++ b/lib/ui/src/settings/about_page.tsx @@ -20,17 +20,15 @@ class NotificationClearer extends Component<{ api: API; notificationId: string } } export default () => ( - - - {({ api }: Combo) => ( - - history.back()} - /> - - )} - - + + {({ api }: Combo) => ( + + history.back()} + /> + + )} + ); diff --git a/lib/ui/src/settings/index.tsx b/lib/ui/src/settings/index.tsx index 8a91b8114ca1..5fd6626c7ab0 100644 --- a/lib/ui/src/settings/index.tsx +++ b/lib/ui/src/settings/index.tsx @@ -1,12 +1,85 @@ -import React, { FunctionComponent, Fragment } from 'react'; +import React, { FunctionComponent, SyntheticEvent, useEffect, useState } from 'react'; +import { Tabs, IconButton, Icons } from '@storybook/components'; +import { useStorybookApi } from '@storybook/api'; +import { Location, Route } from '@storybook/router'; +import { styled } from '@storybook/theming'; import AboutPage from './about_page'; +import ReleaseNotesPage from './release_notes_page'; import ShortcutsPage from './shortcuts_page'; -const SettingsPages: FunctionComponent = () => ( - - - - +const ABOUT = 'about'; +const SHORTCUTS = 'shortcuts'; +const RELEASE_NOTES = 'release-notes'; + +export const Wrapper = styled.div` + div[role='tabpanel'] { + height: 100%; + } +`; + +interface PureSettingsPagesProps { + activeTab: string; + changeTab: (tab: string) => {}; + onClose: () => {}; +} + +const PureSettingsPages: FunctionComponent = ({ + activeTab, + changeTab, + onClose, +}) => ( + + { + e.preventDefault(); + return onClose(); + }} + > + + + } + > +
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+
); +const SettingsPages: FunctionComponent = () => { + const api = useStorybookApi(); + const changeTab = (tab: string) => api.changeSettingsTab(tab); + + return ( + + {(locationData) => ( + + )} + + ); +}; + export { SettingsPages as default }; diff --git a/lib/ui/src/settings/release_notes.stories.tsx b/lib/ui/src/settings/release_notes.stories.tsx new file mode 100644 index 000000000000..93c75d2667cc --- /dev/null +++ b/lib/ui/src/settings/release_notes.stories.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { actions as makeActions } from '@storybook/addon-actions'; + +import { DecoratorFn } from '@storybook/react'; +import { PureReleaseNotes } from './release_notes'; + +export default { + component: PureReleaseNotes, + title: 'UI/Settings/ReleaseNotes', +}; + +const actions = makeActions('setLoaded'); + +const VERSION = '6.0.0'; + +export const Loading = () => ( + +); + +export const DidHitMaxWaitTime = () => ( + +); diff --git a/lib/ui/src/settings/release_notes.tsx b/lib/ui/src/settings/release_notes.tsx new file mode 100644 index 000000000000..274d33e055d8 --- /dev/null +++ b/lib/ui/src/settings/release_notes.tsx @@ -0,0 +1,105 @@ +import React, { FunctionComponent, useEffect, useState } from 'react'; +import { color, styled, typography } from '@storybook/theming'; +import { Icons, Loader } from '@storybook/components'; + +export const Centered = styled.div({ + top: '50%', + position: 'absolute', + transform: 'translateY(-50%)', + width: '100%', + textAlign: 'center', +}); + +export const LoaderWrapper = styled.div({ + position: 'relative', + height: '32px', +}); + +export const Message = styled.div({ + paddingTop: '12px', + color: color.mediumdark, + maxWidth: '295px', + margin: '0 auto', + fontSize: `${typography.size.s1}px`, + lineHeight: `16px`, +}); + +export const Iframe = styled.iframe({ + border: 0, + width: '100%', + height: '100%', +}); + +const getIframeUrl = (version: string) => `https://storybook.js.org/release-notes/${version}`; + +const ReleaseNotesLoader: FunctionComponent = () => ( + + + + + Loading release notes + +); + +const MaxWaitTimeMessaging: FunctionComponent = () => ( + + + + The release notes couldn't be loaded. Check your internet connection and try again. + + +); + +interface ReleaseNotesProps { + didHitMaxWaitTime: boolean; + isLoaded: boolean; + setLoaded: (isLoaded: boolean) => void; + version: string; +} + +const PureReleaseNotes: FunctionComponent = ({ + didHitMaxWaitTime, + isLoaded, + setLoaded, + version, +}) => ( + <> + {!isLoaded && !didHitMaxWaitTime && } + {didHitMaxWaitTime ? ( + + ) : ( +