index.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import {Fragment, useCallback, useMemo, useRef} from 'react';
  2. import styled from '@emotion/styled';
  3. import Alert from 'sentry/components/alert';
  4. import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton';
  5. import {InputGroup} from 'sentry/components/inputGroup';
  6. import LoadingIndicator from 'sentry/components/loadingIndicator';
  7. import TextOverflow from 'sentry/components/textOverflow';
  8. import {IconSearch} from 'sentry/icons/iconSearch';
  9. import {space} from 'sentry/styles/space';
  10. import {useHotkeys} from 'sentry/utils/useHotkeys';
  11. import {useLocation} from 'sentry/utils/useLocation';
  12. import {useNavigate} from 'sentry/utils/useNavigate';
  13. import OrganizationContainer from 'sentry/views/organizationContainer';
  14. import RouteAnalyticsContextProvider from 'sentry/views/routeAnalyticsContextProvider';
  15. import {StoryExports} from 'sentry/views/stories/storyExports';
  16. import {StoryHeader} from 'sentry/views/stories/storyHeader';
  17. import {StorySourceLinks} from 'sentry/views/stories/storySourceLinks';
  18. import {StoryTableOfContents} from 'sentry/views/stories/storyTableOfContents';
  19. import {StoryTree, useStoryTree} from 'sentry/views/stories/storyTree';
  20. import {useStoriesLoader, useStoryBookFiles} from 'sentry/views/stories/useStoriesLoader';
  21. export default function Stories() {
  22. const searchInput = useRef<HTMLInputElement>(null);
  23. const location = useLocation<{name: string; query?: string}>();
  24. const files = useStoryBookFiles();
  25. // If no story is selected, show the landing page stories
  26. const storyFiles = useMemo(() => {
  27. if (!location.query.name) {
  28. return files.filter(
  29. file =>
  30. file.endsWith('styles/colors.stories.tsx') ||
  31. file.endsWith('styles/typography.stories.tsx')
  32. );
  33. }
  34. return [location.query.name];
  35. }, [files, location.query.name]);
  36. const story = useStoriesLoader({files: storyFiles});
  37. const nodes = useStoryTree(files, location.query.query ?? '');
  38. const navigate = useNavigate();
  39. const onSearchInputChange = useCallback(
  40. (e: React.ChangeEvent<HTMLInputElement>) => {
  41. navigate({
  42. query: {...location.query, query: e.target.value, name: location.query.name},
  43. });
  44. },
  45. [location.query, navigate]
  46. );
  47. useHotkeys([{match: '/', callback: () => searchInput.current?.focus()}], []);
  48. return (
  49. <RouteAnalyticsContextProvider>
  50. <OrganizationContainer>
  51. <Layout>
  52. <HeaderContainer>
  53. <StoryHeader />
  54. </HeaderContainer>
  55. <SidebarContainer>
  56. <InputGroup>
  57. <InputGroup.LeadingItems disablePointerEvents>
  58. <IconSearch />
  59. </InputGroup.LeadingItems>
  60. <InputGroup.Input
  61. ref={searchInput}
  62. placeholder="Search stories"
  63. defaultValue={location.query.query ?? ''}
  64. onChange={onSearchInputChange}
  65. />
  66. {/* @TODO (JonasBadalic): Implement clear button when there is an active query */}
  67. </InputGroup>
  68. <StoryTreeContainer>
  69. <StoryTree nodes={nodes} />
  70. </StoryTreeContainer>
  71. </SidebarContainer>
  72. {story.isLoading ? (
  73. <VerticalScroll style={{gridArea: 'body'}}>
  74. <LoadingIndicator />
  75. </VerticalScroll>
  76. ) : story.isError ? (
  77. <VerticalScroll style={{gridArea: 'body'}}>
  78. <Alert type="error" showIcon>
  79. <strong>{story.error.name}:</strong> {story.error.message}
  80. </Alert>
  81. </VerticalScroll>
  82. ) : story.isSuccess ? (
  83. <StoryMainContainer>
  84. {story.data.map((s, _i, arr) => {
  85. // We render extra information if this is the only story that is being rendered
  86. if (arr.length === 1) {
  87. <Fragment key={s.filename}>
  88. <TextOverflow>{s.filename}</TextOverflow>
  89. <CopyToClipboardButton size="xs" iconSize="xs" text={s.filename} />
  90. <StorySourceLinks story={s} />
  91. <StoryExports story={s} />
  92. </Fragment>;
  93. }
  94. // Render just the story exports in case of multiple stories being rendered
  95. return <StoryExports key={s.filename} story={s} />;
  96. })}
  97. </StoryMainContainer>
  98. ) : (
  99. <VerticalScroll style={{gridArea: 'body'}}>
  100. <strong>The file you selected does not export a story.</strong>
  101. </VerticalScroll>
  102. )}
  103. <StoryIndexContainer>
  104. <StoryTableOfContents />
  105. </StoryIndexContainer>
  106. </Layout>
  107. </OrganizationContainer>
  108. </RouteAnalyticsContextProvider>
  109. );
  110. }
  111. const Layout = styled('div')`
  112. --stories-grid-space: ${space(2)};
  113. display: grid;
  114. grid-template:
  115. 'head head head' max-content
  116. 'aside body index' auto/ ${p => p.theme.settings.sidebarWidth} 1fr;
  117. gap: var(--stories-grid-space);
  118. place-items: stretch;
  119. height: 100vh;
  120. padding: var(--stories-grid-space);
  121. `;
  122. const HeaderContainer = styled('div')`
  123. grid-area: head;
  124. `;
  125. const SidebarContainer = styled('div')`
  126. grid-area: aside;
  127. display: flex;
  128. flex-direction: column;
  129. gap: ${space(2)};
  130. min-height: 0;
  131. `;
  132. const StoryTreeContainer = styled('div')`
  133. overflow-y: scroll;
  134. flex-grow: 1;
  135. `;
  136. const StoryIndexContainer = styled('div')`
  137. grid-area: index;
  138. `;
  139. const VerticalScroll = styled('main')`
  140. overflow-x: hidden;
  141. overflow-y: scroll;
  142. grid-area: body;
  143. `;
  144. /**
  145. * Avoid <Panel> here because nested panels will have a modified theme.
  146. * Therefore stories will look different in prod.
  147. */
  148. const StoryMainContainer = styled(VerticalScroll)`
  149. background: ${p => p.theme.background};
  150. border-radius: ${p => p.theme.panelBorderRadius};
  151. border: 1px solid ${p => p.theme.border};
  152. grid-area: body;
  153. padding: var(--stories-grid-space);
  154. padding-top: 0;
  155. overflow-x: hidden;
  156. overflow-y: auto;
  157. position: relative;
  158. h1,
  159. h2,
  160. h3,
  161. h4,
  162. h5,
  163. h6 {
  164. scroll-margin-top: ${space(3)};
  165. }
  166. `;