-
Notifications
You must be signed in to change notification settings - Fork 254
Commit
* feat: rename statusbar to bottom drawer * feat: add preview:log event to send logs from vite to the client * feat: add new addLog method to useLogs to append log messages in the new format * feat: add button to open / close bototm drawer from header * Committing progress * Add tooltip, reuse modal from notebooks for installing apps * Format * Fix up styles for layout + log table. Adjust copy on packages panel * Ability to click on 'Logs' to toggle the pane * Fix deps array --------- Co-authored-by: Nicholas Charriere <[email protected]>
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
import { BanIcon, XIcon } from 'lucide-react'; | ||
import { useHotkeys } from 'react-hotkeys-hook'; | ||
|
||
import { Button } from '@srcbook/components/src/components/ui/button'; | ||
import { cn } from '@/lib/utils.ts'; | ||
import { useLogs } from './use-logs'; | ||
import { useEffect, useRef } from 'react'; | ||
|
||
const maxHeightInPx = 320; | ||
|
||
export default function BottomDrawer() { | ||
const { logs, clearLogs, open, togglePane, closePane } = useLogs(); | ||
|
||
useHotkeys('mod+shift+y', () => { | ||
togglePane(); | ||
}); | ||
|
||
const scrollWrapperRef = useRef<HTMLDivElement | null>(null); | ||
|
||
// Scroll to the bottom of the logs panel when the user opens the panel fresh | ||
useEffect(() => { | ||
if (!scrollWrapperRef.current) { | ||
return; | ||
} | ||
scrollWrapperRef.current.scrollTop = scrollWrapperRef.current.scrollHeight; | ||
}, [open]); | ||
|
||
// Determine if the user has scrolled all the way to the bottom of the div | ||
const scrollPinnedToBottomRef = useRef(false); | ||
useEffect(() => { | ||
if (!scrollWrapperRef.current) { | ||
return; | ||
} | ||
const element = scrollWrapperRef.current; | ||
|
||
const onScroll = () => { | ||
scrollPinnedToBottomRef.current = | ||
element.scrollTop === element.scrollHeight - element.clientHeight; | ||
}; | ||
|
||
element.addEventListener('scroll', onScroll); | ||
return () => element.removeEventListener('scroll', onScroll); | ||
}, []); | ||
|
||
// If the user has scrolled all the way to the bottom, then keep the bottom scroll pinned as new | ||
// logs come in. | ||
useEffect(() => { | ||
if (!scrollWrapperRef.current) { | ||
return; | ||
} | ||
|
||
if (scrollPinnedToBottomRef.current) { | ||
scrollWrapperRef.current.scrollTop = scrollWrapperRef.current.scrollHeight; | ||
} | ||
}, [logs]); | ||
|
||
return ( | ||
<div | ||
className={cn( | ||
'flex flex-col w-full overflow-hidden transition-all duration-200 ease-in-out', | ||
open ? 'flex-grow' : 'flex-shrink-0 h-8', | ||
)} | ||
style={{ maxHeight: open ? `${maxHeightInPx}px` : '2rem' }} | ||
> | ||
<div className="flex-shrink-0 flex items-center justify-between border-t border-b h-8 px-1 w-full bg-muted"> | ||
<span | ||
Check failure on line 66 in packages/web/src/components/apps/bottom-drawer.tsx GitHub Actions / Build and Test (18.x)
Check failure on line 66 in packages/web/src/components/apps/bottom-drawer.tsx GitHub Actions / Build and Test (18.x)
Check failure on line 66 in packages/web/src/components/apps/bottom-drawer.tsx GitHub Actions / Build and Test (20.x)
Check failure on line 66 in packages/web/src/components/apps/bottom-drawer.tsx GitHub Actions / Build and Test (20.x)
Check failure on line 66 in packages/web/src/components/apps/bottom-drawer.tsx GitHub Actions / Build and Test (22.x)
Check failure on line 66 in packages/web/src/components/apps/bottom-drawer.tsx GitHub Actions / Build and Test (22.x)
|
||
onClick={() => togglePane()} | ||
className="text-sm font-medium ml-2 select-none cursor-pointer" | ||
> | ||
Logs | ||
</span> | ||
|
||
<div className="flex items-center gap-1"> | ||
{open && logs.length > 0 && ( | ||
<Button | ||
size="sm" | ||
variant="icon" | ||
onClick={clearLogs} | ||
className="active:translate-y-0 w-6 px-0" | ||
> | ||
<BanIcon size={14} /> | ||
</Button> | ||
)} | ||
<Button | ||
size="sm" | ||
variant="icon" | ||
onClick={() => closePane()} | ||
className="active:translate-y-0 w-6 px-0" | ||
> | ||
<XIcon size={16} /> | ||
</Button> | ||
</div> | ||
</div> | ||
|
||
{open && ( | ||
<div className="flex-grow overflow-auto p-2" ref={scrollWrapperRef}> | ||
<table className="w-full border-collapse text-xs"> | ||
<tbody> | ||
{logs.map((log, index) => ( | ||
<tr key={index}> | ||
<td className="align-top whitespace-nowrap select-none pointer-events-none whitespace-nowrap w-0 pr-4"> | ||
<span className="font-mono text-tertiary-foreground/80"> | ||
{log.timestamp.toISOString()} | ||
</span> | ||
</td> | ||
<td className="align-top whitespace-nowrap select-none pointer-events-none whitespace-nowrap w-0 pr-4"> | ||
<span className="font-mono text-tertiary-foreground">{log.source}</span> | ||
</td> | ||
<td className="align-top"> | ||
<pre | ||
className={cn('font-mono cursor-text whitespace-pre-wrap', { | ||
'text-red-300': log.type === 'stderr', | ||
'text-tertiary-foreground': log.type === 'info', | ||
})} | ||
> | ||
{log.message} | ||
</pre> | ||
</td> | ||
</tr> | ||
))} | ||
</tbody> | ||
</table> | ||
{logs.length === 0 && ( | ||
<div className="w-full h-full flex items-center justify-center"> | ||
<span className="text-tertiary-foreground">No logs</span> | ||
</div> | ||
)} | ||
</div> | ||
)} | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,57 +1,65 @@ | ||
import { Button } from '@srcbook/components/src/components/ui/button'; | ||
import { usePackageJson } from '../use-package-json'; | ||
import { PackagePlus } from 'lucide-react'; | ||
import Shortcut from '@srcbook/components/src/components/keyboard-shortcut'; | ||
import { | ||
Tooltip, | ||
TooltipContent, | ||
TooltipProvider, | ||
TooltipTrigger, | ||
} from '@srcbook/components/src/components/ui/tooltip'; | ||
|
||
export default function PackagesPanel() { | ||
const { status, output, npmInstall, clearNodeModules, nodeModulesExists } = usePackageJson(); | ||
const { setShowInstallModal, npmInstall, clearNodeModules, nodeModulesExists, status } = | ||
usePackageJson(); | ||
|
||
return ( | ||
<div className="flex flex-col gap-4 px-5 w-[360px]"> | ||
<p className="text-sm text-tertiary-foreground"> | ||
Clear your node_modules, re-install packages and inspect the output logs from{' '} | ||
<pre>npm install</pre> | ||
</p> | ||
<div> | ||
<Button onClick={() => npmInstall()} disabled={status === 'installing'}> | ||
Run npm install | ||
</Button> | ||
<div className="flex flex-col gap-6 px-5 w-[360px]"> | ||
<div className="flex flex-col gap-2"> | ||
<p className="text-sm text-tertiary-foreground"> | ||
To add packages, you can simply ask the AI in chat, or use the button below. | ||
</p> | ||
|
||
<div> | ||
<TooltipProvider> | ||
<Tooltip> | ||
<TooltipTrigger asChild> | ||
<Button onClick={() => setShowInstallModal(true)} className="gap-1"> | ||
<PackagePlus size={16} /> | ||
Add a package | ||
</Button> | ||
</TooltipTrigger> | ||
<TooltipContent> | ||
Install packages <Shortcut keys={['mod', 'i']} /> | ||
</TooltipContent> | ||
</Tooltip> | ||
</TooltipProvider> | ||
</div> | ||
</div> | ||
|
||
{status !== 'idle' ? ( | ||
<> | ||
<h3 className="text-sm font-medium">Logs</h3> | ||
<pre className="font-mono text-xs bg-tertiary p-2 overflow-auto rounded-md border"> | ||
{/* FIXME: disambiguate between stdout and stderr in here using n.type! */} | ||
{output.map((n) => n.data).join('\n')} | ||
</pre> | ||
</> | ||
) : null} | ||
<div className="flex flex-col gap-2"> | ||
<p className="text-sm text-tertiary-foreground"> | ||
If you suspect your node_modules are corrupted, you can clear them and reinstall all | ||
packages. | ||
</p> | ||
<div> | ||
<Button onClick={() => clearNodeModules()} disabled={nodeModulesExists !== true}> | ||
Clear all packages | ||
</Button> | ||
</div> | ||
</div> | ||
|
||
{process.env.NODE_ENV !== 'production' && ( | ||
<> | ||
<span> | ||
Status: <code>{status}</code> | ||
</span> | ||
<div> | ||
<Button | ||
onClick={() => npmInstall(['uuid'])} | ||
variant="secondary" | ||
disabled={status === 'installing'} | ||
> | ||
Run npm install uuid | ||
</Button> | ||
</div> | ||
<div> | ||
exists={JSON.stringify(nodeModulesExists)} | ||
<Button | ||
onClick={() => clearNodeModules()} | ||
variant="secondary" | ||
disabled={nodeModulesExists !== true} | ||
> | ||
Clear node_modules | ||
</Button> | ||
</div> | ||
</> | ||
)} | ||
<div className="flex flex-col gap-2"> | ||
<p className="text-sm text-tertiary-foreground"> | ||
Re-run <code className="code">npm install</code>. This will run against the package.json | ||
from the project root. | ||
</p> | ||
<div> | ||
<Button onClick={() => npmInstall()} disabled={status === 'installing'}> | ||
Re-install all packages | ||
</Button> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} |