Skip to content

Commit

Permalink
Merge pull request #4904 from supabase/feat/logs-timestamp-from-custo…
Browse files Browse the repository at this point in the history
…mization

feat/studio/logs: Timestamp from dropdown input
  • Loading branch information
Ziinc authored Jan 20, 2022
2 parents 79b9d90 + 022d54a commit cc605d5
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 53 deletions.
16 changes: 16 additions & 0 deletions studio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,19 @@ Then run the following commands to install dependencies and start the dashboard.
npm install
npm run dev
```

## UI Testing Notes

### `<Popover>` vs `<Dropdown>`

When simulating clicks on these components, do the following:

```js
// for Popovers
import userEvent from '@testing-library/user-event'
userEvent.click('Hello world')

// for Popovers
import clickDropdown from 'tests/helpers'
clickDropdown('Hello world')
```
166 changes: 127 additions & 39 deletions studio/components/interfaces/Settings/Logs/LogPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC, useEffect, useState } from 'react'
import React, { FC, SyntheticEvent, useEffect, useMemo, useState } from 'react'
import {
Button,
Input,
Expand All @@ -9,21 +9,28 @@ import {
IconX,
Toggle,
IconSearch,
IconClock,
Popover,
} from '@supabase/ui'
import { LogTemplate } from '.'

import { LogSearchCallback, LogTemplate } from '.'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import Flag from 'components/ui/Flag/Flag'
interface Props {
defaultSearchValue?: string
defaultFromValue?: string
templates?: any
isLoading: boolean
isCustomQuery: boolean
newCount: number
onRefresh?: () => void
onSearch?: (query: string) => void
onSearch?: LogSearchCallback
onCustomClick?: () => void
onSelectTemplate: (template: LogTemplate) => void
}

dayjs.extend(utc)

/**
* Logs control panel header + wrapper
*/
Expand All @@ -35,18 +42,44 @@ const LogPanel: FC<Props> = ({
onRefresh,
onSearch = () => {},
defaultSearchValue = '',
defaultFromValue = '',
onCustomClick,
onSelectTemplate,
}) => {
const [search, setSearch] = useState('')

const [from, setFrom] = useState({ value: '', error: '' })
const [defaultTimestamp, setDefaultTimestamp] = useState(dayjs().utc().toISOString())
// sync local state with provided default value
useEffect(() => {
if (search !== defaultSearchValue) {
setSearch(defaultSearchValue)
}
}, [defaultSearchValue])

useEffect(() => {
if (from.value !== defaultFromValue) {
setFrom({ value: defaultFromValue, error: '' })
}
}, [defaultFromValue])

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 handleFromReset = async () => {
setFrom({ value: '', error: '' })
const value = dayjs().utc().toISOString()
setDefaultTimestamp(value)
onSearch({ query: search, from: '' })
}

const handleSearch = () => onSearch({ query: search, from: from.value })

const showFromReset = 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">
Expand Down Expand Up @@ -86,7 +119,7 @@ const LogPanel: FC<Props> = ({
</Dropdown.Item>
))}
>
<Button type="text" iconRight={<IconChevronDown />}>
<Button as="span" type="text" iconRight={<IconChevronDown />}>
Templates
</Button>
</Dropdown>
Expand All @@ -99,43 +132,98 @@ const LogPanel: FC<Props> = ({
</div>
</div>
<div className="flex items-center gap-x-4">
{/* wrap with form so that if user presses enter, the search value will submit automatically */}
{!isCustomQuery && (
<form
id="log-panel-search"
onSubmit={(e) => {
// prevent redirection
e.preventDefault()
onSearch(search)
}}
>
<Input
placeholder="Search events"
onChange={(e) => setSearch(e.target.value)}
value={search}
actions={[
search && (
<IconX
key="clear-search"
<>
<Flag name="logsTimestampFilter">
<div className="flex flex-row">
<Popover
side="bottom"
align="end"
portalled
overlay={
<Input
label="From"
labelOptional="UTC"
value={from.value === '' ? defaultTimestamp : from.value}
onChange={handleFromChange}
error={from.error}
className="w-72 p-3"
actions={[
from.value && (
<IconX
key="reset-from"
size="tiny"
className="cursor-pointer mx-1"
title="Reset"
onClick={handleFromReset}
/>
),
<Button
key="set"
size="tiny"
title="Set"
type="secondary"
onClick={handleSearch}
>
Set
</Button>,
]}
/>
}
>
<Button
as="span"
size="tiny"
className={showFromReset ? '!rounded-r-none' : ''}
type={showFromReset ? 'outline' : 'text'}
icon={<IconClock size="tiny" />}
>
{from.value ? 'Custom' : 'Now'}
</Button>
</Popover>
{showFromReset && (
<Button
size="tiny"
className="cursor-pointer mx-1"
title="Clear search"
onClick={() => setSearch('')}
className={showFromReset ? '!rounded-l-none' : ''}
icon={<IconX size="tiny" />}
type="outline"
title="Clear timestamp filter"
onClick={handleFromReset}
/>
),
)}
</div>
</Flag>
{/* wrap with form so that if user presses enter, the search value will submit automatically */}
<form
id="log-panel-search"
onSubmit={(e) => {
// prevent redirection
e.preventDefault()
handleSearch()
}}
>
<Input
placeholder="Search events"
onChange={(e) => setSearch(e.target.value)}
value={search}
actions={[
search && (
<IconX
key="clear-search"
size="tiny"
className="cursor-pointer mx-1"
title="Clear search"
onClick={() => setSearch('')}
/>
),

<Button
key="go"
size="tiny"
title="Go"
type="secondary"
onClick={() => onSearch(search)}
>
<IconSearch size={16} />
</Button>,
]}
/>
</form>
<Button key="go" size="tiny" title="Go" type="secondary" onClick={handleSearch}>
<IconSearch size={16} />
</Button>,
]}
/>
</form>
</>
)}
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion studio/components/interfaces/Settings/Logs/Logs.types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
interface Metadata {
[key: string]: string | number | Object | Object[]
}

export type LogSearchCallback = (filters: { query: string; from: string }) => void
export interface LogData {
id: string
timestamp: number
Expand Down
37 changes: 30 additions & 7 deletions studio/pages/project/[ref]/settings/logs/[type].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ import {
LogTemplate,
TEMPLATES,
LogData,
LogSearchCallback,
} 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'
import { useFlag } from 'hooks'
import dayjs from 'dayjs'

/**
* Acts as a container component for the entire log display
Expand All @@ -48,7 +50,7 @@ export const LogPage: NextPage = () => {
const logsQueryParamsSyncing = useFlag('logsQueryParamsSyncing')
const logsCustomSql = useFlag('logsCustomSql')
const router = useRouter()
const { ref, type, q, s, ts } = router.query
const { ref, type, q, s, te } = router.query
const [editorId, setEditorId] = useState<string>(uuidv4())
const [editorValue, setEditorValue] = useState('')
const [mode, setMode] = useState<'simple' | 'custom'>('simple')
Expand Down Expand Up @@ -82,8 +84,11 @@ export const LogPage: NextPage = () => {
searchString: s as string,
})
}
if (ts) {
setParams({ ...params, timestamp_start: ts as string })
if (te) {
setParams(prev => ({ ...prev, timestamp_end: te as string }))
} else {
setParams(prev => ({ ...prev, timestamp_end: '' }))

}
}, [logsQueryParamsSyncing])

Expand Down Expand Up @@ -147,7 +152,14 @@ export const LogPage: NextPage = () => {

const handleRefresh = () => {
setLatestRefresh(new Date().toISOString())
setParams({ ...params, timestamp_start: '' })
setParams({ ...params, timestamp_end: '' })
router.push({
pathname: router.pathname,
query: {
...router.query,
te: undefined
},
})
setSize(1)
}

Expand All @@ -171,6 +183,7 @@ export const LogPage: NextPage = () => {
where: isSelectQuery ? '' : template.searchString,
sql: isSelectQuery ? template.searchString : '',
search_query: '',
timestamp_end: ''
}))
setEditorId(uuidv4())
}
Expand All @@ -189,18 +202,27 @@ export const LogPage: NextPage = () => {
...router.query,
q: editorValue,
s: undefined,
te: undefined
},
})
}
const handleSearch = (v: string) => {
setParams((prev) => ({ ...prev, search_query: v || '', where: '', sql: '' }))
const handleSearch: LogSearchCallback = ({ query, from }) => {
const unixMicro = dayjs(from).valueOf() * 1000
setParams((prev) => ({
...prev,
search_query: query || '',
timestamp_end: from ? String(unixMicro) : '' ,
where: '',
sql: '',
}))
if (!logsQueryParamsSyncing) return
router.push({
pathname: router.pathname,
query: {
...router.query,
q: undefined,
s: v || '',
s: query || '',
te: unixMicro
},
})
setEditorValue('')
Expand All @@ -217,6 +239,7 @@ export const LogPage: NextPage = () => {
onRefresh={handleRefresh}
onSearch={handleSearch}
defaultSearchValue={params.search_query}
defaultFromValue={params.timestamp_end ? dayjs(Number(params.timestamp_end)/1000).toISOString() : ''}
onCustomClick={handleModeToggle}
onSelectTemplate={onSelectTemplate}
/>
Expand Down
Loading

0 comments on commit cc605d5

Please sign in to comment.