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: add storybook #5162

Merged
merged 3 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ module.exports = {
root: true,
plugins: ["unused-imports", "prettier"],
extends: [
"react-app", // Use the recommended rules from CRA.
"plugin:prettier/recommended", // Ensure this is last in the list.
"react-app",
"plugin:prettier/recommended",
"plugin:storybook/recommended",
],
parserOptions: {
ecmaFeatures: {
Expand Down
9 changes: 9 additions & 0 deletions .storybook/maas-theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { create } from "@storybook/theming/create";

export default create({
base: "light",
brandTitle: "MAAS UI Component Library",
brandUrl: "http://localhost:8400/MAAS/r/",
brandImage: "https://assets.ubuntu.com/v1/142ae045-Canonical%20MAAS.png",
brandTarget: "_self",
});
21 changes: 21 additions & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { StorybookConfig } from "@storybook/react-webpack5";

const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/preset-create-react-app",
"@storybook/addon-interactions",
"@storybook/addon-a11y",
],
framework: {
name: "@storybook/react-webpack5",
options: {},
},
docs: {
autodocs: "tag",
},
staticDirs: ["../assets"],
};
export default config;
7 changes: 7 additions & 0 deletions .storybook/manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { addons } from "@storybook/manager-api";

import maasTheme from "./maas-theme";

addons.setConfig({
theme: maasTheme,
});
3 changes: 3 additions & 0 deletions .storybook/preview-head.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<link rel="preload" href="/fonts/90b59210-Ubuntu-Italic[wdth,wght]-latin-v0.896a.woff2" />
<link rel="preload" href="/fonts/d5fc1819-UbuntuMono[wght]-latin-v0.869.woff2" />
<link rel="preload" href="/fonts/f1ea362b-Ubuntu[wdth,wght]-latin-v0.896a.woff2" />
27 changes: 27 additions & 0 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { RootProviders } from "../src/index";
import React, { StrictMode } from "react";

import type { Preview } from "@storybook/react";

import "../src/scss/index.scss";

const preview: Preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
decorators: [
(Story) => (
<RootProviders>
<Story />
</RootProviders>
),
],
};

export default preview;
18 changes: 18 additions & 0 deletions docs/HACKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,24 @@ maas-ui built with TypeScript in strict mode. Any new modules in should be writt

Prioritize clear, self-explanatory code, and only use JSDoc to provide context or additional information that cannot be inferred from the code itself.

## React Components

We encourage [component-driven](https://www.componentdriven.org/) development, and use of [Storybook](https://storybook.js.org/) for interactive documentation.

Follow the presentational and container components pattern where appropriate. Read more on good component design in the [React documentation](https://reactjs.org/docs/thinking-in-react.html#step-3-identify-the-minimal-but-complete-representation-of-ui-state).

When developing new features or extending existing ones, consider the following:

- Think of all the variations of a UI component and how each can be represented using props.
- Prefer a single `variant` prop for representing visual variations of a component.

```tsx
<Button variant="primary" />
```

- Create stories for each variant in [Storybook](https://storybook.js.org/).
- Add state management, side effects, and application-specific logic into container component passing the state as props to the presentational component.

### Dealing with problems

There are cases where determining a type for a particular object can be difficult. We provide an "escape hatch" type called `TSFixMe` (aliased to `any`) which you can use, but please make a best effort to avoid this and determine the correct types where possible.
Expand Down
20 changes: 18 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
"test": "yarn test-ui",
"test:ci": "yarn test-ui --runInBand",
"unlink-components": "yarn unlink react && yarn unlink \"@canonical/react-components\"",
"wait-on-ui": "wait-on http-get://0.0.0.0:8400/MAAS/r"
"wait-on-ui": "wait-on http-get://0.0.0.0:8400/MAAS/r",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
"@canonical/macaroon-bakery": "1.3.2",
Expand Down Expand Up @@ -80,6 +82,16 @@
"@percy/cli": "1.20.0",
"@percy/cypress": "3.1.2",
"@playwright/test": "1.31.1",
"@storybook/addon-a11y": "7.4.2",
"@storybook/addon-essentials": "7.4.1",
"@storybook/addon-interactions": "7.4.1",
"@storybook/addon-links": "7.4.1",
"@storybook/blocks": "7.4.1",
"@storybook/manager-api": "7.4.1",
"@storybook/preset-create-react-app": "7.4.1",
"@storybook/react": "7.4.1",
"@storybook/react-webpack5": "7.4.1",
"@storybook/theming": "7.4.1",
"@testing-library/cypress": "9.0.0",
"@testing-library/jest-dom": "6.0.0",
"@testing-library/react": "14.0.0",
Expand All @@ -99,6 +111,7 @@
"@types/redux-saga": "0.10.5",
"@welldone-software/why-did-you-render": "7.0.1",
"address": "1.2.2",
"babel-plugin-named-exports-order": "0.0.2",
"colors": "1.4.0",
"concurrently": "8.0.1",
"cooky-cutter": "1.5.4",
Expand All @@ -111,6 +124,7 @@
"eslint-plugin-no-only-tests": "3.1.0",
"eslint-plugin-playwright": "0.15.3",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-storybook": "0.6.13",
"eslint-plugin-unused-imports": "3.0.0",
"formik-devtools-extension": "0.1.8",
"http-proxy-middleware": "2.0.6",
Expand All @@ -127,8 +141,10 @@
"redux-saga-test-plan": "4.0.6",
"sass": "1.58.3",
"start-server-and-test": "2.0.0",
"storybook": "7.4.1",
"timezone-mock": "1.3.6",
"wait-on": "7.0.1"
"wait-on": "7.0.1",
"webpack": "5.88.2"
},
"resolutions": {
"node_modules/@types/react-router-dom/@types/react": "^18.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { Meta } from "@storybook/react";

import { AppSideNavigation } from "./AppSideNavigation";
import { navGroups } from "./constants";

import { user as userFactory } from "testing/factories";

const meta: Meta<typeof AppSideNavigation> = {
title: "Layout/AppSideNavigation",
component: AppSideNavigation,
tags: ["autodocs"],
parameters: {
layout: "fullscreen",
},
decorators: [
(Story) => (
<div className="l-application">
<Story />
</div>
),
],
};
export default meta;

const args = {
authUser: userFactory(),
filteredGroups: navGroups,
isAdmin: true,
isAuthenticated: true,
isCollapsed: true,
setIsCollapsed: () => {},
logout: () => {},
path: "/",
showLinks: true,
theme: "default",
vaultIncomplete: true,
};

export const LoggedIn = {
args,
};

export const LoggedOut = {
args: {
...args,
authUser: null,
isAuthenticated: false,
},
};
158 changes: 100 additions & 58 deletions src/app/base/components/AppSideNavigation/AppSideNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,92 @@ import podSelectors from "app/store/pod/selectors";
import type { RootState } from "app/store/root/types";
import { actions as statusActions } from "app/store/status";

const AppSideNavigation = (): JSX.Element => {
type SideNavigationProps = {
authUser: ReturnType<typeof authSelectors.get>;
filteredGroups: typeof navGroups;
isAdmin: boolean;
isAuthenticated: boolean;
isCollapsed: boolean;
setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
logout: () => void;
path: string;
showLinks: boolean;
theme: string;
vaultIncomplete: boolean;
};

export const AppSideNavigation = ({
authUser,
filteredGroups,
isAdmin,
isAuthenticated,
isCollapsed,
setIsCollapsed,
logout,
path,
showLinks,
theme,
vaultIncomplete,
}: SideNavigationProps) => (
<>
<header aria-label="navigation" className="l-navigation-bar">
<div className={classNames("p-panel is-dark", `is-maas-${theme}`)}>
<div className="p-panel__header">
<NavigationBanner />
<div className="p-panel__controls u-nudge-down--small u-no-margin--top">
<Button
appearance="base"
className="has-icon is-dark"
onClick={() => {
setIsCollapsed(!isCollapsed);
}}
>
Menu
</Button>
</div>
</div>
</div>
</header>
<nav
aria-label="main navigation"
className={classNames(`l-navigation is-maas is-maas-${theme}`, {
"is-collapsed": isCollapsed,
"is-pinned": !isCollapsed,
})}
>
<div className="l-navigation__drawer">
<div className="p-panel is-dark">
<div className={`p-panel__header is-sticky is-maas-${theme}`}>
<NavigationBanner>
<div className="l-navigation__controls">
<AppSideNavCollapseToggle
isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed}
/>
</div>
</NavigationBanner>
</div>
<div className="p-panel__content">
<div className="p-side-navigation--icons is-dark">
<AppSideNavItems
authUser={authUser}
groups={filteredGroups}
isAdmin={isAdmin}
isAuthenticated={isAuthenticated}
logout={logout}
path={path}
showLinks={showLinks}
vaultIncomplete={vaultIncomplete}
/>
</div>
</div>
</div>
</div>
</nav>
</>
);

const AppSideNavigationContainer = (): JSX.Element => {
const dispatch = useDispatch();
const navigate = useNavigate();
const location = useLocation();
Expand Down Expand Up @@ -118,63 +203,20 @@ const AppSideNavigation = (): JSX.Element => {
}, [hideVirsh]);

return (
<>
<header aria-label="navigation" className="l-navigation-bar">
<div className={classNames("p-panel is-dark", `is-maas-${theme}`)}>
<div className="p-panel__header">
<NavigationBanner />
<div className="p-panel__controls u-nudge-down--small u-no-margin--top">
<Button
appearance="base"
className="has-icon is-dark"
onClick={() => {
setIsCollapsed(!isCollapsed);
}}
>
Menu
</Button>
</div>
</div>
</div>
</header>
<nav
aria-label="main navigation"
className={classNames(`l-navigation is-maas is-maas-${theme}`, {
"is-collapsed": isCollapsed,
"is-pinned": !isCollapsed,
})}
>
<div className="l-navigation__drawer">
<div className="p-panel is-dark">
<div className={`p-panel__header is-sticky is-maas-${theme}`}>
<NavigationBanner>
<div className="l-navigation__controls">
<AppSideNavCollapseToggle
isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed}
/>
</div>
</NavigationBanner>
</div>
<div className="p-panel__content">
<div className="p-side-navigation--icons is-dark">
<AppSideNavItems
authUser={authUser}
groups={filteredGroups}
isAdmin={isAdmin}
isAuthenticated={isAuthenticated}
logout={logout}
path={path}
showLinks={showLinks}
vaultIncomplete={vaultIncomplete}
/>
</div>
</div>
</div>
</div>
</nav>
</>
<AppSideNavigation
authUser={authUser}
filteredGroups={filteredGroups}
isAdmin={isAdmin}
isAuthenticated={isAuthenticated}
isCollapsed={isCollapsed}
logout={logout}
path={path}
setIsCollapsed={setIsCollapsed}
showLinks={showLinks}
theme={theme}
vaultIncomplete={vaultIncomplete}
/>
);
};

export default AppSideNavigation;
export default AppSideNavigationContainer;
Loading