rail.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import { ReactElement, useCallback, useMemo, useState } from 'react'
  2. import { Nav, NavLink, Tab, TabContainer } from 'react-bootstrap-5'
  3. import MaterialIcon, {
  4. AvailableUnfilledIcon,
  5. } from '@/shared/components/material-icon'
  6. import { Panel } from 'react-resizable-panels'
  7. import { useLayoutContext } from '@/shared/context/layout-context'
  8. import { FileTree } from '@/features/ide-react/components/file-tree'
  9. import { ErrorIndicator, ErrorPane } from './errors'
  10. type RailElement = {
  11. icon: AvailableUnfilledIcon
  12. key: string
  13. component: ReactElement
  14. indicator?: ReactElement
  15. }
  16. type RailActionLink = { key: string; icon: AvailableUnfilledIcon; href: string }
  17. type RailActionButton = {
  18. key: string
  19. icon: AvailableUnfilledIcon
  20. action: () => void
  21. }
  22. type RailAction = RailActionLink | RailActionButton
  23. const RAIL_TABS: RailElement[] = [
  24. // NOTE: The file tree **MUST** be the first (i.e. default) tab in the list
  25. // since the file tree is responsible for opening the initial document.
  26. {
  27. key: 'file-tree',
  28. icon: 'description',
  29. component: (
  30. <>
  31. {/* TODO: add panel for file outline */}
  32. <FileTree />
  33. </>
  34. ),
  35. },
  36. {
  37. key: 'integrations',
  38. icon: 'integration_instructions',
  39. component: <>Integrations</>,
  40. },
  41. {
  42. key: 'review-panel',
  43. icon: 'rate_review',
  44. component: <>Review panel</>,
  45. },
  46. {
  47. key: 'chat',
  48. icon: 'forum',
  49. component: <>Chat</>,
  50. },
  51. {
  52. key: 'errors',
  53. icon: 'report',
  54. component: <ErrorPane />,
  55. indicator: <ErrorIndicator />,
  56. },
  57. ]
  58. export const RailLayout = () => {
  59. const [selectedTab, setSelectedTab] = useState<string | undefined>(
  60. RAIL_TABS[0]?.key
  61. )
  62. const { setLeftMenuShown } = useLayoutContext()
  63. const railActions: RailAction[] = useMemo(
  64. () => [
  65. { key: 'support', icon: 'help', href: '/learn' },
  66. {
  67. key: 'settings',
  68. icon: 'settings',
  69. action: () => setLeftMenuShown(true),
  70. },
  71. ],
  72. [setLeftMenuShown]
  73. )
  74. return (
  75. <TabContainer
  76. mountOnEnter // Only render when necessary (so that we can lazy load tab content)
  77. unmountOnExit={false} // TODO: Should we unmount the tabs when they're not used?
  78. transition={false}
  79. defaultActiveKey={selectedTab}
  80. onSelect={useCallback(key => setSelectedTab(key ?? undefined), [])}
  81. id="ide-rail-tabs"
  82. >
  83. <div className="ide-rail">
  84. <Nav
  85. defaultActiveKey={RAIL_TABS[0]?.key}
  86. className="d-flex flex-column ide-rail-tabs-nav"
  87. >
  88. {RAIL_TABS.map(({ icon, key, indicator }) => (
  89. <RailTab
  90. active={selectedTab === key}
  91. key={key}
  92. eventKey={key}
  93. icon={icon}
  94. indicator={indicator}
  95. />
  96. ))}
  97. <div className="flex-grow-1" />
  98. {railActions?.map(action => (
  99. <RailActionElement key={action.key} action={action} />
  100. ))}
  101. </Nav>
  102. </div>
  103. <Panel
  104. id="ide-redesign-sidebar-panel"
  105. order={1}
  106. defaultSize={15}
  107. minSize={5}
  108. maxSize={80}
  109. >
  110. <div className="ide-rail-content">
  111. <Tab.Content>
  112. {RAIL_TABS.map(({ key, component }) => (
  113. <Tab.Pane eventKey={key} key={key}>
  114. {component}
  115. </Tab.Pane>
  116. ))}
  117. </Tab.Content>
  118. </div>
  119. </Panel>
  120. </TabContainer>
  121. )
  122. }
  123. const RailTab = ({
  124. icon,
  125. eventKey,
  126. active,
  127. indicator,
  128. }: {
  129. icon: AvailableUnfilledIcon
  130. eventKey: string
  131. active: boolean
  132. indicator?: ReactElement
  133. }) => {
  134. return (
  135. <NavLink eventKey={eventKey} className="ide-rail-tab-link">
  136. {active ? (
  137. <MaterialIcon className="ide-rail-tab-link-icon" type={icon} />
  138. ) : (
  139. <MaterialIcon className="ide-rail-tab-link-icon" type={icon} unfilled />
  140. )}
  141. {indicator}
  142. </NavLink>
  143. )
  144. }
  145. const RailActionElement = ({ action }: { action: RailAction }) => {
  146. const icon = (
  147. <MaterialIcon
  148. className="ide-rail-tab-link-icon"
  149. type={action.icon}
  150. unfilled
  151. />
  152. )
  153. const onActionClick = useCallback(() => {
  154. if ('action' in action) {
  155. action.action()
  156. }
  157. }, [action])
  158. if ('href' in action) {
  159. return (
  160. <a
  161. href={action.href}
  162. target="_blank"
  163. rel="noopener"
  164. className="ide-rail-tab-link"
  165. >
  166. {icon}
  167. </a>
  168. )
  169. } else {
  170. return (
  171. <button
  172. onClick={onActionClick}
  173. className="ide-rail-tab-link ide-rail-tab-button"
  174. type="button"
  175. >
  176. {icon}
  177. </button>
  178. )
  179. }
  180. }