index.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. import {useRef, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import Input from 'sentry/components/input';
  4. import {space} from 'sentry/styles/space';
  5. import type {RouteComponentProps} from 'sentry/types/legacyReactRouter';
  6. import {useHotkeys} from 'sentry/utils/useHotkeys';
  7. import OrganizationContainer from 'sentry/views/organizationContainer';
  8. import RouteAnalyticsContextProvider from 'sentry/views/routeAnalyticsContextProvider';
  9. import EmptyStory from 'sentry/views/stories/emptyStory';
  10. import ErrorStory from 'sentry/views/stories/errorStory';
  11. import storiesContext from 'sentry/views/stories/storiesContext';
  12. import StoryFile from 'sentry/views/stories/storyFile';
  13. import StoryHeader from 'sentry/views/stories/storyHeader';
  14. import StoryTree from 'sentry/views/stories/storyTree';
  15. import type {StoriesQuery} from 'sentry/views/stories/types';
  16. import useStoriesLoader from 'sentry/views/stories/useStoriesLoader';
  17. type Props = RouteComponentProps<{}, {}, any, StoriesQuery>;
  18. export default function Stories({location}: Props) {
  19. const story = useStoriesLoader({filename: location.query.name});
  20. const [searchTerm, setSearchTerm] = useState('');
  21. const searchInput = useRef<HTMLInputElement>(null);
  22. useHotkeys([{match: '/', callback: () => searchInput.current?.focus()}], []);
  23. return (
  24. <RouteAnalyticsContextProvider>
  25. <OrganizationContainer>
  26. <Layout>
  27. <StoryHeader style={{gridArea: 'head'}} />
  28. <Sidebar style={{gridArea: 'aside'}}>
  29. <Input
  30. ref={searchInput}
  31. placeholder="Search files by name"
  32. onChange={e => setSearchTerm(e.target.value.toLowerCase())}
  33. />
  34. <TreeContainer>
  35. <StoryTree
  36. files={storiesContext()
  37. .files()
  38. .filter(s => s.toLowerCase().includes(searchTerm))}
  39. />
  40. </TreeContainer>
  41. </Sidebar>
  42. {story.error ? (
  43. <VerticalScroll style={{gridArea: 'body'}}>
  44. <ErrorStory error={story.error} />
  45. </VerticalScroll>
  46. ) : story.resolved ? (
  47. <Main style={{gridArea: 'body'}}>
  48. <StoryFile filename={story.filename} resolved={story.resolved} />
  49. </Main>
  50. ) : (
  51. <VerticalScroll style={{gridArea: 'body'}}>
  52. <EmptyStory />
  53. </VerticalScroll>
  54. )}
  55. </Layout>
  56. </OrganizationContainer>
  57. </RouteAnalyticsContextProvider>
  58. );
  59. }
  60. const Layout = styled('div')`
  61. --stories-grid-space: ${space(2)};
  62. display: grid;
  63. grid-template:
  64. 'head head' max-content
  65. 'aside body' auto/ ${p => p.theme.settings.sidebarWidth} 1fr;
  66. gap: var(--stories-grid-space);
  67. place-items: stretch;
  68. height: 100vh;
  69. padding: var(--stories-grid-space);
  70. `;
  71. const Sidebar = styled('aside')`
  72. display: flex;
  73. gap: ${space(2)};
  74. flex-direction: column;
  75. min-height: 0;
  76. `;
  77. const TreeContainer = styled('div')`
  78. overflow: scroll;
  79. flex-grow: 1;
  80. `;
  81. const VerticalScroll = styled('main')`
  82. overflow-x: hidden;
  83. overflow-y: scroll;
  84. `;
  85. /**
  86. * Avoid <Panel> here because nested panels will have a modified theme.
  87. * Therefore stories will look different in prod.
  88. */
  89. const Main = styled(VerticalScroll)`
  90. background: ${p => p.theme.background};
  91. border-radius: ${p => p.theme.panelBorderRadius};
  92. border: 1px solid ${p => p.theme.border};
  93. padding: var(--stories-grid-space);
  94. overflow-x: hidden;
  95. overflow-y: auto;
  96. position: relative;
  97. `;