mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
## Summary - Unify Explorer toolbar actions: 16px / 2px Lucide icons, `text-tertiary-foreground` that becomes `text-foreground` on hover (Analyze icon goes brand on hover). - Soften chat scroll edges with top/bottom fades, and align the composer width with the conversation content (`px-7` + `max-w-3xl`). - Put **Run SQL** first on Explorer home, and rename the tab-bar new-tab item from “New query” to **Run SQL** so it matches. ## Test plan - [ ] Open Explorer and check query, notebook, and chat toolbars: icons are 16px, muted by default, and go to foreground on hover. Analyze on a notebook with cells: icon goes brand on hover; empty notebook still disables Analyze. - [ ] Open a query tab: source menu, result settings, save, and more-options all look like the other toolbar actions (including while the dropdown is open). - [ ] Open Explorer chat: scroll a long thread and confirm top/bottom fades sit on the chat surface. Composer lines up with message width (not inset extra). - [ ] On Explorer home, **Run SQL** is the first card; clicking it still opens a SQL tab. - [ ] From the tab bar **+** menu, the first item is **Run SQL** (not “New query”); it still creates a SQL tab. **New notebook** and **New chat** still work. Made with [Cursor](https://cursor.com) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **UI Improvements** * Standardized Explorer toolbar icons for consistent sizing and visual weight. * Updated toolbar action colors, hover states, and keyboard-focus visibility. * Reordered Explorer home actions so “Run SQL” appears first. * Renamed “New query” to “Run SQL” in the new-tab menu. * Improved query source and settings toolbar controls. * Refined AI assistant chat layout with centered content, decorative gradients, and improved focus styling. * Added hover styling for the notebook Analyze action. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
311 lines
10 KiB
TypeScript
311 lines
10 KiB
TypeScript
import { BarChart2, LineChart, Settings2, Table } from 'lucide-react'
|
|
import { useEffect, useEffectEvent, useMemo } from 'react'
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
Switch,
|
|
ToggleGroup,
|
|
ToggleGroupItem,
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from 'ui'
|
|
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
|
import {
|
|
MultiSelector,
|
|
MultiSelectorContent,
|
|
MultiSelectorItem,
|
|
MultiSelectorList,
|
|
MultiSelectorTrigger,
|
|
} from 'ui-patterns/multi-select'
|
|
|
|
import { ExplorerToolbarAction } from '../ExplorerToolbar'
|
|
import { type QueryDisplay, type QueryResult } from '../types'
|
|
import { checkHasNonPositiveValues } from '@/components/ui/QueryBlock/QueryBlock.utils'
|
|
import { MAX_CHART_Y_SERIES, type ChartConfig } from '@/data/content/notebooks/notebook-schema'
|
|
|
|
interface DisplaySettingsButtonProps {
|
|
display: QueryDisplay
|
|
result?: QueryResult
|
|
columns: string[]
|
|
disabled: boolean
|
|
onChange: (display: QueryDisplay) => void
|
|
}
|
|
|
|
const getLogScaleDisabledReason = (y_series: string[]) => {
|
|
if (y_series.length === 0) return 'Select a column for the Y axis first'
|
|
if (y_series.length > 1) return 'Only available with a single Y axis column'
|
|
return 'Data contains zero or negative values'
|
|
}
|
|
|
|
export const DisplaySettingsButton = ({
|
|
display,
|
|
result,
|
|
columns,
|
|
disabled,
|
|
onChange,
|
|
}: DisplaySettingsButtonProps) => {
|
|
const { view, chart } = display
|
|
const {
|
|
type = 'bar',
|
|
x_column,
|
|
y_series = [],
|
|
cumulative = false,
|
|
show_labels = false,
|
|
scale = 'linear',
|
|
} = chart ?? {}
|
|
|
|
const hasNonPositiveValues = useMemo(
|
|
() => checkHasNonPositiveValues(result?.rows ?? [], y_series[0]),
|
|
[result, y_series]
|
|
)
|
|
|
|
// Logarithmic scale only applies to a single series.
|
|
const canToggleLogScale = useMemo(() => {
|
|
if (y_series.length !== 1 || !result || (result.rows ?? []).length === 0) return false
|
|
return !hasNonPositiveValues
|
|
}, [hasNonPositiveValues, result, y_series.length])
|
|
|
|
const onChangeView = (view: 'table' | 'chart') => {
|
|
onChange({ ...display, view })
|
|
}
|
|
|
|
const onUpdateChartConfig = (payload: Partial<ChartConfig>) => {
|
|
onChange({
|
|
...display,
|
|
chart: {
|
|
type: chart?.type ?? 'bar',
|
|
x_column: chart?.x_column ?? '',
|
|
y_series: chart?.y_series ?? [],
|
|
cumulative: chart?.cumulative ?? false,
|
|
scale: chart?.scale ?? 'linear',
|
|
show_labels: chart?.show_labels ?? false,
|
|
...payload,
|
|
},
|
|
})
|
|
}
|
|
|
|
const resetToLinearScale = useEffectEvent(() => {
|
|
onUpdateChartConfig({ scale: 'linear' })
|
|
})
|
|
|
|
useEffect(() => {
|
|
if (scale === 'log' && (hasNonPositiveValues || y_series.length > 1)) {
|
|
resetToLinearScale()
|
|
}
|
|
}, [hasNonPositiveValues, scale, y_series.length])
|
|
|
|
return (
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<ExplorerToolbarAction
|
|
disabled={disabled}
|
|
icon={<Settings2 size={16} strokeWidth={2} />}
|
|
tooltip="Result settings"
|
|
/>
|
|
</PopoverTrigger>
|
|
<PopoverContent
|
|
side="bottom"
|
|
aria-label="Result display settings"
|
|
className="mr-8 flex w-80 flex-col p-3"
|
|
>
|
|
<div className="flex flex-col gap-y-2.5">
|
|
<ToggleGroup
|
|
value={view}
|
|
onValueChange={(value) => {
|
|
if (value === 'table' || value === 'chart') onChangeView(value)
|
|
}}
|
|
aria-label="View result as"
|
|
variant="outline"
|
|
size="tiny"
|
|
type="single"
|
|
className="w-full"
|
|
>
|
|
<ToggleGroupItem value="table" className="w-full gap-2">
|
|
<Table size={14} />
|
|
<span>Table</span>
|
|
</ToggleGroupItem>
|
|
<ToggleGroupItem value="chart" className="w-full gap-2">
|
|
<BarChart2 size={14} />
|
|
<span>Chart</span>
|
|
</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
|
|
{view === 'chart' && (
|
|
<div className="flex min-w-0 flex-col gap-y-2.5">
|
|
<FormItemLayout
|
|
isReactForm={false}
|
|
layout="horizontal"
|
|
size="tiny"
|
|
label="Chart type"
|
|
>
|
|
<ToggleGroup
|
|
value={type}
|
|
onValueChange={(value) => {
|
|
if (value === 'bar' || value === 'line') onUpdateChartConfig({ type: value })
|
|
}}
|
|
aria-label="Chart type"
|
|
variant="default"
|
|
size="tiny"
|
|
type="single"
|
|
className="w-full"
|
|
>
|
|
<ToggleGroupItem value="bar" className="w-full gap-2">
|
|
<BarChart2 size={14} />
|
|
<span>Bar</span>
|
|
</ToggleGroupItem>
|
|
<ToggleGroupItem value="line" className="w-full gap-2">
|
|
<LineChart size={14} />
|
|
<span>Line</span>
|
|
</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
</FormItemLayout>
|
|
|
|
<FormItemLayout
|
|
id="result-x-axis"
|
|
isReactForm={false}
|
|
layout="horizontal"
|
|
size="tiny"
|
|
label="X axis"
|
|
>
|
|
<Select
|
|
value={x_column}
|
|
onValueChange={(x_column) => onUpdateChartConfig({ x_column })}
|
|
>
|
|
<SelectTrigger id="result-x-axis" size="tiny" className="w-full">
|
|
<SelectValue placeholder="Select a column" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{columns.map((x) => (
|
|
<SelectItem key={x} value={x}>
|
|
{x}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</FormItemLayout>
|
|
|
|
<FormItemLayout
|
|
id="result-y-axis"
|
|
isReactForm={false}
|
|
layout="horizontal"
|
|
size="tiny"
|
|
label="Y axis"
|
|
>
|
|
<MultiSelector
|
|
size="tiny"
|
|
values={y_series}
|
|
onValuesChange={(values) => {
|
|
if (values.length > MAX_CHART_Y_SERIES) return
|
|
onUpdateChartConfig({ y_series: values })
|
|
}}
|
|
className="w-full"
|
|
>
|
|
<MultiSelectorTrigger
|
|
id="result-y-axis"
|
|
mode="inline-combobox"
|
|
label={`Select up to ${MAX_CHART_Y_SERIES} columns`}
|
|
deletableBadge
|
|
badgeLimit={1}
|
|
showIcon={false}
|
|
className="min-w-0"
|
|
/>
|
|
<MultiSelectorContent>
|
|
<MultiSelectorList>
|
|
{columns.map((x) => (
|
|
<MultiSelectorItem
|
|
key={x}
|
|
value={x}
|
|
disabled={y_series.length >= MAX_CHART_Y_SERIES && !y_series.includes(x)}
|
|
>
|
|
{x}
|
|
</MultiSelectorItem>
|
|
))}
|
|
</MultiSelectorList>
|
|
</MultiSelectorContent>
|
|
</MultiSelector>
|
|
</FormItemLayout>
|
|
|
|
<FormItemLayout isReactForm={false} layout="horizontal" size="tiny" label="Scale">
|
|
<ToggleGroup
|
|
value={scale}
|
|
onValueChange={(value) => {
|
|
if (value === 'linear' || (value === 'log' && canToggleLogScale)) {
|
|
onUpdateChartConfig({ scale: value })
|
|
}
|
|
}}
|
|
aria-label="Chart scale"
|
|
variant="default"
|
|
size="tiny"
|
|
type="single"
|
|
className="w-full"
|
|
>
|
|
<ToggleGroupItem value="linear" className="w-full">
|
|
Linear
|
|
</ToggleGroupItem>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<span className="flex w-full" tabIndex={!canToggleLogScale ? 0 : undefined}>
|
|
<ToggleGroupItem
|
|
value="log"
|
|
disabled={!canToggleLogScale}
|
|
className="w-full"
|
|
>
|
|
Log
|
|
</ToggleGroupItem>
|
|
</span>
|
|
</TooltipTrigger>
|
|
{!canToggleLogScale && (
|
|
<TooltipContent side="left">
|
|
{getLogScaleDisabledReason(y_series)}
|
|
</TooltipContent>
|
|
)}
|
|
</Tooltip>
|
|
</ToggleGroup>
|
|
</FormItemLayout>
|
|
|
|
<FormItemLayout
|
|
id="cumulative"
|
|
isReactForm={false}
|
|
layout="horizontal"
|
|
size="tiny"
|
|
label="Cumulative"
|
|
>
|
|
<Switch
|
|
id="cumulative"
|
|
aria-label="Cumulative"
|
|
size="small"
|
|
checked={cumulative}
|
|
onCheckedChange={(cumulative) => onUpdateChartConfig({ cumulative })}
|
|
/>
|
|
</FormItemLayout>
|
|
|
|
<FormItemLayout
|
|
id="show_labels"
|
|
isReactForm={false}
|
|
layout="horizontal"
|
|
size="tiny"
|
|
label="Show labels"
|
|
>
|
|
<Switch
|
|
id="show_labels"
|
|
aria-label="Show labels"
|
|
size="small"
|
|
checked={show_labels}
|
|
onCheckedChange={(show_labels) => onUpdateChartConfig({ show_labels })}
|
|
/>
|
|
</FormItemLayout>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
)
|
|
}
|