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
4 changes: 2 additions & 2 deletions apps/docs/internals/generate-guides-markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import path from 'node:path'
import { globby } from 'globby'
import matter from 'gray-matter'

import { getInternalLinkBaseUrl, prefixInternalLinks } from './internal-links'
import { getInternalLinkBaseUrl, prefixInternalLinks, withDocsBasePath } from './internal-links'

const PARTIALS_DIR = path.join(process.cwd(), 'content', '_partials')

Expand Down Expand Up @@ -109,7 +109,7 @@ function convertLinkPanels(content: string): string {
return content.replace(/<Link\b([\s\S]*?)>([\s\S]*?)<\/Link>/g, (full, linkAttrs, body) => {
const hrefMatch = linkAttrs.match(/\bhref="([^"]+)"/)
if (!hrefMatch) return full
const href = hrefMatch[1]
const href = withDocsBasePath(hrefMatch[1])

const panelOpen = body.match(/<(GlassPanel|IconPanel)\b/)
if (!panelOpen) return full
Expand Down
49 changes: 48 additions & 1 deletion apps/docs/internals/internal-links.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,53 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest'

import { getInternalLinkBaseUrl, prefixInternalLinks } from './internal-links'
import { getInternalLinkBaseUrl, prefixInternalLinks, withDocsBasePath } from './internal-links'

describe('withDocsBasePath', () => {
it('prepends /docs to a root-relative href', () => {
expect(withDocsBasePath('/guides/self-hosting/docker')).toBe('/docs/guides/self-hosting/docker')
})

it('prepends /docs to a single-segment href', () => {
expect(withDocsBasePath('/contribute')).toBe('/docs/contribute')
})

it('leaves hrefs that already start with /docs/ alone', () => {
expect(withDocsBasePath('/docs/guides/foo')).toBe('/docs/guides/foo')
})

it('leaves the exact /docs href alone', () => {
expect(withDocsBasePath('/docs')).toBe('/docs')
})

it('does not match prefixes like /docsx that only share leading chars', () => {
expect(withDocsBasePath('/docsx/foo')).toBe('/docs/docsx/foo')
})

it('leaves absolute http(s) URLs alone', () => {
expect(withDocsBasePath('https://example.com/foo')).toBe('https://example.com/foo')
})

it('leaves protocol-relative URLs alone', () => {
expect(withDocsBasePath('//cdn.example.com/foo')).toBe('//cdn.example.com/foo')
})

it('leaves anchor-only hrefs alone', () => {
expect(withDocsBasePath('#section')).toBe('#section')
})

it('leaves mailto: hrefs alone', () => {
expect(withDocsBasePath('mailto:team@example.com')).toBe('mailto:team@example.com')
})

it('leaves relative ./ and ../ hrefs alone', () => {
expect(withDocsBasePath('./sibling')).toBe('./sibling')
expect(withDocsBasePath('../parent')).toBe('../parent')
})

it('preserves query strings and fragments', () => {
expect(withDocsBasePath('/guides/foo?x=1#bar')).toBe('/docs/guides/foo?x=1#bar')
})
})

describe('getInternalLinkBaseUrl', () => {
const ORIGINAL_ENV = process.env
Expand Down
18 changes: 18 additions & 0 deletions apps/docs/internals/internal-links.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
const DOCS_BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH || '/docs'

/**
* Prepend the docs basePath to a root-relative href, mirroring what Next.js
* `<Link>` does at render time. Used when extracting an `href` attribute out
* of MDX `<Link>` components into plain markdown — without this, links like
* `/guides/foo` lose the `/docs` prefix the deployed site adds.
*
* Returns the href unchanged when it's already prefixed, external, anchor-
* only, or relative (`./`, `../`).
*/
export function withDocsBasePath(href: string): string {
if (!href.startsWith('/')) return href
if (href.startsWith('//')) return href
if (href === DOCS_BASE_PATH || href.startsWith(`${DOCS_BASE_PATH}/`)) return href
return `${DOCS_BASE_PATH}${href}`
}

/**
* Returns the absolute base URL to prepend to root-relative markdown links
* (e.g. `/dashboard/...`). Empty when no rewrite should happen — typically
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -356,9 +356,10 @@ export const NewPaymentMethodElement = forwardRef(
/>
</TooltipTrigger>
<TooltipContent side="top" className="w-72">
Select this only if your business is tax-registered. You’ll be asked for a tax ID
(e.g. US EIN, VAT, GST), which is required to issue a compliant business invoice. If
you’re not tax-registered, leave this unchecked. You’ll still receive a receipt.
Check this only if you need a tax ID (e.g. US EIN, VAT, GST) on your invoice. You’ll
be asked to enter it, and it’ll appear on a compliant business invoice. If you don’t
have a tax ID, or don’t need one shown, leave this unchecked. You’ll still receive a
receipt.
</TooltipContent>
</Tooltip>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import dayjs from 'dayjs'
import { Activity, BarChartIcon, Loader2 } from 'lucide-react'
import { useRouter } from 'next/router'
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Bar, BarChart, CartesianGrid, Line, LineChart, XAxis, YAxis } from 'recharts'
import { ChartContainer, ChartTooltip, ChartTooltipContent, WarningIcon } from 'ui'
import { ChartContainer, ChartTooltip, WarningIcon } from 'ui'

import { METRIC_THRESHOLDS } from './ReportBlock.constants'
import { ReportBlockContainer } from './ReportBlockContainer'
import { ChartConfig } from '@/components/interfaces/SQLEditor/UtilityPanel/ChartConfig'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { timestampFormatter } from '@/components/ui/Charts/Charts.utils'
import NoDataPlaceholder from '@/components/ui/Charts/NoDataPlaceholder'
import { PortalChartTooltip } from '@/components/ui/Charts/PortalChartTooltip'
import {
checkHasNonPositiveValues,
computeYAxisWidth,
Expand Down Expand Up @@ -71,6 +72,7 @@ export const ChartBlock = ({
const { ref } = router.query

const state = useDatabaseSelectorStateSnapshot()
const chartRef = useRef<HTMLDivElement>(null)
const [chartStyle, setChartStyle] = useState<string>(defaultChartStyle)
const logScale = useMemo(() => defaultLogScale, [defaultLogScale])
const [latestValue, setLatestValue] = useState<string | undefined>()
Expand Down Expand Up @@ -298,6 +300,7 @@ export const ChartBlock = ({
</p>
)}
<ChartContainer
ref={chartRef}
className="w-full aspect-auto px-3 py-2"
style={{
height: maxHeight ? `${maxHeight}px` : undefined,
Expand All @@ -323,7 +326,8 @@ export const ChartBlock = ({
/>
<ChartTooltip
content={
<ChartTooltipContent
<PortalChartTooltip
chartRef={chartRef}
className="min-w-[200px]"
labelSuffix={isPercentage ? '%' : ''}
labelFormatter={(x) => formatTooltipDate(x as string | number, 'DD MMM YYYY')}
Expand Down Expand Up @@ -351,7 +355,8 @@ export const ChartBlock = ({
/>
<ChartTooltip
content={
<ChartTooltipContent
<PortalChartTooltip
chartRef={chartRef}
labelSuffix={chartData?.format === '%' ? '%' : ''}
labelFormatter={(x) => formatTooltipDate(x as string | number, 'DD MMM YYYY')}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export const ReportBlock = ({
{isSnippet ? (
<QueryBlock
blockWriteQueries
portalTooltip
id={item.id}
label={item.label}
chartConfig={chartConfig}
Expand Down
67 changes: 67 additions & 0 deletions apps/studio/components/ui/Charts/PortalChartTooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useIsomorphicLayoutEffect } from 'common'
import { ComponentProps, RefObject, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import type { TooltipProps } from 'recharts'
import type { NameType, ValueType } from 'recharts/types/component/DefaultTooltipContent'
import { ChartTooltipContent, cn } from 'ui'

const TOOLTIP_OFFSET = 16
const VIEWPORT_MARGIN = 8

type PortalChartTooltipProps = Omit<ComponentProps<typeof ChartTooltipContent>, 'content'> &
Pick<TooltipProps<ValueType, NameType>, 'active' | 'coordinate' | 'payload'> & {
chartRef: RefObject<HTMLElement | null>
}

export const PortalChartTooltip = ({ chartRef, className, ...props }: PortalChartTooltipProps) => {
const { active, coordinate, payload } = props
const innerRef = useRef<HTMLDivElement>(null)
const [position, setPosition] = useState<{ left: number; top: number } | null>(null)

useIsomorphicLayoutEffect(() => {
if (!active || typeof document === 'undefined') {
setPosition(null)
return
}
const el = innerRef.current
if (!el) return

const containerRect = chartRef.current?.getBoundingClientRect()
const anchorX = (containerRect?.left ?? 0) + (coordinate?.x ?? 0)
const anchorY = (containerRect?.top ?? 0) + (coordinate?.y ?? 0)

const { width, height } = el.getBoundingClientRect()

let left = anchorX + TOOLTIP_OFFSET
if (left + width > window.innerWidth - VIEWPORT_MARGIN) {
left = anchorX - width - TOOLTIP_OFFSET
}
left = Math.max(VIEWPORT_MARGIN, Math.min(left, window.innerWidth - width - VIEWPORT_MARGIN))

const top = Math.max(
VIEWPORT_MARGIN,
Math.min(anchorY, window.innerHeight - height - VIEWPORT_MARGIN)
)

setPosition({ left, top })
}, [active, coordinate?.x, coordinate?.y, payload, chartRef])

if (!active || typeof document === 'undefined') return null

return createPortal(
<div
ref={innerRef}
style={{
position: 'fixed',
left: position?.left ?? -9999,
top: position?.top ?? -9999,
zIndex: 50,
pointerEvents: 'none',
opacity: position ? 1 : 0,
}}
>
<ChartTooltipContent className={cn('max-w-xs', className)} {...props} />
</div>,
document.body
)
}
34 changes: 26 additions & 8 deletions apps/studio/components/ui/QueryBlock/QueryBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'

import { ButtonTooltip } from '../ButtonTooltip'
import { CHART_COLORS } from '../Charts/Charts.constants'
import { PortalChartTooltip } from '../Charts/PortalChartTooltip'
import { SqlWarningAdmonition } from '../SqlWarningAdmonition'
import { BlockViewConfiguration } from './BlockViewConfiguration'
import { EditQueryButton } from './EditQueryButton'
Expand Down Expand Up @@ -47,6 +48,8 @@ export interface QueryBlockProps {
draggable?: boolean
disabled?: boolean
blockWriteQueries?: boolean
/** Render the chart tooltip in a portal so it isn't clipped by overflow-hidden ancestors (e.g. report cards). */
portalTooltip?: boolean
onExecute?: (queryType: 'select' | 'mutation') => void
onRemoveChart?: () => void
onUpdateChartConfig?: ({ chartConfig }: { chartConfig: Partial<ChartConfig> }) => void
Expand All @@ -69,11 +72,13 @@ export const QueryBlock = ({
draggable = false,
disabled = false,
blockWriteQueries = false,
portalTooltip = false,
onExecute,
onRemoveChart,
onUpdateChartConfig,
onDragStart,
}: QueryBlockProps) => {
const chartContainerRef = useRef<HTMLDivElement>(null)
const [chartSettings, setChartSettings] = useState<ChartConfig>(chartConfig)
const { xKey, yKey, view = 'table', logScale = false } = chartSettings

Expand Down Expand Up @@ -280,6 +285,7 @@ export const QueryBlock = ({
</p>
)}
<ChartContainer
ref={chartContainerRef}
className="aspect-auto px-3 py-2"
style={{ height: '230px', minHeight: '230px' }}
>
Expand Down Expand Up @@ -318,14 +324,26 @@ export const QueryBlock = ({
/>
<Tooltip
content={
<ChartTooltipContent
className="min-w-[200px]"
labelFormatter={(value) =>
xKeyDateFormat === 'date'
? dayjs(value).format('MMM D YYYY HH:mm')
: String(value)
}
/>
portalTooltip ? (
<PortalChartTooltip
chartRef={chartContainerRef}
className="min-w-[200px]"
labelFormatter={(value) =>
xKeyDateFormat === 'date'
? dayjs(value).format('MMM D YYYY HH:mm')
: String(value)
}
/>
) : (
<ChartTooltipContent
className="min-w-[200px]"
labelFormatter={(value) =>
xKeyDateFormat === 'date'
? dayjs(value).format('MMM D YYYY HH:mm')
: String(value)
}
/>
)
}
/>
<Bar radius={1} dataKey={yKey} fill="hsl(var(--chart-1))">
Expand Down
40 changes: 23 additions & 17 deletions apps/www/components/Legal/LegalDocVersions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,23 +45,29 @@ const LegalDocVersions = ({ versions }: Props) => {

return (
<>
<div className="not-prose mb-8 flex items-center gap-3">
<label htmlFor="legal-doc-version" className="text-foreground-lighter text-sm">
Version
</label>
<Select value={activeId} onValueChange={handleChange}>
<SelectTrigger id="legal-doc-version" className="w-auto min-w-[260px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{versions.map((v) => (
<SelectItem key={v.id} value={v.id}>
{v.label} — {v.effectiveDate}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{versions.length > 1 ? (
<div className="not-prose mb-8 flex items-center gap-3">
<label htmlFor="legal-doc-version" className="text-foreground-lighter text-sm">
Version
</label>
<Select value={activeId} onValueChange={handleChange}>
<SelectTrigger id="legal-doc-version" className="w-auto min-w-[260px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{versions.map((v) => (
<SelectItem key={v.id} value={v.id}>
{v.label} — {v.effectiveDate}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : (
<p className="not-prose mb-8 text-sm text-foreground-lighter">
{versions[0].label} — {versions[0].effectiveDate}
</p>
)}
<ActiveComponent />
</>
)
Expand Down
2 changes: 1 addition & 1 deletion apps/www/components/Products/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ const Products: React.FC<Props> = (props) => {
onClick={() => sendTelemetryEvent(PRODUCT_SHORTNAMES.REALTIME)}
image={<RealtimeVisual className="hidden sm:block" />}
className="
col-span-6 pointer-events-none xl:col-span-3
col-span-6 xl:col-span-3
hover:cursor-[url('/images/index/products/realtime-cursor-light.svg'),auto]!
dark:hover:cursor-[url('/images/index/products/realtime-cursor-dark.svg'),auto]!
"
Expand Down
Loading
Loading