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

feature(website): storybook docusaurus integration #7648

Merged
Merged
54 changes: 0 additions & 54 deletions .github/workflows/pr-check-storybook.yaml

This file was deleted.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ dist
/kind
/kind.exe
/storybook/.storybook/themes.css
/storybook/storybook-static/
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@
"typecheck:extensions:registries": "tsc --noEmit --project extensions/registries",
"typecheck:extensions:kubectl-cli": "tsc --noEmit --project extensions/kubectl-cli",
"typecheck": "npm run typecheck:main && npm run typecheck:preload && npm run typecheck:ui && npm run typecheck:renderer && npm run typecheck:preload-dd-extension && npm run typecheck:preload-webview && npm run typecheck:extensions && npm run typecheck:extension-api",
"website:build": "cd website && yarn run docusaurus build",
"website:prod": "cd website && yarn run docusaurus build && yarn serve",
"website:dev": "cd website && yarn run docusaurus start",
"website:build": "npm run storybook:build && cd website && yarn run docusaurus build",
"website:prod": "npm run storybook:build && cd website && yarn run docusaurus build && yarn serve",
"website:dev": "npm run storybook:build && cd website && yarn run docusaurus start",
"website:screenshots": "cd website-argos && yarn run screenshot",
"storybook:css": "tsx ./scripts/generate-stylesheet.ts --output storybook/.storybook/themes.css",
"storybook:dev": "npm run storybook:css && cd storybook && yarn run dev",
Expand Down
1 change: 1 addition & 0 deletions website/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/src/pages/storybook/sidebar.cjs
11 changes: 11 additions & 0 deletions website/docusaurus.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion
import { resolve } from 'node:path';
import Storybook from './storybook';

const lightCodeTheme = require('prism-react-renderer').themes.github;
const darkCodeTheme = require('prism-react-renderer').themes.dracula;
Expand Down Expand Up @@ -337,6 +338,16 @@ const config = {
hideGenerator: true,
},
],
// Custom Storybook integration
[
Storybook,
/** @type {import('./storybook').PluginOptions} */
({
id: 'storybook-docusaurus-integration',
output: 'src/pages/storybook',
storybookStatic: '../storybook/storybook-static',
}),
],
],
presets: [
[
Expand Down
1 change: 1 addition & 0 deletions website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@docusaurus/plugin-client-redirects": "^3.4.0",
"@docusaurus/preset-classic": "^3.4.0",
"@docusaurus/theme-mermaid": "^3.4.0",
"@docusaurus/theme-common": "^3.4.0",
"@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
Expand Down
124 changes: 124 additions & 0 deletions website/src/pages/storybook/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**********************************************************************
* Copyright (C) 2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

import type { PropSidebarItem } from '@docusaurus/plugin-content-docs';
import { useLocation } from '@docusaurus/router';
import { ThemeClassNames, useColorMode } from '@docusaurus/theme-common';
import DocSidebar from '@theme/DocSidebar';
import Layout from '@theme/Layout';
import clsx from 'clsx';
import React, { useEffect, useRef } from 'react';

import items from './sidebar.cjs'; // generated by storybook plugin
import styles from './styles.module.css';

// the iframe is by default not taking all height
// adding an observer to ensure it always takes all the space it need
function observe(iframe: HTMLIFrameElement): void {
const body = iframe.contentDocument?.body;
if (!body) return;

const observerCallback: ResizeObserverCallback = (entries: ResizeObserverEntry[]) => {
window.requestAnimationFrame((): void | undefined => {
if (!Array.isArray(entries) || !entries.length) {
return;
}
iframe.style.height = `${body.scrollHeight}px`;
});
};
const resizeObserver = new ResizeObserver(observerCallback);
resizeObserver.observe(body);
}

function StorybookRoot(): JSX.Element {
// eslint-disable-next-line no-null/no-null
const iframeRef = useRef<HTMLIFrameElement>(null);
const { isDarkTheme } = useColorMode();

const { search } = useLocation();
const [id, setId] = React.useState<string | undefined>(undefined);

useEffect(() => {
const queryId = new URLSearchParams(search).get('id');
if (queryId) {
setId(queryId);
}
}, [search]);

const storybookItems: PropSidebarItem[] = items.map(item => ({
type: 'link',
href: `/storybook?id=${item.id}`,
label: item.label,
}));

const notifyIframe = (): void => {
// we send the iframe the dark mode change https://storybook.js.org/addons/storybook-dark-mode
iframeRef?.current?.contentWindow?.postMessage(
JSON.stringify({
key: 'storybook-channel',
event: { type: 'DARK_MODE', args: [isDarkTheme] },
}),
);
};

const onLoad = (e: React.SyntheticEvent<HTMLIFrameElement>): void => {
// observe resize
observe(e.currentTarget);

// https://github.com/storybookjs/storybook/blob/1943ee6b88d89c963f15ef4087aeabe64d05c9a1/code/lib/core-events/src/index.ts#L65
window.addEventListener('message', message => {
if (message.source !== iframeRef?.current?.contentWindow) {
return;
}

const data = JSON.parse(message.data);
if (!('key' in data) || data['key'] !== 'storybook-channel') return;
if (!('event' in data) || typeof data['event'] !== 'object' || data['event']['type'] !== 'docsRendered') return;

notifyIframe();
});
};

useEffect(() => {
notifyIframe();
}, [isDarkTheme]);

return (
<div className={clsx(styles.storybookRoot)}>
<aside className={clsx(ThemeClassNames.docs.docSidebarContainer, styles.docSidebarContainer)}>
<DocSidebar isHidden={false} onCollapse={() => {}} sidebar={storybookItems} path="/storybook"></DocSidebar>
</aside>
<iframe
ref={iframeRef}
onLoad={onLoad}
src={`/storybook-iframe?id=${id}`}
style={{ width: '100%', height: '100%' }}
/>
</div>
);
}

// to use `useColorMode` we need to be wrapped in Layout component
// ref https://docusaurus.io/docs/api/themes/configuration#use-color-mode
export default function Storybook(): JSX.Element {
return (
<Layout title="Storybook">
<StorybookRoot />
</Layout>
);
}
33 changes: 33 additions & 0 deletions website/src/pages/storybook/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.docSidebarContainer {
display: none;
}

@media (min-width: 997px) {
.docSidebarContainer {
display: block;
width: var(--doc-sidebar-width);
margin-top: calc(-1 * var(--ifm-navbar-height));
border-right: 1px solid var(--ifm-toc-border-color);
will-change: width;
transition: width var(--ifm-transition-fast) ease;
clip-path: inset(0);
}

.docSidebarContainerHidden {
width: var(--doc-sidebar-hidden-width);
cursor: pointer;
}

.sidebarViewport {
top: 0;
position: sticky;
height: 100%;
max-height: 100vh;
}
}

.storybookRoot {
display: flex;
width: 100%;
flex-grow: 1;
}
109 changes: 109 additions & 0 deletions website/storybook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**********************************************************************
* Copyright (C) 2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

import fs from 'node:fs';
import { join } from 'node:path';

import type { LoadContext, Plugin, PluginOptions as DocusaurusOptions } from '@docusaurus/types';

export interface PluginOptions extends DocusaurusOptions {
id: string;
// Folder where to write the sidebar
output: string;
// The path of the dist storybook folder
storybookStatic: string;
}

function populate(folder: string, storybookStatic: string): void {
const index = require(join(storybookStatic, 'index.json'));

if (index['v'] !== 5)
throw new Error(`index version is not compatible with current script. Expected 5 got ${index['v']}.`);

const items = [];

for (const [key] of Object.entries(index['entries'])) {
items.push({
type: 'doc',
id: `${key}`,
label: `${key}`,
});
}

// Write the generate sidebar to the output directory
fs.writeFileSync(join(folder, 'sidebar.cjs'), `module.exports = ${JSON.stringify(items)};`);
}

function parseOptions(opts: unknown): PluginOptions {
if (!opts || typeof opts !== 'object') throw new Error('invalid plugin options');

if (!('storybookStatic' in opts) || typeof opts['storybookStatic'] !== 'string')
throw new Error('invalid storybookStatic plugin option');
if (!('output' in opts) || typeof opts['output'] !== 'string') throw new Error('invalid output plugin option');
if (!('id' in opts) || typeof opts['id'] !== 'string') throw new Error('invalid id plugin option');

return {
storybookStatic: opts['storybookStatic'],
output: opts['output'],
id: opts['id'],
};
}

// ref
// https://docusaurus.io/docs/advanced/plugins
// https://docusaurus.io/docs/api/plugin-methods
export default async function storybookIntegration(_context: LoadContext, opts: unknown): Promise<Plugin> {
const options = parseOptions(opts);

if (!fs.existsSync(options.storybookStatic)) throw new Error('storybook need to be built.');
populate(options.output, options.storybookStatic);

return {
name: 'docusaurus-storybook-integration',
postBuild(): void {
// copy storybook-static assets to docusaurus build folder
const buildFolder = join(__dirname, 'build');
// those file will be merged with docusaurus file
fs.cpSync(join(options.storybookStatic, 'assets'), join(buildFolder, 'assets'), {
recursive: true,
force: true,
});
// copy sb folders
fs.cpSync(join(options.storybookStatic, 'sb-addons'), join(buildFolder, 'sb-addons'), {
recursive: true,
force: true,
});
fs.cpSync(join(options.storybookStatic, 'sb-common-assets'), join(buildFolder, 'sb-common-assets'), {
recursive: true,
force: true,
});
fs.cpSync(join(options.storybookStatic, 'sb-manager'), join(buildFolder, 'sb-manager'), {
recursive: true,
force: true,
});
fs.cpSync(join(options.storybookStatic, 'sb-preview'), join(buildFolder, 'sb-preview'), {
recursive: true,
force: true,
});
fs.cpSync(join(options.storybookStatic, 'iframe.html'), join(buildFolder, 'storybook-iframe.html'), {
force: true,
});
fs.cpSync(join(options.storybookStatic, 'index.json'), join(buildFolder, 'index.json'), { force: true });
},
};
}