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,30 +1,38 @@
import { useOperationQueueActions } from 'components/grid/hooks/useOperationQueueActions'
import { useOperationQueueShortcuts } from 'components/grid/hooks/useOperationQueueShortcuts'
import { useIsQueueOperationsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { AnimatePresence, motion } from 'framer-motion'
import { Eye } from 'lucide-react'
import { AnimatePresence, motion } from 'framer-motion'
import { createPortal } from 'react-dom'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { Button } from 'ui'

import { getModKeyLabel } from '@/lib/helpers'

const modKey = getModKeyLabel()
import {
useOperationQueueShortcuts,
getModKey,
} from 'components/grid/hooks/useOperationQueueShortcuts'
import { useIsQueueOperationsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { useOperationQueueActions } from 'components/grid/hooks/useOperationQueueActions'

export const SaveQueueActionBar = () => {
const snap = useTableEditorStateSnapshot()
const isQueueOperationsEnabled = useIsQueueOperationsEnabled()
const { handleSave } = useOperationQueueActions()

useOperationQueueShortcuts()

const operationCount = snap.operationQueue.operations.length
const isSaving = snap.operationQueue.status === 'saving'
const isOperationQueuePanelOpen = snap.sidePanel?.type === 'operation-queue'

const isVisible =
isQueueOperationsEnabled && snap.hasPendingOperations && !isOperationQueuePanelOpen

useOperationQueueShortcuts({
enabled: isQueueOperationsEnabled && snap.hasPendingOperations,
onSave: handleSave,
onTogglePanel: () => snap.onViewOperationQueue(),
isSaving,
hasOperations: operationCount > 0,
})

const modKey = getModKey()

const content = (
<AnimatePresence>
{isVisible && (
Expand All @@ -41,7 +49,7 @@ export const SaveQueueActionBar = () => {
</span>
<div className="flex items-center gap-3">
<button
onClick={() => snap.toggleViewOperationQueue()}
onClick={() => snap.onViewOperationQueue()}
className="text-foreground-light hover:text-foreground transition-colors flex items-center"
aria-label="View Details"
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { THRESHOLD_COUNT } from '@supabase/pg-meta/src/query/table-row-query'
import { keepPreviousData } from '@tanstack/react-query'
import { isEqual } from 'lodash'
import { ChevronDown, List } from 'lucide-react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'

import { useParams } from 'common'
import { useTableFilter } from 'components/grid/hooks/useTableFilter'
import type { Sort } from 'components/grid/types'
import { InlineLink } from 'components/ui/InlineLink'
import { useTableRowsCountQuery } from 'data/table-rows/table-rows-count-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { isEqual } from 'lodash'
import { ChevronDown, List } from 'lucide-react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
type RoleImpersonationState,
useRoleImpersonationStateSnapshot,
Expand All @@ -22,7 +23,6 @@ import {
Popover_Shadcn_,
} from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'

import { DropdownControl } from '../../common/DropdownControl'
import SortRow from './SortRow'

Expand Down Expand Up @@ -278,10 +278,8 @@ export const SortPopoverPrimitive = ({
const hasSortNotPK = localSorts.some(
(x) => !snap.table.columns.find((y) => x.column === y.name)?.isPrimaryKey
)
if (hasSortNotPK) return setShowWarning(true)
}

onSelectApplySorts()
if (hasSortNotPK) setShowWarning(true)
} else onSelectApplySorts()
}}
>
Apply sorting
Expand Down
14 changes: 8 additions & 6 deletions apps/studio/components/grid/hooks/useOperationQueueActions.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useQueryClient } from '@tanstack/react-query'
import { useCallback } from 'react'
import { toast } from 'sonner'

import { tableRowKeys } from 'data/table-rows/keys'
import { useOperationQueueSaveMutation } from 'data/table-rows/operation-queue-save-mutation'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useCallback } from 'react'
import { toast } from 'sonner'
import { useGetImpersonatedRoleState } from 'state/role-impersonation-state'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { QueuedOperation } from 'state/table-editor-operation-queue.types'
Expand All @@ -29,7 +30,6 @@ export function useOperationQueueActions(options: UseOperationQueueActionsOption
useOperationQueueSaveMutation({
onSuccess: () => {
snap.clearQueue()
snap.closeSidePanel()
toast.success('Changes saved successfully')
onSaveSuccess?.()
},
Expand All @@ -40,10 +40,12 @@ export function useOperationQueueActions(options: UseOperationQueueActionsOption
})

const isSaving = snap.operationQueue.status === 'saving' || isMutationPending
const operations = snap.operationQueue.operations as readonly QueuedOperation[]

const handleSave = useCallback(() => {
if (!project || operations.length === 0) return
if (!project) return

const operations = snap.operationQueue.operations as readonly QueuedOperation[]
if (operations.length === 0) return

snap.setQueueStatus('saving')

Expand All @@ -53,7 +55,7 @@ export function useOperationQueueActions(options: UseOperationQueueActionsOption
operations,
roleImpersonationState: getImpersonatedRoleState(),
})
}, [snap, project, operations, saveOperationQueue, getImpersonatedRoleState])
}, [snap, project, saveOperationQueue, getImpersonatedRoleState])

const handleCancel = useCallback(() => {
// Get unique table IDs from the queue before clearing
Expand Down
82 changes: 52 additions & 30 deletions apps/studio/components/grid/hooks/useOperationQueueShortcuts.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import { useOperationQueueActions } from './useOperationQueueActions'
import { useIsQueueOperationsEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useHotKey } from '@/hooks/ui/useHotKey'
import { useTableEditorStateSnapshot } from '@/state/table-editor'
import { useCallback, useEffect } from 'react'

import { detectOS } from 'lib/helpers'

export function getModKey() {
const os = detectOS()
return os === 'macos' ? '⌘' : 'Ctrl+'
}

interface UseOperationQueueShortcutsOptions {
enabled: boolean
onSave: () => void
onTogglePanel: () => void
isSaving?: boolean
hasOperations?: boolean
}

/**
* Hook that provides keyboard shortcuts for the operation queue.
Expand All @@ -13,34 +25,44 @@ import { useTableEditorStateSnapshot } from '@/state/table-editor'
* These shortcuts are registered on the capture phase to ensure they fire
* before the data grid handles the keyboard event.
*/
export function useOperationQueueShortcuts() {
const isQueueOperationsEnabled = useIsQueueOperationsEnabled()
const snap = useTableEditorStateSnapshot()
const { handleSave } = useOperationQueueActions()

const isSaving = snap.operationQueue.status === 'saving'
const hasOperations = snap.hasPendingOperations
const isEnabled = isQueueOperationsEnabled && hasOperations

useHotKey(
(event) => {
event.preventDefault()
event.stopPropagation()
if (!isSaving && hasOperations) {
handleSave()
export function useOperationQueueShortcuts({
enabled,
onSave,
onTogglePanel,
isSaving = false,
hasOperations = true,
}: UseOperationQueueShortcutsOptions) {
const os = detectOS()
const modKey = os === 'macos' ? '⌘' : 'Ctrl+'

const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
const isMod = os === 'macos' ? event.metaKey : event.ctrlKey

if (isMod && event.key === 's') {
event.preventDefault()
event.stopPropagation()
if (!isSaving && hasOperations) {
onSave()
}
} else if (isMod && event.key === '.') {
event.preventDefault()
event.stopPropagation()
onTogglePanel()
}
},
's',
{ enabled: isEnabled }
[os, isSaving, hasOperations, onSave, onTogglePanel]
)

useHotKey(
(event) => {
event.preventDefault()
event.stopPropagation()
snap.toggleViewOperationQueue()
},
'.',
{ enabled: isEnabled }
)
// Use capture phase to intercept events before the grid handles them
useEffect(() => {
if (enabled) {
window.addEventListener('keydown', handleKeyDown, true)
return () => {
window.removeEventListener('keydown', handleKeyDown, true)
}
}
}, [enabled, handleKeyDown])

return { modKey }
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ export const BackupsList = ({ onSelectRestore, disabled }: BackupsListProps) =>
return (
<div className="grid grid-cols-4 gap-4 items-center p-4" key={backup.id}>
<div>
<TimestampInfo utcTimestamp={backup.inserted_at} />
<TimestampInfo
displayAs="utc"
utcTimestamp={backup.inserted_at}
labelFormat="DD MMM YYYY HH:mm:ss (ZZ)"
className="text-left !text-sm font-mono tracking-tight"
/>
</div>
<div>
<Badge>{JSON.stringify(backup.status).replaceAll('"', '')}</Badge>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ export const PreviousRestoreItem = ({ clone }: { clone: CloneStatus['clones'][nu
</div>
<div>
<TimestampInfo
className="font-mono text-xs text-foreground-lighter"
className="text-left !text-sm font-mono tracking-tight text-foreground-lighter"
displayAs="utc"
utcTimestamp={clone.inserted_at ?? ''}
labelFormat="DD MMM YYYY HH:mm:ss (ZZ)"
/>
</div>
</div>
Expand All @@ -33,8 +35,10 @@ export const PreviousRestoreItem = ({ clone }: { clone: CloneStatus['clones'][nu
</div>
<div>
<TimestampInfo
className="font-mono text-xs text-foreground-lighter"
className="text-left !text-sm font-mono tracking-tight text-foreground-lighter"
displayAs="utc"
utcTimestamp={clone.inserted_at ?? ''}
labelFormat="DD MMM YYYY HH:mm:ss (ZZ)"
/>
</div>
<div className="flex items-center justify-end text-foreground-lighter group-hover:text-foreground">
Expand Down
33 changes: 16 additions & 17 deletions apps/studio/components/interfaces/Home/ProjectUsage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
import dayjs from 'dayjs'
import { Auth, Database, Realtime, Storage } from 'icons'
import sumBy from 'lodash/sumBy'
import { ChevronDown } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useState } from 'react'

import { useParams } from 'common'
import BarChart from 'components/ui/Charts/BarChart'
import { InlineLink } from 'components/ui/InlineLink'
Expand All @@ -15,10 +7,17 @@ import {
UsageApiCounts,
useProjectLogStatsQuery,
} from 'data/analytics/project-log-stats-query'
import dayjs from 'dayjs'
import { useFillTimeseriesSorted } from 'hooks/analytics/useFillTimeseriesSorted'
import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { Auth, Database, Realtime, Storage } from 'icons'
import sumBy from 'lodash/sumBy'
import { ChevronDown } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useState } from 'react'
import type { ChartIntervals } from 'types'
import {
Button,
Expand Down Expand Up @@ -88,20 +87,20 @@ const ProjectUsage = () => {
selectedInterval.startUnit as dayjs.ManipulateType
)
const endDateLocal = dayjs()
const { data: charts } = useFillTimeseriesSorted(
data?.result || [],
'timestamp',
[
const { data: charts } = useFillTimeseriesSorted({
data: data?.result ?? [],
timestampKey: 'timestamp',
valueKey: [
'total_auth_requests',
'total_rest_requests',
'total_storage_requests',
'total_realtime_requests',
],
0,
startDateLocal.toISOString(),
endDateLocal.toISOString(),
5
)
defaultValue: 0,
startDate: startDateLocal.toISOString(),
endDate: endDateLocal.toISOString(),
minPointsToFill: 5,
})
const datetimeFormat = selectedInterval.format || 'MMM D, ha'

const handleBarClick = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { LogsBarChartDatum } from '../HomeNew/ProjectUsage.metrics'
import { LogsTableName } from '../Settings/Logs/Logs.constants'
import { genChartQuery } from '../Settings/Logs/Logs.utils'
import {
type RawChartData,
calculateAggregatedMetrics,
calculateDateRange,
calculateHealthMetrics,
Expand Down Expand Up @@ -81,11 +82,11 @@ const fetchServiceHealthMetrics = async (
signal,
})

if (error || data?.error) {
throw error || data?.error
if (error ?? data?.error) {
throw error ?? data?.error
}

return (data?.result || []) as ChartQueryResult[]
return (data?.result ?? []) as ChartQueryResult[]
}

/**
Expand Down Expand Up @@ -119,18 +120,18 @@ const useServiceHealthQuery = ({
const normalizedData = useTimeseriesUnixToIso(queryResult.data ?? [], 'timestamp')

// Fill gaps in timeseries
const { data: filledData } = useFillTimeseriesSorted(
normalizedData,
'timestamp',
'ok_count',
0,
const { data: filledData } = useFillTimeseriesSorted({
data: normalizedData,
timestampKey: 'timestamp',
valueKey: 'ok_count',
defaultValue: 0,
startDate,
endDate
)
endDate,
})

// Transform to LogsBarChartDatum format
const eventChartData: LogsBarChartDatum[] = useMemo(
() => transformToBarChartData(filledData),
() => transformToBarChartData(filledData as RawChartData[]),
[filledData]
)

Expand Down
Loading
Loading