added DataGrid view for log browsing, added accompanying unit ui tests.

This commit is contained in:
TzeYiing committed 2021-12-01 05:30:15 +08:00
1 parent 23f39b7a66
commit f802436770
5 files changed
+145 -29

No files matched your search

+3 -2
View File
@@ -21,8 +21,9 @@ const LogSelection = ({ log, onClose }: Props) => (
</div>
}
>
<div className="p-4 h-full">
<Input.TextArea label="Metadata" className="font-mono" rows={25} disabled value={JSON.stringify(log.metadata, null, 2)} />
<div className="p-4 h-full flex-col">
<Typography.Text code className="block" small>{log.event_message} </Typography.Text>
<Input.TextArea label="Metadata" className="font-mono mt-4" size="tiny" rows={25} disabled value={JSON.stringify(log.metadata, null, 2)} />
</div>
+48 -24
View File
@@ -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<LogData | null>(null);
const columns = ["timestamp", "event_message"].map(v => ({
key: v, name: v,
width: v === "timestamp" ? 240 : undefined,
resizable: true,
headerRenderer: () => {
return <div className="flex items-center justify-center font-mono h-full">{v}</div>
},
formatter: ({ row }: any) => {
let value = row[v]
if (v === 'timestamp') {
value = dayjs(Number(row["timestamp"]) / 1000).toString()
}
return <p className={`block whitespace-line-wrap ${row.id === focusedLog?.id ? "font-bold" : ""}`}>{value}</p>
}
}))
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 <Typography.Text className="text-center">No data returned from query</Typography.Text>
}
if (!data) return null
return (<section className="flex flex-row gap-x-4">
<Table containerClassName="mx-auto container overflow-y h-full"
head={columns.map((c) => <Table.th key={c}>{c}</Table.th>)}
body={data.map(d =>
<Table.tr key={d.id}>
{columns.map(c => <Table.td key={c} className={`${d.id === focusedLog?.id ? "bg-green-800" : ""} `} onClick={() => setFocusedLog(d)}>
{c === "timestamp" &&
<Typography.Text className="block bg-green-800" small code>{dayjs(d[c] / 1000).toString()}</Typography.Text>}
{c === "event_message" && <Typography.Text className="block" small code>{d[c]}</Typography.Text>}
</Table.td>)}
</Table.tr>)}
/>
<div className="flex-grow w-2/3">
<DataGrid
onSelectedCellChange={({ idx, rowIdx }) => setFocusedLog(data[rowIdx])}
noRowsFallback={<Typography.Text className="text-center">No data returned from query</Typography.Text>}
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])
}}
/>
</div>
{focusedLog && (
<div className="sticky top-0">
<div className="sticky top-0 w-1/3">
<LogSelection onClose={() => setFocusedLog(null)} log={focusedLog} />
</div>
)}
@@ -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 => {
@@ -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(<LogTable data={[{
id: "seome-uuid",
timestamp: 1621323232312,
event_message: "some event happened",
metadata: {
my_key: "something_value"
}
}]} />)
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/))
})
+74
View File
@@ -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 }) => <div>{children}</div>)
}))
// 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(<LogPage />)
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(
<SWRConfig value={{ dedupingInterval: null }}>
<LogPage />
</SWRConfig>
)
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())
})