Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[docs] Create a handy dandy version switcher #7298

Merged
merged 9 commits into from
Oct 23, 2023
4 changes: 4 additions & 0 deletions scripts/release.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const {
collateChangelogFiles,
updateChangelog,
} = require('./update-changelog');
const updateDocsVersionSwitcher = require('./update-versions-log');

const TYPE_MAJOR = 0;
const TYPE_MINOR = 1;
Expand Down Expand Up @@ -73,6 +74,9 @@ if (args.dry_run) {
// Update CHANGELOG.md
updateChangelog(changelog, versionTarget);

// Update version switcher data
updateDocsVersionSwitcher(versionTarget);

// update package.json & package-lock.json version, git commit, git tag
execSync(`npm version ${versionTarget}`, execOptions);
}
Expand Down
64 changes: 64 additions & 0 deletions scripts/tests/update-versions-log.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import updateDocsVersionSwitcher from '../update-versions-log';

// Mock files
jest.mock('fs', () => ({
readFileSync: jest.fn(() => ''),
writeFileSync: jest.fn(),
}));
const fs = require('fs');
jest.mock('child_process', () => ({
execSync: jest.fn(),
}));
const { execSync } = require('child_process');

describe('updateDocsVersionSwitcher', () => {
const mockInput = `{
"euiVersions": [
"3.0.0",
"2.0.0",
"1.0.0"
]
}`;
const expectedOutput = `{
"euiVersions": [
"4.0.0",
"3.0.0",
"2.0.0",
"1.0.0"
]
}`;

beforeEach(() => jest.clearAllMocks());

it('appends a new version to the top of the array list', () => {
fs.readFileSync.mockReturnValue(mockInput);

updateDocsVersionSwitcher('4.0.0');

expect(fs.writeFileSync).toHaveBeenCalledWith(
expect.stringContaining('versions.json'),
expectedOutput
);
expect(execSync).toHaveBeenCalledWith(
expect.stringMatching(/^git add .+versions\.json$/)
);
});

it('throws an error if the version is missing', () => {
expect(() => updateDocsVersionSwitcher('')).toThrow('Missing version');
});

it('throws an error the version is already at the start of the versions array', () => {
fs.readFileSync.mockReturnValue(mockInput);
expect(() => updateDocsVersionSwitcher('3.0.0')).toThrow(
'Current version has already been logged'
);
});

it('throws an error if the JSON data is somehow malformed', () => {
fs.readFileSync.mockReturnValue('{}');
expect(() => updateDocsVersionSwitcher('4.0.0')).toThrow(
'Invalid JSON data'
);
});
});
40 changes: 40 additions & 0 deletions scripts/update-versions-log.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const path = require('path');
const fs = require('fs');
const { execSync } = require('child_process');

const versionsLogFile = path.resolve(
__dirname,
'../src-docs/src/components/guide_page/versions.json'
);

/**
* Writes to the above `versions.json` file (which is what the docs version switcher
* uses to generate its list of versions) with the latest release
*
* To test locally, run `node -e "require('./scripts/update-versions-log')('vX.Y.Z')"`
*/
const updateDocsVersionSwitcher = (versionToAdd, file = versionsLogFile) => {
if (!versionToAdd) {
throw new Error('Missing version');
}

let fileString = fs.readFileSync(file).toString();
const split = `"euiVersions": [`;
const array = fileString.split(split);

if (array.length <= 1) {
throw new Error(`Invalid JSON data - missing ${split}`);
}
if (array[1].trimStart().startsWith(`"${versionToAdd}`)) {
throw new Error('Current version has already been logged');
}

// Prepend the new version and re-form the file string
array[1] = `\n "${versionToAdd}",` + array[1];
const updatedFileString = array.join(split);

fs.writeFileSync(file, updatedFileString);
execSync(`git add ${file}`);
};

module.exports = updateDocsVersionSwitcher;
97 changes: 75 additions & 22 deletions src-docs/src/components/guide_page/guide_page_header.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState } from 'react';
import React, { useState, useMemo } from 'react';
import { FixedSizeList } from 'react-window';

import {
EuiBadge,
Expand All @@ -9,6 +10,7 @@ import {
EuiHeaderLogo,
EuiHeaderSectionItemButton,
EuiIcon,
EuiListGroupItem,
EuiPopover,
EuiToolTip,
} from '../../../../src/components';
Expand All @@ -19,7 +21,11 @@ import { CodeSandboxLink } from '../../components/codesandbox/link';
import logoEUI from '../../images/logo-eui.svg';
import { GuideThemeSelector, GuideFigmaLink } from '../guide_theme_selector';

const pkg = require('../../../../package.json');
const { euiVersions } = require('./versions.json');
const currentVersion = require('../../../../package.json').version;
const pronounceVersion = (version: string) => {
return `version ${version.replaceAll('.', ' point ')}`; // NVDA pronounciation issue
};

export type GuidePageHeaderProps = {
onToggleLocale: () => {};
Expand All @@ -32,29 +38,76 @@ export const GuidePageHeader: React.FunctionComponent<GuidePageHeaderProps> = ({
}) => {
const isMobileSize = useIsWithinBreakpoints(['xs', 's']);

function renderLogo() {
const logo = useMemo(() => {
return (
<EuiHeaderLogo iconType={logoEUI} href="#/" aria-label="EUI home">
Elastic UI
</EuiHeaderLogo>
);
}
}, []);

function renderVersion() {
const [isVersionPopoverOpen, setIsVersionPopoverOpen] = useState(false);
const versionBadge = useMemo(() => {
const isLocalDev = window.location.host.includes('803');

return (
<EuiBadge
href="#/package/changelog"
aria-label={`Version ${pkg.version}, View changelog`}
onClick={() => setIsVersionPopoverOpen((isOpen) => !isOpen)}
onClickAriaLabel={`${
isLocalDev ? 'Local' : pronounceVersion(currentVersion)
}. Click to switch versions`}
color={isLocalDev ? 'accent' : 'default'}
>
{isLocalDev ? 'Local' : `v${pkg.version}`}
{isLocalDev ? 'Local' : `v${currentVersion}`}
</EuiBadge>
);
}
}, []);
const versionSwitcher = useMemo(() => {
return (
<EuiPopover
isOpen={isVersionPopoverOpen}
closePopover={() => setIsVersionPopoverOpen(false)}
button={versionBadge}
repositionOnScroll
panelPaddingSize="xs"
>
<FixedSizeList
className="eui-yScroll"
itemCount={euiVersions.length}
itemSize={24}
height={200}
width={120}
innerElementType="ul"
>
{({ index, style }) => {
const version = euiVersions[index];
const screenReaderVersion = pronounceVersion(version);
return (
<EuiListGroupItem
style={style}
size="xs"
label={`v${version}`}
aria-label={screenReaderVersion}
href={`https://eui.elastic.co/v${version}/`}
extraAction={{
cee-chen marked this conversation as resolved.
Show resolved Hide resolved
'aria-label': `View release notes for ${screenReaderVersion}`,
title: 'View release',
iconType: 'package',
iconSize: 's',
// @ts-ignore - this is valid
href: `https://github.com/elastic/eui/releases/tag/v${version}`,
target: '_blank',
}}
isActive={version === currentVersion}
color={version === currentVersion ? 'primary' : 'text'}
/>
);
}}
</FixedSizeList>
</EuiPopover>
);
}, [isVersionPopoverOpen, versionBadge]);

function renderGithub() {
const github = useMemo(() => {
const href = 'https://github.com/elastic/eui';
const label = 'EUI GitHub repo';
return isMobileSize ? (
Expand All @@ -68,9 +121,9 @@ export const GuidePageHeader: React.FunctionComponent<GuidePageHeaderProps> = ({
</EuiHeaderSectionItemButton>
</EuiToolTip>
);
}
}, [isMobileSize]);

function renderCodeSandbox() {
const codesandbox = useMemo(() => {
const label = 'Codesandbox';
return isMobileSize ? (
<CodeSandboxLink type="tsx">
Expand All @@ -87,11 +140,11 @@ export const GuidePageHeader: React.FunctionComponent<GuidePageHeaderProps> = ({
</CodeSandboxLink>
</EuiToolTip>
);
}
}, [isMobileSize]);

const [mobilePopoverIsOpen, setMobilePopoverIsOpen] = useState(false);

function renderMobileMenu() {
const mobileMenu = useMemo(() => {
const button = (
<EuiHeaderSectionItemButton
aria-label="Open EUI options menu"
Expand All @@ -113,32 +166,32 @@ export const GuidePageHeader: React.FunctionComponent<GuidePageHeaderProps> = ({
gutterSize="none"
responsive={false}
>
<EuiFlexItem>{renderGithub()}</EuiFlexItem>
<EuiFlexItem>{github}</EuiFlexItem>
<EuiFlexItem>
<GuideFigmaLink />
</EuiFlexItem>
<EuiFlexItem>{renderCodeSandbox()}</EuiFlexItem>
<EuiFlexItem>{codesandbox}</EuiFlexItem>
</EuiFlexGroup>
</EuiPopover>
);
}
}, [mobilePopoverIsOpen, codesandbox, github]);

const rightSideItems = isMobileSize
? [
<GuideThemeSelector
onToggleLocale={onToggleLocale}
selectedLocale={selectedLocale}
/>,
renderMobileMenu(),
mobileMenu,
]
: [
<GuideThemeSelector
onToggleLocale={onToggleLocale}
selectedLocale={selectedLocale}
/>,
renderGithub(),
github,
<GuideFigmaLink key="figma" />,
renderCodeSandbox(),
codesandbox,
];

return (
Expand All @@ -147,7 +200,7 @@ export const GuidePageHeader: React.FunctionComponent<GuidePageHeaderProps> = ({
position="fixed"
theme="dark"
sections={[
{ items: [renderLogo(), renderVersion()] },
{ items: [logo, versionSwitcher] },
{ items: rightSideItems },
]}
/>
Expand Down
Loading
Loading