chore: started cherry picking over prototype changes

This commit is contained in:
Jonathan Summers-Muir authored and Copple committed 2022-03-30 17:07:39 +02:00
1 parent 74b69f4860
commit 6c2a86ee1f
6 files changed
+566 -7

No files matched your search

@@ -11,6 +11,7 @@ import {
IconSearch,
IconClock,
Popover,
IconPlay,
} from '@supabase/ui'
import { LogSearchCallback, LogTemplate } from '.'
import dayjs from 'dayjs'
@@ -29,6 +30,8 @@ interface Props {
onSelectTemplate: (template: LogTemplate) => void
isShowingEventChart: boolean
onToggleEventChart: () => void
editorControls: React.ReactNode
}
dayjs.extend(utc)
@@ -50,6 +53,8 @@ const LogPanel: FC<Props> = ({
onSelectTemplate,
isShowingEventChart,
onToggleEventChart,
editorControls,
}) => {
const [search, setSearch] = useState('')
const [to, setTo] = useState({ value: '', error: '' })
@@ -101,8 +106,15 @@ const LogPanel: FC<Props> = ({
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">
<div
className="
border
border-panel-border-light dark:border-panel-border-dark rounded rounded-bl-none rounded-br-none
bg-panel-header-light dark:bg-panel-header-dark
"
>
<div className="px-5 py-2 flex items-center justify-between w-full">
<div className="flex flex-row gap-x-4 items-center">
{!isCustomQuery && (
<Button
@@ -143,7 +155,7 @@ const LogPanel: FC<Props> = ({
</Dropdown.Item>
))}
>
<Button as="span" type="text" iconRight={<IconChevronDown />}>
<Button as="span" type="default" iconRight={<IconChevronDown />}>
Templates
</Button>
</Dropdown>
@@ -254,6 +266,7 @@ const LogPanel: FC<Props> = ({
</>
)}
</div>
{editorControls}
</div>
</div>
)
@@ -0,0 +1,281 @@
import React, { FC, useEffect, useState } from 'react'
import {
Button,
Input,
Dropdown,
Typography,
IconChevronDown,
IconRefreshCw,
IconX,
Toggle,
IconSearch,
IconClock,
Popover,
IconLink,
IconExternalLink,
IconCalendar,
} from '@supabase/ui'
import { LogSearchCallback, LogTemplate } from '.'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
interface Props {
defaultSearchValue?: string
defaultToValue?: string
templates?: any
isLoading: boolean
isCustomQuery: boolean
newCount: number
onRefresh?: () => void
onSearch?: LogSearchCallback
onCustomClick?: () => void
onSelectTemplate: (template: LogTemplate) => void
isShowingEventChart: boolean
onToggleEventChart: () => void
}
dayjs.extend(utc)
/**
* Logs control panel header + wrapper
*/
const PreviewFilterPanel: FC<Props> = ({
templates = [],
isLoading,
isCustomQuery,
newCount,
onRefresh,
onSearch = () => {},
defaultSearchValue = '',
defaultToValue = '',
onCustomClick,
onSelectTemplate,
isShowingEventChart,
onToggleEventChart,
}) => {
const [search, setSearch] = useState('')
const [to, setTo] = useState({ value: '', error: '' })
const [defaultTimestamp, setDefaultTimestamp] = useState(dayjs().utc().toISOString())
const [localSearchValue, setlocalSearchValue] = useState(search)
const hasEdits = localSearchValue !== search
// Sync local state with provided default value
useEffect(() => {
if (search !== defaultSearchValue) {
setSearch(defaultSearchValue)
}
}, [defaultSearchValue])
useEffect(() => {
if (to.value !== defaultToValue) {
setTo({ value: defaultToValue, error: '' })
}
}, [defaultToValue])
const handleFromChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
if (value !== '' && isNaN(Date.parse(value))) {
setTo({ value, error: 'Invalid ISO 8601 timestamp' })
} else {
setTo({ value, error: '' })
}
}
const handleFromReset = async () => {
setTo({ value: '', error: '' })
const value = dayjs().utc().toISOString()
setDefaultTimestamp(value)
onSearch({ query: search, to: '' })
}
// const handleSearch = () => onSearch({ query: search, to: to.value })
const handleSearch = (value: string) =>
onSearch({
query: value,
to: to.value,
// , from: from.value
})
const RefreshButton = () => {
return !isCustomQuery ? (
<Button
type="default"
icon={
<div className="relative">
{newCount > 0 && (
<div
className={[
'absolute flex items-center justify-center -top-3 right-3',
'h-4 w-4 z-50',
].join(' ')}
>
<div className="absolute z-20">
<Typography.Text style={{ fontSize: '0.6rem' }} className="opacity-80">
{newCount}
</Typography.Text>
</div>
<div className="bg-green-800 rounded-full w-full h-full animate-ping opacity-60"></div>
<div className="absolute z-60 top-0 right-0 bg-green-900 opacity-80 rounded-full w-full h-full"></div>
</div>
)}
<IconRefreshCw size={10} />
</div>
}
loading={isLoading}
onClick={onRefresh}
>
Refresh
</Button>
) : null
}
const showFromReset = to.value !== ''
return (
<div className="flex items-center justify-between w-full">
<div className="flex flex-row gap-4 items-center">
<form
id="log-panel-search"
onSubmit={(e) => {
// prevent redirection
e.preventDefault()
setSearch(localSearchValue)
handleSearch(localSearchValue)
}}
>
<Input
className="w-60"
size="tiny"
placeholder="Search events"
onChange={(e) => setlocalSearchValue(e.target.value)}
onBlur={(e: React.FocusEvent<HTMLInputElement>) => {
setSearch(e.target.value)
handleSearch(localSearchValue)
}}
icon={
<div className="text-scale-900">
<IconSearch size={14} />
</div>
}
value={localSearchValue}
actions={
hasEdits && (
<button
onClick={() => handleSearch(localSearchValue)}
className="text-scale-1100 hover:text-scale-1200 mx-2"
>
{'↲'}
</button>
)
}
/>
</form>
<div>
<RefreshButton />
</div>
<div className="flex items-center">
<Dropdown
size="small"
side="bottom"
align="start"
overlay={
<>
<Dropdown.RadioGroup onChange={(e) => console.log(e)} value={'1_hour'}>
<Dropdown.Radio value="1_hour">1 hour</Dropdown.Radio>
<Dropdown.Radio value="3_hour">Last 3 hour</Dropdown.Radio>
<Dropdown.Radio value="1_day">Last day</Dropdown.Radio>
</Dropdown.RadioGroup>
</>
}
>
<Button
as="span"
type="default"
style={{ borderTopRightRadius: 0, borderBottomRightRadius: 0 }}
>
Last hour
</Button>
</Dropdown>
<Button
type="default"
style={{ borderTopLeftRadius: 0, borderBottomLeftRadius: 0 }}
icon={<IconCalendar size={12} />}
>
Custom
</Button>
</div>
{!isCustomQuery && (
<>
<div className="flex flex-row">
{/* <Popover
side="bottom"
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={[
<Button
key="set"
size="tiny"
title="Set"
type="secondary"
onClick={() => handleSearch(localSearchValue)}
>
Set
</Button>,
]}
/>
}
>
<Button
as="span"
size="tiny"
className={showFromReset ? '!rounded-r-none' : ''}
type={showFromReset ? 'outline' : 'text'}
icon={<IconClock size="tiny" />}
>
{to.value ? 'Custom' : 'Now'}
</Button>
</Popover> */}
{/* Clear the filters could be here */}
{/* {showFromReset && (
<Button
size="tiny"
className={showFromReset ? '!rounded-l-none' : ''}
icon={<IconX size="tiny" />}
type="outline"
title="Clear timestamp filter"
onClick={handleFromReset}
/>
)} */}
</div>
{/* {!isCustomQuery && (
<div className="flex items-center space-x-2">
<p className="text-xs">Show event chart</p>
<Toggle size="tiny" checked={isShowingEventChart} onChange={onToggleEventChart} />
</div>
)} */}
{/* wrap with form so that if user presses enter, the search value will submit automatically */}
</>
)}
</div>
<Button type="secondary" onClick={onCustomClick} iconRight={<IconExternalLink size={10} />}>
Explore via query
</Button>
</div>
)
}
export default PreviewFilterPanel
@@ -0,0 +1,64 @@
import React from 'react'
import BaseLayout from 'components/layouts'
import { observer } from 'mobx-react-lite'
import { withAuth } from 'hooks'
import { IconList } from '@supabase/ui'
// import FunctionsNav from '../interfaces/Settings/Logs/LogsNav'
const PageLayout = ({ children }: { children?: React.ReactNode }) => {
return (
<BaseLayout>
<div className="h-full flex flex-col flex-grow">
<div
className="
py-10
w-full mx-auto
transition-all
px-5
lg:px-20
xl:px-24
1xl:px-28
2xl:px-32
flex flex-col
gap-8
"
>
<div className="flex items-center gap-3">
<div
className="w-6 h-6 bg-brand-300 border border-brand-600 rounded text-brand-900
flex items-center justify-center
"
>
<IconList size={14} strokeWidth={3} />
</div>
<h1 className="text-2xl text-scale-1200">Logs Explorer</h1>
</div>
{/* <FunctionsNav /> */}
</div>
<div
// weird repetitive styling
className="
h-full flex flex-col flex-grow
w-full mx-auto
transition-all
px-5
lg:px-20
xl:px-24
1xl:px-28
2xl:px-32
"
>
{children}
</div>
</div>
</BaseLayout>
)
}
export default withAuth(observer(PageLayout))
@@ -5,6 +5,7 @@ import {
IconSettings,
IconDatabase,
IconBarChart,
IconList,
} from '@supabase/ui'
import SVG from 'react-inlinesvg'
@@ -64,6 +65,16 @@ export const generateProductRoutes = (ref: string): Route[] => {
export const generateOtherRoutes = (ref: string) => {
return [
...(IS_PLATFORM
? [
{
key: 'logsExplorer',
label: 'Logs Explorer',
icon: <IconList size={18} strokeWidth={2} />,
link: `/project/${ref}/logs-explorer`,
},
]
: []),
...(IS_PLATFORM
? [
{
@@ -0,0 +1,190 @@
import useSWR from 'swr'
import React, { useEffect, useState } from 'react'
import { NextPage } from 'next'
import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import {
Typography,
IconLoader,
IconAlertCircle,
IconRewind,
Button,
IconInfo,
Card,
Input,
IconPlay,
} from '@supabase/ui'
import { withAuth } from 'hooks'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { SettingsLayout } from 'components/layouts/'
import CodeEditor from 'components/ui/CodeEditor'
import {
LogPanel,
LogTable,
LogEventChart,
Count,
Logs,
LogTemplate,
TEMPLATES,
LogData,
LogSearchCallback,
LOG_TYPE_LABEL_MAPPING,
genDefaultQuery,
genCountQuery,
LogsTableName,
} from 'components/interfaces/Settings/Logs'
import { uuidv4 } from 'lib/helpers'
import useSWRInfinite, { SWRInfiniteKeyLoader } from 'swr/infinite'
import { isUndefined } from 'lodash'
import dayjs from 'dayjs'
import InformationBox from 'components/ui/InformationBox'
import useLogsPreview from 'hooks/analytics/useLogsPreview'
import useLogsQuery from 'hooks/analytics/useLogsQuery'
import LogsExplorerLayout from 'components/layouts/LogsExplorerLayout/LogsExplorerLayout'
/**
* Acts as a container component for the entire log display
*
* ## Query Params Syncing
* Query params are synced on query submission.
*
* params used are:
* - `q` for the editor query.
* - `s` for search query.
* - `te` for timestamp start value.
*/
export const LogsExplorerPage: NextPage = () => {
const router = useRouter()
const { ref, type, q } = router.query
const [editorId, setEditorId] = useState<string>(uuidv4())
const [editorValue, setEditorValue] = useState<string>('')
const [{ logData, params, error, isLoading }, { changeQuery, runQuery }] = useLogsQuery(
ref as string
)
useEffect(() => {
// on mount, set initial values
if (q !== undefined && q !== '') {
changeQuery(q as string)
runQuery()
onSelectTemplate({
mode: 'custom',
searchString: q as string,
})
}
}, [q])
const onSelectTemplate = (template: LogTemplate) => {
setEditorValue(template.searchString)
changeQuery(template.searchString)
setEditorId(uuidv4())
runQuery()
}
const handleEditorSubmit = () => {
changeQuery(editorValue)
runQuery()
router.push({
pathname: router.pathname,
query: { ...router.query, q: editorValue },
})
}
const EditorControls = () => (
<div className="flex items-center gap-2">
<Button
type="default"
onClick={() => {
setEditorValue('')
setEditorId(uuidv4())
}}
>
Clear query
</Button>
<Button
type={editorValue ? 'alternative' : 'default'}
disabled={!editorValue}
onClick={handleEditorSubmit}
icon={<IconPlay />}
>
Run
</Button>
</div>
)
return (
<LogsExplorerLayout>
<div className="h-full flex flex-col flex-grow gap-4">
<div>
<LogPanel
isShowingEventChart={false}
onToggleEventChart={() => null}
isCustomQuery={true}
isLoading={isLoading}
newCount={0}
templates={TEMPLATES.filter((template) => template.mode === 'custom')}
onRefresh={() => null}
onSearch={() => null}
defaultSearchValue={''}
defaultToValue={
params.timestamp_end
? dayjs(Number(params.timestamp_end) / 1000).toISOString()
: params.timestamp_end
}
onCustomClick={() => null}
onSelectTemplate={onSelectTemplate}
editorControls={<EditorControls />}
/>
<div className="min-h-[7rem] h-28">
<CodeEditor
id={editorId}
language="pgsql"
defaultValue={editorValue}
onInputChange={(v) => setEditorValue(v || '')}
onInputRun={handleEditorSubmit}
/>
</div>
</div>
<div className="flex flex-col flex-grow relative">
{isLoading && (
<div
className={[
'absolute top-0 w-full h-full flex items-center justify-center',
'bg-gray-100 opacity-75 z-50',
].join(' ')}
>
<IconLoader className="animate-spin" />
</div>
)}
<LogTable data={logData} isCustomQuery={true} />
{error && (
<div className="flex w-full h-full justify-center items-center mx-auto">
<Card className="flex flex-col gap-y-2 w-2/5 bg-scale-400">
<div className="flex flex-row gap-x-2 py-2">
<IconAlertCircle size={16} />
<Typography.Text type="secondary">
Sorry! An error occured when fetching data.
</Typography.Text>
</div>
<Input.TextArea
label="Error Messages"
value={JSON.stringify(error, null, 2)}
borderless
className=" border-t-2 border-scale-800 pt-2 font-mono"
/>
</Card>
</div>
)}
</div>
</div>
</LogsExplorerLayout>
)
}
export default withAuth(observer(LogsExplorerPage))
@@ -20,7 +20,6 @@ import { API_URL } from 'lib/constants'
import { SettingsLayout } from 'components/layouts/'
import CodeEditor from 'components/ui/CodeEditor'
import {
LogPanel,
LogTable,
LogEventChart,
Count,
@@ -40,6 +39,7 @@ import { isUndefined } from 'lodash'
import dayjs from 'dayjs'
import InformationBox from 'components/ui/InformationBox'
import useLogsPreview from 'hooks/analytics/useLogsPreview'
import PreviewFilterPanel from 'components/interfaces/Settings/Logs/PreviewFilterPanel'
/**
* Acts as a container component for the entire log display
@@ -125,8 +125,8 @@ export const LogPage: NextPage = () => {
return (
<SettingsLayout title={title}>
<div className="h-full flex flex-col flex-grow">
<LogPanel
<div className="h-full flex flex-col flex-grow px-5 md:px-5 xl:px-16 py-8">
<PreviewFilterPanel
isShowingEventChart={showChart}
onToggleEventChart={() => setShowChart(!showChart)}
isCustomQuery={false}
@@ -149,7 +149,7 @@ export const LogPage: NextPage = () => {
: ''
}
onCustomClick={() => {
router.push(`/project/${ref}/settings/logs/explorer?q=${params.rawSql}`)
router.push(`/project/${ref}/logs-explorer?q=${params.rawSql}`)
}}
onSelectTemplate={onSelectTemplate}
/>