mirror of
https://github.com/supabase/supabase.git
synced 2026-10-15 14:19:11 +03:00
* add more date range options to log previewer * new ui for logs datepickers * update logsquerypanel * update previewfilterpanel * fix some bugs and add copypasting date range support * handle updating times in copypaste events * add handlepaste to timesplitinput * update warehouse datepicker * update reports datepicker * rm consolelog * update logsquerypanel * update tests * fix reportsfilterbar * update api-overview report * rm consolelogs * update storage report * add large range wraning * update large range max const * fix default values and label * fix tests * add default value to logs previewer * disable helpers fix * rm last 7 days helper * add test for disabled helpers * add label test * add helper label test * reset state when closing without saving * undo rm * fm datefns, fix test class * fix year format * rm disabled from label * fix type err in test * fix test * fix tests * max 2 days for warning * fix refresh not using latest timestamp end * refactor log explorer to use new logs datepicker state hook * Remove round to hour/day in helpers * Use now instead of end of day for datepicker TO * fix types * update global timeout for e2e tests * update test with new default * fix test * analytics container taking too long to return data so adding more timeout * force ci * fix first render issues * Smol refactor * Do not reset timestamp end when refreshing in LogsPreviewer --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
324 lines
10 KiB
TypeScript
324 lines
10 KiB
TypeScript
import { useKeyboardShortcuts } from 'components/grid/components/common/Hooks'
|
|
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
|
|
import AlertError from 'components/ui/AlertError'
|
|
import LoadingOpacity from 'components/ui/LoadingOpacity'
|
|
import ShimmerLine from 'components/ui/ShimmerLine'
|
|
import { useWarehouseAccessTokensQuery } from 'data/analytics/warehouse-access-tokens-query'
|
|
import { useWarehouseLogDetailQuery } from 'data/analytics/warehouse-collection-log-detail-query'
|
|
import { useWarehouseCollectionsQuery } from 'data/analytics/warehouse-collections-query'
|
|
import { useWarehouseQueryQuery } from 'data/analytics/warehouse-query'
|
|
import dayjs from 'dayjs'
|
|
import { useSelectedLog } from 'hooks/analytics/useSelectedLog'
|
|
import { useFlag } from 'hooks/ui/useFlag'
|
|
import { RefreshCcw, Rewind, Terminal } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { useRouter } from 'next/router'
|
|
import { useEffect, useState } from 'react'
|
|
import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
|
import { LogsDatePicker } from '../Settings/Logs/Logs.DatePickers'
|
|
import { DatetimeHelper } from '../Settings/Logs/Logs.types'
|
|
import LogTable from '../Settings/Logs/LogTable'
|
|
import { TestCollectionDialog } from './TestCollectionDialog'
|
|
|
|
const INTERVALS: DatetimeHelper[] = [
|
|
{
|
|
text: 'Last hour',
|
|
calcFrom: () => dayjs().subtract(1, 'hour').toISOString(),
|
|
calcTo: () => dayjs().toISOString(),
|
|
default: true,
|
|
},
|
|
{
|
|
text: 'Last 12 hours',
|
|
calcFrom: () => dayjs().subtract(12, 'hour').toISOString(),
|
|
calcTo: () => dayjs().toISOString(),
|
|
},
|
|
{
|
|
text: 'Last day',
|
|
calcFrom: () => dayjs().subtract(1, 'day').toISOString(),
|
|
calcTo: () => dayjs().toISOString(),
|
|
},
|
|
{
|
|
text: 'Last 7 days',
|
|
calcFrom: () => dayjs().subtract(7, 'day').toISOString(),
|
|
calcTo: () => dayjs().toISOString(),
|
|
},
|
|
]
|
|
|
|
export const WarehouseCollectionDetail = () => {
|
|
const router = useRouter()
|
|
const collectionToken = router.query.collectionToken as string
|
|
const projectRef = router.query.ref as string
|
|
const accessTokens = useWarehouseAccessTokensQuery({ projectRef })
|
|
const [testDialogOpen, setTestDialogOpen] = useState(false)
|
|
const hasWarehouse = useFlag('warehouse')
|
|
|
|
const { data: collections, isLoading: collectionsLoading } = useWarehouseCollectionsQuery(
|
|
{ projectRef },
|
|
{ enabled: hasWarehouse }
|
|
)
|
|
const collection = collections?.find((c) => c.token === collectionToken)
|
|
const [search, setSearch] = useState('')
|
|
const [filters, setFilters] = useState({
|
|
limit: 100,
|
|
offset: 0,
|
|
search: '',
|
|
interval: {
|
|
to: INTERVALS[0].calcTo,
|
|
from: INTERVALS[0].calcFrom,
|
|
},
|
|
})
|
|
const [sql, setSQL] = useState('')
|
|
|
|
useEffect(() => {
|
|
if (collection) {
|
|
const from = filters.interval.from()
|
|
const to = filters.interval.to()
|
|
|
|
const sql = `select id, timestamp from \`${collection.name}\`
|
|
where timestamp >= TIMESTAMP('${from}')
|
|
and timestamp <= TIMESTAMP('${to}')
|
|
order by timestamp desc limit ${filters.limit} offset ${filters.offset}
|
|
`
|
|
|
|
setSQL(sql)
|
|
}
|
|
}, [filters.interval, filters.limit, filters.offset, collection])
|
|
|
|
const {
|
|
isLoading: queryLoading,
|
|
data: queryData,
|
|
isError,
|
|
isRefetching,
|
|
} = useWarehouseQueryQuery(
|
|
{ ref: projectRef, sql },
|
|
{
|
|
enabled: !!sql && hasWarehouse,
|
|
}
|
|
)
|
|
|
|
const formatResults = (results: any) => {
|
|
if (!results || !results.length) {
|
|
return []
|
|
}
|
|
|
|
const r = results.map(({ timestamp, ...r }: any) => {
|
|
return {
|
|
timestamp: new Date(timestamp / 1000).toLocaleString(),
|
|
...r,
|
|
}
|
|
})
|
|
|
|
return r
|
|
}
|
|
|
|
const results = formatResults(queryData?.result)
|
|
|
|
function loadMore() {
|
|
setFilters({ ...filters, offset: filters.offset + filters.limit })
|
|
}
|
|
|
|
const isLoading = queryLoading || collectionsLoading || isRefetching
|
|
|
|
function refreshResults(e?: React.FormEvent<HTMLFormElement>) {
|
|
e?.preventDefault()
|
|
setFilters({
|
|
...filters,
|
|
search,
|
|
interval: {
|
|
to: () => dayjs().toISOString(),
|
|
from: () => filters.interval.from() || dayjs().subtract(1, 'hour').toISOString(),
|
|
},
|
|
})
|
|
}
|
|
|
|
const queryUrl = `/project/${projectRef}/logs/explorer?q=${encodeURIComponent(
|
|
sql || ''
|
|
)}&source=warehouse`
|
|
|
|
useKeyboardShortcuts(
|
|
{
|
|
r: () => {
|
|
document.getElementById('refresh-button')?.focus()
|
|
},
|
|
s: () => {
|
|
document.getElementById('search-input')?.focus()
|
|
},
|
|
},
|
|
['INPUT', 'BUTTON', 'TEXTAREA']
|
|
)
|
|
|
|
function KeyboardShortcut({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<span className="flex items-center gap-2 font-medium text-xs font-mono text-foreground-light border rounded-md px-1.5 py-0.5 border-b-2">
|
|
{children}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
// Selected Log Detail
|
|
const [selectedLogId, setSelectedLogId] = useSelectedLog()
|
|
const {
|
|
data: selectedLogDetail,
|
|
isLoading: selectedLogDetailLoading,
|
|
error: selectedLogDetailError,
|
|
} = useWarehouseLogDetailQuery({
|
|
ref: projectRef,
|
|
collectionName: collection?.name,
|
|
logId: selectedLogId ?? undefined,
|
|
})
|
|
|
|
return (
|
|
<div className="relative flex flex-col flex-grow h-full">
|
|
<ShimmerLine active={isLoading} />
|
|
<LoadingOpacity active={isLoading}>
|
|
<div className="flex flex-col w-full">
|
|
<div className="flex justify-between items-center h-12 px-5">
|
|
<form onSubmit={refreshResults} className="flex items-center gap-2">
|
|
<h2 className="text-foreground-light max-w-60 truncate" title={collection?.name}>
|
|
{collection?.name}
|
|
</h2>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
id="refresh-button"
|
|
htmlType="submit"
|
|
icon={<RefreshCcw />}
|
|
type="text"
|
|
loading={isLoading}
|
|
disabled={isLoading}
|
|
className="px-1.5"
|
|
/>
|
|
</TooltipTrigger>
|
|
<TooltipContent className="flex items-center gap-2">
|
|
Refresh <KeyboardShortcut>R</KeyboardShortcut>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
{/* <Input
|
|
type="search"
|
|
id="search-input"
|
|
className="w-52"
|
|
size="tiny"
|
|
placeholder="Search by event message"
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
/> */}
|
|
<LogsDatePicker
|
|
helpers={INTERVALS}
|
|
value={{
|
|
from: filters.interval.from(),
|
|
to: filters.interval.to(),
|
|
}}
|
|
onSubmit={(e) =>
|
|
setFilters({
|
|
...filters,
|
|
interval: {
|
|
to: () => e.to || '',
|
|
from: () => e.from || '',
|
|
},
|
|
})
|
|
}
|
|
/>
|
|
</form>
|
|
<div className="flex items-center gap-2">
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button asChild className="px-1.5" type="outline" icon={<Terminal />}>
|
|
<Link href={queryUrl} />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom" className="text-xs">
|
|
Open query in Collections Explorer
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
|
|
<TestCollectionDialog
|
|
accessTokens={accessTokens.data?.data || []}
|
|
collectionToken={collectionToken}
|
|
projectRef={projectRef}
|
|
collections={collections || []}
|
|
open={testDialogOpen}
|
|
onOpenChange={setTestDialogOpen}
|
|
onSubmit={() => {
|
|
refreshResults()
|
|
setTestDialogOpen(false)
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{isError && (
|
|
<div className="p-4">
|
|
<AlertError subject="Failed to load query results" />
|
|
</div>
|
|
)}
|
|
|
|
{!isError && (
|
|
<LogTable
|
|
collectionName={collection?.name}
|
|
queryType="warehouse"
|
|
hasEditorValue={false}
|
|
projectRef={projectRef}
|
|
isLoading={isLoading}
|
|
onSelectedLogChange={(log) => setSelectedLogId(log?.id ?? null)}
|
|
selectedLog={selectedLogDetail?.result[0] ?? undefined}
|
|
selectedLogError={selectedLogDetailError ?? undefined}
|
|
isSelectedLogLoading={!!selectedLogId && selectedLogDetailLoading}
|
|
data={results}
|
|
showHeader={false}
|
|
EmptyState={
|
|
<ProductEmptyState title="No events found" size="large">
|
|
<div className="space-y-4">
|
|
<p>Try adjusting your filters, send a test event or refresh the results.</p>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
onClick={() => {
|
|
setTestDialogOpen(true)
|
|
}}
|
|
>
|
|
Send test event
|
|
</Button>
|
|
<Button type="outline" onClick={() => refreshResults()}>
|
|
Refresh
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</ProductEmptyState>
|
|
}
|
|
/>
|
|
)}
|
|
</div>
|
|
</LoadingOpacity>
|
|
|
|
{!isError && (
|
|
<div className="border-t flex flex-row justify-between">
|
|
<div className="flex items-center gap-2 p-2">
|
|
{results.length > 0 && (
|
|
<Button
|
|
onClick={loadMore}
|
|
icon={<Rewind />}
|
|
type="default"
|
|
loading={isLoading}
|
|
disabled={isLoading}
|
|
>
|
|
Load more
|
|
</Button>
|
|
)}
|
|
{filters.offset !== 0 && (
|
|
<>
|
|
<Button
|
|
onClick={() => setFilters({ ...filters, offset: 0 })}
|
|
type="default"
|
|
loading={isLoading}
|
|
disabled={isLoading}
|
|
>
|
|
Load latest
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|