diff --git a/studio/components/interfaces/Home/ProjectUsage.tsx b/studio/components/interfaces/Home/ProjectUsage.tsx index c7b4e7bb732..89ca4e7f65f 100644 --- a/studio/components/interfaces/Home/ProjectUsage.tsx +++ b/studio/components/interfaces/Home/ProjectUsage.tsx @@ -40,7 +40,6 @@ interface Props { } const ProjectUsage: FC = ({ project }) => { - const logsUsageCodesPaths = useFlag('logsUsageCodesPaths') const [interval, setInterval] = useState('hourly') const router = useRouter() const { ref } = router.query @@ -50,20 +49,6 @@ const ProjectUsage: FC = ({ project }) => { get ) - const { data: codesData, error: codesFetchError } = useSWR>( - logsUsageCodesPaths - ? `${API_URL}/projects/${ref}/analytics/endpoints/usage.api-codes?interval=${interval}` - : null, - get - ) - - const { data: pathsData, error: _pathsFetchError }: any = useSWR>( - logsUsageCodesPaths - ? `${API_URL}/projects/${ref}/analytics/endpoints/usage.api-paths?interval=${interval}` - : null, - get - ) - const selectedInterval = CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1] const startDate = dayjs() .subtract(selectedInterval.startValue, selectedInterval.startUnit) @@ -223,70 +208,6 @@ const ProjectUsage: FC = ({ project }) => { )} - {logsUsageCodesPaths && ( -
- - - - - - - - - - - Path - Count - Avg. Latency (ms) - - } - body={ - <> - {(pathsData?.result ?? []).map((row: PathsDatum) => ( - - -

{row.method}

-

{row.path}

-
- {row.count} - {Number(row.avg_origin_time).toFixed(2)} -
- ))} - - } - /> - - - - )} ) diff --git a/studio/components/interfaces/Reports/PresetReport.tsx b/studio/components/interfaces/Reports/PresetReport.tsx new file mode 100644 index 00000000000..35bdd0bb09b --- /dev/null +++ b/studio/components/interfaces/Reports/PresetReport.tsx @@ -0,0 +1,228 @@ +import { + Button, + Collapsible, + IconChevronLeft, + IconChevronRight, + IconChevronUp, + IconRefreshCw, +} from '@supabase/ui' +import Table from 'components/to-be-cleaned/Table' +import { USAGE_COLORS } from 'components/ui/Charts/Charts.constants' +import StackedAreaChart from 'components/ui/Charts/StackedAreaChart' +import Panel from 'components/ui/Panel' +import useLogsQuery from 'hooks/analytics/useLogsQuery' +import { useState } from 'react' +import { DatePickerToFrom } from '../Settings/Logs' +import DatePickers from '../Settings/Logs/Logs.DatePickers' +import { jsonSyntaxHighlight } from '../Settings/Logs/LogsFormatters' +import { DATETIME_FORMAT, PRESET_CONFIG, REPORTS_DATEPICKER_HELPERS } from './Reports.constants' +import { Presets } from './Reports.types' + +interface Props { + preset: Presets + projectRef: string +} + +const DEFAULT_PARAMS = { + iso_timestamp_start: REPORTS_DATEPICKER_HELPERS[0].calcFrom(), + iso_timestamp_end: REPORTS_DATEPICKER_HELPERS[0].calcTo(), +} + +const PresetReport: React.FC = ({ projectRef, preset }) => { + const config = PRESET_CONFIG[preset] + const [statusCodes, statusCodesHandlers] = useLogsQuery(projectRef, { + sql: config.sql.statusCodes(), + ...DEFAULT_PARAMS, + }) + + const [requestPaths, requestPathsHandlers] = useLogsQuery(projectRef, { + sql: config.sql.requestPaths(12), + ...DEFAULT_PARAMS, + }) + + const handleRefresh = () => { + statusCodesHandlers.runQuery() + requestPathsHandlers.runQuery() + } + const handleDatepickerChange = ({ to, from }: DatePickerToFrom) => { + const newParams = { iso_timestamp_start: from || '', iso_timestamp_end: to || '' } + statusCodesHandlers.setParams((prev) => ({ ...prev, ...newParams })) + requestPathsHandlers.setParams((prev) => ({ ...prev, ...newParams })) + } + const isLoading = statusCodes.isLoading || requestPaths.isLoading + const requestPathsSums = requestPaths.logData.map((v) => v.sum) as number[] + const requestPathsSumMax = Math.max(...requestPathsSums) + const requestPathsSumMin = Math.min(...requestPathsSums) + + return ( +
+

{config.title}

+
+ + +
+ +
+ + +

API Status Codes

+ +
+
+ + +

Slow API Requests

+
+ Path + Count + Avg. Time (ms) + Total Query Time + + } + body={ + <> + {requestPaths.logData.map((row: any, index) => { + const totalQueryTimePercentage = + ((row.sum - requestPathsSumMin) / requestPathsSumMax) * 100 + return ( + + + + + + + +
+                                
+
+
+
+
+ + {row.count} + + + {Number(row.avg_origin_time).toFixed(2)} + + +
+ + + ) + })} + + } + /> + + +
+ + ) +} + +const queryParamsToObject = (params: string) => { + return Object.fromEntries(new URLSearchParams(params)) +} +// const usePresetReport = (projectRef: string, preset: Presets) => { +// const params: LogsEndpointParams = { ...paramsToMerge, project: projectRef, sql } +// const endpointUrl = `${API_URL}/projects/${projectRef}/analytics/endpoints/logs.all?${genQueryParams( +// params as any +// )}` +// const { +// data, +// error: swrError, +// isValidating, +// mutate, +// } = useSWR(endpointUrl, get, { +// revalidateOnFocus: false, +// revalidateIfStale: false, +// revalidateOnReconnect: false, +// dedupingInterval: 5000, +// }) + +// let error: null | string | object = swrError ? swrError.message : null +// return [ +// { +// logData: data?.result ? data.result[0] : undefined, +// isLoading: isValidating, +// error, +// }, +// { +// refresh: () => mutate(), +// }, +// ] +// } + +// const useWidgetQuery = (sql: string) => {} + +export default PresetReport diff --git a/studio/components/interfaces/Reports/Reports.constants.ts b/studio/components/interfaces/Reports/Reports.constants.ts new file mode 100644 index 00000000000..ac3dc225a3a --- /dev/null +++ b/studio/components/interfaces/Reports/Reports.constants.ts @@ -0,0 +1,83 @@ +import dayjs from 'dayjs' +import { DatetimeHelper } from '../Settings/Logs' +import { Presets } from './Reports.types' + +export const REPORTS_DATEPICKER_HELPERS: DatetimeHelper[] = [ + // { + // text: 'Last 1 day', + // calcFrom: () => dayjs().subtract(1, 'day').startOf('day').toISOString(), + // calcTo: () => '', + // }, + { + text: 'Last 7 days', + calcFrom: () => dayjs().subtract(7, 'day').startOf('day').toISOString(), + calcTo: () => '', + default: true, + }, + { + text: 'Last 14 days', + calcFrom: () => dayjs().subtract(7, 'day').startOf('day').toISOString(), + calcTo: () => '', + }, + { + text: 'Last 30 days', + calcFrom: () => dayjs().subtract(30, 'day').startOf('day').toISOString(), + calcTo: () => '', + }, + { + text: 'Last 90 days', + calcFrom: () => dayjs().subtract(90, 'day').startOf('day').toISOString(), + calcTo: () => '', + }, +] + +export const PRESET_CONFIG = { + [Presets.OVERVIEW]: { + title: 'Overview', + sql: { + statusCodes: () => ` + select + timestamp_trunc(timestamp, hour) as timestamp, + r.status_code as status_code, + count(status_code) as count + FROM + edge_logs + CROSS JOIN UNNEST(metadata) AS m + CROSS JOIN UNNEST(m.response) AS r + GROUP BY + timestamp, + status_code + ORDER BY + timestamp ASC`, + requestPaths: (limit: number) => ` + select + f2.path as path, + f2.search as query_params, + f2.method as method, + f3.status_code as status_code, + avg(f3.origin_time) as avg_origin_time, + sum(f3.origin_time) as sum, + --APPROX_QUANTILES(f3.origin_time, 100) as p95_array, + --APPROX_QUANTILES(f3.origin_time, 100) as p99_array, + count(timestamp) as count + FROM + edge_logs + LEFT JOIN UNNEST(metadata) AS f1 ON TRUE + LEFT JOIN UNNEST(f1.request) AS f2 ON TRUE + LEFT JOIN UNNEST(f1.response) AS f3 ON TRUE + GROUP BY + path, + query_params, + method, + status_code + ORDER BY + sum DESC, + count desc, + avg_origin_time DESC + LIMIT ${limit} +` + }, + }, +} + +export const DATETIME_FORMAT = 'MMM D, ha' diff --git a/studio/components/interfaces/Reports/Reports.types.ts b/studio/components/interfaces/Reports/Reports.types.ts new file mode 100644 index 00000000000..8d6efa7627e --- /dev/null +++ b/studio/components/interfaces/Reports/Reports.types.ts @@ -0,0 +1,3 @@ +export enum Presets { + OVERVIEW = "overview" +} \ No newline at end of file diff --git a/studio/components/layouts/ReportsLayout/ReportsLayout.tsx b/studio/components/layouts/ReportsLayout/ReportsLayout.tsx new file mode 100644 index 00000000000..886c7582901 --- /dev/null +++ b/studio/components/layouts/ReportsLayout/ReportsLayout.tsx @@ -0,0 +1,56 @@ +import { FC, ReactNode } from 'react' +import { observer } from 'mobx-react-lite' +import { useRouter } from 'next/router' +import { useStore, withAuth } from 'hooks' +import { generateSettingsMenu } from './SettingsMenu.utils' + +import BaseLayout from '..' +import ProductMenu from 'components/ui/ProductMenu' + +interface Props { + title: string + children: ReactNode +} + +const ReportsLayout: FC = ({ title, children }) => { + const { ui } = useStore() + const projectRef = ui.selectedProjectRef as string + + const router = useRouter() + const page = router.pathname.split('/')[4] || '' + const menuRoutes = [ + { + items: [ + { + name: 'Overview', + key: '', + url: `/project/${projectRef}/reports`, + items: [], + }, + ], + }, + { + items: [ + { + name: 'Dashboard', + key: 'dashboard', + url: `/project/${projectRef}/reports/dashboard`, + items: [], + }, + ], + }, + ] + return ( + } + > +
+ {children} +
+
+ ) +} + +export default withAuth(observer(ReportsLayout)) diff --git a/studio/components/ui/ProductMenu/ProductMenu.tsx b/studio/components/ui/ProductMenu/ProductMenu.tsx index c0544b03459..e59b970dbcf 100644 --- a/studio/components/ui/ProductMenu/ProductMenu.tsx +++ b/studio/components/ui/ProductMenu/ProductMenu.tsx @@ -19,10 +19,12 @@ const ProductMenu: FC = ({ page, menu }) => { - {group.title} - {group.isPreview && Not production ready} - + group.title ? ( +
+ {group.title} + {group.isPreview && Not production ready} +
+ ) : null } />
diff --git a/studio/components/ui/ProductMenu/ProductMenu.types.ts b/studio/components/ui/ProductMenu/ProductMenu.types.ts index 876dc3ffec0..4b7f66ac816 100644 --- a/studio/components/ui/ProductMenu/ProductMenu.types.ts +++ b/studio/components/ui/ProductMenu/ProductMenu.types.ts @@ -1,7 +1,7 @@ import { ReactNode } from 'react' export interface ProductMenuGroup { - title: string + title?: string isPreview?: boolean items: ProductMenuGroupItem[] } diff --git a/studio/hooks/analytics/useLogsQuery.tsx b/studio/hooks/analytics/useLogsQuery.tsx index 310a3903af6..7b102dbc2d5 100644 --- a/studio/hooks/analytics/useLogsQuery.tsx +++ b/studio/hooks/analytics/useLogsQuery.tsx @@ -26,7 +26,7 @@ const useLogsQuery = ( ): [Data, Handlers] => { const defaultHelper = getDefaultHelper(EXPLORER_DATEPICKER_HELPERS) const [params, setParams] = useState({ - sql: '', + sql: initialParams?.sql || '', project: projectRef, iso_timestamp_start: initialParams.iso_timestamp_start ? initialParams.iso_timestamp_start diff --git a/studio/pages/project/[ref]/reports/dashboard.tsx b/studio/pages/project/[ref]/reports/dashboard.tsx new file mode 100644 index 00000000000..d80310fe22d --- /dev/null +++ b/studio/pages/project/[ref]/reports/dashboard.tsx @@ -0,0 +1,91 @@ +import { useEffect, useState } from 'react' +import { observer } from 'mobx-react-lite' +import { useRouter } from 'next/router' +import { PermissionAction } from '@supabase/shared-types/out/constants' + +import { NextPageWithLayout } from 'types' +import { checkPermissions, useFlag, useStore } from 'hooks' +import { post } from 'lib/common/fetch' +import { API_URL, PROJECT_STATUS } from 'lib/constants' +import { useProjectContentStore } from 'stores/projectContentStore' +import Loading from 'components/ui/Loading' +import { ProjectLayoutWithAuth } from 'components/layouts' +import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' +import { createReport } from 'components/to-be-cleaned/Reports/Reports.utils' +import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout' + +export const DashboardReportPage: NextPageWithLayout = () => { + const [loading, setLoading] = useState(true) + const reportsOverview = useFlag('reportsOverview') + const Layout = reportsOverview ? ReportsLayout : ProjectLayoutWithAuth + + const router = useRouter() + const { ref } = router.query + + const { ui } = useStore() + const project = ui.selectedProject + + // const canCreateReport = checkPermissions(PermissionAction.CREATE, 'user_content', { + // resource: { type: 'report' }, + // }) + const contentStore = useProjectContentStore(ref) + + useEffect(() => { + if (project && project.status === PROJECT_STATUS.INACTIVE) { + post(`${API_URL}/projects/${ref}/restore`, {}) + } + }, [project]) + + async function loadReports() { + await contentStore.load() + const reports = contentStore.reports() + + if (reports.length >= 1) { + router.push(`/project/${ref}/reports/${reports[0].id}`) + } else { + setLoading(false) + } + } + + useEffect(() => { + loadReports() + }, [ref]) + + return ( + // TODO: use .getLayout when reportsOverview flag is removed + +
+ {loading ? ( + + ) : ( + { + try { + createReport({ router }) + } catch (error: any) { + ui.setNotification({ + category: 'error', + message: `Failed to create report: ${error.message}`, + }) + } + }} + > +

Create custom reports for your projects.

+

+ Get a high level overview of your network traffic, user actions, and infrastructure + health. +

+
+ )} +
+
+ ) +} + +// TODO: uncomment when reportsOverview flag is removed +// hooks do not work with next.js .getLayout +// DashboardReportPage.getLayout = (page) => {page} + +export default observer(DashboardReportPage) diff --git a/studio/pages/project/[ref]/reports/[id].tsx b/studio/pages/project/[ref]/reports/dashboard/[id].tsx similarity index 96% rename from studio/pages/project/[ref]/reports/[id].tsx rename to studio/pages/project/[ref]/reports/dashboard/[id].tsx index 1c36a62281f..0c140756ba8 100644 --- a/studio/pages/project/[ref]/reports/[id].tsx +++ b/studio/pages/project/[ref]/reports/dashboard/[id].tsx @@ -17,7 +17,7 @@ import { IconSettings, } from '@supabase/ui' -import { checkPermissions } from 'hooks' +import { checkPermissions, useFlag } from 'hooks' import { uuidv4 } from 'lib/helpers' import { METRIC_CATEGORIES, METRICS, TIME_PERIODS_REPORTS } from 'lib/constants' import { useProjectContentStore } from 'stores/projectContentStore' @@ -28,6 +28,7 @@ import ChartHandler from 'components/to-be-cleaned/Charts/ChartHandler' import DateRangePicker from 'components/to-be-cleaned/DateRangePicker' import { NextPageWithLayout } from 'types' import { PermissionAction } from '@supabase/shared-types/out/constants' +import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout' const ReactGridLayout = WidthProvider(RGL) @@ -35,21 +36,22 @@ const LAYOUT_COLUMN_COUNT = 24 const DEFAULT_CHART_COLUMN_COUNT = 12 const DEFAULT_CHART_ROW_COUNT = 4 -/* - * PageLayout is used to setup layout - as usual it will requires inject global store - */ const PageLayout: NextPageWithLayout = () => { + const reportsOverview = useFlag('reportsOverview') + const Layout = reportsOverview ? ReportsLayout : ProjectLayoutWithAuth return ( - <> + // TODO: use .getLayout when reportsOverview flag is removed +
- +
) } -PageLayout.getLayout = (page) => {page} +// TODO: uncomment when reportsOverview flag is removed +// PageLayout.getLayout = (page) => {page} export default observer(PageLayout) diff --git a/studio/pages/project/[ref]/reports/index.tsx b/studio/pages/project/[ref]/reports/index.tsx index 0a32a8ab1f4..eeb34ccab10 100644 --- a/studio/pages/project/[ref]/reports/index.tsx +++ b/studio/pages/project/[ref]/reports/index.tsx @@ -1,81 +1,35 @@ -import { useEffect, useState } from 'react' +import { useState } from 'react' import { observer } from 'mobx-react-lite' import { useRouter } from 'next/router' -import { PermissionAction } from '@supabase/shared-types/out/constants' import { NextPageWithLayout } from 'types' -import { checkPermissions, useStore } from 'hooks' -import { post } from 'lib/common/fetch' -import { API_URL, PROJECT_STATUS } from 'lib/constants' -import { useProjectContentStore } from 'stores/projectContentStore' -import Loading from 'components/ui/Loading' +import { useFlag, useStore } from 'hooks' import { ProjectLayoutWithAuth } from 'components/layouts' -import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' -import { createReport } from 'components/to-be-cleaned/Reports/Reports.utils' - -const PageLayout: NextPageWithLayout = () => { - const [loading, setLoading] = useState(true) +import PresetReport from 'components/interfaces/Reports/PresetReport' +import { Presets } from 'components/interfaces/Reports/Reports.types' +import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout' +import { DashboardReportPage } from './dashboard' +export const ReportsOverviewPage: NextPageWithLayout = () => { const router = useRouter() const { ref } = router.query + const reportsOverview = useFlag('reportsOverview') - const { ui } = useStore() - const project = ui.selectedProject - - // const canCreateReport = checkPermissions(PermissionAction.CR - const contentStore = useProjectContentStore(ref) - - useEffect(() => { - if (project && project.status === PROJECT_STATUS.INACTIVE) { - post(`${API_URL}/projects/${ref}/restore`, {}) - } - }, [project]) - - async function loadReports() { - await contentStore.load() - const reports = contentStore.reports() - - if (reports.length >= 1) { - router.push(`/project/${ref}/reports/${reports[0].id}`) - } else { - setLoading(false) - } - } - - useEffect(() => { - loadReports() - }, [ref]) + const Layout = reportsOverview ? ReportsLayout : ProjectLayoutWithAuth return ( -
- {loading ? ( - + + {reportsOverview ? ( + ) : ( - { - try { - createReport({ router }) - } catch (error: any) { - ui.setNotification({ - category: 'error', - message: `Failed to create report: ${error.message}`, - }) - } - }} - > -

Create custom reports for your projects.

-

- Get a high level overview of your network traffic, user actions, and infrastructure - health. -

-
+ )} -
+ ) } -PageLayout.getLayout = (page) => {page} +// TODO: uncomment when reportsOverview flag is removed +// hooks do not work with next.js .getLayout +// ReportsOverviewPage.getLayout = (page) => {page} -export default observer(PageLayout) +export default observer(ReportsOverviewPage) diff --git a/studio/tests/components/Reports/PresetReport.test.js b/studio/tests/components/Reports/PresetReport.test.js new file mode 100644 index 00000000000..41129f9d467 --- /dev/null +++ b/studio/tests/components/Reports/PresetReport.test.js @@ -0,0 +1,372 @@ +import dayjs from 'dayjs' +import utc from 'dayjs/plugin/utc' + +dayjs.extend(utc) + +// mock the fetch function +jest.mock('lib/common/fetch') +import { get } from 'lib/common/fetch' + +// mock mobx +jest.mock('mobx-react-lite') +import { observer } from 'mobx-react-lite' +observer.mockImplementation((v) => v) + +// mock the router +jest.mock('next/router') +import { useRouter } from 'next/router' +const defaultRouterMock = () => { + const router = jest.fn() + router.query = {} + router.push = jest.fn() + router.pathname = 'logs/path' + return router +} +useRouter.mockReturnValue(defaultRouterMock()) + +// mock monaco editor +jest.mock('@monaco-editor/react') +import Editor, { useMonaco } from '@monaco-editor/react' +Editor = jest.fn() +Editor.mockImplementation((props) => { + return ( + + ) +}) +useMonaco.mockImplementation((v) => v) + +// mock usage flags +jest.mock('components/ui/Flag/Flag') +import Flag from 'components/ui/Flag/Flag' +Flag.mockImplementation(({ children }) => <>{children}) +jest.mock('hooks') +import { useFlag } from 'hooks' +useFlag.mockReturnValue(true) + +import { SWRConfig } from 'swr' +jest.mock('components/interfaces/Reports/PresetReport') +import PresetReport from 'components/interfaces/Reports/PresetReport' +PresetReport.mockImplementation((props) => { + const Comp = jest.requireActual('components/interfaces/Reports/PresetReport').default + // wrap with SWR to reset the cache each time + return ( + new Map(), + shouldRetryOnError: false, + }} + > + + + ) +}) + +jest.mock('hooks') +import { useProjectSubscription } from 'hooks' +useProjectSubscription = jest.fn((ref) => ({ + subscription: { + tier: { + supabase_prod_id: 'tier_free', + }, + }, +})) + +import { render, fireEvent, waitFor, screen, act } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { wait } from '@testing-library/user-event/dist/utils' +import { logDataFixture } from '../../fixtures' +import { LogsTableName } from 'components/interfaces/Settings/Logs' +import { Presets } from 'components/interfaces/Reports/Reports.types' +beforeEach(() => { + // reset mocks between tests + get.mockReset() + useRouter.mockReset() + useRouter.mockReturnValue(defaultRouterMock()) +}) + +test('static elements', async () => { + render() + await screen.findByText(/Last 7 days/) + await screen.findAllByText(/Overview/) + await screen.findAllByText(/Refresh/) +}) + +test('changing date range triggers query refresh', async () => { + render() + await waitFor(() => expect(get).toBeCalled()) + get.mockReset() + const refresh = await screen.findByText(/Refresh/) + fireEvent.click(refresh) + await waitFor(() => expect(get).toBeCalled()) +}) + +// test.each([ +// { +// queryType: 'api', +// tableName: undefined, +// allLog: logDataFixture({ +// id: 'some-id', +// request: { path: 'some-path', method: 'POST' }, +// status_code: '400', +// metadata: undefined, +// }), +// singleLog: { +// id: 'some-id', +// metadata: [{ request: [{ method: 'POST' }] }], +// }, +// tableTexts: [/POST/, /some\-path/, /400/], +// selectionTexts: [/POST/], +// }, +// // TODO: add more tests for each type of ui +// ])( +// 'selection $queryType $tableName , can display log data and metadata', +// async ({ queryType, tableName, allLog, singleLog, tableTexts, selectionTexts }) => { +// get.mockImplementation((url) => { +// // counts +// if (url.includes('count')) { +// return { result: [{ count: 0 }] } +// } +// // single +// if (url.includes('where+id')) { +// return { result: [singleLog] } +// } +// // all +// return { result: [allLog] } +// }) +// render() + +// await waitFor(() => { +// expect(get).toHaveBeenCalledWith(expect.stringContaining('iso_timestamp_start')) +// expect(get).not.toHaveBeenCalledWith(expect.stringContaining('iso_timestamp_end')) +// }) +// // reset mock so that we can check for selection call +// get.mockClear() + +// for (const text of tableTexts) { +// await screen.findByText(text) +// } +// const row = await screen.findByText(tableTexts[0]) +// fireEvent.click(row) + +// await waitFor(() => { +// expect(get).toHaveBeenCalledWith(expect.stringContaining('iso_timestamp_start')) +// expect(get).not.toHaveBeenCalledWith(expect.stringContaining('iso_timestamp_end')) +// }) + +// for (const text of selectionTexts) { +// await screen.findAllByText(text) +// } +// } +// ) + +// test('Search will trigger a log refresh', async () => { +// get.mockImplementation((url) => { +// if (url.includes('something')) { +// return { +// result: [logDataFixture({ id: 'some-event-id' })], +// } +// } +// return { result: [] } +// }) +// render() + +// userEvent.type(screen.getByPlaceholderText(/Search events/), 'something{enter}') + +// await waitFor( +// () => { +// expect(get).toHaveBeenCalledWith(expect.stringContaining('something')) + +// // updates router query params +// const router = useRouter() +// expect(router.push).toHaveBeenCalledWith( +// expect.objectContaining({ +// pathname: expect.any(String), +// query: expect.objectContaining({ +// s: expect.stringContaining('something'), +// }), +// }) +// ) +// }, +// { timeout: 1500 } +// ) +// await screen.findByText(/some-event-id/) +// }) + +// test('poll count for new messages', async () => { +// get.mockImplementation((url) => { +// if (url.includes('count')) { +// return { result: [{ count: 125 }] } +// } +// return { +// result: [logDataFixture({ id: 'some-uuid123' })], +// } +// }) +// render() +// await waitFor(() => screen.queryByText(/some-uuid123/) === null) +// // should display new logs count +// await waitFor(() => screen.getByText(/125/)) + +// userEvent.click(screen.getByText(/Refresh/)) +// await waitFor(() => screen.queryByText(/125/) === null) +// await screen.findByText(/some-uuid123/) +// }) + +// test('s= query param will populate the search bar', async () => { +// const router = defaultRouterMock() +// router.query = { ...router.query, s: 'someSearch' } +// useRouter.mockReturnValue(router) +// render() +// // should populate search input with the search param +// await screen.findByDisplayValue('someSearch') +// expect(get).toHaveBeenCalledWith(expect.stringContaining('someSearch')) +// }) + +// test('te= query param will populate the timestamp to input', async () => { +// // get time 20 mins before +// const newDate = new Date() +// newDate.setMinutes(new Date().getMinutes() - 20) +// const iso = newDate.toISOString() +// const router = defaultRouterMock() +// router.query = { ...router.query, ite: iso } +// useRouter.mockReturnValue(router) +// render() + +// await waitFor(() => { +// expect(get).toHaveBeenCalledWith( +// expect.stringContaining(`iso_timestamp_end=${encodeURIComponent(iso)}`) +// ) +// }) +// userEvent.click(await screen.findByText('Custom')) +// }) +// test('ts= query param will populate the timestamp from input', async () => { +// // get time 20 mins before +// const newDate = new Date() +// newDate.setMinutes(new Date().getMinutes() - 20) +// const iso = newDate.toISOString() +// const router = defaultRouterMock() +// router.query = { ...router.query, its: iso } +// useRouter.mockReturnValue(router) +// render() + +// await waitFor(() => { +// expect(get).toHaveBeenCalledWith( +// expect.stringContaining(`iso_timestamp_start=${encodeURIComponent(iso)}`) +// ) +// }) +// userEvent.click(await screen.findByText('Custom')) +// await screen.findByText(new RegExp(newDate.getFullYear())) +// }) + +// test('load older btn will fetch older logs', async () => { +// get.mockImplementation((url) => { +// if (url.includes('count')) { +// return {} +// } +// return { +// result: [logDataFixture({ id: 'first event' })], +// } +// }) +// render() +// // should display first log but not second +// await waitFor(() => screen.getByText('first event')) +// await expect(screen.findByText('second event')).rejects.toThrow() + +// get.mockResolvedValueOnce({ +// result: [logDataFixture({ id: 'second event' })], +// }) +// // should display first and second log +// userEvent.click(await screen.findByText('Load older')) +// await screen.findByText('first event') +// await screen.findByText('second event') +// expect(get).toHaveBeenCalledWith(expect.stringContaining('timestamp_end=')) +// }) + +// test('bug: load older btn does not error out when previous page is empty', async () => { +// // bugfix for https://sentry.io/organizations/supabase/issues/2903331460/?project=5459134&referrer=slack +// get.mockImplementation((url) => { +// if (url.includes('count')) { +// return {} +// } +// return { result: [] } +// }) +// render() + +// userEvent.click(await screen.findByText('Load older')) +// // NOTE: potential race condition, since we are asserting that something DOES NOT EXIST +// // wait for 500s to make sure all ui logic is complete +// // need to wrap in act because internal react state is changing during this time. +// await act(async () => await wait(100)) + +// // clicking load older multiple times should not give error +// await waitFor(() => { +// expect(screen.queryByText(/Sorry/)).toBeNull() +// expect(screen.queryByText(/An error occured/)).toBeNull() +// expect(screen.queryByText(/undefined/)).toBeNull() +// }) +// }) + +// test('log event chart hide', async () => { +// render() +// await screen.findByText('Events') +// const toggle = await screen.findByText(/Chart/) +// userEvent.click(toggle) +// await expect(screen.findByText('Events')).rejects.toThrow() +// }) + +// test('bug: nav backwards with params change results in ui changing', async () => { +// // bugfix for https://sentry.io/organizations/supabase/issues/2903331460/?project=5459134&referrer=slack +// get.mockImplementation((url) => { +// if (url.includes('count')) { +// return {} +// } +// return { data: [] } +// }) +// const { container, rerender } = render( +// +// ) + +// await expect(screen.findByDisplayValue('simple-query')).rejects.toThrow() + +// const router = defaultRouterMock() +// router.query = { ...router.query, s: 'simple-query' } +// useRouter.mockReturnValue(router) +// rerender() + +// await screen.findByDisplayValue('simple-query') +// }) + +// test('bug: nav to explorer preserves newlines', async () => { +// render() +// const router = useRouter() +// userEvent.click(await screen.findByText(/Explore/)) +// await expect(router.push).toBeCalledWith(expect.stringContaining(encodeURIComponent('\n'))) +// }) +// test('filters alter generated query', async () => { +// render() +// userEvent.click(await screen.findByRole('button', { name: 'Status' })) +// userEvent.click(await screen.findByText(/500 error codes/)) +// userEvent.click(await screen.findByText(/200 codes/)) +// userEvent.click(await screen.findByText(/Save/)) + +// await waitFor(() => { +// expect(get).toHaveBeenCalledWith(expect.stringContaining('select')) +// expect(get).toHaveBeenCalledWith(expect.stringContaining('500')) +// expect(get).toHaveBeenCalledWith(expect.stringContaining('200')) +// expect(get).toHaveBeenCalledWith(expect.stringContaining('where')) +// expect(get).toHaveBeenCalledWith(expect.stringContaining('and')) +// }) +// }) +// test('filters accept filterOverride', async () => { +// render( +// +// ) + +// await waitFor(() => { +// expect(get).toHaveBeenCalledWith(expect.stringContaining('my.nestedkey')) +// expect(get).toHaveBeenCalledWith(expect.stringContaining('myvalue')) +// }) +// })