Files
supabase/apps/studio/components/interfaces/ProjectHome/NotebooksSection.tsx
T
Joshen Lim 09a1f431bc Mass update for all lucide icon imports to use the component with Icon suffix (#51659)
## Context

This is largely just a mechanical change here:
Opting to use lucide icons with the `Icon` suffix across all icon
imports within `studio`
- so that it’s clearer which components are icons + reduces the
possibility of name collision with our own components
- e.g <User /> vs <UserIcon />

Also added a eslint rule for this:
<img width="1069" height="167" alt="image"
src="https://github.com/user-attachments/assets/7f36f443-5ca2-4bf6-8d22-86e774513bfc"
/>

And updated `AGENTS.md` as such:
```
Icons come from `lucide-react`, always imported with the `Icon` suffix (`import { CheckIcon, Trash2Icon } from 'lucide-react'`) — never the bare names (`Check`) or `Check as CheckIcon` aliases (lint-enforced).
```
2026-10-14 12:56:55 +02:00

141 lines
4.3 KiB
TypeScript

import { useParams } from 'common'
import dayjs from 'dayjs'
import { NotebookTextIcon, PlusIcon } from 'lucide-react'
import { AiIconAnimation, Badge, Button } from 'ui'
import { Row } from 'ui-patterns/Row'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { HomeCard } from './HomeCard'
import { useAnalyzeNotebook, useCreateNotebook } from '@/components/interfaces/Explorer/hooks'
import { AlertError } from '@/components/ui/AlertError'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import {
useNotebooksInfiniteQuery,
type NotebookRow,
} from '@/data/content/notebooks/notebooks-infinite-query'
const MAX_HOMEPAGE_NOTEBOOKS = 4
export const NotebooksSection = () => {
const { ref: projectRef } = useParams()
const { createNotebook } = useCreateNotebook()
const { data, error, isPending, isError, isSuccess, refetch } = useNotebooksInfiniteQuery({
projectRef,
limit: MAX_HOMEPAGE_NOTEBOOKS,
sort: 'inserted_at',
})
// The content endpoint can return more rows than `limit`, so cap the row here too
const notebooks = (data?.pages[0]?.content ?? []).slice(0, MAX_HOMEPAGE_NOTEBOOKS)
return (
<div>
{isPending && (
<>
<ShimmeringLoader className="w-96 mb-6" />
<div className="flex flex-col gap-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
</>
)}
{!isPending && (
<div className="flex justify-between items-center mb-6">
<h2>Notebooks</h2>
<Button icon={<PlusIcon />} onClick={() => createNotebook()}>
New notebook
</Button>
</div>
)}
{isError && (
<AlertError
error={error instanceof Error ? error : null}
subject="Failed to retrieve notebooks"
projectRef={projectRef}
additionalActions={
<Button variant="default" onClick={() => refetch()}>
Retry
</Button>
}
/>
)}
{isSuccess && notebooks.length === 0 && (
<EmptyState onCreateNotebook={() => createNotebook()} />
)}
{isSuccess && notebooks.length > 0 && (
<Row maxColumns={4} minWidth={280}>
{notebooks.map((notebook) => (
<NotebookCard key={notebook.id} notebook={notebook} projectRef={projectRef} />
))}
</Row>
)}
</div>
)
}
function NotebookCard({
notebook,
projectRef,
}: {
notebook: NotebookRow
projectRef: string | undefined
}) {
const { analyzeNotebook, isCreating } = useAnalyzeNotebook()
const cellCount = notebook.content?.cells?.length
// Treat an unknown cell count as empty so Analyze is never offered on a notebook with no cells
const isEmpty = !cellCount
return (
<HomeCard
href={`/project/${projectRef}/explorer/notebook/${notebook.id}`}
icon={<NotebookTextIcon size={16} strokeWidth={1.5} className="text-foreground-light" />}
label="NOTEBOOK"
title={notebook.name}
description={notebook.description?.trim()}
meta={`Created ${dayjs(notebook.inserted_at).fromNow()}`}
actions={
<>
{cellCount !== undefined && (
<Badge className="w-fit">
{cellCount} {cellCount === 1 ? 'CELL' : 'CELLS'}
</Badge>
)}
<ButtonTooltip
variant="text"
className="w-7 h-7 px-1.5"
icon={<AiIconAnimation size={16} loading={isCreating} />}
disabled={isEmpty}
onClick={() => analyzeNotebook({ id: notebook.id, name: notebook.name })}
aria-label="Analyze"
tooltip={{
content: {
text: isEmpty ? 'Add a cell to the notebook to analyze it' : 'Analyze',
},
}}
/>
</>
}
/>
)
}
function EmptyState({ onCreateNotebook }: { onCreateNotebook: () => void }) {
return (
<div className="h-64 flex flex-col items-center justify-center rounded-sm border-2 border-dashed p-16">
<h4>Create a notebook</h4>
<p className="text-sm text-foreground-light mb-4">
Save queries and notes together to revisit and share
</p>
<Button iconRight={<PlusIcon size={14} />} onClick={onCreateNotebook}>
Create your first notebook
</Button>
</div>
)
}