rail.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. import { FC, 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, useRailContext } from '../contexts/rail-context'
  10. import FileTreeOutlinePanel from './file-tree-outline-panel'
  11. import { ChatIndicator, ChatPane } from './chat/chat'
  12. import getMeta from '@/utils/meta'
  13. import { HorizontalResizeHandle } from '@/features/ide-react/components/resize/horizontal-resize-handle'
  14. import { HorizontalToggler } from '@/features/ide-react/components/resize/horizontal-toggler'
  15. import { useTranslation } from 'react-i18next'
  16. import classNames from 'classnames'
  17. import IntegrationsPanel from './integrations-panel/integrations-panel'
  18. import OLButton from '@/features/ui/components/ol/ol-button'
  19. type RailElement = {
  20. icon: AvailableUnfilledIcon
  21. key: RailTabKey
  22. component: ReactElement
  23. indicator?: ReactElement
  24. hide?: boolean
  25. }
  26. type RailActionLink = { key: string; icon: AvailableUnfilledIcon; href: string }
  27. type RailActionButton = {
  28. key: string
  29. icon: AvailableUnfilledIcon
  30. action: () => void
  31. }
  32. type RailAction = RailActionLink | RailActionButton
  33. const RAIL_TABS: RailElement[] = [
  34. {
  35. key: 'file-tree',
  36. icon: 'description',
  37. component: <FileTreeOutlinePanel />,
  38. },
  39. {
  40. key: 'integrations',
  41. icon: 'integration_instructions',
  42. component: <IntegrationsPanel />,
  43. },
  44. {
  45. key: 'review-panel',
  46. icon: 'rate_review',
  47. component: <>Review panel</>,
  48. },
  49. {
  50. key: 'chat',
  51. icon: 'forum',
  52. component: <ChatPane />,
  53. indicator: <ChatIndicator />,
  54. hide: !getMeta('ol-chatEnabled'),
  55. },
  56. {
  57. key: 'errors',
  58. icon: 'report',
  59. component: <ErrorPane />,
  60. indicator: <ErrorIndicator />,
  61. },
  62. ]
  63. export const RailLayout = () => {
  64. const { t } = useTranslation()
  65. const {
  66. selectedTab,
  67. setSelectedTab,
  68. isOpen,
  69. setIsOpen,
  70. panelRef,
  71. handlePaneCollapse,
  72. handlePaneExpand,
  73. togglePane,
  74. setResizing,
  75. } = useRailContext()
  76. const { setLeftMenuShown } = useLayoutContext()
  77. const railActions: RailAction[] = useMemo(
  78. () => [
  79. { key: 'support', icon: 'help', href: '/learn' },
  80. {
  81. key: 'settings',
  82. icon: 'settings',
  83. action: () => setLeftMenuShown(true),
  84. },
  85. ],
  86. [setLeftMenuShown]
  87. )
  88. const onTabSelect = useCallback(
  89. (key: string | null) => {
  90. if (key === selectedTab) {
  91. togglePane()
  92. } else {
  93. // Change the selected tab and make sure it's open
  94. setSelectedTab((key ?? 'file-tree') as RailTabKey)
  95. setIsOpen(true)
  96. }
  97. },
  98. [setSelectedTab, selectedTab, setIsOpen, togglePane]
  99. )
  100. return (
  101. <TabContainer
  102. mountOnEnter // Only render when necessary (so that we can lazy load tab content)
  103. unmountOnExit={false} // TODO: Should we unmount the tabs when they're not used?
  104. transition={false}
  105. activeKey={selectedTab}
  106. onSelect={onTabSelect}
  107. id="ide-rail-tabs"
  108. >
  109. <div className="ide-rail">
  110. <Nav activeKey={selectedTab} className="ide-rail-tabs-nav">
  111. {RAIL_TABS.filter(({ hide }) => !hide).map(
  112. ({ icon, key, indicator }) => (
  113. <RailTab
  114. open={isOpen && selectedTab === key}
  115. key={key}
  116. eventKey={key}
  117. icon={icon}
  118. indicator={indicator}
  119. />
  120. )
  121. )}
  122. <div className="flex-grow-1" />
  123. {railActions?.map(action => (
  124. <RailActionElement key={action.key} action={action} />
  125. ))}
  126. </Nav>
  127. </div>
  128. <Panel
  129. id="ide-redesign-sidebar-panel"
  130. order={1}
  131. defaultSize={15}
  132. minSize={5}
  133. maxSize={80}
  134. ref={panelRef}
  135. collapsible
  136. onCollapse={handlePaneCollapse}
  137. onExpand={handlePaneExpand}
  138. >
  139. <div className="ide-rail-content">
  140. <Tab.Content>
  141. {RAIL_TABS.filter(({ hide }) => !hide).map(({ key, component }) => (
  142. <Tab.Pane eventKey={key} key={key}>
  143. {component}
  144. </Tab.Pane>
  145. ))}
  146. </Tab.Content>
  147. </div>
  148. </Panel>
  149. <HorizontalResizeHandle
  150. resizable
  151. hitAreaMargins={{ coarse: 0, fine: 0 }}
  152. onDoubleClick={togglePane}
  153. onDragging={setResizing}
  154. >
  155. <HorizontalToggler
  156. id="ide-redesign-sidebar-panel"
  157. togglerType="west"
  158. isOpen={isOpen}
  159. setIsOpen={setIsOpen}
  160. tooltipWhenOpen={t('tooltip_hide_panel')}
  161. tooltipWhenClosed={t('tooltip_show_panel')}
  162. />
  163. </HorizontalResizeHandle>
  164. </TabContainer>
  165. )
  166. }
  167. const RailTab = ({
  168. icon,
  169. eventKey,
  170. open,
  171. indicator,
  172. }: {
  173. icon: AvailableUnfilledIcon
  174. eventKey: string
  175. open: boolean
  176. indicator?: ReactElement
  177. }) => {
  178. return (
  179. <NavLink
  180. eventKey={eventKey}
  181. className={classNames('ide-rail-tab-link', {
  182. 'open-rail': open,
  183. })}
  184. >
  185. {open ? (
  186. <MaterialIcon className="ide-rail-tab-link-icon" type={icon} />
  187. ) : (
  188. <MaterialIcon className="ide-rail-tab-link-icon" type={icon} unfilled />
  189. )}
  190. {indicator}
  191. </NavLink>
  192. )
  193. }
  194. const RailActionElement = ({ action }: { action: RailAction }) => {
  195. const icon = (
  196. <MaterialIcon
  197. className="ide-rail-tab-link-icon"
  198. type={action.icon}
  199. unfilled
  200. />
  201. )
  202. const onActionClick = useCallback(() => {
  203. if ('action' in action) {
  204. action.action()
  205. }
  206. }, [action])
  207. if ('href' in action) {
  208. return (
  209. <a
  210. href={action.href}
  211. target="_blank"
  212. rel="noopener"
  213. className="ide-rail-tab-link"
  214. >
  215. {icon}
  216. </a>
  217. )
  218. } else {
  219. return (
  220. <button
  221. onClick={onActionClick}
  222. className="ide-rail-tab-link ide-rail-tab-button"
  223. type="button"
  224. >
  225. {icon}
  226. </button>
  227. )
  228. }
  229. }
  230. export const RailPanelHeader: FC<{ title: string }> = ({ title }) => {
  231. const { handlePaneCollapse } = useRailContext()
  232. return (
  233. <header className="rail-panel-header">
  234. <h4 className="rail-panel-title">{title}</h4>
  235. <OLButton
  236. onClick={handlePaneCollapse}
  237. className="rail-panel-header-button-subdued"
  238. size="sm"
  239. >
  240. <MaterialIcon type="close" />
  241. </OLButton>
  242. </header>
  243. )
  244. }