Skip to content
Merged
6 changes: 3 additions & 3 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
import invariant from 'invariant';
import typescript from 'typescript-eslint';

import * as sentryScrapsPlugin from './static/eslint/eslintPluginScraps/index.mjs';

Check warning on line 36 in eslint.config.mjs

View workflow job for this annotation

GitHub Actions / eslint

configs is not allowed to import eslint

invariant(react.configs.flat, 'For typescript');
invariant(react.configs.flat.recommended, 'For typescript');
Expand Down Expand Up @@ -129,17 +129,17 @@
{
name: 'sentry/views/insights/common/components/insightsTimeSeriesWidget',
message:
'Do not use this directly in your view component, see https://sentry.sentry.io/stories/?name=app%2Fviews%2Fdashboards%2Fwidgets%2FtimeSeriesWidget%2FtimeSeriesWidgetVisualization.stories.tsx&query=timeseries#deeplinking for more information',
'Do not use this directly in your view component, see https://sentry.sentry.io/stories/shared/views/dashboards/widgets/timeserieswidget/timeserieswidgetvisualization#deeplinking for more information',
},
{
name: 'sentry/views/insights/common/components/insightsLineChartWidget',
message:
'Do not use this directly in your view component, see https://sentry.sentry.io/stories/?name=app%2Fviews%2Fdashboards%2Fwidgets%2FtimeSeriesWidget%2FtimeSeriesWidgetVisualization.stories.tsx&query=timeseries#deeplinking for more information',
'Do not use this directly in your view component, see https://sentry.sentry.io/stories/shared/views/dashboards/widgets/timeserieswidget/timeserieswidgetvisualization#deeplinking for more information',
},
{
name: 'sentry/views/insights/common/components/insightsAreaChartWidget',
message:
'Do not use this directly in your view component, see https://sentry.sentry.io/stories/?name=app%2Fviews%2Fdashboards%2Fwidgets%2FtimeSeriesWidget%2FtimeSeriesWidgetVisualization.stories.tsx&query=timeseries#deeplinking for more information',
'Do not use this directly in your view component, see https://sentry.sentry.io/stories/shared/views/dashboards/widgets/timeserieswidget/timeserieswidgetvisualization#deeplinking for more information',
},
];

Expand Down
4 changes: 2 additions & 2 deletions static/app/router/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -318,9 +318,9 @@ function buildRoutes(): RouteObject[] {
],
},
{
path: '/stories/:storyType?/:storySlug?/',
component: make(() => import('sentry/stories/view/index')),
path: '/stories/*',
withOrgPath: true,
component: make(() => import('sentry/stories/view/index')),
},
{
path: '/debug/notifications/:notificationSource?/',
Expand Down
2 changes: 1 addition & 1 deletion static/app/stories/type-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ function prodTypeloader(this: LoaderContext<any>, _source: string) {

function noopTypeLoader(this: LoaderContext<any>, _source: string) {
const callback = this.async();
return callback(null, 'export default {}');
return callback(null, 'export default {props: {},exports: {}}');
}

export default function typeLoader(this: LoaderContext<any>, _source: string) {
Expand Down
91 changes: 82 additions & 9 deletions static/app/stories/view/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import styled from '@emotion/styled';

import {Alert} from 'sentry/components/core/alert';
import LoadingIndicator from 'sentry/components/loadingIndicator';
import {StorySidebar} from 'sentry/stories/view/storySidebar';
import {useStoryRedirect} from 'sentry/stories/view/useStoryRedirect';
import {
StorySidebar,
useStoryBookFilesByCategory,
} from 'sentry/stories/view/storySidebar';
import {StoryTreeNode, type StoryCategory} from 'sentry/stories/view/storyTree';
import {useLocation} from 'sentry/utils/useLocation';
import OrganizationContainer from 'sentry/views/organizationContainer';
import RouteAnalyticsContextProvider from 'sentry/views/routeAnalyticsContextProvider';
Expand All @@ -16,13 +19,24 @@ import {StoryHeader} from './storyHeader';
import {useStoryDarkModeTheme} from './useStoriesDarkMode';
import {useStoriesLoader} from './useStoriesLoader';

export default function Stories() {
export function useStoryParams(): {storyCategory?: StoryCategory; storySlug?: string} {

This comment was marked as outdated.

const location = useLocation();
return isLandingPage(location) ? <StoriesLanding /> : <StoryDetail />;
// Match: /stories/:category/(one/optional/or/more/path/segments)
// Handles both /stories/... and /organizations/{org}/stories/...
const match = location.pathname.match(/\/stories\/([^/]+)\/(.+)/);
return {
storyCategory: match?.[1] as StoryCategory | undefined,
storySlug: match?.[2] ?? undefined,
};
}

function isLandingPage(location: ReturnType<typeof useLocation>) {
return /\/stories\/?$/.test(location.pathname) && !location.query.name;
export default function Stories() {
const location = useLocation();
return isLandingPage(location) && !location.query.name ? (
<StoriesLanding />
) : (
<StoryDetail />
);
}

function StoriesLanding() {
Expand All @@ -36,11 +50,37 @@ function StoriesLanding() {
}

function StoryDetail() {
useStoryRedirect();
const location = useLocation();
const {storyCategory, storySlug} = useStoryParams();
const stories = useStoryBookFilesByCategory();

let storyNode = getStoryFromParams(stories, {
category: storyCategory,
slug: storySlug,
});

// If we don't have a story node, try to find it by the filesystem path
if (!storyNode && location.query.name) {
const nodes = Object.values(stories).flat();
const queue = [...nodes];

while (queue.length > 0) {
const node = queue.pop();
if (!node) break;

if (node.filesystemPath === location.query.name) {
storyNode = node;
break;
}

for (const key in node.children) {
queue.push(node.children[key]!);
}
}
}

const location = useLocation<{name: string; query?: string}>();
const story = useStoriesLoader({
files: [location.state?.storyPath ?? location.query.name],
files: storyNode ? [storyNode.filesystemPath] : [],
});

return (
Expand Down Expand Up @@ -91,6 +131,39 @@ function StoriesLayout(props: PropsWithChildren) {
);
}

function isLandingPage(location: ReturnType<typeof useLocation>) {
// Handles both /stories and /organizations/{org}/stories
return /\/stories\/?$/.test(location.pathname);
}

function getStoryFromParams(
stories: ReturnType<typeof useStoryBookFilesByCategory>,
context: {category?: StoryCategory; slug?: string}
): StoryTreeNode | undefined {
const nodes = stories[context.category as keyof typeof stories] ?? [];

if (!nodes || nodes.length === 0) {
return undefined;
}

const queue = [...nodes];

while (queue.length > 0) {
const node = queue.pop();
if (!node) break;

if (node.slug === context.slug) {
return node;
}

for (const key in node.children) {
queue.push(node.children[key]!);
}
}

return undefined;
}

function GlobalStoryStyles() {
const theme = useTheme();
const darkTheme = useStoryDarkModeTheme();
Expand Down
56 changes: 46 additions & 10 deletions static/app/stories/view/landing/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {PropsWithChildren} from 'react';
import {Fragment} from 'react';
import {useTheme} from '@emotion/react';
import styled from '@emotion/styled';
import type {LocationDescriptor} from 'history';

import performanceWaitingForSpan from 'sentry-images/spot/performance-waiting-for-span.svg';
import heroImg from 'sentry-images/stories/landing/robopigeon.png';
Expand All @@ -13,6 +14,8 @@ import {Link} from 'sentry/components/core/link';
import {IconOpen} from 'sentry/icons';
import {Acronym} from 'sentry/stories/view/landing/acronym';
import {StoryDarkModeProvider} from 'sentry/stories/view/useStoriesDarkMode';
import normalizeUrl from 'sentry/utils/url/normalizeUrl';
import useOrganization from 'sentry/utils/useOrganization';

import {Colors, Icons, Typography} from './figures';

Expand All @@ -29,7 +32,7 @@ const frontmatter = {
actions: [
{
children: 'Get Started',
to: '/stories?name=app/styles/colors.mdx',
to: '/stories/foundations/colors',
Copy link
Member

Choose a reason for hiding this comment

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

why are there no trailing / for story urls?

Copy link
Member Author

Choose a reason for hiding this comment

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

The current stories don't use them either, so this isn't a breaking change. That said, I'll open a PR to add support for it separately so that this remains django compat

priority: 'primary',
},
{
Expand All @@ -43,6 +46,8 @@ const frontmatter = {
};

export function StoryLanding() {
const organization = useOrganization();

return (
<Fragment>
<StoryDarkModeProvider>
Expand All @@ -57,9 +62,14 @@ export function StoryLanding() {
<p>{frontmatter.hero.tagline}</p>
</Flex>
<Flex gap="md">
{frontmatter.hero.actions.map(props => (
<LinkButton {...props} key={props.to} />
))}
{frontmatter.hero.actions.map(props => {
// Normalize internal paths with organization context
const to =
typeof props.to === 'string' && !props.external
? normalizeUrl(`/organizations/${organization.slug}${props.to}`)
: props.to;
return <LinkButton {...props} to={to} key={props.to} />;
})}
</Flex>
</Flex>
<img
Expand All @@ -86,25 +96,50 @@ export function StoryLanding() {
</p>
</Flex>
<CardGrid>
<Card href="/stories?name=app/styles/colors.mdx" title="Color">
<Card
to={{
pathname: normalizeUrl(
`/organizations/${organization.slug}/stories/foundations/colors`
),
}}
title="Color"
>
<CardFigure>
<Colors />
</CardFigure>
</Card>
<Card href="/stories/?name=app%2Ficons%2Ficons.stories.tsx" title="Icons">
<Card
to={{
pathname: normalizeUrl(
`/organizations/${organization.slug}/stories/foundations/icons`
),
}}
title="Icons"
>
<CardFigure>
<Icons />
</CardFigure>
</Card>
<Card
href="/stories/?name=app%2Fstyles%2Ftypography.stories.tsx"
to={{
pathname: normalizeUrl(
`/organizations/${organization.slug}/stories/foundations/typography`
),
}}
title="Typography"
>
<CardFigure>
<Typography />
</CardFigure>
</Card>
<Card href="/stories/?name=app%2Fstyles%2Fimages.stories.tsx" title="Images">
<Card
to={{
pathname: normalizeUrl(
`/organizations/${organization.slug}/stories/foundations/images`
),
}}
title="Images"
>
<CardFigure>
<img src={performanceWaitingForSpan} />
</CardFigure>
Expand Down Expand Up @@ -192,12 +227,13 @@ const CardGrid = styled('div')`

interface CardProps {
children: React.ReactNode;
href: string;
title: string;
to: LocationDescriptor;
}

function Card(props: CardProps) {
return (
<CardLink to={props.href}>
<CardLink to={props.to}>
{props.children}
<CardTitle>{props.title}</CardTitle>
</CardLink>
Expand Down
9 changes: 8 additions & 1 deletion static/app/stories/view/storyExports.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, {Fragment, useEffect} from 'react';
import {useTheme} from '@emotion/react';
import styled from '@emotion/styled';
import {ErrorBoundary} from '@sentry/react';
import {parseAsString, useQueryState} from 'nuqs';

import {Alert} from 'sentry/components/core/alert';
import {Tag} from 'sentry/components/core/badge/tag';
Expand Down Expand Up @@ -37,8 +38,13 @@ export function StoryExports(props: {story: StoryDescriptor}) {

function StoryLayout() {
const {story} = useStory();
const [tab, setTab] = useQueryState(
'tab',
parseAsString.withOptions({history: 'push'}).withDefault('usage')
);

return (
<Tabs>
<Tabs value={tab} onChange={setTab}>
{isMDXStory(story) ? <MDXStoryTitle story={story} /> : null}
<StoryGrid>
<StoryContainer>
Expand Down Expand Up @@ -123,6 +129,7 @@ function MDXStoryTitle(props: {story: MDXStoryDescriptor}) {

function StoryTabList() {
const {story} = useStory();

if (!isMDXStory(story)) return null;
if (story.exports.frontmatter?.layout === 'document') return null;

Expand Down
Loading
Loading