From 30e73ee44d0d6b1f6475d612688908cee85e35e1 Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Tue, 16 Sep 2025 13:00:25 +1000 Subject: [PATCH 1/5] Home New: Report (#38341) * new home top * advisors * fix ts * add report section * add report * Nit refactor * refactor row * prevent adding snippet twice --------- Co-authored-by: Joshen Lim --- .../HomeNew/CustomReportSection.tsx | 338 ++++++++++++++++++ .../components/interfaces/HomeNew/Home.tsx | 8 + .../interfaces/HomeNew/SnippetDropdown.tsx | 108 ++++++ packages/ui-patterns/src/Row/Row.utils.ts | 38 ++ packages/ui-patterns/src/Row/index.tsx | 151 ++++---- 5 files changed, 558 insertions(+), 85 deletions(-) create mode 100644 apps/studio/components/interfaces/HomeNew/CustomReportSection.tsx create mode 100644 apps/studio/components/interfaces/HomeNew/SnippetDropdown.tsx create mode 100644 packages/ui-patterns/src/Row/Row.utils.ts diff --git a/apps/studio/components/interfaces/HomeNew/CustomReportSection.tsx b/apps/studio/components/interfaces/HomeNew/CustomReportSection.tsx new file mode 100644 index 0000000000000..19f479ada64ba --- /dev/null +++ b/apps/studio/components/interfaces/HomeNew/CustomReportSection.tsx @@ -0,0 +1,338 @@ +import { + DndContext, + DragEndEvent, + PointerSensor, + closestCenter, + useSensor, + useSensors, +} from '@dnd-kit/core' +import { SortableContext, arrayMove, rectSortingStrategy, useSortable } from '@dnd-kit/sortable' +import { PermissionAction } from '@supabase/shared-types/out/constants' +import dayjs from 'dayjs' +import { Plus } from 'lucide-react' +import type { CSSProperties, ReactNode } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' + +import { useParams } from 'common' +import { SnippetDropdown } from 'components/interfaces/HomeNew/SnippetDropdown' +import { ReportBlock } from 'components/interfaces/Reports/ReportBlock/ReportBlock' +import type { ChartConfig } from 'components/interfaces/SQLEditor/UtilityPanel/ChartConfig' +import { DEFAULT_CHART_CONFIG } from 'components/ui/QueryBlock/QueryBlock' +import { AnalyticsInterval } from 'data/analytics/constants' +import { useContentInfiniteQuery } from 'data/content/content-infinite-query' +import { Content } from 'data/content/content-query' +import { useContentUpsertMutation } from 'data/content/content-upsert-mutation' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' +import { uuidv4 } from 'lib/helpers' +import { useProfile } from 'lib/profile' +import type { Dashboards } from 'types' +import { Button } from 'ui' +import { Row } from 'ui-patterns' +import { toast } from 'sonner' + +export function CustomReportSection() { + const startDate = dayjs().subtract(7, 'day').toISOString() + const endDate = dayjs().toISOString() + const { ref } = useParams() + const { profile } = useProfile() + + const { data: reportsData } = useContentInfiniteQuery( + { projectRef: ref, type: 'report', name: 'Home', limit: 1 }, + { keepPreviousData: true } + ) + const homeReport = reportsData?.pages?.[0]?.content?.[0] as Content | undefined + const reportContent = homeReport?.content as Dashboards.Content | undefined + const [editableReport, setEditableReport] = useState( + reportContent + ) + + useEffect(() => { + if (reportContent) setEditableReport(reportContent) + }, [reportContent]) + + const { can: canUpdateReport } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'user_content', + { + resource: { + type: 'report', + visibility: homeReport?.visibility, + owner_id: homeReport?.owner_id, + }, + subject: { id: profile?.id }, + } + ) + + const { can: canCreateReport } = useAsyncCheckProjectPermissions( + PermissionAction.CREATE, + 'user_content', + { resource: { type: 'report', owner_id: profile?.id }, subject: { id: profile?.id } } + ) + + const { mutate: upsertContent } = useContentUpsertMutation() + + const persistReport = useCallback( + (updated: Dashboards.Content) => { + if (!ref || !homeReport) return + upsertContent({ projectRef: ref, payload: { ...homeReport, content: updated } }) + }, + [homeReport, ref, upsertContent] + ) + + const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } })) + + const handleDragStart = () => {} + + const recomputeSimpleGrid = useCallback( + (layout: Dashboards.Chart[]) => + layout.map( + (block, idx): Dashboards.Chart => ({ + ...block, + x: idx % 2, + y: Math.floor(idx / 2), + w: 1, + h: 1, + }) + ), + [] + ) + + const handleDragEnd = useCallback( + (event: DragEndEvent) => { + const { active, over } = event + if (!editableReport || !active || !over || active.id === over.id) return + const items = editableReport.layout.map((x) => String(x.id)) + const oldIndex = items.indexOf(String(active.id)) + const newIndex = items.indexOf(String(over.id)) + if (oldIndex === -1 || newIndex === -1) return + + const moved = arrayMove(editableReport.layout, oldIndex, newIndex) + const recomputed = recomputeSimpleGrid(moved) + const updated = { ...editableReport, layout: recomputed } + setEditableReport(updated) + persistReport(updated) + }, + [editableReport, persistReport, recomputeSimpleGrid] + ) + + const findNextPlacement = useCallback((current: Dashboards.Chart[]) => { + const occupied = new Set(current.map((c) => `${c.y}-${c.x}`)) + let y = 0 + for (; ; y++) { + const left = occupied.has(`${y}-0`) + const right = occupied.has(`${y}-1`) + if (!left || !right) { + const x = left ? 1 : 0 + return { x, y } + } + } + }, []) + + const createSnippetChartBlock = useCallback( + ( + snippet: { id: string; name: string }, + position: { x: number; y: number } + ): Dashboards.Chart => ({ + x: position.x, + y: position.y, + w: 1, + h: 1, + id: snippet.id, + label: snippet.name, + attribute: `snippet_${snippet.id}` as unknown as Dashboards.Chart['attribute'], + provider: 'daily-stats', + chart_type: 'bar', + chartConfig: DEFAULT_CHART_CONFIG, + }), + [] + ) + + const addSnippetToReport = (snippet: { id: string; name: string }) => { + if ( + editableReport?.layout?.some( + (x) => + String(x.id) === String(snippet.id) || String(x.attribute) === `snippet_${snippet.id}` + ) + ) { + toast('This block is already in your report') + return + } + // If the Home report doesn't exist yet, create it with the new block + if (!editableReport || !homeReport) { + if (!ref || !profile) return + + // Initial placement for first block + const initialBlock = createSnippetChartBlock(snippet, { x: 0, y: 0 }) + + const newReport: Dashboards.Content = { + schema_version: 1, + period_start: { time_period: '7d', date: '' }, + period_end: { time_period: 'today', date: '' }, + interval: '1d', + layout: [initialBlock], + } + + setEditableReport(newReport) + upsertContent({ + projectRef: ref, + payload: { + id: uuidv4(), + type: 'report', + name: 'Home', + description: '', + visibility: 'project', + owner_id: profile.id, + content: newReport, + }, + }) + return + } + const current = [...editableReport.layout] + const { x, y } = findNextPlacement(current) + current.push(createSnippetChartBlock(snippet, { x, y })) + const updated = { ...editableReport, layout: current } + setEditableReport(updated) + persistReport(updated) + } + + const handleRemoveChart = ({ metric }: { metric: { key: string } }) => { + if (!editableReport) return + const nextLayout = editableReport.layout.filter( + (x) => x.attribute !== (metric.key as unknown as Dashboards.Chart['attribute']) + ) + const updated = { ...editableReport, layout: nextLayout } + setEditableReport(updated) + persistReport(updated) + } + + const handleUpdateChart = ( + id: string, + { + chart, + chartConfig, + }: { chart?: Partial; chartConfig?: Partial } + ) => { + if (!editableReport) return + const currentChart = editableReport.layout.find((x) => x.id === id) + if (!currentChart) return + const updatedChart: Dashboards.Chart = { ...currentChart, ...(chart ?? {}) } + if (chartConfig) { + updatedChart.chartConfig = { ...(currentChart.chartConfig ?? {}), ...chartConfig } + } + const updatedLayouts = editableReport.layout.map((x) => (x.id === id ? updatedChart : x)) + const updated = { ...editableReport, layout: updatedLayouts } + setEditableReport(updated) + persistReport(updated) + } + + const layout = useMemo(() => editableReport?.layout ?? [], [editableReport]) + + return ( +
+
+

At a glance

+ {canUpdateReport || canCreateReport ? ( + }> + Add block + + } + side="bottom" + align="end" + autoFocus + /> + ) : null} +
+
+ {(() => { + if (layout.length === 0) { + return ( +
+ {canUpdateReport || canCreateReport ? ( + }> + Add your first chart + + } + side="bottom" + align="center" + autoFocus + /> + ) : ( +

No charts set up yet in report

+ )} +
+ ) + } + return ( + + String(x.id))} + strategy={rectSortingStrategy} + > + + {layout.map((item) => ( + +
+ handleUpdateChart(item.id, config)} + /> +
+
+ ))} +
+
+
+ ) + })()} +
+
+ ) +} + +function SortableReportBlock({ id, children }: { id: string; children: ReactNode }) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ + id, + }) + + const style: CSSProperties = { + transform: transform + ? `translate3d(${Math.round(transform.x)}px, ${Math.round(transform.y)}px, 0)` + : undefined, + transition, + } + + return ( +
+ {children} +
+ ) +} diff --git a/apps/studio/components/interfaces/HomeNew/Home.tsx b/apps/studio/components/interfaces/HomeNew/Home.tsx index e2e85de77ffac..6dd50690283ba 100644 --- a/apps/studio/components/interfaces/HomeNew/Home.tsx +++ b/apps/studio/components/interfaces/HomeNew/Home.tsx @@ -17,6 +17,7 @@ import { import { PROJECT_STATUS } from 'lib/constants' import { useAppStateSnapshot } from 'state/app-state' import { AdvisorSection } from './AdvisorSection' +import { CustomReportSection } from './CustomReportSection' import { GettingStartedSection, type GettingStartedState, @@ -131,6 +132,13 @@ export const HomeV2 = () => { ) } + if (id === 'custom-report') { + return ( + + + + ) + } })} diff --git a/apps/studio/components/interfaces/HomeNew/SnippetDropdown.tsx b/apps/studio/components/interfaces/HomeNew/SnippetDropdown.tsx new file mode 100644 index 0000000000000..b9f8fc56abf0b --- /dev/null +++ b/apps/studio/components/interfaces/HomeNew/SnippetDropdown.tsx @@ -0,0 +1,108 @@ +import { useIntersectionObserver } from '@uidotdev/usehooks' +import { ReactNode, useEffect, useMemo, useRef, useState } from 'react' + +import { useContentInfiniteQuery } from 'data/content/content-infinite-query' +import type { Content } from 'data/content/content-query' +import { SNIPPET_PAGE_LIMIT } from 'data/content/sql-folders-query' +import { + Command_Shadcn_, + CommandEmpty_Shadcn_, + CommandGroup_Shadcn_, + CommandInput_Shadcn_, + CommandItem_Shadcn_, + CommandList_Shadcn_, + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, +} from 'ui' + +type SnippetDropdownProps = { + projectRef?: string + trigger: ReactNode + side?: 'top' | 'bottom' | 'left' | 'right' + align?: 'start' | 'center' | 'end' + className?: string + autoFocus?: boolean + onSelect: (snippet: { id: string; name: string }) => void +} + +type SqlContentItem = Extract + +export const SnippetDropdown = ({ + projectRef, + trigger, + side = 'bottom', + align = 'end', + className, + autoFocus = false, + onSelect, +}: SnippetDropdownProps) => { + const [snippetSearch, setSnippetSearch] = useState('') + const scrollRootRef = useRef(null) + + const { data, isLoading, hasNextPage, fetchNextPage, isFetchingNextPage } = + useContentInfiniteQuery( + { + projectRef, + type: 'sql', + limit: SNIPPET_PAGE_LIMIT, + name: snippetSearch.length === 0 ? undefined : snippetSearch, + }, + { keepPreviousData: true } + ) + + const snippets = useMemo(() => { + const items = data?.pages.flatMap((page) => page.content) ?? [] + return items as SqlContentItem[] + }, [data?.pages]) + + const [sentinelRef, entry] = useIntersectionObserver({ + root: scrollRootRef.current, + threshold: 0, + rootMargin: '0px', + }) + + useEffect(() => { + if (entry?.isIntersecting && hasNextPage && !isFetchingNextPage && !isLoading) { + fetchNextPage() + } + }, [entry?.isIntersecting, hasNextPage, isFetchingNextPage, isLoading, fetchNextPage]) + + return ( + + {trigger} + + + + + {isLoading ? ( + Loading… + ) : snippets.length === 0 ? ( + No snippets found + ) : null} + + {snippets.map((snippet) => ( + onSelect({ id: snippet.id, name: snippet.name })} + > + {snippet.name} + + ))} +
+ + + + + + ) +} diff --git a/packages/ui-patterns/src/Row/Row.utils.ts b/packages/ui-patterns/src/Row/Row.utils.ts new file mode 100644 index 0000000000000..4abd879e8ca5f --- /dev/null +++ b/packages/ui-patterns/src/Row/Row.utils.ts @@ -0,0 +1,38 @@ +import { useLayoutEffect, useState } from 'react' + +export const useMeasuredWidth = (ref: React.RefObject) => { + const [measuredWidth, setMeasuredWidth] = useState(null) + + useLayoutEffect(() => { + const element = ref.current + if (!element) return + + const initial = element.getBoundingClientRect().width + setMeasuredWidth((prev) => (prev === initial ? prev : initial)) + + if (typeof ResizeObserver !== 'undefined') { + let frame = 0 + const resizeObserver = new ResizeObserver((entries) => { + const width = entries[0]?.contentRect.width ?? 0 + if (frame) cancelAnimationFrame(frame) + frame = requestAnimationFrame(() => { + setMeasuredWidth((prev) => (prev === width ? prev : width)) + }) + }) + resizeObserver.observe(element) + return () => { + if (frame) cancelAnimationFrame(frame) + resizeObserver.disconnect() + } + } else { + const handleResize = () => { + const width = element.getBoundingClientRect().width + setMeasuredWidth((prev) => (prev === width ? prev : width)) + } + window.addEventListener('resize', handleResize) + return () => window.removeEventListener('resize', handleResize) + } + }, [ref]) + + return measuredWidth +} diff --git a/packages/ui-patterns/src/Row/index.tsx b/packages/ui-patterns/src/Row/index.tsx index c28abb6e11e47..76a8922ec874b 100644 --- a/packages/ui-patterns/src/Row/index.tsx +++ b/packages/ui-patterns/src/Row/index.tsx @@ -1,13 +1,14 @@ 'use client' -import { ChevronLeft, ChevronRight } from 'lucide-react' import type React from 'react' -import type { ReactNode } from 'react' import { forwardRef, useEffect, useMemo, useRef, useState } from 'react' import { Button, cn } from 'ui' +import type { ReactNode } from 'react' +import { ChevronLeft, ChevronRight } from 'lucide-react' +import { useMeasuredWidth } from './Row.utils' interface RowProps extends React.HTMLAttributes { - /** columns can be a fixed number or an array [lg, md, sm] */ + // columns can be a fixed number or an array [lg, md, sm] columns: number | [number, number, number] children: ReactNode className?: string @@ -24,13 +25,11 @@ export const Row = forwardRef(function Row( ref ) { const containerRef = useRef(null) - // We forward the ref to the outer wrapper; consumers needing the scroll container - // can use a separate ref prop in the future if required. const childrenArray = useMemo(() => (Array.isArray(children) ? children : [children]), [children]) const [scrollPosition, setScrollPosition] = useState(0) - const [maxScroll, setMaxScroll] = useState(0) + const measuredWidth = useMeasuredWidth(containerRef) const resolveColumnsForWidth = (width: number): number => { if (!Array.isArray(columns)) return columns @@ -41,101 +40,77 @@ export const Row = forwardRef(function Row( return smCols } - const getRenderColumns = (): number => { - const width = containerRef.current?.getBoundingClientRect().width ?? 0 - return resolveColumnsForWidth(width) - } + const renderColumns = useMemo( + () => resolveColumnsForWidth(measuredWidth ?? 0), + [measuredWidth, columns] + ) const scrollByStep = (direction: -1 | 1) => { const el = containerRef.current if (!el) return - const widthLocal = el.getBoundingClientRect().width - const colsLocal = resolveColumnsForWidth(widthLocal) + const widthLocal = measuredWidth ?? el.getBoundingClientRect().width + const colsLocal = renderColumns const columnWidth = (widthLocal - (colsLocal - 1) * gap) / colsLocal const scrollAmount = columnWidth + gap - setScrollPosition((prev) => Math.max(0, Math.min(maxScroll, prev + direction * scrollAmount))) + setScrollPosition((prev) => { + const next = Math.max(0, Math.min(maxScroll, prev + direction * scrollAmount)) + return next === prev ? prev : next + }) } const scrollLeft = () => scrollByStep(-1) const scrollRight = () => scrollByStep(1) + const maxScroll = useMemo(() => { + if (measuredWidth == null) return -1 + const colsLocal = renderColumns + const columnWidth = (measuredWidth - (colsLocal - 1) * gap) / colsLocal + const totalWidth = childrenArray.length * columnWidth + (childrenArray.length - 1) * gap + return Math.max(0, totalWidth - measuredWidth) + }, [measuredWidth, renderColumns, childrenArray.length, gap]) + const canScrollLeft = scrollPosition > 0 const canScrollRight = scrollPosition < maxScroll - useEffect(() => { - const element = containerRef.current - if (!element) return - - const computeMaxScroll = (width: number) => { - const colsLocal = resolveColumnsForWidth(width) - const columnWidth = (width - (colsLocal - 1) * gap) / colsLocal - const totalWidth = childrenArray.length * columnWidth + (childrenArray.length - 1) * gap - const maxScrollValue = Math.max(0, totalWidth - width) - setMaxScroll(maxScrollValue) - } - - // Initial calculation - computeMaxScroll(element.getBoundingClientRect().width) - - if (typeof ResizeObserver !== 'undefined') { - const resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - computeMaxScroll(entry.contentRect.width) - } + const rafIdRef = useRef(0 as number) + const pendingDeltaRef = useRef(0) + + const handleWheel: React.WheelEventHandler = (e) => { + if (e.deltaX === 0) return + + const delta = Math.abs(e.deltaX) * 2 * (e.deltaX > 0 ? 1 : -1) + pendingDeltaRef.current += delta + + if (!rafIdRef.current) { + rafIdRef.current = requestAnimationFrame(() => { + rafIdRef.current = 0 + const accumulated = pendingDeltaRef.current + pendingDeltaRef.current = 0 + setScrollPosition((prev) => { + const target = prev + accumulated + const next = Math.max(0, Math.min(maxScroll, target)) + return next === prev ? prev : next + }) }) - resizeObserver.observe(element) - return () => resizeObserver.disconnect() - } else { - const handleResize = () => computeMaxScroll(element.getBoundingClientRect().width) - window.addEventListener('resize', handleResize) - return () => window.removeEventListener('resize', handleResize) } - }, [childrenArray.length, gap, columns]) - - useEffect(() => { - const handleWheel = (e: WheelEvent) => { - if (containerRef.current && containerRef.current.contains(e.target as Node)) { - if (e.deltaX !== 0) { - e.preventDefault() - - const scrollAmount = Math.abs(e.deltaX) * 2 - const direction = e.deltaX > 0 ? 1 : -1 - - setScrollPosition((prev) => { - const newPosition = prev + scrollAmount * direction - return Math.max(0, Math.min(maxScroll, newPosition)) - }) - } - } - } - - const container = containerRef.current - if (container) { - container.addEventListener('wheel', handleWheel, { passive: false }) - return () => container.removeEventListener('wheel', handleWheel) - } - }, [maxScroll]) + } useEffect(() => { - setScrollPosition((prev) => Math.min(prev, maxScroll)) + setScrollPosition((prev) => { + const next = Math.min(prev, maxScroll) + return next === prev ? prev : next + }) }, [maxScroll]) - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (containerRef.current && document.activeElement === containerRef.current) { - if (e.key === 'ArrowLeft' && canScrollLeft) { - e.preventDefault() - scrollLeft() - } else if (e.key === 'ArrowRight' && canScrollRight) { - e.preventDefault() - scrollRight() - } - } + const handleKeyDown: React.KeyboardEventHandler = (e) => { + if (e.key === 'ArrowLeft' && canScrollLeft) { + e.preventDefault() + scrollLeft() + } else if (e.key === 'ArrowRight' && canScrollRight) { + e.preventDefault() + scrollRight() } - - document.addEventListener('keydown', handleKeyDown) - return () => document.removeEventListener('keydown', handleKeyDown) - }, [canScrollLeft, canScrollRight]) + } return (
@@ -145,8 +120,9 @@ export const Row = forwardRef(function Row( onClick={scrollLeft} className="absolute w-8 h-8 left-0 top-1/2 -translate-y-1/2 z-10 rounded-full p-2" aria-label="Scroll left" - icon={} - /> + > + + )} {showArrows && canScrollRight && ( @@ -155,8 +131,9 @@ export const Row = forwardRef(function Row( onClick={scrollRight} className="absolute w-8 h-8 right-0 top-1/2 -translate-y-1/2 z-10 rounded-full p-2" aria-label="Scroll right" - icon={} - /> + > + + )}
(function Row( role="region" aria-roledescription="carousel" aria-label="Horizontally scrollable content" + style={{ overscrollBehaviorX: 'contain' }} + onWheel={handleWheel} + onKeyDown={handleKeyDown} >
From 0c3377975a0705efc6a1edcb28ee9a614f449e28 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Mon, 15 Sep 2025 23:04:45 -0400 Subject: [PATCH 2/5] fix: handle undefined count when fetching table rows (#38727) fix: handle undefined count when fetchint table rows count can be undefined in the return from fetching table rows (it uses executeSqlQuery under the hood which has no type safety) so we need to deal with that case --- .../grid/components/footer/pagination/Pagination.tsx | 11 ++++++----- apps/studio/data/table-rows/table-rows-count-query.ts | 6 +++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/apps/studio/components/grid/components/footer/pagination/Pagination.tsx b/apps/studio/components/grid/components/footer/pagination/Pagination.tsx index 8c0ff3af65754..d9cc6f64e2d88 100644 --- a/apps/studio/components/grid/components/footer/pagination/Pagination.tsx +++ b/apps/studio/components/grid/components/footer/pagination/Pagination.tsx @@ -69,9 +69,10 @@ const Pagination = () => { } ) - const count = data?.is_estimate ? formatEstimatedCount(data.count) : data?.count.toLocaleString() - const maxPages = Math.ceil((data?.count ?? 0) / tableEditorSnap.rowsPerPage) - const totalPages = (data?.count ?? 0) > 0 ? maxPages : 1 + const count = data?.count ?? 0 + const countString = data?.is_estimate ? formatEstimatedCount(count) : count.toLocaleString() + const maxPages = Math.ceil(count / tableEditorSnap.rowsPerPage) + const totalPages = count > 0 ? maxPages : 1 const onPreviousPage = () => { if (page > 1) { @@ -202,7 +203,7 @@ const Pagination = () => {

- {`${count} ${data.count === 0 || data.count > 1 ? `records` : 'record'}`}{' '} + {`${countString} ${count === 0 || count > 1 ? `records` : 'record'}`}{' '} {data.is_estimate ? '(estimated)' : ''}

@@ -217,7 +218,7 @@ const Pagination = () => { icon={} onClick={() => { // Show warning if either NOT a table entity, or table rows estimate is beyond threshold - if (rowsCountEstimate === null || data.count > THRESHOLD_COUNT) { + if (rowsCountEstimate === null || count > THRESHOLD_COUNT) { setIsConfirmFetchExactCountModalOpen(true) } else snap.setEnforceExactCount(true) }} diff --git a/apps/studio/data/table-rows/table-rows-count-query.ts b/apps/studio/data/table-rows/table-rows-count-query.ts index acfede4a8caa1..f6d57cb4a2ee6 100644 --- a/apps/studio/data/table-rows/table-rows-count-query.ts +++ b/apps/studio/data/table-rows/table-rows-count-query.ts @@ -90,7 +90,7 @@ from approximation; } export type TableRowsCount = { - count: number + count?: number is_estimate?: boolean } @@ -144,8 +144,8 @@ export async function getTableRowsCount( ) return { - count: result[0].count, - is_estimate: result[0].is_estimate ?? false, + count: result?.[0]?.count, + is_estimate: result?.[0]?.is_estimate ?? false, } as TableRowsCount } From 7f0fbe378d36f299b86cd0283c7b7b0f57508b2b Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Tue, 16 Sep 2025 11:13:34 +0800 Subject: [PATCH 3/5] chore: add compute price to enabled features (#38696) --- .../fields/ComputeSizeField.tsx | 35 +++++++++++-------- .../enabled-features/enabled-features.json | 1 + .../enabled-features.schema.json | 5 +++ 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/apps/studio/components/interfaces/DiskManagement/fields/ComputeSizeField.tsx b/apps/studio/components/interfaces/DiskManagement/fields/ComputeSizeField.tsx index 75d12fc5d41b2..6f7245d9d5e14 100644 --- a/apps/studio/components/interfaces/DiskManagement/fields/ComputeSizeField.tsx +++ b/apps/studio/components/interfaces/DiskManagement/fields/ComputeSizeField.tsx @@ -33,6 +33,7 @@ import { import { BillingChangeBadge } from '../ui/BillingChangeBadge' import FormMessage from '../ui/FormMessage' import { NoticeBar } from '../ui/NoticeBar' +import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' /** * to do: this could be a type from api-types @@ -55,6 +56,8 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) { const { data: org } = useSelectedOrganizationQuery() const { data: project, isLoading: isProjectLoading } = useSelectedProjectQuery() + const showComputePrice = useIsFeatureEnabled('project_addons:show_compute_price') + const { computeSize, storageType } = form.watch() const { @@ -221,21 +224,23 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
) : ( - <> - - ${price} - - - {' '} - /{' '} - {compute.price_interval === 'monthly' - ? 'month' - : 'hour'} - - + showComputePrice && ( + <> + + ${price} + + + {' '} + /{' '} + {compute.price_interval === 'monthly' + ? 'month' + : 'hour'} + + + ) )}
diff --git a/packages/common/enabled-features/enabled-features.json b/packages/common/enabled-features/enabled-features.json index 16b692a3b8359..30ca1cb6f7b18 100644 --- a/packages/common/enabled-features/enabled-features.json +++ b/packages/common/enabled-features/enabled-features.json @@ -63,6 +63,7 @@ "project_homepage:show_examples": true, "project_addons:dedicated_ipv4_address": true, + "project_addons:show_compute_price": true, "project_settings:custom_domains": true, "project_settings:show_disable_legacy_api_keys": true, diff --git a/packages/common/enabled-features/enabled-features.schema.json b/packages/common/enabled-features/enabled-features.schema.json index 6e49d7d2f12a9..313975ad619ab 100644 --- a/packages/common/enabled-features/enabled-features.schema.json +++ b/packages/common/enabled-features/enabled-features.schema.json @@ -209,6 +209,10 @@ "type": "boolean", "description": "Show the dedicated IPv4 address addon" }, + "project_addons:show_compute_price": { + "type": "boolean", + "description": "Show the compute price in the compute and disk page" + }, "project_settings:custom_domains": { "type": "boolean", @@ -295,6 +299,7 @@ "project_homepage:show_instance_size", "project_homepage:show_examples", "project_addons:dedicated_ipv4_address", + "project_addons:show_compute_price", "project_settings:custom_domains", "project_settings:show_disable_legacy_api_keys", "project_settings:legacy_jwt_keys", From 57a10f2f6d01d76f937fbfad11714e2339a07714 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 16 Sep 2025 11:24:53 +0800 Subject: [PATCH 4/5] Fix project card provider undefined (#38705) --- .../components/interfaces/Home/ProjectList/ProjectCard.tsx | 3 +-- .../components/interfaces/Home/ProjectList/ProjectTableRow.tsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx b/apps/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx index 650cb768cbd7a..ada3f39721b7e 100644 --- a/apps/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx +++ b/apps/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx @@ -28,8 +28,7 @@ export const ProjectCard = ({ resourceWarnings, }: ProjectCardProps) => { const { name, ref: projectRef } = project - const infraInformation = project.databases.find((x) => x.identifier === project.ref) - const desc = `${infraInformation?.cloud_provider} | ${project.region}` + const desc = `${project.cloud_provider} | ${project.region}` const { projectHomepageShowInstanceSize } = useIsFeatureEnabled([ 'project_homepage:show_instance_size', diff --git a/apps/studio/components/interfaces/Home/ProjectList/ProjectTableRow.tsx b/apps/studio/components/interfaces/Home/ProjectList/ProjectTableRow.tsx index 30563af30bb53..7039687da790a 100644 --- a/apps/studio/components/interfaces/Home/ProjectList/ProjectTableRow.tsx +++ b/apps/studio/components/interfaces/Home/ProjectList/ProjectTableRow.tsx @@ -110,7 +110,7 @@ export const ProjectTableRow = ({ - {infraInformation?.cloud_provider} | {project.region || 'N/A'} + {project.cloud_provider} | {project.region || 'N/A'} From 466760099d4c721b57e73ea006b69cd17eebf13e Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 16 Sep 2025 11:54:03 +0800 Subject: [PATCH 5/5] Support opening table rows for foreign tables in side panel (read only) (#38699) * Support opening table rows for foreign tables in side panel (read only) * Fix TS --- .../SidePanelEditor/RowEditor/RowEditor.utils.ts | 2 +- .../interfaces/TableGridEditor/TableGridEditor.tsx | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils.ts b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils.ts index 1dbff8c42edad..03236f0f374b9 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils.ts +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils.ts @@ -46,7 +46,7 @@ export const generateRowFields = ( table: PostgresTable, foreignKeys: ForeignKey[] ): RowField[] => { - const { primary_keys } = table + const { primary_keys = [] } = table const primaryKeyColumns = primary_keys.map((key) => key.name) return (table.columns ?? []).map((column) => { diff --git a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx index 5a31e04f07c87..2cceac5f76d64 100644 --- a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx @@ -8,9 +8,11 @@ import { SupabaseGrid } from 'components/grid/SupabaseGrid' import { useLoadTableEditorStateFromLocalStorageIntoUrl } from 'components/grid/SupabaseGrid.utils' import { Entity, + isForeignTable, isMaterializedView, isTableLike, isView, + TableLike, } from 'data/table-editor/table-editor-types' import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useDashboardHistory } from 'hooks/misc/useDashboardHistory' @@ -147,6 +149,7 @@ export const TableGridEditor = ({ const isViewSelected = isView(selectedTable) || isMaterializedView(selectedTable) const isTableSelected = isTableLike(selectedTable) + const isForeignTableSelected = isForeignTable(selectedTable) const canEditViaTableEditor = isTableSelected && !isSchemaLocked const editable = !isReadOnly && canEditViaTableEditor @@ -188,11 +191,13 @@ export const TableGridEditor = ({