From 003cfd642594a3766e965d43d20ea0836b78c078 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Thu, 11 Jul 2024 12:05:44 +0200 Subject: [PATCH] feat: warehouse explorer (#27611) * add basic feature * rename to datasource * rename elements in left sidebar * update copy in titles in left sidebar * Add optional 'enabled' parameter in useLogsQuery hook * Update warehouse query state and components * add template selector for warehouse query builder * update deprecated input compo * fix create token and console warnings * fix type errors * fix dupped import * update endpoints and add missing permissions * update queries with new api path * fix type errors * undo last commit * undo missing file * fix imports in logs page * import fixes * add settings link like in storage menu * sort imports, fix form submit * rename access tokens settings section * align dropdwn * add overflow to templates dropdown * add name as defaultvalue to udpate form * update rm collection dialog * capitalize * Update apps/studio/pages/project/[ref]/logs/explorer/index.tsx Co-authored-by: Joshen Lim * fix typo --------- Co-authored-by: Joshen Lim --- .../CreateWarehouseAccessToken.tsx | 53 +++-- .../CreateWarehouseCollection.tsx | 11 +- .../DataWarehouse/WarehouseAccessTokens.tsx | 4 +- .../DataWarehouse/WarehouseMenuItem.tsx | 35 +-- .../interfaces/Settings/Logs/LogTable.tsx | 2 +- .../interfaces/Settings/Logs/Logs.types.ts | 6 + .../Settings/Logs/LogsQueryPanel.tsx | 171 +++++++++++---- .../Settings/Logs/Warehouse.utils.ts | 18 ++ .../layouts/LogsLayout/LogsLayout.tsx | 29 ++- .../layouts/LogsLayout/LogsMenu.utils.ts | 4 +- ...warehouse-access-tokens-create-mutation.ts | 3 +- .../warehouse-access-tokens-query.ts | 1 + .../analytics/warehouse-collections-query.ts | 7 +- .../data/analytics/warehouse-tenant-query.ts | 3 +- apps/studio/hooks/analytics/useLogsQuery.tsx | 10 +- .../project/[ref]/logs/explorer/index.tsx | 207 ++++++++++++++---- .../pages/projects/LogsQueryPanel.test.tsx | 6 + package.json | 3 +- 18 files changed, 412 insertions(+), 161 deletions(-) create mode 100644 apps/studio/components/interfaces/Settings/Logs/Warehouse.utils.ts diff --git a/apps/studio/components/interfaces/DataWarehouse/CreateWarehouseAccessToken.tsx b/apps/studio/components/interfaces/DataWarehouse/CreateWarehouseAccessToken.tsx index 9688ad5a74..af20da05fd 100644 --- a/apps/studio/components/interfaces/DataWarehouse/CreateWarehouseAccessToken.tsx +++ b/apps/studio/components/interfaces/DataWarehouse/CreateWarehouseAccessToken.tsx @@ -1,18 +1,12 @@ -import { zodResolver } from '@hookform/resolvers/zod' -import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useForm } from 'react-hook-form' import { z } from 'zod' +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from 'react-hook-form' +import { PermissionAction } from '@supabase/shared-types/out/constants' import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { FormControl_Shadcn_, FormField_Shadcn_, FormItem_Shadcn_, Form_Shadcn_, Modal } from 'ui' +import { Input } from '@ui/components/shadcn/ui/input' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { - FormControl_Shadcn_, - FormField_Shadcn_, - FormItem_Shadcn_, - Form_Shadcn_, - Input_Shadcn_, - Modal, -} from 'ui' type CreateWarehouseProps = { onSubmit: (values: { description: string }) => void @@ -61,27 +55,44 @@ const CreateWarehouseAccessToken = ({ onSubmit, loading, open, setOpen }: Create visible={open} alignFooter="right" loading={loading} + onConfirm={() => { + form.handleSubmit((data) => { + onSubmit(data) + })() + + form.reset() + }} > - - -
onSubmit(data))} - > + + { + e.preventDefault() + form.handleSubmit((data) => { + onSubmit(data) + })() + + form.reset() + }} + id="create-access-token-form" + > + +

+ Enter a unique description to identify this token. +

( - + )} /> - -
-
+ + + ) diff --git a/apps/studio/components/interfaces/DataWarehouse/CreateWarehouseCollection.tsx b/apps/studio/components/interfaces/DataWarehouse/CreateWarehouseCollection.tsx index 17879fac03..224032b4b8 100644 --- a/apps/studio/components/interfaces/DataWarehouse/CreateWarehouseCollection.tsx +++ b/apps/studio/components/interfaces/DataWarehouse/CreateWarehouseCollection.tsx @@ -9,11 +9,12 @@ import { z } from 'zod' import { FormMessage } from '@ui/components/shadcn/ui/form' import { useParams } from 'common' +import { Button, FormControl_Shadcn_, FormField_Shadcn_, Form_Shadcn_, Modal } from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { Input } from '@ui/components/shadcn/ui/input' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useCreateCollection } from 'data/analytics/warehouse-collections-create-mutation' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { Button, FormControl_Shadcn_, FormField_Shadcn_, Form_Shadcn_, Input, Modal } from 'ui' -import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' export const CreateWarehouseCollectionModal = () => { const [isOpen, setIsOpen] = useState(false) @@ -24,8 +25,9 @@ export const CreateWarehouseCollectionModal = () => { const { mutate: createCollection, isLoading } = useCreateCollection({ onSuccess: (data) => { + // todo: remove typecast once api types are fixed setIsOpen(false) - router.push(`/project/${ref}/logs/collections/${data?.token}`) + router.push(`/project/${ref}/logs/collections/${(data as any).token}`) }, onError: (error) => { toast.error(error.message) @@ -74,6 +76,7 @@ export const CreateWarehouseCollectionModal = () => { > New collection + {/* */} setIsOpen(!isOpen)} @@ -96,7 +99,7 @@ export const CreateWarehouseCollectionModal = () => { render={({ field }) => ( - + )} diff --git a/apps/studio/components/interfaces/DataWarehouse/WarehouseAccessTokens.tsx b/apps/studio/components/interfaces/DataWarehouse/WarehouseAccessTokens.tsx index f8444943d1..86a96ce8b7 100644 --- a/apps/studio/components/interfaces/DataWarehouse/WarehouseAccessTokens.tsx +++ b/apps/studio/components/interfaces/DataWarehouse/WarehouseAccessTokens.tsx @@ -142,7 +142,7 @@ export const WarehouseAccessTokens = () => {
{ Description, - Created at, + Created at, Token, , ]} diff --git a/apps/studio/components/interfaces/DataWarehouse/WarehouseMenuItem.tsx b/apps/studio/components/interfaces/DataWarehouse/WarehouseMenuItem.tsx index 5db62bee25..e328a432fd 100644 --- a/apps/studio/components/interfaces/DataWarehouse/WarehouseMenuItem.tsx +++ b/apps/studio/components/interfaces/DataWarehouse/WarehouseMenuItem.tsx @@ -81,13 +81,7 @@ export const WarehouseMenuItem = ({ item }: Props) => { resolver: zodResolver(UpdateFormSchema), }) - const DeleteFormSchema = z.object({ - confirm: z.boolean(), - }) - - const deleteForm = useForm>({ - resolver: zodResolver(DeleteFormSchema), - }) + const deleteForm = useForm() return ( <> @@ -117,7 +111,7 @@ export const WarehouseMenuItem = ({ item }: Props) => { onClick={(e) => { e.stopPropagation() }} - align="end" + align="start" className="w-44 *:space-x-2" > @@ -188,7 +182,7 @@ export const WarehouseMenuItem = ({ item }: Props) => { render={({ field }) => ( - + )} @@ -227,28 +221,11 @@ export const WarehouseMenuItem = ({ item }: Props) => { deleteCollection.mutate({ projectRef, collectionToken: item.token }) })} > -
+

Are you sure you want to delete the selected collection?
This action cannot be undone.

-
- ( - - - - - - Yes, I want to delete {item.name}. - - - - )} - /> -
@@ -262,8 +239,8 @@ export const WarehouseMenuItem = ({ item }: Props) => { > Cancel -
diff --git a/apps/studio/components/interfaces/Settings/Logs/LogTable.tsx b/apps/studio/components/interfaces/Settings/Logs/LogTable.tsx index b0403f9f10..4f3e470409 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogTable.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogTable.tsx @@ -47,7 +47,6 @@ interface Props { isLoading?: boolean error?: LogQueryError | null showDownload?: boolean - // TODO: move all common params to a context to avoid prop drilling queryType?: QueryType projectRef: string params: LogSelectionProps['params'] @@ -57,6 +56,7 @@ interface Props { maxHeight?: string className?: string collectionName?: string // Used for warehouse queries + warehouseError?: string emptyState?: ReactNode showHeader?: boolean showHistogramToggle?: boolean diff --git a/apps/studio/components/interfaces/Settings/Logs/Logs.types.ts b/apps/studio/components/interfaces/Settings/Logs/Logs.types.ts index 566e7fa10e..f9c055618a 100644 --- a/apps/studio/components/interfaces/Settings/Logs/Logs.types.ts +++ b/apps/studio/components/interfaces/Settings/Logs/Logs.types.ts @@ -138,3 +138,9 @@ export interface DatetimeHelper { default?: boolean disabled?: boolean } + +export interface WarehouseCollection { + name: string + id: number + token: string +} diff --git a/apps/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx b/apps/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx index 5c7f8e9982..525392c468 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx @@ -1,11 +1,7 @@ -import * as Tooltip from '@radix-ui/react-tooltip' import Link from 'next/link' -import React, { useState } from 'react' +import React, { ReactNode, useState } from 'react' -import Table from 'components/to-be-cleaned/Table' -import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' -import { copyToClipboard } from 'lib/helpers' -import { logConstants } from 'shared-data' +import * as Tooltip from '@radix-ui/react-tooltip' import { Alert, Badge, @@ -14,7 +10,6 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, - IconBookOpen, IconCheck, IconChevronDown, IconClipboard, @@ -25,18 +20,28 @@ import { Tabs, } from 'ui' import DatePickers from './Logs.DatePickers' +import Table from 'components/to-be-cleaned/Table' +import { logConstants } from 'shared-data' +import { copyToClipboard } from 'lib/helpers' +import { BookOpen, ChevronDown } from 'lucide-react' +import { WarehouseQueryTemplate } from './Warehouse.utils' import { EXPLORER_DATEPICKER_HELPERS, LOGS_SOURCE_DESCRIPTION, LogsTableName, } from './Logs.constants' -import type { LogTemplate, LogsWarning } from './Logs.types' +import { LogTemplate, LogsWarning, WarehouseCollection } from './Logs.types' +import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' +import { useFlag } from 'hooks/ui/useFlag' import { IS_PLATFORM } from 'common' +export type SourceType = 'logs' | 'warehouse' export interface LogsQueryPanelProps { templates?: LogTemplate[] + warehouseTemplates?: WarehouseQueryTemplate[] onSelectTemplate: (template: LogTemplate) => void - onSelectSource: (source: LogsTableName) => void + onSelectWarehouseTemplate: (template: WarehouseQueryTemplate) => void + onSelectSource: (source: string) => void onClear: () => void onSave?: () => void hasEditorValue: boolean @@ -45,18 +50,35 @@ export interface LogsQueryPanelProps { defaultTo: string defaultFrom: string warnings: LogsWarning[] + warehouseCollections: WarehouseCollection[] + dataSource: SourceType + onDataSourceChange: (sourceType: SourceType) => void +} + +function DropdownMenuItemContent({ name, desc }: { name: ReactNode; desc?: string }) { + return ( +
+
{name}
+ {desc &&
{desc}
} +
+ ) } const LogsQueryPanel = ({ templates = [], + warehouseTemplates = [], onSelectTemplate, + onSelectWarehouseTemplate, onSelectSource, defaultFrom, defaultTo, onDateChange, warnings, + warehouseCollections, + dataSource, + onDataSourceChange, }: LogsQueryPanelProps) => { - const [showReference, setShowReference] = React.useState(false) + const [showReference, setShowReference] = useState(false) const { projectAuthAll: authEnabled, @@ -64,46 +86,107 @@ 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 === 'WAREHOUSE') return false return true }) .map(([, value]) => value) return ( -
+
- - - - - - {logsTableNames - .sort((a, b) => a.localeCompare(b)) - .map((source) => ( - onSelectSource(source)}> -
- {source} - - {LOGS_SOURCE_DESCRIPTION[source]} - -
-
- ))} -
-
- - {IS_PLATFORM && ( + {warehouseEnabled && ( - + + + onDataSourceChange('logs')}> + + + onDataSourceChange('warehouse')}> + + Warehouse NEW + + } + desc="Query your data warehouse collections" + /> + + + + )} + + {dataSource === 'warehouse' && ( + + + + + + {warehouseTemplates.map((template) => ( + onSelectWarehouseTemplate(template)} + > + + + ))} + {warehouseCollections.length === 0 && ( + + + + )} + + + )} + + {dataSource === 'logs' && ( + + + + + + {dataSource === 'logs' && + logsTableNames + .sort((a, b) => a.localeCompare(b)) + .map((source) => ( + onSelectSource(source)}> + + + ))} + + + )} + + {dataSource === 'logs' && IS_PLATFORM && ( + + + @@ -122,12 +205,15 @@ const LogsQueryPanel = ({ )} - + {dataSource === 'logs' && ( + + )} +
setShowReference(true)} - icon={} + icon={} + className="px-2" > Field Reference diff --git a/apps/studio/components/interfaces/Settings/Logs/Warehouse.utils.ts b/apps/studio/components/interfaces/Settings/Logs/Warehouse.utils.ts new file mode 100644 index 0000000000..a891844d51 --- /dev/null +++ b/apps/studio/components/interfaces/Settings/Logs/Warehouse.utils.ts @@ -0,0 +1,18 @@ +export type WarehouseQueryTemplate = { + query: string + name: string + description: string +} + +export function createWarehouseQueryTemplates( + collections: { name: string }[] +): WarehouseQueryTemplate[] { + return collections.map((collection) => ({ + query: + 'select id, timestamp, event_message from `' + + collection.name + + '`\nwhere timestamp > timestamp_sub(current_timestamp(), interval 7 day)\norder by timestamp desc limit 10', + name: `${collection.name}`, + description: `Select last 10 events from ${collection.name} collection`, + })) +} diff --git a/apps/studio/components/layouts/LogsLayout/LogsLayout.tsx b/apps/studio/components/layouts/LogsLayout/LogsLayout.tsx index 3b4995d9ab..e27e9bcc79 100644 --- a/apps/studio/components/layouts/LogsLayout/LogsLayout.tsx +++ b/apps/studio/components/layouts/LogsLayout/LogsLayout.tsx @@ -18,6 +18,8 @@ import { Badge, Menu } from 'ui' import { GenericSkeletonLoader } from 'ui-patterns' import ProjectLayout from '../ProjectLayout/ProjectLayout' import { generateLogsMenu } from './LogsMenu.utils' +import Link from 'next/link' +import { ArrowUpRight } from 'lucide-react' interface LogsLayoutProps { title?: string } @@ -36,7 +38,7 @@ const LogsLayout = ({ title, children }: PropsWithChildren) => const showWarehouse = useFlag('warehouse') const project = useSelectedProject() const { ref } = useParams() - const projectRef = ref || 'default' + const projectRef = ref as string const { data: tenant } = useWarehouseTenantQuery( { projectRef }, @@ -46,7 +48,7 @@ const LogsLayout = ({ title, children }: PropsWithChildren) => ) const { data: collections, isLoading: collectionsLoading } = useWarehouseCollectionsQuery( { - projectRef: !tenant ? 'undefined' : projectRef, + projectRef, }, { enabled: !!tenant } ) @@ -66,7 +68,7 @@ const LogsLayout = ({ title, children }: PropsWithChildren) => return ( ) => /> {showWarehouse && ( <> -
+
- Events + Warehouse Events New @@ -96,7 +98,7 @@ const LogsLayout = ({ title, children }: PropsWithChildren) =>
-
+
{collectionsLoading ? ( ) : ( @@ -108,6 +110,21 @@ const LogsLayout = ({ title, children }: PropsWithChildren) =>
+
+ +
+ Configuration} + /> + + +
+

Warehouse Settings

+ +
+
+ +
)} diff --git a/apps/studio/components/layouts/LogsLayout/LogsMenu.utils.ts b/apps/studio/components/layouts/LogsLayout/LogsMenu.utils.ts index c7a8399010..80d84450fb 100644 --- a/apps/studio/components/layouts/LogsLayout/LogsMenu.utils.ts +++ b/apps/studio/components/layouts/LogsLayout/LogsMenu.utils.ts @@ -17,7 +17,7 @@ export const generateLogsMenu = ( return [ { - title: 'Logs Explorer', + title: 'Explorer', items: ( [ { key: 'explorer', name: 'Query', root: true }, @@ -33,7 +33,7 @@ export const generateLogsMenu = ( })), }, { - title: 'Infrastructure', + title: 'Infrastructure Logs', items: [ { name: 'API Gateway', diff --git a/apps/studio/data/analytics/warehouse-access-tokens-create-mutation.ts b/apps/studio/data/analytics/warehouse-access-tokens-create-mutation.ts index 5e3a320736..07966b3f0e 100644 --- a/apps/studio/data/analytics/warehouse-access-tokens-create-mutation.ts +++ b/apps/studio/data/analytics/warehouse-access-tokens-create-mutation.ts @@ -12,12 +12,13 @@ async function createWarehouseAccessToken({ ref, description, }: WarehouseAccessTokenCreateVariables) { + // TODO: remove type cast when fetcher fn params are typed const { data, error } = await post(`/v1/projects/{ref}/analytics/warehouse/access-tokens`, { params: { path: { ref }, }, body: { description }, - } as any) // TODO: remove type cast when fetcher fn params are typed + } as any) if (error) handleError(error) diff --git a/apps/studio/data/analytics/warehouse-access-tokens-query.ts b/apps/studio/data/analytics/warehouse-access-tokens-query.ts index 1086610396..004531f5ac 100644 --- a/apps/studio/data/analytics/warehouse-access-tokens-query.ts +++ b/apps/studio/data/analytics/warehouse-access-tokens-query.ts @@ -15,6 +15,7 @@ export async function getWarehouseAccessTokens( throw new Error('projectRef is required') } + // TODO: Remove typecast when codegen types are fixed const response = await get(`/v1/projects/{ref}/analytics/warehouse/access-tokens`, { params: { path: { ref: projectRef } }, signal, diff --git a/apps/studio/data/analytics/warehouse-collections-query.ts b/apps/studio/data/analytics/warehouse-collections-query.ts index 282a74e14e..c78ebd5d77 100644 --- a/apps/studio/data/analytics/warehouse-collections-query.ts +++ b/apps/studio/data/analytics/warehouse-collections-query.ts @@ -10,10 +10,11 @@ export async function getWarehouseCollections( { projectRef }: WarehouseCollectionsVariables, signal?: AbortSignal ) { - if (!projectRef || projectRef === 'undefined') { + if (!projectRef) { throw new Error('projectRef is required') } + // TODO: Remove type cast when codegen types are fixed const { data, error } = await get(`/v1/projects/{ref}/analytics/warehouse/collections`, { params: { path: { ref: projectRef } }, signal, @@ -28,12 +29,12 @@ export type WarehouseCollectionsData = Awaited useQuery( analyticsKeys.warehouseCollections(projectRef), ({ signal }) => getWarehouseCollections({ projectRef }, signal), { - enabled: !!projectRef || enabled, + enabled: enabled && !!projectRef, } ) diff --git a/apps/studio/data/analytics/warehouse-tenant-query.ts b/apps/studio/data/analytics/warehouse-tenant-query.ts index 57fb3eebc5..1c983f8346 100644 --- a/apps/studio/data/analytics/warehouse-tenant-query.ts +++ b/apps/studio/data/analytics/warehouse-tenant-query.ts @@ -16,7 +16,6 @@ export async function getWarehouseTenant( if (!projectRef) { throw new Error('projectRef is required') } - const { data, error } = await get(`/v1/projects/{ref}/analytics/warehouse/tenant`, { params: { path: { ref: projectRef } }, signal, @@ -42,7 +41,7 @@ export const useWarehouseTenantQuery = ( analyticsKeys.warehouseTenant(projectRef), ({ signal }) => getWarehouseTenant({ projectRef }, signal), { - enabled: enabled && typeof projectRef !== 'undefined', + enabled: enabled && !!projectRef, staleTime: Infinity, // 2H mins cache time cacheTime: 120 * 60 * 1000, diff --git a/apps/studio/hooks/analytics/useLogsQuery.tsx b/apps/studio/hooks/analytics/useLogsQuery.tsx index 78c6ef9691..84c599c8f6 100644 --- a/apps/studio/hooks/analytics/useLogsQuery.tsx +++ b/apps/studio/hooks/analytics/useLogsQuery.tsx @@ -23,11 +23,13 @@ export interface LogsQueryHook { changeQuery: (newQuery?: string) => void runQuery: () => void setParams: Dispatch> + enabled?: boolean } const useLogsQuery = ( projectRef: string, - initialParams: Partial = {} + initialParams: Partial = {}, + enabled = true ): LogsQueryHook => { const defaultHelper = getDefaultHelper(EXPLORER_DATEPICKER_HELPERS) const [params, setParams] = useState({ @@ -41,7 +43,7 @@ const useLogsQuery = ( : defaultHelper.calcTo(), }) - const enabled = typeof projectRef !== 'undefined' && Boolean(params.sql) + const _enabled = enabled && typeof projectRef !== 'undefined' && Boolean(params.sql) const queryParams = genQueryParams(params as any) @@ -58,7 +60,7 @@ const useLogsQuery = ( signal, }), { - enabled, + enabled: _enabled, refetchOnWindowFocus: false, } ) @@ -74,7 +76,7 @@ const useLogsQuery = ( return { params, - isLoading: (enabled && isLoading) || isRefetching, + isLoading: (_enabled && isLoading) || isRefetching, logData: isResponseOk(data) && data.result ? data.result : [], error, changeQuery, diff --git a/apps/studio/pages/project/[ref]/logs/explorer/index.tsx b/apps/studio/pages/project/[ref]/logs/explorer/index.tsx index a561618b34..cbb4731a1f 100644 --- a/apps/studio/pages/project/[ref]/logs/explorer/index.tsx +++ b/apps/studio/pages/project/[ref]/logs/explorer/index.tsx @@ -4,36 +4,6 @@ import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import toast from 'react-hot-toast' -import LogTable from 'components/interfaces/Settings/Logs/LogTable' -import { - LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD, - LogsTableName, - TEMPLATES, -} from 'components/interfaces/Settings/Logs/Logs.constants' -import type { - DatePickerToFrom, - LogTemplate, - LogsWarning, -} from 'components/interfaces/Settings/Logs/Logs.types' -import { - maybeShowUpgradePrompt, - useEditorHints, -} from 'components/interfaces/Settings/Logs/Logs.utils' -import LogsQueryPanel from 'components/interfaces/Settings/Logs/LogsQueryPanel' -import UpgradePrompt from 'components/interfaces/Settings/Logs/UpgradePrompt' -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 { useContentInsertMutation } from 'data/content/content-insert-mutation' -import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' -import useLogsQuery from 'hooks/analytics/useLogsQuery' -import { useLocalStorage } from 'hooks/misc/useLocalStorage' -import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' -import { useUpgradePrompt } from 'hooks/misc/useUpgradePrompt' -import { LOCAL_STORAGE_KEYS } from 'lib/constants' -import { uuidv4 } from 'lib/helpers' -import type { LogSqlSnippets, NextPageWithLayout } from 'types' import { Button, Form, @@ -45,6 +15,42 @@ import { } from 'ui' import { IS_PLATFORM } from 'common' +import UpgradePrompt from 'components/interfaces/Settings/Logs/UpgradePrompt' +import LoadingOpacity from 'components/ui/LoadingOpacity' +import ShimmerLine from 'components/ui/ShimmerLine' +import { useContentInsertMutation } from 'data/content/content-insert-mutation' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' +import useLogsQuery from 'hooks/analytics/useLogsQuery' +import { useUpgradePrompt } from 'hooks/misc/useUpgradePrompt' +import { LOCAL_STORAGE_KEYS } from 'lib/constants' +import { uuidv4 } from 'lib/helpers' +import type { LogSqlSnippets, NextPageWithLayout } from 'types' +import { useWarehouseCollectionsQuery } from 'data/analytics/warehouse-collections-query' +import LogsQueryPanel, { SourceType } from 'components/interfaces/Settings/Logs/LogsQueryPanel' +import { createWarehouseQueryTemplates } from 'components/interfaces/Settings/Logs/Warehouse.utils' +import { + maybeShowUpgradePrompt, + useEditorHints, +} from 'components/interfaces/Settings/Logs/Logs.utils' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' +import { + DatePickerToFrom, + LogTemplate, + LogsWarning, +} from 'components/interfaces/Settings/Logs/Logs.types' +import { useWarehouseQueryQuery } from 'data/analytics/warehouse-query' +import { useLocalStorage } from '@uidotdev/usehooks' +import { + LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD, + LOGS_TABLES, + TEMPLATES, +} from 'components/interfaces/Settings/Logs/Logs.constants' +import CodeEditor from 'components/ui/CodeEditor/CodeEditor' +import LogTable from 'components/interfaces/Settings/Logs/LogTable' +import LogsLayout from 'components/layouts/LogsLayout/LogsLayout' + +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' @@ -56,24 +62,66 @@ const PLACEHOLDER_QUERY = IS_PLATFORM ? PLATFORM_PLACEHOLDER_QUERY : LOCAL_PLACE export const LogsExplorerPage: NextPageWithLayout = () => { useEditorHints() const router = useRouter() - const { ref: projectRef, q, ite, its } = useParams() + const { ref, q, ite, its } = useParams() + const projectRef = ref as string const organization = useSelectedOrganization() const [editorId, setEditorId] = useState(uuidv4()) + const [warehouseEditorId, setWarehouseEditorId] = useState(uuidv4()) const [editorValue, setEditorValue] = useState(PLACEHOLDER_QUERY) + const [warehouseEditorValue, setWarehouseEditorValue] = useState( + PLACEHOLDER_WAREHOUSE_QUERY + ) const [saveModalOpen, setSaveModalOpen] = useState(false) const [warnings, setWarnings] = useState([]) + + const routerSource = router.query.source as SourceType + const [sourceType, setSourceType] = useState(routerSource || 'logs') + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) - const { params, logData, error, isLoading, changeQuery, runQuery, setParams } = useLogsQuery( - projectRef as string, + const { + params, + logData, + error, + isLoading: logsLoading, + changeQuery, + runQuery, + setParams, + } = useLogsQuery( + projectRef, { iso_timestamp_start: its ? (its as string) : undefined, iso_timestamp_end: ite ? (ite as string) : undefined, + }, + sourceType === 'logs' + ) + + const { + refetch: runWarehouseQuery, + data: warehouseResults, + isFetching: warehouseFetching, + error: warehouseError, + } = useWarehouseQueryQuery( + { ref: projectRef, sql: warehouseEditorValue }, + { + enabled: false, } ) + + useEffect(() => { + if (warehouseError) { + toast.error(warehouseError.message) + } + }, [warehouseError]) + + const isLoading = logsLoading || warehouseFetching + const [recentLogs, setRecentLogs] = useLocalStorage( `project-content-${projectRef}-recent-log-sql`, [] ) + + const { data: warehouseCollections } = useWarehouseCollectionsQuery({ projectRef }) + const addRecentLogSqlSnippet = (snippet: Partial) => { const defaults: LogSqlSnippets.Content = { schema_version: '1', @@ -136,9 +184,49 @@ export const LogsExplorerPage: NextPageWithLayout = () => { const handleRun = (value?: string | React.MouseEvent) => { const query = typeof value === 'string' ? value || editorValue : editorValue + if (value && typeof value === 'string') { setEditorValue(value) } + + if (sourceType === 'warehouse') { + const whQuery = warehouseEditorValue + + if (!warehouseCollections?.length) { + toast.error('You do not have any collections in your warehouse 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) { + toast.error( + 'Cannot query logs tables from a warehouse query. Please remove the logs table from the query.' + ) + return + } + + runWarehouseQuery() + router.push({ + pathname: router.pathname, + query: { ...router.query, q: query }, + }) + return + } + changeQuery(query) runQuery() router.push({ @@ -151,10 +239,19 @@ export const LogsExplorerPage: NextPageWithLayout = () => { const handleClear = () => { setEditorValue('') setEditorId(uuidv4()) + setWarehouseEditorId(uuidv4()) changeQuery('') } - const handleInsertSource = (source: LogsTableName) => { + 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 + + setWarehouseEditorId(uuidv4()) + + return + } + setEditorValue((prev) => { const index = prev.indexOf('from') if (index === -1) return `${prev}${source}` @@ -214,20 +311,44 @@ export const LogsExplorerPage: NextPageWithLayout = () => { onClear={handleClear} hasEditorValue={Boolean(editorValue)} templates={TEMPLATES.filter((template) => template.mode === 'custom')} + warehouseCollections={warehouseCollections || []} onSelectTemplate={onSelectTemplate} + warehouseTemplates={createWarehouseQueryTemplates(warehouseCollections || [])} + onSelectWarehouseTemplate={(template) => { + setWarehouseEditorValue(template.query) + setWarehouseEditorId(uuidv4()) + }} onSave={handleOnSave} isLoading={isLoading} warnings={warnings} + dataSource={sourceType} + onDataSourceChange={(srcType) => { + setSourceType(srcType) + router.push({ + pathname: router.pathname, + query: { ...router.query, source: srcType }, + }) + }} /> - setEditorValue(v || '')} - onInputRun={handleRun} - /> + {sourceType === 'warehouse' ? ( + setWarehouseEditorValue(v || '')} + onInputRun={handleRun} + /> + ) : ( + setEditorValue(v || '')} + onInputRun={handleRun} + /> + )} @@ -238,9 +359,9 @@ export const LogsExplorerPage: NextPageWithLayout = () => { onSave={handleOnSave} hasEditorValue={Boolean(editorValue)} params={params} - data={logData} + data={sourceType === 'warehouse' ? warehouseResults?.result : logData} error={error} - projectRef={projectRef as string} + projectRef={projectRef} />
diff --git a/apps/studio/tests/pages/projects/LogsQueryPanel.test.tsx b/apps/studio/tests/pages/projects/LogsQueryPanel.test.tsx index 40ba4b7bb3..60c4e48994 100644 --- a/apps/studio/tests/pages/projects/LogsQueryPanel.test.tsx +++ b/apps/studio/tests/pages/projects/LogsQueryPanel.test.tsx @@ -18,6 +18,12 @@ test('run and clear', async () => { warnings={[]} onClear={mockClear} hasEditorValue + warehouseCollections={[]} + dataSource="logs" + onDataSourceChange={() => {}} + templates={[]} + warehouseTemplates={[]} + onSelectWarehouseTemplate={() => {}} /> ) await expect(screen.findByPlaceholderText(/Search/)).rejects.toThrow() diff --git a/package.json b/package.json index 7e5fb60167..023c32b5c4 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "test:playwright": "npm --prefix playwright-tests run test", "perf:kong": "ab -t 5 -c 20 -T application/json http://localhost:8000/", "perf:meta": "ab -t 5 -c 20 -T application/json http://localhost:5555/tables", - "generate:types": "supabase gen types typescript --local > ./supabase/functions/common/database-types.ts" + "generate:types": "supabase gen types typescript --local > ./supabase/functions/common/database-types.ts", + "api:codegen": "cd packages/api-types && npm run codegen" }, "devDependencies": { "eslint": "^8.57.0",