mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Warehouse Clean Up (#37507)
This commit is contained in:
1 parent
5c77b07aa4
commit
e4837267f6
29 files changed
+148
-1834
No files matched your search
@@ -260,14 +260,6 @@ The table below shows the actions each role can take on the resources belonging
|
||||
| | View | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> |
|
||||
| | List | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> |
|
||||
| | Run | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> |
|
||||
| Events Collections | Create | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconX size={14} /> | <IconX size={14} /> |
|
||||
| | Update | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconX size={14} /> | <IconX size={14} /> |
|
||||
| | Delete | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconX size={14} /> | <IconX size={14} /> |
|
||||
| | View | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> |
|
||||
| | List | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> |
|
||||
| Warehouse Access Tokens | Create | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconX size={14} /> | <IconX size={14} /> |
|
||||
| | Revoke | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconX size={14} /> | <IconX size={14} /> |
|
||||
| | List | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconX size={14} /> |
|
||||
| <a href="#branching-permissions" id="branching-permissions">**Branching**</a> | | | | | |
|
||||
| Production Branch | Read | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> |
|
||||
| | Write | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconX size={14} /> |
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { z } from 'zod'
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormControl_Shadcn_,
|
||||
FormField_Shadcn_,
|
||||
FormMessage_Shadcn_,
|
||||
Form_Shadcn_,
|
||||
Input_Shadcn_,
|
||||
Modal,
|
||||
} from 'ui'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan'
|
||||
import { Admonition } from 'ui-patterns'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
|
||||
|
||||
export interface CollectionFormValues {
|
||||
name: string
|
||||
retention_days: number
|
||||
}
|
||||
|
||||
interface CollectionFormProps {
|
||||
initialValues?: Partial<CollectionFormValues>
|
||||
onSubmit: (values: CollectionFormValues) => void
|
||||
isLoading?: boolean
|
||||
onCancelClick: () => void
|
||||
}
|
||||
|
||||
export const CollectionForm = ({
|
||||
initialValues,
|
||||
onSubmit,
|
||||
isLoading,
|
||||
onCancelClick,
|
||||
}: CollectionFormProps) => {
|
||||
const { plan, isLoading: planLoading } = useCurrentOrgPlan()
|
||||
const router = useRouter()
|
||||
const currentOrg = useSelectedOrganization()
|
||||
|
||||
function getMaxRetentionDays() {
|
||||
if (planLoading) return 1
|
||||
if (plan?.id === 'free') return 3
|
||||
else return 90
|
||||
}
|
||||
const maxRetentionDays = getMaxRetentionDays()
|
||||
|
||||
const FormSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
retention_days: z.coerce.number().min(1).max(maxRetentionDays).multipleOf(1).positive().int(),
|
||||
})
|
||||
|
||||
const form = useForm<CollectionFormValues>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
name: initialValues?.name || '',
|
||||
retention_days: initialValues?.retention_days || maxRetentionDays,
|
||||
},
|
||||
})
|
||||
|
||||
const handleSubmit = form.handleSubmit(onSubmit)
|
||||
|
||||
const formatDays = (days: number) => {
|
||||
return days === 1 ? `1 day` : `${days} days`
|
||||
}
|
||||
|
||||
const retentionDescription = (days: number) => {
|
||||
if (!Boolean(days) || days < 1)
|
||||
return <>Your plan allows for a maximum of {formatDays(maxRetentionDays)}.</>
|
||||
return (
|
||||
<>
|
||||
Your logs will be removed after {formatDays(days)}. <br />
|
||||
Your plan allows for a maximum of {formatDays(maxRetentionDays)}.
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Modal.Content className="space-y-3">
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout label="Collection name" layout="horizontal">
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_ autoFocus placeholder="Events" {...field} />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="retention_days"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
label="Retention"
|
||||
layout="horizontal"
|
||||
description={retentionDescription(Number(field.value))}
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<div className="relative flex items-center gap-2">
|
||||
<Input_Shadcn_ type="number" {...field} />
|
||||
<span className="absolute right-3 text-foreground-lighter text-sm pointer-events-none">
|
||||
days
|
||||
</span>
|
||||
</div>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</Modal.Content>
|
||||
|
||||
{plan?.id === 'free' && (
|
||||
<>
|
||||
<Modal.Separator />
|
||||
<Admonition
|
||||
className="border-none bg-transparent"
|
||||
type="default"
|
||||
title="Upgrade your plan to increase retention"
|
||||
description={
|
||||
<div>
|
||||
Upgrade to the pro plan to increase retention to 90 days.
|
||||
<Button
|
||||
className="mt-2"
|
||||
type="primary"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/org/${currentOrg?.slug}/billing?panel=subscriptionPlan&source=dataWarehouseIncreaseRetention`
|
||||
)
|
||||
}
|
||||
>
|
||||
Upgrade plan
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<FormMessage_Shadcn_ />
|
||||
<Modal.Content className="py-4 border-t flex items-center justify-end gap-2">
|
||||
<Button size="tiny" type="default" onClick={onCancelClick}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="tiny" loading={isLoading} disabled={isLoading} htmlType="submit">
|
||||
Create collection
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
)
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useCreateCollection } from 'data/analytics/warehouse-collections-create-mutation'
|
||||
import { Modal } from 'ui'
|
||||
import { CollectionForm } from './CollectionForm'
|
||||
|
||||
export const CreateWarehouseCollectionModal = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
|
||||
const { mutate: createCollection, isLoading } = useCreateCollection({
|
||||
onSuccess: (data) => {
|
||||
onOpenChange(false)
|
||||
toast.success('Collection created successfully')
|
||||
router.push(`/project/${ref}/logs/collections/${data.token}`)
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size="medium"
|
||||
onCancel={() => onOpenChange(false)}
|
||||
header="Create collection"
|
||||
visible={open}
|
||||
hideFooter
|
||||
>
|
||||
<CollectionForm
|
||||
onCancelClick={() => onOpenChange(false)}
|
||||
isLoading={isLoading}
|
||||
onSubmit={(vals) => {
|
||||
if (!ref) {
|
||||
return
|
||||
}
|
||||
createCollection({
|
||||
projectRef: ref,
|
||||
name: vals.name,
|
||||
retention_days: vals.retention_days,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from '@ui/components/shadcn/ui/select'
|
||||
import CopyButton from 'components/ui/CopyButton'
|
||||
import type { WarehouseAccessTokensData } from 'data/analytics/warehouse-access-tokens-query'
|
||||
import type { WarehouseCollectionsData } from 'data/analytics/warehouse-collections-query'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Checkbox_Shadcn_,
|
||||
CodeBlock,
|
||||
Input_Shadcn_,
|
||||
Label_Shadcn_,
|
||||
Separator,
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetSection,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from 'ui'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
|
||||
export function TestCollectionDialog({
|
||||
accessTokens,
|
||||
collectionToken,
|
||||
collections,
|
||||
open,
|
||||
onOpenChange,
|
||||
onSubmit,
|
||||
}: {
|
||||
accessTokens: WarehouseAccessTokensData['data']
|
||||
collections: WarehouseCollectionsData
|
||||
collectionToken: string
|
||||
projectRef: string
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onSubmit: () => void
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const BASE_WAREHOUSE_URL = `https://api.warehouse.tech/api/events`
|
||||
const [testAccessToken, setTestAccessToken] = useState('')
|
||||
const [selectedCollection, setSelectedCollection] = useState(collectionToken || '')
|
||||
const [showAccessToken, setShowAccessToken] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedCollection(collectionToken)
|
||||
}, [collectionToken])
|
||||
|
||||
useEffect(() => {
|
||||
if (accessTokens.length > 0) {
|
||||
setTestAccessToken(accessTokens[0].token)
|
||||
}
|
||||
}, [accessTokens])
|
||||
|
||||
const selectedAccessToken = accessTokens.find((token) => token.token === testAccessToken)
|
||||
const selectedCollectionName = collections?.find((col) => col.token === selectedCollection)?.name
|
||||
|
||||
function getcURL(accessToken: string) {
|
||||
return `curl -X "POST" "${BASE_WAREHOUSE_URL}?source=${selectedCollection}" \\
|
||||
-H 'Content-Type: application/json' \\
|
||||
-H 'Authorization: Bearer ${accessToken}' \\
|
||||
-d $'{
|
||||
"event_message": "Test event message",
|
||||
"metadata": {
|
||||
"ip_address": "100.100.100.100",
|
||||
"request_method": "POST",
|
||||
"custom_user_data": {
|
||||
"foo": "bar"
|
||||
},
|
||||
"datacenter": "aws",
|
||||
"request_headers": {
|
||||
"connection": "close",
|
||||
"user_agent": "chrome"
|
||||
}
|
||||
}
|
||||
}'
|
||||
`
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SheetTrigger asChild>
|
||||
<Button disabled={accessTokens.length === 0} type="outline">
|
||||
Connect
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
</TooltipTrigger>
|
||||
{accessTokens.length === 0 && (
|
||||
<TooltipContent>Create an access token to connect to your collection</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
|
||||
<SheetContent size="lg" className="h-screen overflow-auto flex flex-col">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Send events to this collection</SheetTitle>
|
||||
<SheetDescription>
|
||||
Use the following cURL command to send events to this collection
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<SheetSection className="flex flex-col gap-4 overflow-auto">
|
||||
<FormItemLayout label="Collection" isReactForm={false} layout="horizontal">
|
||||
<Select value={selectedCollection} onValueChange={setSelectedCollection}>
|
||||
<SelectTrigger size="tiny">
|
||||
<span className="truncate">{selectedCollectionName || 'Collection'}</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-[260px]">
|
||||
{collections?.map((col) => (
|
||||
<SelectItem key={col.id} value={col.token}>
|
||||
{col.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItemLayout>
|
||||
<FormItemLayout label="Token" isReactForm={false} layout="horizontal">
|
||||
<Select value={testAccessToken} onValueChange={setTestAccessToken}>
|
||||
<SelectTrigger size="tiny">
|
||||
<span className="truncate">
|
||||
{selectedAccessToken?.description || 'Access token'}
|
||||
</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-[260px]">
|
||||
{accessTokens?.map((token: any) => (
|
||||
<SelectItem key={token.id + '-token'} value={token.token}>
|
||||
{token.description || 'No description'}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItemLayout>
|
||||
|
||||
<FormItemLayout layout="horizontal" label="Ingest API URL" isReactForm={false}>
|
||||
<Input_Shadcn_ size="tiny" className="font-mono" value={BASE_WAREHOUSE_URL} readOnly />
|
||||
</FormItemLayout>
|
||||
<FormItemLayout layout="horizontal" label="Collection ID" isReactForm={false}>
|
||||
<Input_Shadcn_ size="tiny" className="font-mono" value={selectedCollection} readOnly />
|
||||
</FormItemLayout>
|
||||
</SheetSection>
|
||||
|
||||
<Separator />
|
||||
|
||||
<SheetSection className="flex-1">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex gap-2 items-center">
|
||||
<Checkbox_Shadcn_
|
||||
name="showAccessToken"
|
||||
id="showAccessToken"
|
||||
checked={showAccessToken}
|
||||
onCheckedChange={() => setShowAccessToken(!showAccessToken)}
|
||||
/>
|
||||
<Label_Shadcn_ htmlFor="showAccessToken">Show access token</Label_Shadcn_>
|
||||
</div>
|
||||
<CopyButton type="default" text={getcURL(testAccessToken)} />
|
||||
</div>
|
||||
|
||||
<div className="mt-4 relative">
|
||||
<CodeBlock
|
||||
hideCopy
|
||||
className={'p-1 language-bash prose max-w-full transition-colors'}
|
||||
language="bash"
|
||||
>
|
||||
{getcURL(showAccessToken ? testAccessToken : '********************')}
|
||||
</CodeBlock>
|
||||
</div>
|
||||
</SheetSection>
|
||||
|
||||
<SheetFooter className="mt-auto">
|
||||
<p className="text-sm text-foreground-light mr-auto">
|
||||
Once you send an event, refresh the page to see it in the collection.
|
||||
</p>
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={onSubmit}>Refresh results</Button>
|
||||
</div>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
@@ -1,323 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { Database, EditIcon, TrashIcon } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useDeleteCollectionMutation } from 'data/analytics/warehouse-collections-delete-mutation'
|
||||
import { useUpdateCollection } from 'data/analytics/warehouse-collections-update-mutation'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
Modal,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from 'ui'
|
||||
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { LogsSidebarItem } from '../Settings/Logs/SidebarV2/SidebarItem'
|
||||
import { CollectionForm } from './CollectionForm'
|
||||
|
||||
type Props = {
|
||||
item: {
|
||||
id: number
|
||||
token: string
|
||||
name: string
|
||||
retention_days: number
|
||||
}
|
||||
}
|
||||
|
||||
export const WarehouseMenuItem = ({ item }: Props) => {
|
||||
const router = useRouter()
|
||||
const { ref, collectionToken } = useParams()
|
||||
const projectRef = ref || 'default'
|
||||
|
||||
const [showUpdateDialog, setShowUpdateDialog] = useState(false)
|
||||
const [showDeleteDialog, setDeleteDialog] = useState(false)
|
||||
|
||||
const canUpdateCollection = useCheckPermissions(PermissionAction.ANALYTICS_WRITE, 'logflare')
|
||||
|
||||
const updateCollection = useUpdateCollection({
|
||||
onSuccess: () => {
|
||||
toast.success('Collection updated successfully')
|
||||
setShowUpdateDialog(false)
|
||||
},
|
||||
})
|
||||
const deleteCollection = useDeleteCollectionMutation({
|
||||
onSuccess: () => {
|
||||
// If the current collection is deleted, redirect to default logs view
|
||||
if (item.token === collectionToken) {
|
||||
router.push(`/project/${projectRef}/logs/explorer`)
|
||||
}
|
||||
setDeleteDialog(false)
|
||||
toast.success('Collection deleted successfully')
|
||||
},
|
||||
})
|
||||
|
||||
const isLoading = updateCollection.isLoading || deleteCollection.isLoading
|
||||
|
||||
return (
|
||||
<>
|
||||
<LogsSidebarItem
|
||||
isActive={item.token === collectionToken}
|
||||
label={item.name}
|
||||
icon={<Database size="14" className="text-foreground-light" />}
|
||||
href={`/project/${projectRef}/logs/collections/${item.token}`}
|
||||
dropdownItems={
|
||||
<>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
disabled={!canUpdateCollection}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
setShowUpdateDialog(true)
|
||||
}}
|
||||
>
|
||||
<EditIcon className="mr-2" size={14} />
|
||||
<div>Update collection</div>
|
||||
</DropdownMenuItem>
|
||||
</TooltipTrigger>
|
||||
{!canUpdateCollection && (
|
||||
<TooltipContent side="right">
|
||||
You need additional permissions to update collections
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
<DropdownMenuSeparator />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
disabled={!canUpdateCollection}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
setDeleteDialog(true)
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="mr-2" size={14} />
|
||||
<div>Delete collection</div>
|
||||
</DropdownMenuItem>
|
||||
</TooltipTrigger>
|
||||
{!canUpdateCollection && (
|
||||
<TooltipContent side="right">
|
||||
You need additional permissions to delete collections
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</>
|
||||
}
|
||||
></LogsSidebarItem>
|
||||
|
||||
<Modal
|
||||
size="medium"
|
||||
visible={showUpdateDialog}
|
||||
onCancel={() => setShowUpdateDialog(false)}
|
||||
hideFooter
|
||||
header={`Update collection "${item.name}"`}
|
||||
>
|
||||
<CollectionForm
|
||||
onCancelClick={() => setShowUpdateDialog(false)}
|
||||
isLoading={isLoading}
|
||||
initialValues={{ name: item.name, retention_days: item.retention_days }}
|
||||
onSubmit={({ name, retention_days }) => {
|
||||
updateCollection.mutate({
|
||||
projectRef,
|
||||
collectionToken: item.token,
|
||||
name,
|
||||
retention_days,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
<ConfirmationModal
|
||||
variant="destructive"
|
||||
visible={showDeleteDialog}
|
||||
onCancel={() => setDeleteDialog(false)}
|
||||
confirmLabel="Delete collection"
|
||||
title={`Delete collection "${item.name}"`}
|
||||
size="small"
|
||||
onConfirm={() => {
|
||||
deleteCollection.mutate({ projectRef, collectionToken: item.token })
|
||||
}}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
All data in this collection will be deleted.
|
||||
<br />
|
||||
<div className="mt-2">This action cannot be undone.</div>
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -22,7 +22,6 @@ export interface LogSelectionProps {
|
||||
onClose: () => void
|
||||
queryType?: QueryType
|
||||
projectRef: string
|
||||
collectionName?: string
|
||||
isLoading: boolean
|
||||
error?: string | object
|
||||
}
|
||||
@@ -41,8 +40,6 @@ const LogSelection = ({ log, onClose, queryType, isLoading, error }: LogSelectio
|
||||
if (!log) return <LogDetailEmptyState />
|
||||
|
||||
switch (queryType) {
|
||||
// case 'warehouse':
|
||||
// return <WarehouseSelectionRenderer log={log} />
|
||||
case 'api':
|
||||
const status = log?.metadata?.[0]?.response?.[0]?.status_code
|
||||
const method = log?.metadata?.[0]?.request?.[0]?.method
|
||||
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
import { cn } from 'ui'
|
||||
import { LOGS_TAILWIND_CLASSES } from '../Logs.constants'
|
||||
import { jsonSyntaxHighlight, SelectionDetailedRow } from '../LogsFormatters'
|
||||
|
||||
const DetailedJsonRow = ({
|
||||
label,
|
||||
value,
|
||||
code,
|
||||
}: {
|
||||
label: string
|
||||
value: Object
|
||||
code?: boolean
|
||||
}) => {
|
||||
return (
|
||||
<div className="grid grid-cols-12">
|
||||
<span className="text-foreground-lighter text-sm col-span-3">{label}</span>
|
||||
<span
|
||||
className={cn(
|
||||
'text-foreground text-sm col-span-9 overflow-x-auto',
|
||||
code && 'text-xs font-mono'
|
||||
)}
|
||||
>
|
||||
<pre
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: jsonSyntaxHighlight(value),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const WarehouseSelectionRenderer = ({ log }: any) => {
|
||||
return (
|
||||
<div className="overflow-hidden overflow-x-auto space-y-6">
|
||||
{Object.entries(log).map(([key, value], index) => {
|
||||
return (
|
||||
<div
|
||||
key={`${key}-${index}`}
|
||||
className={`${LOGS_TAILWIND_CLASSES.log_selection_x_padding}`}
|
||||
>
|
||||
{value && typeof value === 'object' ? (
|
||||
<DetailedJsonRow label={key} value={value} />
|
||||
) : (
|
||||
<SelectionDetailedRow label={key} value={value === null ? 'NULL ' : String(value)} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -49,8 +49,6 @@ interface Props {
|
||||
onSave?: () => void
|
||||
hasEditorValue?: boolean
|
||||
className?: string
|
||||
collectionName?: string // Used for warehouse queries
|
||||
warehouseError?: string
|
||||
EmptyState?: ReactNode
|
||||
showHeader?: boolean
|
||||
showHistogramToggle?: boolean
|
||||
@@ -79,7 +77,6 @@ const LogTable = ({
|
||||
onSave,
|
||||
hasEditorValue,
|
||||
className,
|
||||
collectionName,
|
||||
EmptyState,
|
||||
showHeader = true,
|
||||
showHistogramToggle = true,
|
||||
@@ -154,9 +151,6 @@ const LogTable = ({
|
||||
columns
|
||||
} else {
|
||||
switch (queryType) {
|
||||
case 'warehouse':
|
||||
columns = DEFAULT_COLUMNS
|
||||
break
|
||||
case 'api':
|
||||
columns = DatabaseApiColumnRender
|
||||
break
|
||||
@@ -460,7 +454,6 @@ const LogTable = ({
|
||||
log={selectedLog}
|
||||
error={selectedLogError}
|
||||
queryType={queryType}
|
||||
collectionName={collectionName}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
)}
|
||||
|
||||
@@ -369,7 +369,6 @@ export enum LogsTableName {
|
||||
POSTGREST = 'postgrest_logs',
|
||||
SUPAVISOR = 'supavisor_logs',
|
||||
PGBOUNCER = 'pgbouncer_logs',
|
||||
WAREHOUSE = 'warehouse_logs',
|
||||
PG_UPGRADE = 'pg_upgrade_logs',
|
||||
PG_CRON = 'pg_cron_logs',
|
||||
}
|
||||
@@ -384,7 +383,6 @@ export const LOGS_TABLES = {
|
||||
storage: LogsTableName.STORAGE,
|
||||
postgrest: LogsTableName.POSTGREST,
|
||||
supavisor: LogsTableName.SUPAVISOR,
|
||||
warehouse: LogsTableName.WAREHOUSE,
|
||||
pg_upgrade: LogsTableName.PG_UPGRADE,
|
||||
pg_cron: LogsTableName.POSTGRES,
|
||||
pgbouncer: LogsTableName.PGBOUNCER,
|
||||
@@ -401,7 +399,6 @@ export const LOGS_SOURCE_DESCRIPTION = {
|
||||
[LogsTableName.POSTGREST]: 'RESTful API web server logs',
|
||||
[LogsTableName.SUPAVISOR]: 'Shared connection pooler logs for PostgreSQL',
|
||||
[LogsTableName.PGBOUNCER]: 'Dedicated connection pooler for PostgreSQL',
|
||||
[LogsTableName.WAREHOUSE]: 'Logs obtained from a data warehouse collection',
|
||||
[LogsTableName.PG_UPGRADE]: 'Logs generated by the Postgres version upgrade process',
|
||||
[LogsTableName.PG_CRON]: 'Postgres logs from pg_cron cron jobs',
|
||||
}
|
||||
|
||||
@@ -90,7 +90,6 @@ export type QueryType =
|
||||
| 'storage'
|
||||
| 'supavisor'
|
||||
| 'postgrest'
|
||||
| 'warehouse'
|
||||
| 'pg_upgrade'
|
||||
| 'pg_cron'
|
||||
| 'pgbouncer'
|
||||
@@ -145,9 +144,3 @@ export interface DatetimeHelper {
|
||||
disabled?: boolean
|
||||
availableIn?: PlanId[]
|
||||
}
|
||||
|
||||
export interface WarehouseCollection {
|
||||
name: string
|
||||
id: number
|
||||
token: string
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import { ReactNode, useState } from 'react'
|
||||
import { IS_PLATFORM } from 'common'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import { useFlag } from 'hooks/ui/useFlag'
|
||||
import { logConstants } from 'shared-data'
|
||||
import {
|
||||
Badge,
|
||||
@@ -28,23 +27,16 @@ import {
|
||||
LogsTableName,
|
||||
} from './Logs.constants'
|
||||
import { DatePickerValue, LogsDatePicker } from './Logs.DatePickers'
|
||||
import { LogsWarning, LogTemplate, WarehouseCollection } from './Logs.types'
|
||||
import { WarehouseQueryTemplate } from './Warehouse.utils'
|
||||
import { LogsWarning, LogTemplate } from './Logs.types'
|
||||
|
||||
export type SourceType = 'logs' | 'warehouse'
|
||||
export interface LogsQueryPanelProps {
|
||||
templates?: LogTemplate[]
|
||||
warehouseTemplates?: WarehouseQueryTemplate[]
|
||||
defaultFrom: string
|
||||
defaultTo: string
|
||||
warnings: LogsWarning[]
|
||||
warehouseCollections: WarehouseCollection[]
|
||||
dataSource: SourceType
|
||||
onSelectTemplate: (template: LogTemplate) => void
|
||||
onSelectWarehouseTemplate: (template: WarehouseQueryTemplate) => void
|
||||
onSelectSource: (source: string) => void
|
||||
onDateChange: (value: DatePickerValue) => void
|
||||
onDataSourceChange: (sourceType: SourceType) => void
|
||||
}
|
||||
|
||||
function DropdownMenuItemContent({ name, desc }: { name: ReactNode; desc?: string }) {
|
||||
@@ -58,17 +50,12 @@ function DropdownMenuItemContent({ name, desc }: { name: ReactNode; desc?: strin
|
||||
|
||||
const LogsQueryPanel = ({
|
||||
templates = [],
|
||||
warehouseTemplates = [],
|
||||
defaultFrom,
|
||||
defaultTo,
|
||||
warnings,
|
||||
warehouseCollections,
|
||||
dataSource,
|
||||
onSelectTemplate,
|
||||
onSelectWarehouseTemplate,
|
||||
onSelectSource,
|
||||
onDateChange,
|
||||
onDataSourceChange,
|
||||
}: LogsQueryPanelProps) => {
|
||||
const [showReference, setShowReference] = useState(false)
|
||||
|
||||
@@ -78,15 +65,12 @@ const LogsQueryPanel = ({
|
||||
projectEdgeFunctionAll: edgeFunctionsEnabled,
|
||||
} = useIsFeatureEnabled(['project_auth:all', 'project_storage:all', 'project_edge_function:all'])
|
||||
|
||||
const warehouseEnabled = useFlag('warehouse')
|
||||
|
||||
const logsTableNames = Object.entries(LogsTableName)
|
||||
.filter(([key]) => {
|
||||
if (key === 'AUTH') return authEnabled
|
||||
if (key === 'STORAGE') return storageEnabled
|
||||
if (key === 'FN_EDGE') return edgeFunctionsEnabled
|
||||
if (key === 'PG_CRON') return false
|
||||
if (key === 'WAREHOUSE') return false
|
||||
return true
|
||||
})
|
||||
.map(([, value]) => value)
|
||||
@@ -117,90 +101,31 @@ const LogsQueryPanel = ({
|
||||
<div className="flex w-full items-center justify-between px-4 md:px-5 py-2 overflow-x-scroll no-scrollbar">
|
||||
<div className="flex w-full flex-row items-center justify-between gap-x-4">
|
||||
<div className="flex items-center gap-2">
|
||||
{warehouseEnabled && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="default" iconRight={<ChevronDown />}>
|
||||
Data source{' '}
|
||||
<span className="ml-2 font-mono opacity-50">
|
||||
{dataSource === 'warehouse' ? 'collections' : 'logs'}
|
||||
</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="start">
|
||||
<DropdownMenuItem onClick={() => onDataSourceChange('logs')}>
|
||||
<DropdownMenuItemContent name="Logs" desc="Logs for all Supabase products" />
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onDataSourceChange('warehouse')}>
|
||||
<DropdownMenuItemContent
|
||||
name={
|
||||
<span>
|
||||
Collections <Badge variant="warning">NEW</Badge>
|
||||
</span>
|
||||
}
|
||||
desc="Query your collections"
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
{dataSource === 'warehouse' && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="default" iconRight={<ChevronDown />}>
|
||||
Templates
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="max-h-96 overflow-auto" side="bottom" align="start">
|
||||
{warehouseTemplates.map((template) => (
|
||||
<DropdownMenuItem
|
||||
key={template.name}
|
||||
onClick={() => onSelectWarehouseTemplate(template)}
|
||||
>
|
||||
<DropdownMenuItemContent name={template.name} desc={template.description} />
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{warehouseCollections.length === 0 && (
|
||||
<DropdownMenuItem className="hover:bg-transparent cursor-default">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="default" iconRight={<ChevronDown />}>
|
||||
Insert source
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="bottom"
|
||||
align="start"
|
||||
className="max-h-[70vh] overflow-auto"
|
||||
>
|
||||
{logsTableNames
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((source) => (
|
||||
<DropdownMenuItem key={source} onClick={() => onSelectSource(source)}>
|
||||
<DropdownMenuItemContent
|
||||
name="No collections found"
|
||||
desc="You can create collections in the left sidebar."
|
||||
name={source}
|
||||
desc={LOGS_SOURCE_DESCRIPTION[source]}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{dataSource === 'logs' && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="default" iconRight={<ChevronDown />}>
|
||||
Insert source
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="bottom"
|
||||
align="start"
|
||||
className="max-h-[70vh] overflow-auto"
|
||||
>
|
||||
{dataSource === 'logs' &&
|
||||
logsTableNames
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((source) => (
|
||||
<DropdownMenuItem key={source} onClick={() => onSelectSource(source)}>
|
||||
<DropdownMenuItemContent
|
||||
name={source}
|
||||
desc={LOGS_SOURCE_DESCRIPTION[source]}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
{dataSource === 'logs' && IS_PLATFORM && (
|
||||
{IS_PLATFORM && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="default" iconRight={<ChevronDown />}>
|
||||
@@ -222,16 +147,14 @@ const LogsQueryPanel = ({
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
{dataSource === 'logs' && (
|
||||
<LogsDatePicker
|
||||
value={selectedDatePickerValue}
|
||||
onSubmit={(value) => {
|
||||
setSelectedDatePickerValue(value)
|
||||
onDateChange(value)
|
||||
}}
|
||||
helpers={EXPLORER_DATEPICKER_HELPERS}
|
||||
/>
|
||||
)}
|
||||
<LogsDatePicker
|
||||
value={selectedDatePickerValue}
|
||||
onSubmit={(value) => {
|
||||
setSelectedDatePickerValue(value)
|
||||
onDateChange(value)
|
||||
}}
|
||||
helpers={EXPLORER_DATEPICKER_HELPERS}
|
||||
/>
|
||||
|
||||
<div
|
||||
data-testid="log-explorer-warnings"
|
||||
@@ -261,93 +184,92 @@ const LogsQueryPanel = ({
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{dataSource === 'logs' && (
|
||||
<SidePanel
|
||||
size="large"
|
||||
header={
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<h3>Field Reference</h3>
|
||||
<Button
|
||||
type="text"
|
||||
className="px-1"
|
||||
onClick={() => setShowReference(false)}
|
||||
icon={<X />}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
visible={showReference}
|
||||
cancelText="Close"
|
||||
onCancel={() => setShowReference(false)}
|
||||
hideFooter
|
||||
triggerElement={
|
||||
|
||||
<SidePanel
|
||||
size="large"
|
||||
header={
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<h3>Field Reference</h3>
|
||||
<Button
|
||||
asChild // ?: we don't want a button inside a button
|
||||
type="text"
|
||||
onClick={() => setShowReference(true)}
|
||||
icon={<BookOpen />}
|
||||
className="px-2"
|
||||
>
|
||||
<span>Field Reference</span>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<SidePanel.Content>
|
||||
<div className="pt-4 pb-2 space-y-1">
|
||||
<p className="text-sm">
|
||||
The following table shows all the available paths that can be queried from each
|
||||
respective source. Do note that to access nested keys, you would need to perform
|
||||
the necessary{' '}
|
||||
<Link
|
||||
href="https://supabase.com/docs/guides/platform/logs#unnesting-arrays"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-brand"
|
||||
>
|
||||
unnesting joins
|
||||
<ExternalLink
|
||||
size="14"
|
||||
className="ml-1 inline -translate-y-[2px]"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</SidePanel.Content>
|
||||
<SidePanel.Separator />
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="edge_logs"
|
||||
listClassNames="px-2"
|
||||
className="px-1"
|
||||
onClick={() => setShowReference(false)}
|
||||
icon={<X />}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
visible={showReference}
|
||||
cancelText="Close"
|
||||
onCancel={() => setShowReference(false)}
|
||||
hideFooter
|
||||
triggerElement={
|
||||
<Button
|
||||
asChild // ?: we don't want a button inside a button
|
||||
type="text"
|
||||
onClick={() => setShowReference(true)}
|
||||
icon={<BookOpen />}
|
||||
className="px-2"
|
||||
>
|
||||
{logConstants.schemas.map((schema) => (
|
||||
<Tabs.Panel
|
||||
key={schema.reference}
|
||||
id={schema.reference}
|
||||
label={schema.name}
|
||||
className="px-4 pb-4"
|
||||
<span>Field Reference</span>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<SidePanel.Content>
|
||||
<div className="pt-4 pb-2 space-y-1">
|
||||
<p className="text-sm">
|
||||
The following table shows all the available paths that can be queried from each
|
||||
respective source. Do note that to access nested keys, you would need to perform
|
||||
the necessary{' '}
|
||||
<Link
|
||||
href="https://supabase.com/docs/guides/platform/logs#unnesting-arrays"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-brand"
|
||||
>
|
||||
<Table
|
||||
head={[
|
||||
<Table.th className="text-xs !p-2" key="path">
|
||||
Path
|
||||
</Table.th>,
|
||||
<Table.th key="type" className="text-xs !p-2">
|
||||
Type
|
||||
</Table.th>,
|
||||
]}
|
||||
body={schema.fields
|
||||
.sort((a: any, b: any) => a.path - b.path)
|
||||
.map((field) => (
|
||||
<Field key={field.path} field={field} />
|
||||
))}
|
||||
unnesting joins
|
||||
<ExternalLink
|
||||
size="14"
|
||||
className="ml-1 inline -translate-y-[2px]"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
))}
|
||||
</Tabs>
|
||||
</SidePanel>
|
||||
)}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</SidePanel.Content>
|
||||
<SidePanel.Separator />
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="edge_logs"
|
||||
listClassNames="px-2"
|
||||
>
|
||||
{logConstants.schemas.map((schema) => (
|
||||
<Tabs.Panel
|
||||
key={schema.reference}
|
||||
id={schema.reference}
|
||||
label={schema.name}
|
||||
className="px-4 pb-4"
|
||||
>
|
||||
<Table
|
||||
head={[
|
||||
<Table.th className="text-xs !p-2" key="path">
|
||||
Path
|
||||
</Table.th>,
|
||||
<Table.th key="type" className="text-xs !p-2">
|
||||
Type
|
||||
</Table.th>,
|
||||
]}
|
||||
body={schema.fields
|
||||
.sort((a: any, b: any) => a.path - b.path)
|
||||
.map((field) => (
|
||||
<Field key={field.path} field={field} />
|
||||
))}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
))}
|
||||
</Tabs>
|
||||
</SidePanel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
export type WarehouseQueryTemplate = {
|
||||
query: string
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export function createWarehouseQueryTemplates(
|
||||
collections: { name: string }[]
|
||||
): WarehouseQueryTemplate[] {
|
||||
return collections.flatMap((collection) => [
|
||||
{
|
||||
query: `select count(*) as event_count from \`${collection.name}\`
|
||||
where timestamp > timestamp_sub(current_timestamp(), interval 1 day)`,
|
||||
name: `${collection.name} - Daily Event Count`,
|
||||
description: `Count events in the last 24 hours from ${collection.name} collection`,
|
||||
},
|
||||
{
|
||||
query: `select id, timestamp, event_message from \`${collection.name}\`
|
||||
where timestamp > timestamp_sub(current_timestamp(), interval 1 day)
|
||||
and event_message like '%YOUR_TEXT_HERE%'
|
||||
order by timestamp desc limit 10`,
|
||||
name: `${collection.name} - Filter by text`,
|
||||
description: `Select last 10 events from ${collection.name} collection`,
|
||||
},
|
||||
])
|
||||
}
|
||||
@@ -223,8 +223,6 @@ const ProjectLinks = () => {
|
||||
const isNewAPIDocsEnabled = useIsAPIDocsSidePanelEnabled()
|
||||
const { securityLints, errorLints } = useLints()
|
||||
|
||||
const showWarehouse = useFlag('warehouse')
|
||||
|
||||
const activeRoute = router.pathname.split('/')[3]
|
||||
|
||||
const {
|
||||
@@ -310,14 +308,11 @@ const ProjectLinks = () => {
|
||||
</div>
|
||||
)
|
||||
} else if (route.key === 'logs') {
|
||||
// TODO: Undo this when warehouse flag is removed
|
||||
const label = showWarehouse ? 'Logs & Analytics' : route.label
|
||||
const newRoute = { ...route, label }
|
||||
return (
|
||||
<SideBarNavLink
|
||||
key={`other-routes-${i}`}
|
||||
route={newRoute}
|
||||
active={activeRoute === newRoute.key}
|
||||
route={route}
|
||||
active={activeRoute === route.key}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -9,13 +9,9 @@ import {
|
||||
useFeaturePreviewModal,
|
||||
useUnifiedLogsPreview,
|
||||
} from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { CreateWarehouseCollectionModal } from 'components/interfaces/DataWarehouse/CreateWarehouseCollection'
|
||||
import { WarehouseMenuItem } from 'components/interfaces/DataWarehouse/WarehouseMenuItem'
|
||||
import SavedQueriesItem from 'components/interfaces/Settings/Logs/Logs.SavedQueriesItem'
|
||||
import { LogsSidebarItem } from 'components/interfaces/Settings/Logs/SidebarV2/SidebarItem'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import { useWarehouseCollectionsQuery } from 'data/analytics/warehouse-collections-query'
|
||||
import { useWarehouseTenantQuery } from 'data/analytics/warehouse-tenant-query'
|
||||
import { useContentQuery } from 'data/content/content-query'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan'
|
||||
@@ -27,14 +23,7 @@ import {
|
||||
Collapsible_Shadcn_,
|
||||
CollapsibleContent_Shadcn_,
|
||||
CollapsibleTrigger_Shadcn_,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
Separator,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from 'ui'
|
||||
import {
|
||||
InnerSideBarEmptyPanel,
|
||||
@@ -90,16 +79,11 @@ export function LogsSidebarMenuV2() {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams() as { ref: string }
|
||||
|
||||
const warehouseEnabled = useFlag('warehouse')
|
||||
const unifiedLogsFlagEnabled = useFlag('unifiedLogs')
|
||||
const { selectFeaturePreview } = useFeaturePreviewModal()
|
||||
const { enable: enableUnifiedLogs } = useUnifiedLogsPreview()
|
||||
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const [createCollectionOpen, setCreateCollectionOpen] = useState(false)
|
||||
const canCreateCollection = useCheckPermissions(PermissionAction.ANALYTICS_WRITE, 'logflare')
|
||||
|
||||
const { data: tenantData } = useWarehouseTenantQuery({ projectRef: ref })
|
||||
|
||||
const {
|
||||
projectAuthAll: authEnabled,
|
||||
@@ -107,11 +91,6 @@ export function LogsSidebarMenuV2() {
|
||||
realtimeAll: realtimeEnabled,
|
||||
} = useIsFeatureEnabled(['project_storage:all', 'project_auth:all', 'realtime:all'])
|
||||
|
||||
const { data: whCollections, isLoading: whCollectionsLoading } = useWarehouseCollectionsQuery(
|
||||
{ projectRef: ref },
|
||||
{ enabled: IS_PLATFORM && warehouseEnabled && !!tenantData }
|
||||
)
|
||||
|
||||
const { plan: orgPlan, isLoading: isOrgPlanLoading } = useCurrentOrgPlan()
|
||||
const isFreePlan = !isOrgPlanLoading && orgPlan?.id === 'free'
|
||||
|
||||
@@ -223,9 +202,6 @@ export function LogsSidebarMenuV2() {
|
||||
const filteredOperationalLogs = OPERATIONAL_COLLECTIONS.filter((collection) => {
|
||||
return collection?.name.toLowerCase().includes(searchText.toLowerCase())
|
||||
})
|
||||
const filteredWarehouse = whCollections?.filter((collection) => {
|
||||
return collection.name.toLowerCase().includes(searchText.toLowerCase())
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="pb-12 relative">
|
||||
@@ -285,56 +261,11 @@ export function LogsSidebarMenuV2() {
|
||||
></InnerSideBarFilterSearchInput>
|
||||
</InnerSideBarFilters>
|
||||
|
||||
{warehouseEnabled ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<Plus className="text-foreground" />}
|
||||
className="w-[26px]"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuItem className="gap-x-2" asChild>
|
||||
<Link href={`/project/${ref}/logs/explorer`}>
|
||||
<FilePlus size={14} />
|
||||
Create query
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuItem className="gap-x-2" asChild>
|
||||
<button
|
||||
onClick={() => setCreateCollectionOpen(true)}
|
||||
className="w-full flex items-center text-xs px-2 py-1"
|
||||
disabled={!canCreateCollection}
|
||||
>
|
||||
<Plus size={14} />
|
||||
Create collection
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
</TooltipTrigger>
|
||||
{!canCreateCollection && (
|
||||
<TooltipContent>
|
||||
You need additional permissions to create a collection
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<Button
|
||||
type="default"
|
||||
icon={<Plus className="text-foreground" />}
|
||||
className="w-[26px]"
|
||||
onClick={() => router.push(`/project/${ref}/logs/explorer`)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<CreateWarehouseCollectionModal
|
||||
open={createCollectionOpen}
|
||||
onOpenChange={setCreateCollectionOpen}
|
||||
<Button
|
||||
type="default"
|
||||
icon={<Plus className="text-foreground" />}
|
||||
className="w-[26px]"
|
||||
onClick={() => router.push(`/project/${ref}/logs/explorer`)}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-2">
|
||||
@@ -361,17 +292,6 @@ export function LogsSidebarMenuV2() {
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{whCollectionsLoading && warehouseEnabled ? (
|
||||
<div className="p-4">
|
||||
<GenericSkeletonLoader />
|
||||
</div>
|
||||
) : filteredWarehouse?.length ? (
|
||||
<div>
|
||||
{filteredWarehouse.map((collection) => (
|
||||
<WarehouseMenuItem item={collection} key={collection.token}></WarehouseMenuItem>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</SidebarCollapsible>
|
||||
{OPERATIONAL_COLLECTIONS.length > 0 && (
|
||||
<>
|
||||
|
||||
@@ -1,19 +1,4 @@
|
||||
export const analyticsKeys = {
|
||||
// warehouse
|
||||
warehouseQuery: (projectRef: string, query: string) =>
|
||||
['projects', projectRef, 'warehouse', 'query', query] as const,
|
||||
warehouseTenant: (projectRef: string) => ['projects', projectRef, 'warehouse', 'tenant'] as const,
|
||||
warehouseCollections: (projectRef: string) =>
|
||||
['projects', projectRef, 'warehouse', 'collections'] as const,
|
||||
warehouseCollectionsCreate: (projectRef: string) =>
|
||||
['projects', projectRef, 'warehouse', 'collections', 'create'] as const,
|
||||
warehouseEndpoints: (projectRef: string) =>
|
||||
['projects', projectRef, 'warehouse', 'endpoints'] as const,
|
||||
warehouseBackends: (projectRef: string) =>
|
||||
['projects', projectRef, 'warehouse', 'backends'] as const,
|
||||
warehouseAccessTokens: (projectRef: string) =>
|
||||
['projects', projectRef, 'warehouse', 'access-tokens'] as const,
|
||||
|
||||
// logs/reports endpoints
|
||||
functionsInvStats: (
|
||||
projectRef: string | undefined,
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { analyticsKeys } from './keys'
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
|
||||
export type WarehouseAccessTokensVariables = {
|
||||
projectRef: string
|
||||
}
|
||||
|
||||
export async function getWarehouseAccessTokens(
|
||||
{ projectRef }: WarehouseAccessTokensVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!projectRef) {
|
||||
throw new Error('projectRef is required')
|
||||
}
|
||||
|
||||
// TODO: Remove typecast when codegen types are fixed
|
||||
const response = await get(`/platform/projects/{ref}/analytics/warehouse/access-tokens`, {
|
||||
params: { path: { ref: projectRef } },
|
||||
signal,
|
||||
})
|
||||
|
||||
if (response.error) {
|
||||
handleError(response)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
export type WarehouseAccessTokensData = Awaited<ReturnType<typeof getWarehouseAccessTokens>>
|
||||
export type WarehouseAccessTokensError = ResponseError
|
||||
/**
|
||||
* This will be deprecated or rewritten in favor of the new project API keys
|
||||
*/
|
||||
export const useWarehouseAccessTokensQuery = <TData = WarehouseAccessTokensData>(
|
||||
{ projectRef }: WarehouseAccessTokensVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<WarehouseAccessTokensData, WarehouseAccessTokensError, TData> = {}
|
||||
) =>
|
||||
useQuery<WarehouseAccessTokensData, WarehouseAccessTokensError, TData>(
|
||||
analyticsKeys.warehouseAccessTokens(projectRef),
|
||||
({ signal }) => getWarehouseAccessTokens({ projectRef }, signal),
|
||||
{
|
||||
enabled: enabled && typeof projectRef !== 'undefined',
|
||||
...options,
|
||||
}
|
||||
)
|
||||
@@ -1,19 +0,0 @@
|
||||
import { useWarehouseQueryQuery } from './warehouse-query'
|
||||
|
||||
export function useWarehouseLogDetailQuery({
|
||||
ref,
|
||||
collectionName,
|
||||
logId,
|
||||
columns = ['id', 'timestamp'],
|
||||
}: {
|
||||
ref: string
|
||||
collectionName?: string
|
||||
logId?: string
|
||||
columns?: string[]
|
||||
}) {
|
||||
const enabled = !!logId && !!collectionName
|
||||
const sql = `select ${columns.join(',')} from \`${collectionName}\`
|
||||
where id = '${logId}' and timestamp > '2024-01-01' limit 1`
|
||||
|
||||
return useWarehouseQueryQuery({ ref, sql }, { enabled })
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { handleError, post } from 'data/fetchers'
|
||||
import { toast } from 'sonner'
|
||||
import type { ResponseError } from 'types'
|
||||
import { analyticsKeys } from './keys'
|
||||
|
||||
export type CreateCollectionArgs = {
|
||||
projectRef: string
|
||||
name: string
|
||||
retention_days: number
|
||||
}
|
||||
|
||||
export async function createCollection({ projectRef, name, retention_days }: CreateCollectionArgs) {
|
||||
const { data, error } = await post('/platform/projects/{ref}/analytics/warehouse/collections', {
|
||||
params: { path: { ref: projectRef } },
|
||||
body: { name, retention_days },
|
||||
} as any)
|
||||
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
}
|
||||
|
||||
type CreateCollectionData = Awaited<ReturnType<typeof createCollection>>
|
||||
|
||||
export const useCreateCollection = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<CreateCollectionData, ResponseError, CreateCollectionArgs>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<CreateCollectionData, ResponseError, CreateCollectionArgs>(
|
||||
(vars) => {
|
||||
// temporal fix
|
||||
// prevent users from creating collections with the same name
|
||||
// this will be handled by the backend
|
||||
// - jordi
|
||||
|
||||
const existingCollectionNames = queryClient
|
||||
.getQueryData<CreateCollectionData[]>(analyticsKeys.warehouseCollections(vars.projectRef))
|
||||
?.map((c) => c.name)
|
||||
|
||||
if (existingCollectionNames?.includes(vars.name)) {
|
||||
throw new Error(`Collection with the name ${vars.name} already exists`)
|
||||
}
|
||||
|
||||
return createCollection(vars)
|
||||
},
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
|
||||
await queryClient.invalidateQueries(analyticsKeys.warehouseCollections(projectRef))
|
||||
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to create collection: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { del, handleError } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
import { analyticsKeys } from './keys'
|
||||
|
||||
export type DeleteCollectionVariables = {
|
||||
projectRef: string
|
||||
collectionToken: string
|
||||
}
|
||||
|
||||
export async function deleteCollection({ projectRef, collectionToken }: DeleteCollectionVariables) {
|
||||
const { data, error } = await del(
|
||||
'/platform/projects/{ref}/analytics/warehouse/collections/{token}',
|
||||
{
|
||||
params: { path: { ref: projectRef, token: collectionToken } } as any,
|
||||
}
|
||||
)
|
||||
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
}
|
||||
|
||||
type DeleteCollectionData = Awaited<ReturnType<typeof deleteCollection>>
|
||||
|
||||
export const useDeleteCollectionMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<DeleteCollectionData, ResponseError, DeleteCollectionVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<DeleteCollectionData, ResponseError, DeleteCollectionVariables>(
|
||||
(vars) => deleteCollection(vars),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
|
||||
await queryClient.invalidateQueries(analyticsKeys.warehouseCollections(projectRef))
|
||||
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to delete collection: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { analyticsKeys } from './keys'
|
||||
|
||||
type WarehouseCollectionsVariables = {
|
||||
projectRef: string
|
||||
}
|
||||
|
||||
export async function getWarehouseCollections(
|
||||
{ projectRef }: WarehouseCollectionsVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!projectRef) {
|
||||
throw new Error('projectRef is required')
|
||||
}
|
||||
|
||||
// TODO: Remove type cast when codegen types are fixed
|
||||
const { data, error } = await get(`/platform/projects/{ref}/analytics/warehouse/collections`, {
|
||||
params: { path: { ref: projectRef } },
|
||||
signal,
|
||||
})
|
||||
|
||||
if (error) handleError(error)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export type WarehouseCollectionsData = Awaited<ReturnType<typeof getWarehouseCollections>>
|
||||
|
||||
export const useWarehouseCollectionsQuery = (
|
||||
{ projectRef }: WarehouseCollectionsVariables,
|
||||
{ enabled }: { enabled: boolean } = { enabled: true }
|
||||
) =>
|
||||
useQuery(
|
||||
analyticsKeys.warehouseCollections(projectRef),
|
||||
({ signal }) => getWarehouseCollections({ projectRef }, signal),
|
||||
{
|
||||
enabled: enabled && !!projectRef && IS_PLATFORM,
|
||||
}
|
||||
)
|
||||
@@ -1,75 +0,0 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { handleError, patch } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
import { analyticsKeys } from './keys'
|
||||
|
||||
export type UpdateCollectionArgs = {
|
||||
projectRef: string
|
||||
collectionToken: string
|
||||
}
|
||||
|
||||
export type UpdateCollectionPayload = {
|
||||
name?: string
|
||||
// description?: string
|
||||
retention_days?: number
|
||||
}
|
||||
|
||||
export async function updateCollection(
|
||||
{ projectRef, collectionToken }: UpdateCollectionArgs,
|
||||
payload: UpdateCollectionPayload
|
||||
) {
|
||||
const { data, error } = await patch(
|
||||
'/platform/projects/{ref}/analytics/warehouse/collections/{token}',
|
||||
{
|
||||
params: { path: { ref: projectRef, token: collectionToken } },
|
||||
body: payload,
|
||||
} as any
|
||||
)
|
||||
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
}
|
||||
|
||||
type UpdateCollectionData = Awaited<ReturnType<typeof updateCollection>>
|
||||
|
||||
export const useUpdateCollection = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<UpdateCollectionData, ResponseError, UpdateCollectionPayload>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<
|
||||
UpdateCollectionData,
|
||||
ResponseError,
|
||||
UpdateCollectionPayload & UpdateCollectionArgs
|
||||
>(
|
||||
(payload) =>
|
||||
updateCollection(
|
||||
{ projectRef: payload.projectRef, collectionToken: payload.collectionToken },
|
||||
{ name: payload.name, retention_days: payload.retention_days }
|
||||
),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
|
||||
await queryClient.invalidateQueries(analyticsKeys.warehouseCollections(projectRef))
|
||||
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to update collection: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import { analyticsKeys } from './keys'
|
||||
import { ResponseError } from 'types'
|
||||
|
||||
export type WarehouseQueryVariables = {
|
||||
ref: string
|
||||
sql: string
|
||||
}
|
||||
|
||||
export async function getWarehouseQuery(
|
||||
{ ref, sql }: WarehouseQueryVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!sql) {
|
||||
throw new Error('SQL must be provided')
|
||||
}
|
||||
|
||||
const { data, error } = await get(`/platform/projects/{ref}/analytics/warehouse/query`, {
|
||||
params: { path: { ref: ref }, query: { bq_sql: sql } },
|
||||
signal,
|
||||
} as any)
|
||||
|
||||
if (error) {
|
||||
handleError(error)
|
||||
}
|
||||
|
||||
// TODO!: Remove type assertion when we have a proper type for the response
|
||||
return data as { result: any[] }
|
||||
}
|
||||
|
||||
export type WarehouseQueryData = Awaited<ReturnType<typeof getWarehouseQuery>>
|
||||
export type WarehouseQueryError = ResponseError
|
||||
|
||||
export const useWarehouseQueryQuery = <TData = WarehouseQueryData>(
|
||||
{ ref, sql }: WarehouseQueryVariables,
|
||||
{ enabled, ...options }: UseQueryOptions<WarehouseQueryData, WarehouseQueryError, TData> = {}
|
||||
) =>
|
||||
useQuery<WarehouseQueryData, WarehouseQueryError, TData>(
|
||||
analyticsKeys.warehouseQuery(ref, sql),
|
||||
({ signal }) => getWarehouseQuery({ ref, sql }, signal),
|
||||
{
|
||||
enabled,
|
||||
staleTime: Infinity,
|
||||
cacheTime: 15 * 60 * 1000, // 15 mins cache time
|
||||
refetchOnMount: false,
|
||||
refetchInterval: false,
|
||||
...options,
|
||||
}
|
||||
)
|
||||
@@ -1,53 +0,0 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { ResponseError } from 'types'
|
||||
import { analyticsKeys } from './keys'
|
||||
|
||||
export type WarehouseTenantVariables = {
|
||||
projectRef: string
|
||||
}
|
||||
|
||||
export type WarehouseTenantResponse = any
|
||||
|
||||
export async function getWarehouseTenant(
|
||||
{ projectRef }: WarehouseTenantVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!projectRef) {
|
||||
throw new Error('projectRef is required')
|
||||
}
|
||||
const { data, error } = await get(`/platform/projects/{ref}/analytics/warehouse/tenant`, {
|
||||
params: { path: { ref: projectRef } },
|
||||
signal,
|
||||
})
|
||||
if (error) {
|
||||
handleError(error)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export type WarehouseTenantData = Awaited<ReturnType<typeof getWarehouseTenant>>
|
||||
export type WarehouseTenantError = ResponseError
|
||||
|
||||
export const useWarehouseTenantQuery = <TData = WarehouseTenantData>(
|
||||
{ projectRef }: WarehouseTenantVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<WarehouseTenantData, WarehouseTenantError, TData> = {}
|
||||
) =>
|
||||
useQuery<WarehouseTenantData, WarehouseTenantError, TData>(
|
||||
analyticsKeys.warehouseTenant(projectRef),
|
||||
({ signal }) => getWarehouseTenant({ projectRef }, signal),
|
||||
{
|
||||
enabled: enabled && !!projectRef && IS_PLATFORM,
|
||||
staleTime: Infinity,
|
||||
// 2H mins cache time
|
||||
cacheTime: 120 * 60 * 1000,
|
||||
refetchOnMount: false,
|
||||
refetchInterval: false,
|
||||
...options,
|
||||
}
|
||||
)
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useQueryState } from 'nuqs'
|
||||
|
||||
// Used in logs and warehouse.
|
||||
export function useSelectedLog() {
|
||||
return useQueryState('log')
|
||||
}
|
||||
@@ -33,9 +33,7 @@ function useSingleLog({
|
||||
|
||||
const params: LogsEndpointParams = { ...paramsToMerge, sql }
|
||||
|
||||
const isWarehouseQuery = queryType === 'warehouse'
|
||||
// Warehouse queries are handled differently
|
||||
const enabled = Boolean(id && table && !isWarehouseQuery)
|
||||
const enabled = Boolean(id && table)
|
||||
|
||||
const {
|
||||
data,
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { WarehouseCollectionDetail } from 'components/interfaces/DataWarehouse/WarehouseCollectionDetail'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import LogsLayout from 'components/layouts/LogsLayout/LogsLayout'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
|
||||
const PageLayout: NextPageWithLayout = () => <WarehouseCollectionDetail />
|
||||
|
||||
PageLayout.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<LogsLayout>{page}</LogsLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
|
||||
export default PageLayout
|
||||
@@ -22,17 +22,14 @@ import {
|
||||
maybeShowUpgradePrompt,
|
||||
useEditorHints,
|
||||
} from 'components/interfaces/Settings/Logs/Logs.utils'
|
||||
import LogsQueryPanel, { SourceType } from 'components/interfaces/Settings/Logs/LogsQueryPanel'
|
||||
import LogsQueryPanel from 'components/interfaces/Settings/Logs/LogsQueryPanel'
|
||||
import LogTable from 'components/interfaces/Settings/Logs/LogTable'
|
||||
import UpgradePrompt from 'components/interfaces/Settings/Logs/UpgradePrompt'
|
||||
import { createWarehouseQueryTemplates } from 'components/interfaces/Settings/Logs/Warehouse.utils'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import LogsLayout from 'components/layouts/LogsLayout/LogsLayout'
|
||||
import CodeEditor from 'components/ui/CodeEditor/CodeEditor'
|
||||
import LoadingOpacity from 'components/ui/LoadingOpacity'
|
||||
import ShimmerLine from 'components/ui/ShimmerLine'
|
||||
import { useWarehouseCollectionsQuery } from 'data/analytics/warehouse-collections-query'
|
||||
import { useWarehouseQueryQuery } from 'data/analytics/warehouse-query'
|
||||
import { useContentQuery } from 'data/content/content-query'
|
||||
import {
|
||||
UpsertContentPayload,
|
||||
@@ -55,8 +52,6 @@ import {
|
||||
ResizablePanelGroup,
|
||||
} from 'ui'
|
||||
|
||||
const PLACEHOLDER_WAREHOUSE_QUERY =
|
||||
'-- Fetch the last 10 logs in the last 7 days \nselect id, timestamp, event_message from `COLLECTION_NAME` \nwhere timestamp > timestamp_sub(current_timestamp(), interval 7 day) \norder by timestamp desc limit 10'
|
||||
const LOCAL_PLACEHOLDER_QUERY =
|
||||
'select\n timestamp, event_message, metadata\n from edge_logs limit 5'
|
||||
|
||||
@@ -79,13 +74,8 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
|
||||
const { timestampStart, timestampEnd, setTimeRange } = useLogsUrlState()
|
||||
|
||||
const [editorValue, setEditorValue] = useState<string>(PLACEHOLDER_QUERY)
|
||||
const [warehouseEditorId, setWarehouseEditorId] = useState<string>(uuidv4())
|
||||
const [warehouseEditorValue, setWarehouseEditorValue] = useState<string>(
|
||||
PLACEHOLDER_WAREHOUSE_QUERY
|
||||
)
|
||||
const [saveModalOpen, setSaveModalOpen] = useState<boolean>(false)
|
||||
const [warnings, setWarnings] = useState<LogsWarning[]>([])
|
||||
const [sourceType, setSourceType] = useState<SourceType>((routerSource as SourceType) || 'logs')
|
||||
const [selectedLog, setSelectedLog] = useState<any>(null)
|
||||
|
||||
const [recentLogs, setRecentLogs] = useLocalStorage<LogSqlSnippets.Content[]>(
|
||||
@@ -98,7 +88,6 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
|
||||
type: 'log_sql',
|
||||
})
|
||||
const query = content?.content.find((x) => x.id === queryId)
|
||||
|
||||
const {
|
||||
params,
|
||||
logData,
|
||||
@@ -112,24 +101,11 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
|
||||
iso_timestamp_start: timestampStart,
|
||||
iso_timestamp_end: timestampEnd,
|
||||
},
|
||||
sourceType === 'logs'
|
||||
true
|
||||
)
|
||||
|
||||
const {
|
||||
refetch: runWarehouseQuery,
|
||||
data: warehouseResults,
|
||||
isFetching: warehouseFetching,
|
||||
error: warehouseError,
|
||||
} = useWarehouseQueryQuery(
|
||||
{ ref: projectRef, sql: warehouseEditorValue },
|
||||
{
|
||||
enabled: false,
|
||||
}
|
||||
)
|
||||
const results = sourceType === 'warehouse' ? warehouseResults?.result : logData
|
||||
const isLoading = logsLoading || warehouseFetching
|
||||
|
||||
const { data: warehouseCollections } = useWarehouseCollectionsQuery({ projectRef })
|
||||
const results = logData
|
||||
const isLoading = logsLoading
|
||||
|
||||
const { mutate: upsertContent, isLoading: isUpsertingContent } = useContentUpsertMutation({
|
||||
onError: (e) => {
|
||||
@@ -187,40 +163,6 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
|
||||
const handleRun = (value?: string | React.MouseEvent<HTMLButtonElement>) => {
|
||||
const query = typeof value === 'string' ? value || editorValue : editorValue
|
||||
|
||||
if (sourceType === 'warehouse') {
|
||||
const whQuery = warehouseEditorValue
|
||||
|
||||
if (!warehouseCollections?.length) {
|
||||
toast.error('You do not have any collections yet.')
|
||||
return
|
||||
}
|
||||
|
||||
// Check that a collection name is included in the query
|
||||
const collectionNames = warehouseCollections?.map((collection) => collection.name)
|
||||
const collectionExists = collectionNames?.find((collectionName) =>
|
||||
whQuery.includes(collectionName)
|
||||
)
|
||||
|
||||
if (!collectionExists) {
|
||||
toast.error('Please specify a collection name in the query')
|
||||
return
|
||||
}
|
||||
|
||||
// Check that the user is not trying to query logs tables and warehouse collections at the same time
|
||||
const logsSources = Object.values(LOGS_TABLES)
|
||||
const logsSourceExists = logsSources.find((source) => whQuery.includes(source))
|
||||
|
||||
if (logsSourceExists) {
|
||||
return toast.error('Cannot query logs tables from current query.')
|
||||
}
|
||||
|
||||
runWarehouseQuery()
|
||||
return router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...router.query, q: query },
|
||||
})
|
||||
}
|
||||
|
||||
changeQuery(query)
|
||||
runQuery()
|
||||
router.push({
|
||||
@@ -231,30 +173,25 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
|
||||
}
|
||||
|
||||
const handleInsertSource = (source: string) => {
|
||||
if (sourceType === 'warehouse') {
|
||||
//TODO: Only one collection can be queried at a time, we need to replace the current collection from the query for the new one
|
||||
return setWarehouseEditorId(uuidv4())
|
||||
} else {
|
||||
if (editorRef.current && monaco) {
|
||||
const editorModel = editorRef.current?.getModel()
|
||||
const currentValue = editorRef.current.getValue()
|
||||
const index = currentValue.indexOf('from')
|
||||
if (editorRef.current && monaco) {
|
||||
const editorModel = editorRef.current?.getModel()
|
||||
const currentValue = editorRef.current.getValue()
|
||||
const index = currentValue.indexOf('from')
|
||||
|
||||
const updatedValue =
|
||||
index < 0
|
||||
? `${currentValue}${source}`
|
||||
: `${currentValue.substring(0, index + 4)} ${source} ${currentValue.substring(index + 5)}`
|
||||
const updatedValue =
|
||||
index < 0
|
||||
? `${currentValue}${source}`
|
||||
: `${currentValue.substring(0, index + 4)} ${source} ${currentValue.substring(index + 5)}`
|
||||
|
||||
editorRef.current.pushUndoStop()
|
||||
editorRef.current.executeEdits(`insert-identifier`, [
|
||||
{
|
||||
text: updatedValue,
|
||||
range: editorModel?.getFullModelRange() ?? new monaco.Range(1, 1, 1, 1),
|
||||
},
|
||||
])
|
||||
editorRef.current.pushUndoStop()
|
||||
editorRef.current.focus()
|
||||
}
|
||||
editorRef.current.pushUndoStop()
|
||||
editorRef.current.executeEdits(`insert-identifier`, [
|
||||
{
|
||||
text: updatedValue,
|
||||
range: editorModel?.getFullModelRange() ?? new monaco.Range(1, 1, 1, 1),
|
||||
},
|
||||
])
|
||||
editorRef.current.pushUndoStop()
|
||||
editorRef.current.focus()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,14 +253,8 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (warehouseError) toast.error(warehouseError.message)
|
||||
}, [warehouseError])
|
||||
|
||||
useEffect(() => {
|
||||
// on mount, set initial values
|
||||
if (q) {
|
||||
setEditorValue(q)
|
||||
setWarehouseEditorValue(q)
|
||||
}
|
||||
}, [q])
|
||||
|
||||
@@ -368,43 +299,18 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
|
||||
onDateChange={handleDateChange}
|
||||
onSelectSource={handleInsertSource}
|
||||
templates={TEMPLATES.filter((template) => template.mode === 'custom')}
|
||||
warehouseCollections={warehouseCollections || []}
|
||||
onSelectTemplate={onSelectTemplate}
|
||||
warehouseTemplates={createWarehouseQueryTemplates(warehouseCollections || [])}
|
||||
onSelectWarehouseTemplate={(template) => {
|
||||
setWarehouseEditorValue(template.query)
|
||||
setWarehouseEditorId(uuidv4())
|
||||
}}
|
||||
warnings={warnings}
|
||||
dataSource={sourceType}
|
||||
onDataSourceChange={(srcType) => {
|
||||
setSourceType(srcType)
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...router.query, source: srcType },
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
<ShimmerLine active={isLoading} />
|
||||
{sourceType === 'warehouse' ? (
|
||||
<CodeEditor
|
||||
id={warehouseEditorId}
|
||||
language="pgsql" // its bq sql but monaco doesn't have a language for it
|
||||
defaultValue={warehouseEditorValue}
|
||||
onInputChange={(v) => setWarehouseEditorValue(v || '')}
|
||||
actions={{ runQuery: { enabled: true, callback: handleRun } }}
|
||||
/>
|
||||
) : (
|
||||
<CodeEditor
|
||||
id={editorId}
|
||||
editorRef={editorRef}
|
||||
language="pgsql"
|
||||
defaultValue={editorValue}
|
||||
onInputChange={(v) => setEditorValue(v || '')}
|
||||
actions={{ runQuery: { enabled: true, callback: handleRun } }}
|
||||
/>
|
||||
)}
|
||||
<CodeEditor
|
||||
id={editorId}
|
||||
editorRef={editorRef}
|
||||
language="pgsql"
|
||||
defaultValue={editorValue}
|
||||
onInputChange={(v) => setEditorValue(v || '')}
|
||||
actions={{ runQuery: { enabled: true, callback: handleRun } }}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel collapsible minSize={5} className="overflow-auto">
|
||||
|
||||
@@ -13,12 +13,7 @@ test('run and clear', async () => {
|
||||
onSelectSource={() => {}}
|
||||
onSelectTemplate={() => {}}
|
||||
warnings={[]}
|
||||
warehouseCollections={[]}
|
||||
dataSource="logs"
|
||||
onDataSourceChange={() => {}}
|
||||
templates={[]}
|
||||
warehouseTemplates={[]}
|
||||
onSelectWarehouseTemplate={() => {}}
|
||||
/>
|
||||
)
|
||||
await expect(screen.findByPlaceholderText(/Search/)).rejects.toThrow()
|
||||
|
||||
Reference in new issue
Block a user