Implemented from-to inputs

This commit is contained in:
TzeYiing authored and Ziinc committed 2022-03-23 14:05:22 +01:00
1 parent b688f281f2
commit 751afd4680
6 files changed
+141 -54

No files matched your search

@@ -17,6 +17,7 @@ import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
interface Props {
defaultSearchValue?: string
defaultFromValue?: string
defaultToValue?: string
templates?: any
isLoading: boolean
@@ -43,6 +44,7 @@ const LogPanel: FC<Props> = ({
onRefresh,
onSearch = () => {},
defaultSearchValue = '',
defaultFromValue = '',
defaultToValue = '',
onCustomClick,
onSelectTemplate,
@@ -51,6 +53,7 @@ const LogPanel: FC<Props> = ({
}) => {
const [search, setSearch] = useState('')
const [to, setTo] = useState({ value: '', error: '' })
const [from, setFrom] = useState({ value: '', error: '' })
const [defaultTimestamp, setDefaultTimestamp] = useState(dayjs().utc().toISOString())
// Sync local state with provided default value
@@ -64,9 +67,12 @@ const LogPanel: FC<Props> = ({
if (to.value !== defaultToValue) {
setTo({ value: defaultToValue, error: '' })
}
}, [defaultToValue])
if (from.value !== defaultFromValue) {
setFrom({ value: defaultFromValue, error: '' })
}
}, [defaultToValue, defaultFromValue])
const handleFromChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleToChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
if (value !== '' && isNaN(Date.parse(value))) {
setTo({ value, error: 'Invalid ISO 8601 timestamp' })
@@ -74,16 +80,26 @@ const LogPanel: FC<Props> = ({
setTo({ value, error: '' })
}
}
const handleFromReset = async () => {
const handleFromChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
if (value !== '' && isNaN(Date.parse(value))) {
setFrom({ value, error: 'Invalid ISO 8601 timestamp' })
} else {
setFrom({ value, error: '' })
}
}
const handleReset = async () => {
setTo({ value: '', error: '' })
setFrom({ value: '', error: '' })
const value = dayjs().utc().toISOString()
setDefaultTimestamp(value)
onSearch({ query: search, to: '' })
onSearch({ query: search, to: '', from: '' })
}
const handleSearch = () => onSearch({ query: search, to: to.value })
const handleSearch = () => onSearch({ query: search, to: to.value, from: from.value })
const showFromReset = to.value !== ''
const showReset = to.value !== '' || from.value !== ''
return (
<div className="bg-panel-header-light dark:bg-panel-header-dark">
<div className="px-2 py-1 flex items-center justify-between w-full">
@@ -147,14 +163,24 @@ const LogPanel: FC<Props> = ({
align="end"
portalled
overlay={
<Input
label="To"
labelOptional="UTC"
value={to.value === '' ? defaultTimestamp : to.value}
onChange={handleFromChange}
error={to.error}
className="w-72 p-3"
actions={[
<>
<Input
label="From"
labelOptional="UTC"
value={from.value === '' ? defaultTimestamp : from.value}
onChange={handleFromChange}
error={from.error}
className="w-72 p-3"
/>
<Input
label="To"
labelOptional="UTC"
value={to.value === '' ? defaultTimestamp : to.value}
onChange={handleToChange}
error={to.error}
className="w-72 p-3"
/>
<div className="flex flex-row justify-end pb-2 px-4">
<Button
key="set"
size="tiny"
@@ -163,29 +189,29 @@ const LogPanel: FC<Props> = ({
onClick={handleSearch}
>
Set
</Button>,
]}
/>
</Button>
</div>
</>
}
>
<Button
as="span"
size="tiny"
className={showFromReset ? '!rounded-r-none' : ''}
type={showFromReset ? 'outline' : 'text'}
className={showReset ? '!rounded-r-none' : ''}
type={showReset ? 'outline' : 'text'}
icon={<IconClock size="tiny" />}
>
{to.value ? 'Custom' : 'Now'}
{to.value || from.value ? 'Custom' : 'Now'}
</Button>
</Popover>
{showFromReset && (
{showReset && (
<Button
size="tiny"
className={showFromReset ? '!rounded-l-none' : ''}
className={showReset ? '!rounded-l-none' : ''}
icon={<IconX size="tiny" />}
type="outline"
title="Clear timestamp filter"
onClick={handleFromReset}
onClick={handleReset}
/>
)}
</div>
@@ -1,7 +1,7 @@
interface Metadata {
[key: string]: string | number | Object | Object[]
}
export type LogSearchCallback = (filters: { query: string; to?: string; toMicro?: number }) => void
export type LogSearchCallback = (filters: { query: string; to?: string; from?: string; fromMicro?: number, toMicro?: number }) => void
export interface LogsEndpointParams {
// project ref
+14 -2
View File
@@ -23,6 +23,7 @@ interface Data<T> {
pageSize: number
filters: T
params: LogsEndpointParams
oldestTimestamp?: string
}
interface Handlers<T> {
loadOlder: () => void
@@ -58,6 +59,8 @@ function useLogsPreview<Filters>(
rawSql: genDefaultQuery(table),
period_start: '',
period_end: '',
timestamp_start: '',
timestamp_end: '',
})
const [filters, setFilters] = useState<Filters>(options.initialFilters)
@@ -122,9 +125,18 @@ function useLogsPreview<Filters>(
error = response.error
}
})
const oldestTimestamp = logData[logData.length - 1]?.timestamp
return [
{ newCount, logData, isLoading: isValidating, pageSize: size, error, filters, params },
{
newCount,
logData,
isLoading: isValidating,
pageSize: size,
error,
filters,
params,
oldestTimestamp: oldestTimestamp ? String(oldestTimestamp) : undefined,
},
{
setFrom: (value) => setParams((prev) => ({ ...prev, timestamp_start: value })),
setTo: (value) => setParams((prev) => ({ ...prev, timestamp_end: value })),
@@ -54,13 +54,13 @@ import useLogsPreview from 'hooks/analytics/useLogsPreview'
*/
export const LogPage: NextPage = () => {
const router = useRouter()
const { ref, type, s, te } = router.query
const { ref, type, s, te, ts } = router.query
const [showChart, setShowChart] = useState(true)
const table = type === 'api' ? LogsTableName.EDGE : LogsTableName.POSTGRES
const [
{ error, logData, params, newCount, filters, isLoading },
{ loadOlder, setFilters, refresh, setTo },
{ error, logData, params, newCount, filters, isLoading, oldestTimestamp },
{ loadOlder, setFilters, refresh, setTo, setFrom },
] = useLogsPreview(ref as string, table, {
initialFilters: { search_query: s as string },
whereStatementFactory: (filterObj) =>
@@ -78,8 +78,25 @@ export const LogPage: NextPage = () => {
} else {
setTo('')
}
}, [s, te])
if (ts) {
setFrom(ts as string)
} else {
setFrom('')
}
}, [s, te, ts])
useEffect(() => {
router.push({
pathname: router.pathname,
query: {
...router.query,
q: undefined,
s: filters.search_query || '',
ts: params.timestamp_start,
te: params.timestamp_end,
},
})
}, [params.timestamp_end, params.timestamp_start, filters.search_query])
const onSelectTemplate = (template: LogTemplate) => {
setFilters((prev) => ({ ...prev, search_query: template.searchString }))
}
@@ -91,23 +108,23 @@ export const LogPage: NextPage = () => {
query: {
...router.query,
te: undefined,
ts: undefined,
},
})
}
const handleSearch: LogSearchCallback = ({ query, to, toMicro }) => {
const unixMicro = toMicro ? toMicro : dayjs(to).valueOf() * 1000
setTo(unixMicro ? String(unixMicro) : '')
const handleSearch: LogSearchCallback = ({ query, to, from, fromMicro, toMicro }) => {
let toValue, fromValue
if (to || toMicro) {
toValue = toMicro ? toMicro : dayjs(to).valueOf() * 1000
setTo(String(toValue))
}
if (from || fromMicro) {
const fromValue = fromMicro ? fromMicro : dayjs(from).valueOf() * 1000
setFrom(String(fromValue))
}
setFilters((prev) => ({ ...prev, search_query: query || '' }))
router.push({
pathname: router.pathname,
query: {
...router.query,
q: undefined,
s: query || '',
te: unixMicro,
},
})
}
return (
@@ -128,6 +145,13 @@ export const LogPage: NextPage = () => {
defaultToValue={
params.timestamp_end ? dayjs(Number(params.timestamp_end) / 1000).toISOString() : ''
}
defaultFromValue={
params.timestamp_start
? dayjs(Number(params.timestamp_start) / 1000).toISOString()
: oldestTimestamp
? dayjs(Number(oldestTimestamp) / 1000).toISOString()
: ''
}
onCustomClick={() => {
router.push(`/project/${ref}/settings/logs/explorer?q=${params.rawSql}`)
}}
+16 -11
View File
@@ -78,42 +78,47 @@ test('reset search filter', async () => {
expect(() => screen.getByDisplayValue(/something123/)).toThrow()
})
test('timestamp To filter default value', async () => {
test('timestamp to/from filter default value', async () => {
render(<LogPanel defaultToValue="2022-01-18T10:43:39+0000" />)
userEvent.click(await screen.findByText('Custom'))
await screen.findByDisplayValue('2022-01-18T10:43:39+0000')
// TODO: use screen.findByLabelText when https://github.com/supabase/ui/issues/310 is resolved
await screen.findByText('To')
await screen.findByText('From')
})
test('timestamp from filter error handling', async () => {
test('timestamp to/from filter error handling', async () => {
const mockFn = jest.fn()
render(<LogPanel onSearch={mockFn} />)
userEvent.click(await screen.findByText(/Now/))
// display iso timestamp
const year = new Date().getFullYear()
const input = await screen.findByDisplayValue(RegExp(year))
const inputs = await screen.findAllByDisplayValue(RegExp(year))
expect(inputs.length).toBe(2)
const input = inputs[0]
userEvent.clear(input)
userEvent.type(input, '123456')
await screen.findByText(/[iI]nvalid ISO 8601 timestamp/)
})
test('timestamp To filter value change', async () => {
test('timestamp to/from filter value change', async () => {
const mockFn = jest.fn()
render(<LogPanel onSearch={mockFn} />)
userEvent.click(await screen.findByText(/Now/))
// display iso timestamp
const year = new Date().getFullYear()
const input = await screen.findByDisplayValue(RegExp(year))
const inputs = await screen.findAllByDisplayValue(RegExp(year))
// replace the input's value
userEvent.clear(input)
for (const input of inputs) {
// replace the input's value
userEvent.clear(input)
// get time 20 mins before
const newDate = new Date()
newDate.setMinutes(new Date().getMinutes() - 20)
userEvent.type(input, newDate.toISOString())
// get time 20 mins before
const newDate = new Date()
newDate.setMinutes(new Date().getMinutes() - 20)
userEvent.type(input, newDate.toISOString())
}
// input actions
const set = await screen.findByRole('button', { name: 'Set' })
@@ -178,7 +178,7 @@ test('s= query param will populate the search bar', async () => {
expect(get).toHaveBeenCalledWith(expect.stringContaining('someSearch'))
})
test('te= query param will populate the timestamp from input', async () => {
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)
@@ -197,6 +197,26 @@ test('te= query param will populate the timestamp from input', async () => {
userEvent.click(await screen.findByText('Custom'))
await screen.findByDisplayValue(isoString)
})
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 isoString = newDate.toISOString()
const unixMicro = newDate.getTime() * 1000 //microseconds
const router = defaultRouterMock()
router.query = { ...router.query, ts: unixMicro }
useRouter.mockReturnValue(router)
render(<LogPage />)
await waitFor(() => {
expect(get).toHaveBeenCalledWith(
expect.stringContaining(`timestamp_start=${encodeURIComponent(unixMicro)}`)
)
})
userEvent.click(await screen.findByText('Custom'))
await screen.findByDisplayValue(isoString)
})
test('load older btn will fetch older logs', async () => {
get.mockImplementation((url) => {