mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
Implemented from-to inputs
This commit is contained in:
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
|
||||
|
||||
@@ -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}`)
|
||||
}}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in new issue
Block a user