rail.tsx 4.4 KB

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