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

feat(storybook): Add guide for adding tabs to a scene #18495

Merged
merged 9 commits into from
Nov 10, 2023
129 changes: 129 additions & 0 deletions frontend/src/stories/How to add tabs to a scene.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { Meta } from '@storybook/addon-docs'

<Meta title="How to add tabs to a scene?" />

# How to add tabs to a scene?

This guide follows on from ["How to build a scene"](/docs/how-to-build-a-scene--page) – we'll be adding tabs to that guide's Dashboards scene.

1. Add an enum to `frontend/src/types.ts` to define the tabs:

```ts title="frontend/src/types.ts"
export enum DashboardsTabs {
First = 'first',
Second = 'second',
}
```

2. Update the URL in `frontend/src/scenes/urls.ts` to handle tabs:

```ts title="frontend/src/scenes/urls.ts"
dashboards: (tab: DashboardsTabs = DashboardsTabs.First): string => `/dashboards/${tab}`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default args > optional args with a constant fallback deeper in the function

```

3. Add entries to routes per tab in `frontend/src/scenes/scenes.ts`:

```ts title="frontend/src/scenes/scenes.ts"
// One entry for every available tab
...Object.fromEntries(
Object.values(DashboardsTabs).map((tab) => [urls.dashboards(tab), Scene.Dashboards])
) as Record<string, Scene>,
Comment on lines +28 to +30
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object.fromEntries() > Array.prototype.reduce()

```

4. Update `dashboardsLogic` to handle tab changes in `frontend/src/scenes/dashboards/dashboardsLogic.ts`:

```ts title="frontend/src/scenes/dashboards/dashboardsLogic.ts"
import { kea, reducers, path, actions, selectors } from 'kea'
import { actionToUrl, urlToAction } from 'kea-router'

import { Breadcrumb, DashboardsTabs } from '~/types'
import { urls } from 'scenes/urls'

export const DASHBOARDS_TAB_TO_NAME: Record<DashboardsTabs, string> = {
[DashboardsTabs.First]: 'First',
[DashboardsTabs.Second]: 'Second',
}
Comment on lines +42 to +45
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was slightly overkill for this to be a function, so turned into an object


export const dashboardsLogic = kea([
path(['scenes', 'dashboard', 'dashboardsLogic']),
actions({
setCurrentTab: (tab: DashboardsTabs = DashboardsTabs.First) => ({ tab }),
}),
reducers({
currentTab: [
DashboardsTabs.First as DashboardsTabs,
{
setCurrentTab: (_, { tab }) => tab,
},
],
}),
selectors(() => ({
breadcrumbs: [
// Optional if you'd like the breadcrumbs to show the current tab
(s) => [s.currentTab],
(tab): Breadcrumb[] => {
const breadcrumbs: Breadcrumb[] = [{ name: 'Dashboards' }]
breadcrumbs.push({
name: DASHBOARDS_TAB_TO_NAME[tab],
})

return breadcrumbs
},
],
})),
actionToUrl(({ values }) => {
return {
setCurrentTab: () => [urls.dashboards(values.currentTab)],
}
}),
urlToAction(({ actions, values }) => ({
'/dashboards/:tab': ({ tab }) => {
if (tab !== values.currentTab) {
actions.setCurrentTab(tab as DashboardsTabs)
}
},
})),
])
```

5. Update `frontend/src/scenes/dashboards/Dashboards.tsx` to render the tabs:

```tsx title="frontend/src/scenes/dashboards/Dashboards.tsx"
import { useValues } from 'kea'
import { router } from 'kea-router'
import { DashboardsTabs } from '~/types'
import { SceneExport } from 'scenes/sceneTypes'
import { PageHeader } from 'lib/components/PageHeader'
import { LemonTabs } from 'lib/lemon-ui/LemonTabs/LemonTabs'
import { urls } from 'scenes/urls'
import { dashboardsLogic, humanFriendlyTabName } from './dashboardsLogic'

const DASHBOARDS_TAB_TO_CONTENT: Record<DashboardsTabs, JSX.Element> = {
[DashboardsTabs.First]: <div>First tab content</div>,
[DashboardsTabs.Second]: <div>Second tab content</div>,
}

export const scene: SceneExport = {
component: Dashboards,
logic: dashboardsLogic,
}

export function Dashboards(): JSX.Element {
const { currentTab } = useValues(dashboardsLogic)

return (
<div>
<PageHeader title="Dashboards" />
<LemonTabs
activeKey={currentTab}
onChange={(tab) => router.actions.push(urls.dashboards(tab as DashboardsTabs))}
tabs={Object.values(DashboardsTabs).map((tab) => ({
label: DASHBOARDS_TAB_TO_NAME[tab],
key: tab,
content: DASHBOARDS_TAB_TO_CONTENT[tab],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For non-advanced use cases it's better for the content to be part of the tab's definition

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you bring me examples of when it's advanced vs not?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically if you want to put some extra UI between the tab bar and tab contents. Otherwise, it's simpler to keep everything together,

}))}
/>
</div>
)
}
```
1 change: 1 addition & 0 deletions frontend/src/stories/How to build a scene.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,4 @@ export function NewDashboard (): JSX.Element {
Read next:
- [How to use components](/docs/how-to-use-components--page)
- [How to build a form](/docs/how-to-build-a-form--page)
- [How to add tabs to a scene](/docs/how-to-add-tabs-to-a-scene--page)
Loading