From f802436770547dcedbe7324ea62dcd4d1b55c088 Mon Sep 17 00:00:00 2001 From: TzeYiing Date: Wed, 1 Dec 2021 05:30:15 +0800 Subject: [PATCH] added DataGrid view for log browsing, added accompanying unit ui tests. --- studio/components/ui/Logs/LogSelection.tsx | 5 +- studio/components/ui/Logs/LogTable.tsx | 72 ++++++++++++------ .../project/[ref]/settings/logs/[type].tsx | 5 +- studio/tests/pages/projects/LogTable.test.js | 18 +++++ studio/tests/pages/projects/logs.test.js | 74 +++++++++++++++++++ 5 files changed, 145 insertions(+), 29 deletions(-) create mode 100644 studio/tests/pages/projects/LogTable.test.js create mode 100644 studio/tests/pages/projects/logs.test.js diff --git a/studio/components/ui/Logs/LogSelection.tsx b/studio/components/ui/Logs/LogSelection.tsx index 4492cbacb1e..21baa8c608f 100644 --- a/studio/components/ui/Logs/LogSelection.tsx +++ b/studio/components/ui/Logs/LogSelection.tsx @@ -21,8 +21,9 @@ const LogSelection = ({ log, onClose }: Props) => ( } > -
- +
+ {log.event_message} +
diff --git a/studio/components/ui/Logs/LogTable.tsx b/studio/components/ui/Logs/LogTable.tsx index c77b30d7c88..71d687a9e64 100644 --- a/studio/components/ui/Logs/LogTable.tsx +++ b/studio/components/ui/Logs/LogTable.tsx @@ -1,10 +1,8 @@ import React, { useEffect, useState } from 'react' -import Table from 'components/to-be-cleaned/Table' -import { Typography, SidePanel } from '@supabase/ui' +import { Typography } from '@supabase/ui' import dayjs from 'dayjs' -import { Panel } from '@supabase/ui/dist/cjs/components/Tabs/Tabs' import LogSelection from './LogSelection' - +import DataGrid from '@supabase/react-data-grid' interface Props { data: LogData[] | null | undefined } @@ -19,36 +17,62 @@ export interface LogData { metadata: Metadata } +interface Column { + key: string + name: string +} + /** * Logs table view with focus side panel */ const LogTable = ({ data }: Props) => { - const columns = ["timestamp", "event_message"] - const [focusedLog, setFocusedLog] = useState(null); + const columns = ["timestamp", "event_message"].map(v => ({ + key: v, name: v, + width: v === "timestamp" ? 240 : undefined, + resizable: true, + headerRenderer: () => { + return
{v}
+ }, + formatter: ({ row }: any) => { + let value = row[v] + if (v === 'timestamp') { + value = dayjs(Number(row["timestamp"]) / 1000).toString() + } + return

{value}

+ } + })) - if (!data) { - return null - } + const logMap = (data || []).reduce((acc: any, d) => { + acc[d.id] = d + return acc + }, {}) + useEffect(() => { + if (!data) return + if (focusedLog && !(focusedLog.id in logMap)) { + setFocusedLog(null) + } + }, [Object.keys(logMap)]) - if (data === []) { - return No data returned from query - } + if (!data) return null return (
- {c})} - body={data.map(d => - - {columns.map(c => setFocusedLog(d)}> - {c === "timestamp" && - {dayjs(d[c] / 1000).toString()}} - {c === "event_message" && {d[c]}} - )} - )} - /> +
+ setFocusedLog(data[rowIdx])} + noRowsFallback={No data returned from query} + columns={columns as any} + rowClass={r => `${r.id === focusedLog?.id ? "bg-green-800" : "cursor-pointer"}`} + rows={data} + rowKeyGetter={r => r.id} + onRowClick={r => { + const log = logMap[r.id] + setFocusedLog(logMap[r.id]) + }} + /> +
{focusedLog && ( -
+
setFocusedLog(null)} log={focusedLog} />
)} diff --git a/studio/pages/project/[ref]/settings/logs/[type].tsx b/studio/pages/project/[ref]/settings/logs/[type].tsx index 7ad605a0ca2..b94e3be193f 100644 --- a/studio/pages/project/[ref]/settings/logs/[type].tsx +++ b/studio/pages/project/[ref]/settings/logs/[type].tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react' -import { SettingsLayout } from 'components/layouts' +import { SettingsLayout } from 'components/layouts/' import { useRouter } from 'next/router' import LogPanel from 'components/ui/Logs/LogPanel' import { LOG_TYPE_LABEL_MAPPING } from 'lib/constants' @@ -14,7 +14,7 @@ import { Typography, Input } from '@supabase/ui' /** * Acts as a container component for the entire log display */ -const LogPage = () => { +export const LogPage = () => { const router = useRouter() const { ref, type } = router.query const title = LOG_TYPE_LABEL_MAPPING[type as string] @@ -28,7 +28,6 @@ const LogPage = () => { } = useSWR<{ data: LogData[], count: number, error: any }>(`${API_URL}/projects/${ref}/logs?type=${type}`, get, { revalidateOnFocus: false, }) - console.log(data) const { data: logData, error } = data || {} const filteredData = logData && searchQuery ? logData.filter(d => { diff --git a/studio/tests/pages/projects/LogTable.test.js b/studio/tests/pages/projects/LogTable.test.js new file mode 100644 index 00000000000..b515acaf67b --- /dev/null +++ b/studio/tests/pages/projects/LogTable.test.js @@ -0,0 +1,18 @@ +import LogTable from 'components/ui/Logs/LogTable' +import { render, fireEvent, waitFor, screen } from '@testing-library/react' +test('can display log data', async () => { + render() + + await waitFor(() => screen.getByText(/happened/)) + const row = screen.getByText(/happened/) + fireEvent.click(row) + await waitFor(() => screen.getByText(/my_key/)) + await waitFor(() => screen.getByText(/something_value/)) +}) diff --git a/studio/tests/pages/projects/logs.test.js b/studio/tests/pages/projects/logs.test.js new file mode 100644 index 00000000000..f6c9333f1f7 --- /dev/null +++ b/studio/tests/pages/projects/logs.test.js @@ -0,0 +1,74 @@ +// mock the fetch function +jest.mock('lib/common/fetch') +import { get } from 'lib/common/fetch' + +// mock the settings layout +jest.mock('components/layouts', () => ({ + SettingsLayout: jest.fn() + .mockImplementation(({ children }) =>
{children}
) +})) + +// 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' +useRouter.mockReturnValue({ query: { ref: "123", type: "auth" } }) + +import { LogPage } from 'pages/project/[ref]/settings/logs/[type]' +import { render, fireEvent, waitFor, screen } from '@testing-library/react' +import { SWRConfig } from 'swr' + +test('can display log data and metadata', async () => { + const data = [{ + id: "seome-uuid", + timestamp: 1621323232312, + event_message: "some event happened", + metadata: { + my_key: "something_value" + } + }] + get.mockResolvedValue({ data }) + render() + + await waitFor(() => screen.getByText(/happened/)) + const row = screen.getByText(/happened/) + fireEvent.click(row) + await waitFor(() => screen.getByText(/my_key/)) + await waitFor(() => screen.getByText(/something_value/)) +}) + +test('Refresh', async () => { + const data = [{ + id: "some-uuid", + timestamp: 1621323232312, + event_message: "some event happened", + metadata: { + my_key: "something_value" + } + }] + get.mockResolvedValue({ data }) + render( + + + + ) + + const row = screen.getByText(/happened/) + fireEvent.click(row) + await waitFor(() => screen.getByText(/my_key/)) + expect(get).toBeCalledTimes(1) + get.mockResolvedValue({ data: [] }) + + // simulate full refresh, loop because swr dedupe behaves weirdly + await waitFor(() => { + fireEvent.click(screen.getByText(/Refresh/)) + expect(get).toBeCalledTimes(2) + }) + // when log line unmounts and it was focused, should close focus panel + await waitFor(() => expect(() => screen.getByText(/my_key/)).toThrow()) + await waitFor(() => expect(() => screen.getByText(/happened/)).toThrow()) +})