Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BarChart2, Settings2, Table } from 'lucide-react'
import { useEffect, useEffectEvent, useMemo } from 'react'
import {
Checkbox,
Popover,
Expand All @@ -12,23 +13,34 @@ import {
SelectValue,
ToggleGroup,
ToggleGroupItem,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { type Snapshot } from 'valtio'

import { ExplorerToolbarAction } from '../ExplorerToolbar'
import { type QueryResult } from '../types'
import { checkHasNonPositiveValues } from '@/components/ui/QueryBlock/QueryBlock.utils'
import { type DatabaseCell as DatabaseCellSchema } from '@/data/content/notebooks/notebook-schema'
import { useCurrentNotebook, useNotebooksStateSnapshot } from '@/state/notebooks/notebooks-state'

interface DisplaySettingsButtonProps {
cell: Snapshot<DatabaseCellSchema>
result?: QueryResult
columns: string[]
disabled: boolean
}

// [Joshen] TODO support multiple y axis charts

export const DisplaySettingsButton = ({ cell, columns, disabled }: DisplaySettingsButtonProps) => {
export const DisplaySettingsButton = ({
cell,
result,
columns,
disabled,
}: DisplaySettingsButtonProps) => {
const snap = useNotebooksStateSnapshot()
const currentNotebook = useCurrentNotebook()
const cells = currentNotebook?.notebook.content?.cells ?? []
Expand All @@ -37,12 +49,22 @@ export const DisplaySettingsButton = ({ cell, columns, disabled }: DisplaySettin
const {
type = 'bar',
x_column,
y_columns,
y_columns = [],
cumulative = false,
show_labels = false,
scale = 'linear',
} = chart ?? {}

const hasNonPositiveValues = useMemo(
() => checkHasNonPositiveValues(result?.rows ?? [], y_columns[0]),
[result, y_columns]
)

const canToggleLogScale = useMemo(() => {
if (y_columns.length === 0 || !result || (result.rows ?? []).length === 0) return false
return !hasNonPositiveValues
}, [hasNonPositiveValues, result, y_columns.length])

const onChangeView = (view: 'table' | 'chart') => {
const notebookId = currentNotebook?.notebook.id
if (!notebookId) return
Expand Down Expand Up @@ -84,12 +106,22 @@ export const DisplaySettingsButton = ({ cell, columns, disabled }: DisplaySettin
snap.updateCells({ id: notebookId, cells: nextCells })
}

const resetToLinearScale = useEffectEvent(() => {
onUpdateChartConfig({ scale: 'linear' })
})

useEffect(() => {
if (hasNonPositiveValues && scale === 'log') {
resetToLinearScale()
}
}, [hasNonPositiveValues, scale])

return (
<Popover>
<PopoverTrigger asChild>
<ExplorerToolbarAction disabled={disabled} icon={<Settings2 />} tooltip="Result settings" />
</PopoverTrigger>
<PopoverContent side="bottom" className="flex flex-col gap-y-3 p-0 py-3">
<PopoverContent side="bottom" className="flex flex-col gap-y-3 p-0 py-3 mr-8">
<div className="flex flex-col gap-y-3 px-3">
<p className="text-xs tracking-tighter uppercase font-mono text-foreground-lighter">
Result display settings
Expand Down Expand Up @@ -187,7 +219,24 @@ export const DisplaySettingsButton = ({ cell, columns, disabled }: DisplaySettin
</SelectTrigger>
<SelectContent>
<SelectItem value="linear">Linear</SelectItem>
<SelectItem value="log">Logarithmic</SelectItem>
<Tooltip>
<TooltipTrigger asChild>
<SelectItem
disabled={!canToggleLogScale}
value="log"
className={!canToggleLogScale ? '!pointer-events-auto' : undefined}
>
Logarithmic
</SelectItem>
</TooltipTrigger>
{!canToggleLogScale && (
<TooltipContent side="right">
{y_columns.length === 0
? 'Select a column for the Y axis first'
: 'Data contains zero or negative values'}
</TooltipContent>
)}
</Tooltip>
</SelectContent>
</Select>
</FormItemLayout>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { type Snapshot } from 'valtio'

import { type QueryResult } from '../types'
import NoDataPlaceholder from '@/components/ui/Charts/NoDataPlaceholder'
import { getCumulativeResults } from '@/components/ui/QueryBlock/QueryBlock.utils'
import { formatLogTick, getCumulativeResults } from '@/components/ui/QueryBlock/QueryBlock.utils'
import { type DatabaseCell as DatabaseCellSchema } from '@/data/content/notebooks/notebook-schema'

interface QueryResultChartProps {
Expand All @@ -23,7 +23,7 @@ const toChartValue = (value: unknown): string | number => {

export const QueryResultChart = ({ cell, result }: QueryResultChartProps) => {
const { chart } = cell
const { type, x_column, y_columns = [], cumulative, show_labels } = chart ?? {}
const { type, x_column, y_columns = [], cumulative, show_labels, scale } = chart ?? {}

const hasConfig = !!x_column && y_columns.length > 0
const chartRows = useMemo(() => {
Expand Down Expand Up @@ -76,6 +76,11 @@ export const QueryResultChart = ({ cell, result }: QueryResultChartProps) => {
showXAxis={show_labels}
showYAxis={show_labels}
data={resultToRender}
YAxisProps={{
scale: scale === 'log' ? 'log' : 'auto',
domain: scale === 'log' ? [1, 'auto'] : undefined,
tickFormatter: scale === 'log' ? formatLogTick : undefined,
}}
/>
)}
{type === 'line' && (
Expand All @@ -86,6 +91,11 @@ export const QueryResultChart = ({ cell, result }: QueryResultChartProps) => {
showXAxis={show_labels}
showYAxis={show_labels}
data={resultToRender}
YAxisProps={{
scale: scale === 'log' ? 'log' : 'auto',
domain: scale === 'log' ? [1, 'auto'] : undefined,
tickFormatter: scale === 'log' ? formatLogTick : undefined,
}}
/>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export const QueryCell = ({ cell }: QueryCellProps) => {
<ExplorerToolbarActions>
<DisplaySettingsButton
cell={cell}
result={result}
columns={columns}
disabled={(result?.rows ?? []).length === 0}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,6 @@ export const DowngradeModal = ({
</div>
</li>
</ul>

{subscription?.billing_via_partner === true && subscription.billing_partner === 'fly' && (
<p className="mt-4 text-sm">
Your organization will be downgraded at the end of your current billing cycle.
</p>
)}
</div>
<DialogFooter>
<Button variant={'default'} onClick={onClose}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,14 +378,6 @@ export const SubscriptionPlanUpdateDialog = ({
<>You will be charged by them directly.</>
)}
</p>
{billingViaPartner &&
billingPartner === 'fly' &&
subscriptionPreview?.plan_change_type === 'downgrade' && (
<p className="text-sm">
Your organization will be downgraded at the end of your current billing
cycle.
</p>
)}
</div>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useParams } from 'common'
import { Check, Plus } from 'lucide-react'
import Link from 'next/link'
import {
Expand All @@ -12,45 +12,34 @@ import {
import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
import { formatDatabaseID, formatDatabaseRegion } from '@/data/read-replicas/replicas.utils'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
import { useSqlEditorSessionSnapshot } from '@/state/sql-editor/sql-editor-session-state'

/** The label a database row/summary shows: the primary, or a replica by region + id. */
function databaseLabel(identifier: string, region: string, projectRef: string | undefined) {
if (identifier === projectRef) return 'Primary database'
return `Read replica (${formatDatabaseRegion(region)} - ${formatDatabaseID(identifier)})`
}

export const DatabaseSubMenu = ({ id }: { id: string }) => {
export const DatabaseParametersSubMenu = ({
identifier,
onIdentifierChange,
}: {
identifier?: string
onIdentifierChange: (identifier: string) => void
}) => {
const { ref: projectRef } = useParams()
const sessionSnap = useSqlEditorSessionSnapshot()
const dbSelector = useDatabaseSelectorStateSnapshot()
const { infrastructureReadReplicas } = useIsFeatureEnabled(['infrastructure:read_replicas'])

const [lastSelectedDb, setLastSelectedDb] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.SQL_EDITOR_LAST_SELECTED_DB(projectRef ?? ''),
''
)

const { data } = useReadReplicasQuery({ projectRef })
const databases = (data ?? [])
.slice()
.sort((a, b) => (a.inserted_at > b.inserted_at ? 1 : 0))
.sort((database) => (database.identifier === projectRef ? -1 : 0))

const selectedDatabaseId =
lastSelectedDb.length > 0 ? lastSelectedDb : (dbSelector.selectedDatabaseId ?? projectRef)
const selectedDatabaseId = identifier ?? projectRef
const selectedDatabase = databases.find((db) => db.identifier === selectedDatabaseId)

const newReplicaURL = `/project/${projectRef}/database/replication?destinationType=Read+Replica`

const handleSelect = (databaseId: string) => {
dbSelector.setSelectedDatabaseId(databaseId)
setLastSelectedDb(databaseId)
sessionSnap.resetResult(id)
}

return (
<DropdownMenuSub>
<DropdownMenuSubTrigger>
Expand All @@ -71,7 +60,7 @@ export const DatabaseSubMenu = ({ id }: { id: string }) => {
key={database.identifier}
className="justify-between"
disabled={isUnhealthy}
onClick={() => handleSelect(database.identifier)}
onClick={() => onIdentifierChange(database.identifier)}
>
<span>{databaseLabel(database.identifier, database.region, projectRef)}</span>
{database.identifier === selectedDatabaseId && <Check size={14} />}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import dayjs from 'dayjs'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

import {
customDateRangeToLogTimeRange,
datePickerValueToLogTimeRange,
logTimeRangesEqual,
logTimeRangeToDatePickerValue,
resolveLogTimeRange,
} from './LogTimeRange.utils'
import { generateDynamicHelper } from '@/components/interfaces/Settings/Logs/Logs.datePickerHelpers'
import {
DEFAULT_LOG_TIME_RANGE,
type LogTimeRange,
} from '@/data/query-sources/query-source-registry'

describe('LogTimeRange.utils', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2025-01-08T12:00:00.000Z'))
})

afterEach(() => vi.useRealTimers())

it.each([
['30m', 30, 'minute'],
['2h', 2, 'hour'],
['7d', 7, 'day'],
] as const)('round-trips the picker helper for %s', (_, amount, unit) => {
const helper = generateDynamicHelper(amount, unit)
const pickerValue = {
from: helper.calcFrom(),
to: helper.calcTo(),
isHelper: true,
text: helper.text,
}
const range: LogTimeRange = { type: 'relative', amount, unit }

expect(datePickerValueToLogTimeRange(pickerValue)).toEqual(range)
expect(logTimeRangeToDatePickerValue(range)).toEqual(pickerValue)
})

it('falls back to the default when a custom value has no valid start', () => {
expect(datePickerValueToLogTimeRange({ from: '', to: '', isHelper: false })).toEqual(
DEFAULT_LOG_TIME_RANGE
)
})

it('uses now when an absolute helper has an empty end', () => {
expect(
datePickerValueToLogTimeRange({
from: '2025-01-01T00:00:00.000Z',
to: '',
isHelper: true,
text: 'Custom',
})
).toEqual({
type: 'absolute',
from: '2025-01-01T00:00:00.000Z',
to: '2025-01-08T12:00:00.000Z',
})
})

it('compares relative and absolute ranges structurally', () => {
expect(
logTimeRangesEqual(
{ type: 'relative', amount: 1, unit: 'hour' },
{ type: 'relative', amount: 1, unit: 'hour' }
)
).toBe(true)
expect(
logTimeRangesEqual(
{ type: 'relative', amount: 1, unit: 'hour' },
{ type: 'relative', amount: 1, unit: 'day' }
)
).toBe(false)
expect(
logTimeRangesEqual(
{ type: 'absolute', from: '2025-01-01T00:00:00.000Z', to: '2025-01-02T00:00:00.000Z' },
{ type: 'absolute', from: '2025-01-01T00:00:00.000Z', to: '2025-01-02T00:00:00.000Z' }
)
).toBe(true)
})

it('resolves a relative range against the current time', () => {
expect(resolveLogTimeRange({ type: 'relative', amount: 2, unit: 'day' })).toEqual({
from: dayjs().subtract(2, 'day').toISOString(),
to: dayjs().toISOString(),
})
})

it('passes an absolute range through unchanged', () => {
const range: LogTimeRange = {
type: 'absolute',
from: '2025-01-01T00:00:00.000Z',
to: '2025-01-02T00:00:00.000Z',
}
expect(resolveLogTimeRange(range)).toEqual({ from: range.from, to: range.to })
})

it('clamps a custom range ending today to now', () => {
const from = new Date('2025-01-07T09:00:00.000Z')
expect(
customDateRangeToLogTimeRange({
from,
to: new Date('2025-01-08T09:00:00.000Z'),
})
).toEqual({
type: 'absolute',
from: dayjs(from).startOf('day').toISOString(),
to: '2025-01-08T12:00:00.000Z',
})
})
})
Loading
Loading