Skip to content

Commit

Permalink
Move tabs to dedicated component and update usage
Browse files Browse the repository at this point in the history
  • Loading branch information
wesbillman committed Sep 6, 2023
1 parent 1b14365 commit 75e26c7
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 292 deletions.
41 changes: 13 additions & 28 deletions console/client/src/features/modules/ModuleDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
import React from 'react'
import {SelectedModuleContext} from '../../providers/selected-module-provider'
import {
TabSearchParams,
TabType,
TabsContext,
Tab,
} from '../../providers/tabs-provider'
import {textColor} from '../../utils'
import {useSearchParams} from 'react-router-dom'
import {modulesContext} from '../../providers/modules-provider'
import {Verb} from '../../protos/xyz/block/ftl/v1/console/console_pb'
import {modulesContext} from '../../providers/modules-provider'
import {SelectedModuleContext} from '../../providers/selected-module-provider'
import {TabsContext} from '../../providers/tabs-provider'
import {textColor} from '../../utils'
import {VerbTab} from '../verbs/VerbTab'

export function ModuleDetails() {
const modules = React.useContext(modulesContext)
const {selectedModule, setSelectedModule} = React.useContext(
SelectedModuleContext
)
const {tabs, setTabs, setActiveTab} = React.useContext(TabsContext)
const [searchParams, setSearchParams] = useSearchParams()
const {openTab} = React.useContext(TabsContext)
const [searchParams] = useSearchParams()
const moduleId = searchParams.get('module')
// When mounting with a valid module in query params set selected module
React.useEffect(() => {
Expand All @@ -36,23 +32,12 @@ export function ModuleDetails() {
}

const handleVerbClicked = (verb: Verb) => {
const tabId = [selectedModule.name, verb.verb?.name].join('.')
const index = tabs.findIndex(tab => tab.id === tabId)
const existingTab = index !== -1
let newTab: Tab | undefined
if (!existingTab) {
newTab = {
id: [selectedModule.name, verb.verb?.name].join('.'),
label: verb.verb?.name ?? 'Verb',
type: TabType.Verb,
}
setTabs([...tabs, newTab])
}
setActiveTab({id: tabId, type: TabType.Verb})
setSearchParams({
...Object.fromEntries(searchParams),
[TabSearchParams.id]: newTab?.id ?? tabs[index].id,
[TabSearchParams.type]: TabType.Verb,
const verbId = [selectedModule.name, verb.verb?.name].join('.')
openTab({
id: verbId,
label: verb.verb?.name ?? 'Verb',
isClosable: true,
component: <VerbTab id={verbId} />,
})
}

Expand Down
19 changes: 12 additions & 7 deletions console/client/src/features/verbs/VerbForm.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import Editor, {Monaco} from '@monaco-editor/react'
import type {JSONSchema4, JSONSchema6, JSONSchema7} from 'json-schema'
import {JSONSchemaFaker} from 'json-schema-faker'
import React from 'react'
import {CodeBlock} from '../../components/CodeBlock'
import {useClient} from '../../hooks/use-client'
import {Module, Verb} from '../../protos/xyz/block/ftl/v1/console/console_pb'
import {VerbService} from '../../protos/xyz/block/ftl/v1/ftl_connect'
import {VerbRef} from '../../protos/xyz/block/ftl/v1/schema/schema_pb'
import Editor, {Monaco} from '@monaco-editor/react'
import {useDarkMode} from '../../providers/dark-mode-provider'
import type {JSONSchema4, JSONSchema6, JSONSchema7} from 'json-schema'
import {TabsContext} from '../../providers/tabs-provider'

export type Schema = JSONSchema4 | JSONSchema6 | JSONSchema7

Expand All @@ -18,9 +19,10 @@ type Props = {
verb?: Verb
}

export const VerbForm: React.FC<Props> = ({module, verb}) => {
export const VerbForm: React.FC<Props> = React.memo(({module, verb}) => {
const client = useClient(VerbService)
const {isDarkMode} = useDarkMode()
const {activeTabId} = React.useContext(TabsContext)
const [editorText, setEditorText] = React.useState<string>('')
const [response, setResponse] = React.useState<string | null>(null)
const [error, setError] = React.useState<string | null>(null)
Expand All @@ -39,7 +41,8 @@ export const VerbForm: React.FC<Props> = ({module, verb}) => {
JSON.stringify(JSONSchemaFaker.generate(verbSchema), null, 2)
)
}
}, [])
}, [module, verb])

function handleEditorChange(value: string | undefined, _) {
setEditorText(value ?? '')
}
Expand Down Expand Up @@ -86,7 +89,7 @@ export const VerbForm: React.FC<Props> = ({module, verb}) => {
{schema, uri: 'http://myserver/foo-schema.json', fileMatch: ['*']},
],
})
}, [monaco, schema])
}, [monaco, schema, activeTabId])

return (
<>
Expand All @@ -96,10 +99,12 @@ export const VerbForm: React.FC<Props> = ({module, verb}) => {
>
<div className='border border-gray-200 dark:border-slate-800 rounded-sm'>
<Editor
key={[module?.name, verb?.verb?.name].join('.')}
height='35vh'
theme={`${isDarkMode ? 'vs-dark' : 'light'}`}
defaultLanguage='json'
defaultValue={editorText}
path={[module?.name, verb?.verb?.name].join('.')}
value={editorText}
options={{
lineNumbers: 'off',
scrollBeyondLastLine: false,
Expand Down Expand Up @@ -134,4 +139,4 @@ export const VerbForm: React.FC<Props> = ({module, verb}) => {
)}
</>
)
}
})
187 changes: 2 additions & 185 deletions console/client/src/layout/IDELayout.tsx
Original file line number Diff line number Diff line change
@@ -1,141 +1,18 @@
import {Tab} from '@headlessui/react'
import {XMarkIcon} from '@heroicons/react/24/outline'
import React from 'react'
import {useSearchParams} from 'react-router-dom'
import {ModuleDetails} from '../features/modules/ModuleDetails'
import {ModulesList} from '../features/modules/ModulesList'
import {Timeline} from '../features/timeline/Timeline'
import {VerbTab} from '../features/verbs/VerbTab'
import {
NotificationType,
NotificationsContext,
} from '../providers/notifications-provider'
import {
TabSearchParams,
TabType,
TabsContext,
timelineTab,
} from '../providers/tabs-provider'
import {
bgColor,
headerColor,
headerTextColor,
invalidTab,
panelColor,
textColor,
} from '../utils'
import {Navigation} from './Navigation'
import {Notification} from './Notification'
import {SidePanel} from './SidePanel'
import {modulesContext} from '../providers/modules-provider'
const selectedTabStyle = `${headerTextColor} ${headerColor}`
const unselectedTabStyle = `text-gray-300 bg-slate-100 dark:bg-slate-600`
import {Tabs} from './Tabs'

export function IDELayout() {
const {modules} = React.useContext(modulesContext)
const {tabs, activeTab, setActiveTab, setTabs} = React.useContext(TabsContext)
const {showNotification} = React.useContext(NotificationsContext)
const [searchParams, setSearchParams] = useSearchParams()
const [activeIndex, setActiveIndex] = React.useState(0)
const id = searchParams.get(TabSearchParams.id) as string
const type = searchParams.get(TabSearchParams.type) as string

const handleCloseTab = (id: string, index: number) => {
const nextActiveTab = {
id: tabs[index - 1].id,
type: tabs[index - 1].type,
}
setSearchParams({
...Object.fromEntries(searchParams),
[TabSearchParams.id]: nextActiveTab.id,
[TabSearchParams.type]: nextActiveTab.type,
})
setActiveTab(nextActiveTab)
setTabs(tabs.filter(tab => tab.id !== id))
}

const handleChangeTab = (index: number) => {
const nextActiveTab = tabs[index]
setActiveTab({id: nextActiveTab.id, type: nextActiveTab.type})
setSearchParams({
...Object.fromEntries(searchParams),
[TabSearchParams.id]: nextActiveTab.id,
[TabSearchParams.type]: nextActiveTab.type,
})
}

// Handle loading a page with the tab query parameters
React.useEffect(() => {
const msg = invalidTab({id, type})
if (msg) {
// Default fallback to timeline
setActiveTab({id: timelineTab.id, type: timelineTab.type})
if (type === null && id === null) return

return showNotification({
title: 'Invalid Tab',
message: msg,
type: NotificationType.Error,
})
}
// Handle timeline tab id
if (id === timelineTab.id) {
return setActiveTab({id: timelineTab.id, type: timelineTab.type})
}
if (modules.length) {
const ids = id.split('.')
// Handle edge case where the id contains and invalid module or verb
if (modules.length) {
const [moduleId, verbId] = ids
// Handle Module does not exist
const moduleExist = modules.find(module => module?.name === moduleId)
if (!moduleExist) {
showNotification({
title: 'Module not found',
message: `Module '${moduleId}' does not exist`,
type: NotificationType.Error,
})
return setActiveTab({id: timelineTab.id, type: timelineTab.type})
}
// Handle Verb does not exists
const verbExist = moduleExist?.verbs.some(
({verb}) => verb?.name === verbId
)
if (!verbExist) {
showNotification({
title: 'Verb not found',
message: `Verb '${verbId}' does not exist on module '${moduleId}'`,
type: NotificationType.Error,
})
return setActiveTab({id: timelineTab.id, type: timelineTab.type})
}
}
// Handle if tab is not already in tab list
if (
!tabs.some(
({id: tabId, type: tabType}) => tabId === id && tabType === type
)
) {
const newTab = {
id,
label: ids[1],
type: TabType.Verb,
}
const nextTabs = [...tabs, newTab]
setTabs(nextTabs)
return setActiveTab({id: newTab.id, type: newTab.type})
}
// Handle if tab is in tab list
return setActiveTab({id, type})
}
}, [id, type, modules])

// Set active tab index whenever our activeTab context changes
React.useEffect(() => {
const index = tabs.findIndex(tab => tab.id === activeTab?.id)
setActiveIndex(index)
}, [activeTab])

return (
<>
<div className={`h-screen flex flex-col ${bgColor} ${textColor}`}>
Expand Down Expand Up @@ -173,67 +50,7 @@ export function IDELayout() {
<main className='flex-grow flex flex-col overflow-hidden pl-1'>
<section className='flex-grow overflow-y-auto'>
<div className='flex flex-grow overflow-hidden h-full'>
<div className={`flex-1 flex flex-col rounded`}>
<Tab.Group
selectedIndex={activeIndex}
onChange={handleChangeTab}
>
<div>
<Tab.List
className={`flex items-center rounded-t ${headerTextColor}`}
>
{tabs.map(({label, id}, i) => {
return (
<Tab
key={id}
className='flex items-center mr-1 relative'
as='span'
>
<span
className={`px-4 py-2 rounded-t ${
id !== 'timeline' ? 'pr-8' : ''
} ${
activeIndex === i
? `${selectedTabStyle}`
: `${unselectedTabStyle}`
}`}
>
{label}
</span>
{i !== 0 && (
<button
onClick={e => {
e.stopPropagation()
handleCloseTab(id, i)
}}
className='absolute right-0 mr-2 text-gray-400 hover:text-white'
>
<XMarkIcon className={`h-5 w-5`} />
</button>
)}
</Tab>
)
})}
</Tab.List>
<div className='flex-grow'></div>
</div>
<div className={`flex-1 overflow-y-scroll ${panelColor}`}>
<Tab.Panels>
{tabs.map(({id}, i) => {
return i === 0 ? (
<Tab.Panel key={id}>
<Timeline />
</Tab.Panel>
) : (
<Tab.Panel key={id}>
<VerbTab id={id} />
</Tab.Panel>
)
})}
</Tab.Panels>
</div>
</Tab.Group>
</div>
<Tabs />
<SidePanel />
</div>
</section>
Expand Down
Loading

0 comments on commit 75e26c7

Please sign in to comment.