From de3409e6e6408bda25ddd4069a41ba6e54dad8e8 Mon Sep 17 00:00:00 2001 From: TzeYiing Date: Sat, 18 Dec 2021 01:56:09 +0800 Subject: [PATCH 1/8] added failing test for loading more logs --- studio/tests/pages/projects/logs.test.js | 37 +++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/studio/tests/pages/projects/logs.test.js b/studio/tests/pages/projects/logs.test.js index b6c4b1bba04..7976d40f8e3 100644 --- a/studio/tests/pages/projects/logs.test.js +++ b/studio/tests/pages/projects/logs.test.js @@ -162,4 +162,39 @@ test("where clause will trigger a log refresh", async () => { }, { timeout: 1000 }) await waitFor(() => screen.getByText(/happened/)) -}) \ No newline at end of file +}) + +test('load older btn will fetch older logs', async () => { + get + .mockResolvedValueOnce({ + data: [ + { + id: 'some-uuid', + timestamp: 1621323232312, + event_message: 'first event', + metadata: {}, + }, + ], + }) + .mockResolvedValueOnce({ + data: [ + { + id: 'some-uuid2', + timestamp: 1621323232310, + event_message: 'second event', + metadata: {}, + }, + ], + }) + render() + // should display first log but not second + await waitFor(() => screen.getByText('first event')) + expect(() => screen.getByText('second event')).toThrow() + + userEvent.click(screen.getByText('Load older')) + // should display first and second log + await waitFor(() => screen.getByText('first event')) + await waitFor(() => screen.getByText('second event')) + expect(get).toBeCalledTimes(2) +}) + From 937bb292afcf7d12191c5b53300fb4918c7208a2 Mon Sep 17 00:00:00 2001 From: TzeYiing Date: Tue, 21 Dec 2021 02:18:58 +0800 Subject: [PATCH 2/8] Adds infinite-scroll log pagination, with feature flag --- .../project/[ref]/settings/logs/[type].tsx | 94 +++++++-- studio/tests/pages/projects/logs.test.js | 190 ++++++++++-------- 2 files changed, 183 insertions(+), 101 deletions(-) diff --git a/studio/pages/project/[ref]/settings/logs/[type].tsx b/studio/pages/project/[ref]/settings/logs/[type].tsx index ea65f2bcb66..c8f137679d1 100644 --- a/studio/pages/project/[ref]/settings/logs/[type].tsx +++ b/studio/pages/project/[ref]/settings/logs/[type].tsx @@ -1,10 +1,10 @@ -import useSWR from 'swr' +import useSWR, { KeyLoader } from 'swr' import debounce from 'lodash/debounce' import { useEffect, useRef, useState } from 'react' import { NextPage } from 'next' import { useRouter } from 'next/router' import { observer } from 'mobx-react-lite' -import { Typography, IconLoader, IconAlertCircle } from '@supabase/ui' +import { Typography, IconLoader, IconAlertCircle, IconRewind, Button } from '@supabase/ui' import { withAuth } from 'hooks' import { get } from 'lib/common/fetch' @@ -18,11 +18,17 @@ import { Logs, LogTemplate, TEMPLATES, + LogData, } from 'components/interfaces/Settings/Logs' import { uuidv4 } from 'lib/helpers' +import useSWRInfinite from 'swr/infinite' +import { isUndefined } from 'lodash' +import Flag from 'components/ui/Flag/Flag' /** * Acts as a container component for the entire log display + * + * */ export const LogPage: NextPage = () => { const router = useRouter() @@ -34,27 +40,76 @@ export const LogPage: NextPage = () => { const [where, setWhere] = useState('') const [mode, setMode] = useState<'simple' | 'custom'>('simple') const [latestRefresh, setLatestRefresh] = useState(new Date().toISOString()) - + const [params, setParams] = useState({ + type: '', + search_query: '', + where: '', + timestamp_start: '', + timestamp_end: '', + }) const title = `Logs - ${LOG_TYPE_LABEL_MAPPING[type as string]}` const debouncedQueryParams = useRef(debounce(setQueryParams, 600)).current useEffect(() => { - const params = { - type: type as string, - search_query: search || '', - where: mode === 'custom' ? where || '' : '', - } - const qs = new URLSearchParams(params).toString() + const qs = genQueryParams(params) debouncedQueryParams(qs) return () => debouncedQueryParams.cancel() }, [mode, search, where, type]) + const genQueryParams = (params: { [k: string]: string }) => { + // remove keys which are empty strings, null, or undefined + for (const k in params) { + const v = params[k] + if (v === null || v === '' || isUndefined(v)) { + delete params[k] + } + } + const qs = new URLSearchParams(params).toString() + return qs + } // handle log fetching - const logUrl = `${API_URL}/projects/${ref}/logs?${queryParams}` - const { data, isValidating, mutate } = useSWR(logUrl, get, { revalidateOnFocus: false }) - const { data: logData, error } = data || {} + const getKeyLogs: KeyLoader = (_pageIndex: number, prevPageData) => { + let queryParams + // if prev page data is 100 items, could possibly have more records that are not yet fetched within this interval + if (prevPageData === null) { + // reduce interval window limit by using the timestamp of the last log + queryParams = genQueryParams(params) + } else { + const len = prevPageData.data.length + const { timestamp: tsLimit }: LogData = prevPageData.data[len - 1] + // create new key from params + queryParams = genQueryParams({ ...params, timestamp_end: String(tsLimit) }) + } - const countUrl = `${API_URL}/projects/${ref}/logs?${queryParams}&count=true&period_start=${latestRefresh}` + const logUrl = `${API_URL}/projects/${ref}/logs?${queryParams}` + return logUrl + } + const { + data = [], + isValidating, + mutate, + size, + setSize, + } = useSWRInfinite(getKeyLogs, get, { revalidateOnFocus: false }) + + // const { data, isValidating, mutate } = useSWR(logUrl, get, { revalidateOnFocus: false }) + let logData: LogData[] = [] + let error: null | string = null + + data.forEach((response: Logs) => { + if (response && response.data) { + logData = [...logData, ...response.data] + } + if (!error && response && response.error) { + error = response.error + } + }) + + const countUrl = `${API_URL}/projects/${ref}/logs?${genQueryParams({ + ...params, + count: String(true), + period_start: String(latestRefresh), + })}` const { data: countData } = useSWR(countUrl, get, { refreshInterval: 5000 }) const newCount = countData?.data?.[0]?.count ?? 0 @@ -135,6 +190,19 @@ export const LogPage: NextPage = () => { )} + {/* Footer section of log ui, appears below table */} +
+ + + +
diff --git a/studio/tests/pages/projects/logs.test.js b/studio/tests/pages/projects/logs.test.js index 7976d40f8e3..4310b329698 100644 --- a/studio/tests/pages/projects/logs.test.js +++ b/studio/tests/pages/projects/logs.test.js @@ -4,33 +4,33 @@ import { get } from 'lib/common/fetch' // mock the settings layout jest.mock('components/layouts', () => ({ - SettingsLayout: jest.fn() - .mockImplementation(({ children }) =>
{children}
) + SettingsLayout: jest.fn().mockImplementation(({ children }) =>
{children}
), })) // mock mobx jest.mock('mobx-react-lite') import { observer } from 'mobx-react-lite' -observer.mockImplementation(v => v) +observer.mockImplementation((v) => v) // mock the router jest.mock('next/router') import { useRouter } from 'next/router' -useRouter.mockReturnValue({ query: { ref: "123", type: "auth" } }) +useRouter.mockReturnValue({ query: { ref: '123', type: 'auth' } }) // mock monaco editor -jest.mock("@monaco-editor/react") -import Editor, { useMonaco } from "@monaco-editor/react" +jest.mock('@monaco-editor/react') +import Editor, { useMonaco } from '@monaco-editor/react' Editor = jest.fn() Editor.mockImplementation((props) => { return ( - + ) }) -useMonaco.mockImplementation(v => v) +useMonaco.mockImplementation((v) => v) + +jest.mock('components/ui/Flag/Flag') +import Flag from 'components/ui/Flag/Flag' +Flag.mockImplementation(({ children }) => <>{children}) import { LogPage } from 'pages/project/[ref]/settings/logs/[type]' import { render, fireEvent, waitFor, screen } from '@testing-library/react' @@ -40,14 +40,16 @@ beforeEach(() => { get.mockReset() }) 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" - } - }] + const data = [ + { + id: 'seome-uuid', + timestamp: 1621323232312, + event_message: 'some event happened', + metadata: { + my_key: 'something_value', + }, + }, + ] get.mockResolvedValue({ data }) render() @@ -59,15 +61,16 @@ test('can display log data and metadata', async () => { }) test('Refresh', async () => { - - const data = [{ - id: "some-uuid", - timestamp: 1621323232312, - event_message: "some event happened", - metadata: { - my_key: "something_value" - } - }] + const data = [ + { + id: 'some-uuid', + timestamp: 1621323232312, + event_message: 'some event happened', + metadata: { + my_key: 'something_value', + }, + }, + ] get.mockResolvedValueOnce({ data }).mockResolvedValueOnce({ data: [] }) render() @@ -82,50 +85,53 @@ test('Refresh', async () => { await waitFor(() => screen.queryByText(/happened/) === null, { timeout: 1000 }) }) - -test("Search will trigger a log refresh", async () => { +test('Search will trigger a log refresh', async () => { get.mockImplementation((url) => { - - if (url.includes("search_query") && url.includes("something")) { + if (url.includes('search_query') && url.includes('something')) { return { - data: [{ - id: "some-uuid", - timestamp: 1621323232312, - event_message: "some event happened", - metadata: {} - }] + data: [ + { + id: 'some-uuid', + timestamp: 1621323232312, + event_message: 'some event happened', + metadata: {}, + }, + ], } } return { data: [] } }) render() - userEvent.type(screen.getByPlaceholderText(/Filter/), "something") - await waitFor(() => { - expect(get).toHaveBeenCalledWith(expect.stringContaining("search_query")) - expect(get).toHaveBeenCalledWith(expect.stringContaining("something")) - }, { timeout: 1500 }) + userEvent.type(screen.getByPlaceholderText(/Filter/), 'something') + await waitFor( + () => { + expect(get).toHaveBeenCalledWith(expect.stringContaining('search_query')) + expect(get).toHaveBeenCalledWith(expect.stringContaining('something')) + }, + { timeout: 1500 } + ) await waitFor(() => screen.getByText(/happened/), { timeout: 1000 }) }) -test("poll count for new messages", async () => { +test('poll count for new messages', async () => { get.mockImplementation((url) => { - if (url.includes("count")) { + if (url.includes('count')) { return { data: [{ count: 3 }] } } return { - data: [{ - id: "some-uuid", - timestamp: 1621323232312, - event_message: "some event happened", - metadata: {} - }] + data: [ + { + id: 'some-uuid', + timestamp: 1621323232312, + event_message: 'some event happened', + metadata: {}, + }, + ], } }) - render( - - ) + render() await waitFor(() => screen.queryByText(/happened/) === null) await waitFor(() => screen.getByText(/Load new logs/)) @@ -134,39 +140,45 @@ test("poll count for new messages", async () => { await waitFor(() => screen.getByText(/happened/)) }) - -test("where clause will trigger a log refresh", async () => { +test('where clause will trigger a log refresh', async () => { get.mockImplementation((url) => { - - if (url.includes("where") && url.includes("something")) { + if (url.includes('where') && url.includes('something')) { return { - data: [{ - id: "some-uuid", - timestamp: 1621323232312, - event_message: "some event happened", - metadata: {} - }] + data: [ + { + id: 'some-uuid', + timestamp: 1621323232312, + event_message: 'some event happened', + metadata: {}, + }, + ], } } return { data: [] } }) const { container } = render() - let editor = container.querySelector('.monaco-editor'); + let editor = container.querySelector('.monaco-editor') expect(editor).toBeFalsy() - userEvent.click(screen.getByText("Custom query")) - editor = container.querySelector('.monaco-editor'); - userEvent.type(editor, "metadata.field = something") - await waitFor(() => { - expect(get).toHaveBeenCalledWith(expect.stringContaining("where")) - expect(get).toHaveBeenCalledWith(expect.stringContaining("metadata.field")) - }, { timeout: 1000 }) + userEvent.click(screen.getByText('Custom query')) + editor = container.querySelector('.monaco-editor') + userEvent.type(editor, 'metadata.field = something') + await waitFor( + () => { + expect(get).toHaveBeenCalledWith(expect.stringContaining('where')) + expect(get).toHaveBeenCalledWith(expect.stringContaining('metadata.field')) + }, + { timeout: 1000 } + ) await waitFor(() => screen.getByText(/happened/)) }) test('load older btn will fetch older logs', async () => { - get - .mockResolvedValueOnce({ + get.mockImplementation((url) => { + if (url.includes('count')) { + return {} + } + return { data: [ { id: 'some-uuid', @@ -175,26 +187,28 @@ test('load older btn will fetch older logs', async () => { metadata: {}, }, ], - }) - .mockResolvedValueOnce({ - data: [ - { - id: 'some-uuid2', - timestamp: 1621323232310, - event_message: 'second event', - metadata: {}, - }, - ], - }) + } + }) render() // should display first log but not second await waitFor(() => screen.getByText('first event')) expect(() => screen.getByText('second event')).toThrow() - userEvent.click(screen.getByText('Load older')) + get.mockResolvedValueOnce({ + data: [ + { + id: 'some-uuid2', + timestamp: 1621323232310, + event_message: 'second event', + metadata: {}, + }, + ], + }) // should display first and second log + userEvent.click(screen.getByText('Load older')) await waitFor(() => screen.getByText('first event')) + await waitFor(() => { + expect(get).toHaveBeenCalledWith(expect.stringContaining('timestamp_end=1')) + }) await waitFor(() => screen.getByText('second event')) - expect(get).toBeCalledTimes(2) }) - From 5cb73cdffe49edc9edd259544da6169167bcc5d4 Mon Sep 17 00:00:00 2001 From: TzeYiing Date: Tue, 21 Dec 2021 02:33:33 +0800 Subject: [PATCH 3/8] adds in debouncing for params changing --- .../project/[ref]/settings/logs/[type].tsx | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/studio/pages/project/[ref]/settings/logs/[type].tsx b/studio/pages/project/[ref]/settings/logs/[type].tsx index c8f137679d1..5eed494c18e 100644 --- a/studio/pages/project/[ref]/settings/logs/[type].tsx +++ b/studio/pages/project/[ref]/settings/logs/[type].tsx @@ -36,8 +36,6 @@ export const LogPage: NextPage = () => { const [editorId, setEditorId] = useState(uuidv4()) const [search, setSearch] = useState('') - const [queryParams, setQueryParams] = useState('') - const [where, setWhere] = useState('') const [mode, setMode] = useState<'simple' | 'custom'>('simple') const [latestRefresh, setLatestRefresh] = useState(new Date().toISOString()) const [params, setParams] = useState({ @@ -48,13 +46,12 @@ export const LogPage: NextPage = () => { timestamp_end: '', }) const title = `Logs - ${LOG_TYPE_LABEL_MAPPING[type as string]}` - const debouncedQueryParams = useRef(debounce(setQueryParams, 600)).current + const debouncedSetParams = useRef(debounce(setParams, 600)).current useEffect(() => { - const qs = genQueryParams(params) - debouncedQueryParams(qs) - return () => debouncedQueryParams.cancel() - }, [mode, search, where, type]) + debouncedSetParams((prev) => ({ ...prev, type: type as string })) + return () => debouncedSetParams.cancel() + }, [type]) const genQueryParams = (params: { [k: string]: string }) => { // remove keys which are empty strings, null, or undefined @@ -114,8 +111,7 @@ export const LogPage: NextPage = () => { const newCount = countData?.data?.[0]?.count ?? 0 const handleReset = () => { - setWhere('') - setSearch('') + setParams((prev) => ({ ...prev, search_query: '', where: '' })) setEditorId(uuidv4()) } @@ -136,14 +132,15 @@ export const LogPage: NextPage = () => { const onSelectTemplate = (template: LogTemplate) => { setMode(template.mode) if (template.mode === 'simple') { - setSearch(template.searchString) - setWhere('') + setParams((prev) => ({ ...prev, search_query: template.searchString, where: '' })) } else { - setWhere(template.searchString) - setSearch('') + setParams((prev) => ({ ...prev, where: template.searchString, search_query: '' })) setEditorId(uuidv4()) } } + const handleEditorChange = (v: string | undefined) => { + debouncedSetParams((prev) => ({ ...prev, where: v || '' })) + } return ( @@ -166,8 +163,8 @@ export const LogPage: NextPage = () => { setWhere(v || '')} + defaultValue={params['where']} + onInputChange={handleEditorChange} onInputRun={handleRefresh} /> From 7bdf4b2e4927a6988a0d97fcd1c146b2e7186768 Mon Sep 17 00:00:00 2001 From: TzeYiing Date: Wed, 22 Dec 2021 19:57:41 +0800 Subject: [PATCH 4/8] Remove onChange searching for search input and query editor. Fixed failing fetching tests --- .../interfaces/Settings/Logs/LogPanel.tsx | 66 ++++++++++++++----- .../project/[ref]/settings/logs/[type].tsx | 58 ++++++++-------- studio/tests/pages/projects/LogPanel.test.js | 37 +++++------ studio/tests/pages/projects/logs.test.js | 28 ++++++-- 4 files changed, 123 insertions(+), 66 deletions(-) diff --git a/studio/components/interfaces/Settings/Logs/LogPanel.tsx b/studio/components/interfaces/Settings/Logs/LogPanel.tsx index 7d0f6d0abc4..a5e2aaa11be 100644 --- a/studio/components/interfaces/Settings/Logs/LogPanel.tsx +++ b/studio/components/interfaces/Settings/Logs/LogPanel.tsx @@ -1,4 +1,4 @@ -import { FC } from 'react' +import { FC, useEffect, useState } from 'react' import { Button, Input, @@ -13,13 +13,11 @@ import { import { LogTemplate } from '.' interface Props { - searchValue?: string + defaultSearchValue?: string templates?: any isLoading: boolean isCustomQuery: boolean newCount: number - showReset: boolean - onReset: () => void onRefresh?: () => void onSearch?: (query: string) => void onCustomClick?: () => void @@ -30,18 +28,25 @@ interface Props { * Logs control panel header + wrapper */ const LogPanel: FC = ({ - searchValue, templates = [], isLoading, isCustomQuery, newCount, - showReset, - onReset, onRefresh, onSearch = () => {}, + defaultSearchValue = '', onCustomClick, onSelectTemplate, }) => { + const [search, setSearch] = useState('') + + // sync local state with provided default value + useEffect(() => { + if (search !== defaultSearchValue) { + setSearch(defaultSearchValue) + } + }, [defaultSearchValue]) + return (
@@ -94,16 +99,43 @@ const LogPanel: FC = ({
- } - placeholder="Search event messages" - onChange={(e) => onSearch(e.target.value)} - value={searchValue} - actions={ - showReset && - } - /> + {/* wrap with form so that if user presses enter, the search value will submit automatically */} +
diff --git a/studio/pages/project/[ref]/settings/logs/[type].tsx b/studio/pages/project/[ref]/settings/logs/[type].tsx index 5eed494c18e..911951523c0 100644 --- a/studio/pages/project/[ref]/settings/logs/[type].tsx +++ b/studio/pages/project/[ref]/settings/logs/[type].tsx @@ -1,6 +1,5 @@ import useSWR, { KeyLoader } from 'swr' -import debounce from 'lodash/debounce' -import { useEffect, useRef, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { NextPage } from 'next' import { useRouter } from 'next/router' import { observer } from 'mobx-react-lite' @@ -35,7 +34,7 @@ export const LogPage: NextPage = () => { const { ref, type } = router.query const [editorId, setEditorId] = useState(uuidv4()) - const [search, setSearch] = useState('') + const [editorValue, setEditorValue] = useState('') const [mode, setMode] = useState<'simple' | 'custom'>('simple') const [latestRefresh, setLatestRefresh] = useState(new Date().toISOString()) const [params, setParams] = useState({ @@ -46,11 +45,9 @@ export const LogPage: NextPage = () => { timestamp_end: '', }) const title = `Logs - ${LOG_TYPE_LABEL_MAPPING[type as string]}` - const debouncedSetParams = useRef(debounce(setParams, 600)).current useEffect(() => { - debouncedSetParams((prev) => ({ ...prev, type: type as string })) - return () => debouncedSetParams.cancel() + setParams({ ...params, type: type as string }) }, [type]) const genQueryParams = (params: { [k: string]: string }) => { @@ -110,11 +107,6 @@ export const LogPage: NextPage = () => { const { data: countData } = useSWR(countUrl, get, { refreshInterval: 5000 }) const newCount = countData?.data?.[0]?.count ?? 0 - const handleReset = () => { - setParams((prev) => ({ ...prev, search_query: '', where: '' })) - setEditorId(uuidv4()) - } - const handleRefresh = () => { setLatestRefresh(new Date().toISOString()) mutate() @@ -134,12 +126,16 @@ export const LogPage: NextPage = () => { if (template.mode === 'simple') { setParams((prev) => ({ ...prev, search_query: template.searchString, where: '' })) } else { + setEditorValue(template.searchString) setParams((prev) => ({ ...prev, where: template.searchString, search_query: '' })) setEditorId(uuidv4()) } } - const handleEditorChange = (v: string | undefined) => { - debouncedSetParams((prev) => ({ ...prev, where: v || '' })) + const handleEditorSubmit = () => { + setParams((prev) => ({ ...prev, where: editorValue })) + } + const handleSearch = (v: string) => { + setParams((prev) => ({ ...prev, search_query: v || '' })) } return ( @@ -149,25 +145,35 @@ export const LogPage: NextPage = () => { isCustomQuery={mode === 'custom'} isLoading={isValidating} newCount={newCount} - showReset={search.length > 0} - searchValue={search} templates={TEMPLATES} - onReset={handleReset} onRefresh={handleRefresh} - onSearch={setSearch} + onSearch={handleSearch} + defaultSearchValue={params.search_query} onCustomClick={handleModeToggle} onSelectTemplate={onSelectTemplate} /> {mode === 'custom' && ( -
- -
+ +
+ setEditorValue(v || '')} + onInputRun={handleRefresh} + /> +
+ {editorValue && ( + + )} + +
+
+
)}
{isValidating && ( diff --git a/studio/tests/pages/projects/LogPanel.test.js b/studio/tests/pages/projects/LogPanel.test.js index 89b629e7043..35afe931a27 100644 --- a/studio/tests/pages/projects/LogPanel.test.js +++ b/studio/tests/pages/projects/LogPanel.test.js @@ -4,12 +4,9 @@ import userEvent from '@testing-library/user-event' test('templates', async () => { const mockFn = jest.fn() - render() + render() const search = screen.getByPlaceholderText(/Search/) - userEvent.type(search, "12345") - + userEvent.type(search, '12345') // TODO templates dropdown interaction currently cannot be tested // https://github.com/supabase/ui/issues/299 @@ -21,21 +18,22 @@ test('templates', async () => { // expect(mockFn).toBeCalled() }) -test("filter input change", async () => { +test('filter input change and submit', async () => { const mockFn = jest.fn() render() const search = screen.getByPlaceholderText(/Search/) - userEvent.type(search, "12345") + userEvent.type(search, '12345') + expect(mockFn).not.toBeCalled() + userEvent.click(screen.getByText('Go')) expect(mockFn).toBeCalled() }) -test("filter input value", async () => { - render() - screen.getByDisplayValue("1234") +test('filter input value', async () => { + render() + screen.getByDisplayValue('1234') }) - -test("Manual refresh", async () => { +test('Manual refresh', async () => { const mockFn = jest.fn() render() let btn @@ -46,12 +44,13 @@ test("Manual refresh", async () => { expect(mockFn).toBeCalled() }) -test("reset filters", () => { - const mockFn = jest.fn() - const { rerender } = render() - expect(() => screen.getByText(/Clear search/)).toThrow() +test('reset search filter', async () => { + const { rerender } = render() + expect(() => screen.getByTitle(/Clear search/)).toThrow() - rerender() + rerender() + await waitFor(() => screen.getByDisplayValue(/something123/)) userEvent.click(screen.getByTitle(/Clear search/)) - expect(mockFn).toBeCalled() -}) \ No newline at end of file + expect(() => screen.getByTitle(/Clear search/)).toThrow() + expect(() => screen.getByDisplayValue(/something123/)).toThrow() +}) diff --git a/studio/tests/pages/projects/logs.test.js b/studio/tests/pages/projects/logs.test.js index 56ef5ca90f4..5d963b3df15 100644 --- a/studio/tests/pages/projects/logs.test.js +++ b/studio/tests/pages/projects/logs.test.js @@ -32,12 +32,25 @@ jest.mock('components/ui/Flag/Flag') import Flag from 'components/ui/Flag/Flag' Flag.mockImplementation(({ children }) => <>{children}) +import { SWRConfig } from 'swr' +jest.mock('pages/project/[ref]/settings/logs/[type]') import { LogPage } from 'pages/project/[ref]/settings/logs/[type]' +LogPage.mockImplementation((props) => { + const Page = jest.requireActual('pages/project/[ref]/settings/logs/[type]').LogPage + // wrap with SWR to reset the cache each time + return ( + new Map() }}> + + + ) +}) + import { render, fireEvent, waitFor, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import {getToggleByText} from "../../helpers" +import { getToggleByText } from '../../helpers' beforeEach(() => { + // reset mocks between tests get.mockReset() }) test('can display log data and metadata', async () => { @@ -61,7 +74,7 @@ test('can display log data and metadata', async () => { await waitFor(() => screen.getByText(/something_value/)) }) -test('Refresh', async () => { +test('Refreshpage', async () => { const data = [ { id: 'some-uuid', @@ -72,9 +85,13 @@ test('Refresh', async () => { }, }, ] - get.mockResolvedValueOnce({ data }).mockResolvedValueOnce({ data: [] }) + get.mockImplementation((url) => { + if (url.includes('count')) return { count: 0 } + return { data } + }) render() - + await waitFor(() => screen.getByText(/happened/)) + get.mockResolvedValueOnce({ data: [] }) const row = screen.getByText(/happened/) fireEvent.click(row) await waitFor(() => screen.getByText(/my_key/)) @@ -105,6 +122,8 @@ test('Search will trigger a log refresh', async () => { render() userEvent.type(screen.getByPlaceholderText(/Search/), 'something') + userEvent.click(screen.getByText('Go')) + await waitFor( () => { expect(get).toHaveBeenCalledWith(expect.stringContaining('search_query')) @@ -171,6 +190,7 @@ test('where clause will trigger a log refresh', async () => { }) editor = container.querySelector('.monaco-editor') userEvent.type(editor, 'metadata.field = something') + userEvent.click(screen.getByText('Run')) await waitFor( () => { expect(get).toHaveBeenCalledWith(expect.stringContaining('where')) From 02ad7dba2f39e3d7a986fc7ebe5bd3daf3d46838 Mon Sep 17 00:00:00 2001 From: TzeYiing Date: Tue, 28 Dec 2021 02:47:33 +0800 Subject: [PATCH 5/8] added log deduping and memoization --- .../interfaces/Settings/Logs/LogTable.tsx | 17 +++++++++++------ studio/tests/pages/projects/LogTable.test.js | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/studio/components/interfaces/Settings/Logs/LogTable.tsx b/studio/components/interfaces/Settings/Logs/LogTable.tsx index fb02efb0c98..9709dc6965d 100644 --- a/studio/components/interfaces/Settings/Logs/LogTable.tsx +++ b/studio/components/interfaces/Settings/Logs/LogTable.tsx @@ -1,5 +1,5 @@ import dayjs from 'dayjs' -import { useEffect, useState } from 'react' +import { useEffect, useState, useMemo } from 'react' import { Typography } from '@supabase/ui' import DataGrid from '@supabase/react-data-grid' @@ -43,10 +43,12 @@ const LogTable = ({ isCustomQuery, data }: Props) => { }, })) - const logMap = (data || []).reduce((acc: any, d) => { - acc[d.id] = d - return acc - }, {}) + const logMap: { [id: string]: LogData } = useMemo(() => { + return (data || []).reduce((acc: any, d) => { + acc[d.id] = d + return acc + }, {}) + }, [JSON.stringify(data)]) useEffect(() => { if (!data) return @@ -60,6 +62,9 @@ const LogTable = ({ isCustomQuery, data }: Props) => { // [Joshen] Hmm quite hacky now, but will do const maxHeight = isCustomQuery ? 'calc(100vh - 42px - 10rem)' : 'calc(100vh - 42px - 3rem)' + const logDataRows = useMemo(() => { + return Object.values(logMap).sort((a, b) => a.timestamp - b.timestamp) + }, [JSON.stringify(Object.keys(logMap))]) return (
{ } columns={columns as any} rowClass={(r) => `${r.id === focusedLog?.id ? 'bg-green-800' : 'cursor-pointer'}`} - rows={data} + rows={logDataRows} rowKeyGetter={(r) => r.id} onRowClick={(r) => setFocusedLog(logMap[r.id])} /> diff --git a/studio/tests/pages/projects/LogTable.test.js b/studio/tests/pages/projects/LogTable.test.js index 51e2b439968..0bdcefa489d 100644 --- a/studio/tests/pages/projects/LogTable.test.js +++ b/studio/tests/pages/projects/LogTable.test.js @@ -16,3 +16,18 @@ test('can display log data', async () => { await waitFor(() => screen.getByText(/my_key/)) await waitFor(() => screen.getByText(/something_value/)) }) + +test ("dedupes log lines with exact id", async ()=>{ + render() + + // should only have one element, this line will fail if there are >1 element + await waitFor(() => screen.getByText(/happened/)) +}) \ No newline at end of file From eb4c2e0f21f34aaa353d1cef48d61523829d5ed5 Mon Sep 17 00:00:00 2001 From: TzeYiing Date: Tue, 28 Dec 2021 02:47:56 +0800 Subject: [PATCH 6/8] adjusted button action size and placeholder message --- studio/components/interfaces/Settings/Logs/LogPanel.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/studio/components/interfaces/Settings/Logs/LogPanel.tsx b/studio/components/interfaces/Settings/Logs/LogPanel.tsx index a5e2aaa11be..ed80630b45e 100644 --- a/studio/components/interfaces/Settings/Logs/LogPanel.tsx +++ b/studio/components/interfaces/Settings/Logs/LogPanel.tsx @@ -111,7 +111,7 @@ const LogPanel: FC = ({ } - placeholder="Search event messages" + placeholder="Search events" onChange={(e) => setSearch(e.target.value)} value={search} actions={[ @@ -126,7 +126,7 @@ const LogPanel: FC = ({ ), - )} -
+
+ {editorValue && ( + -
+ )} + )} From e95674a77eef5a301b855b0d6fa44ec0eefa3e95 Mon Sep 17 00:00:00 2001 From: TzeYiing Date: Tue, 28 Dec 2021 02:50:39 +0800 Subject: [PATCH 8/8] added page size reset when refreshing --- studio/pages/project/[ref]/settings/logs/[type].tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/studio/pages/project/[ref]/settings/logs/[type].tsx b/studio/pages/project/[ref]/settings/logs/[type].tsx index 7198aa5d6de..75c7af9cfd2 100644 --- a/studio/pages/project/[ref]/settings/logs/[type].tsx +++ b/studio/pages/project/[ref]/settings/logs/[type].tsx @@ -109,6 +109,7 @@ export const LogPage: NextPage = () => { const handleRefresh = () => { setLatestRefresh(new Date().toISOString()) + setSize(1) mutate() }