chore: started UI changes on log explorer. adding improved loading state. added dedicated templates page

This commit is contained in:
Jonathan Summers-Muir authored and Copple committed 2022-03-30 17:07:40 +02:00
1 parent 4f9c3af4c3
commit c836588059
12 files changed
+291 -125

No files matched your search

@@ -29,6 +29,7 @@ interface Props {
onSelectTemplate: (template: LogTemplate) => void
isShowingEventChart: boolean
onToggleEventChart: () => void
editorControls: React.ReactNode
}
dayjs.extend(utc)
@@ -50,6 +51,7 @@ const LogPanel: FC<Props> = ({
onSelectTemplate,
isShowingEventChart,
onToggleEventChart,
editorControls,
}) => {
const [search, setSearch] = useState('')
const [to, setTo] = useState({ value: '', error: '' })
@@ -100,6 +102,7 @@ const LogPanel: FC<Props> = ({
const handleSearch = () => onSearch({ query: search, to: to.value, from: from.value })
const showReset = to.value !== '' || from.value !== ''
return (
<div
className="
@@ -110,7 +113,7 @@ const LogPanel: FC<Props> = ({
"
>
<div className="px-5 py-2 flex items-center justify-between w-full">
<div className="flex flex-row gap-x-4 items-center">
<div className="flex flex-row gap-x-4 items-center justify-between w-full">
{!isCustomQuery && (
<Button
type="text"
@@ -141,25 +144,38 @@ const LogPanel: FC<Props> = ({
Refresh
</Button>
)}
<Dropdown
side="bottom"
align="start"
overlay={templates.map((template: LogTemplate) => (
<Dropdown.Item key={template.label} onClick={() => onSelectTemplate(template)}>
<Typography.Text>{template.label}</Typography.Text>
</Dropdown.Item>
))}
>
<Button as="span" type="default" iconRight={<IconChevronDown />}>
Templates
</Button>
</Dropdown>
{!isCustomQuery && (
<Button as="span" type="outline" onClick={onCustomClick}>
Explore via query
</Button>
)}
<div className="flex items-center gap-2">
<Dropdown
side="bottom"
align="start"
overlay={templates.map((template: LogTemplate) => (
<Dropdown.Item key={template.label} onClick={() => onSelectTemplate(template)}>
<Typography.Text>{template.label}</Typography.Text>
</Dropdown.Item>
))}
>
<Button as="span" type="default" iconRight={<IconChevronDown />}>
Insert source
</Button>
</Dropdown>
<Dropdown
side="bottom"
align="start"
overlay={templates.map((template: LogTemplate) => (
<Dropdown.Item key={template.label} onClick={() => onSelectTemplate(template)}>
<Typography.Text>{template.label}</Typography.Text>
</Dropdown.Item>
))}
>
<Button as="span" type="default" iconRight={<IconChevronDown />}>
Templates
</Button>
</Dropdown>
</div>
<div>{editorControls}</div>
</div>
<div className="flex items-center gap-x-4">
{!isCustomQuery && (
@@ -229,6 +245,7 @@ const LogPanel: FC<Props> = ({
</div>
)}
{/* wrap with form so that if user presses enter, the search value will submit automatically */}
<form
id="log-panel-search"
onSubmit={(e) => {
@@ -35,8 +35,7 @@ const LogTable = ({ data = [], queryType }: Props) => {
if (typeof value === 'object') {
formatter = () => `[Object]`
}
return { key: v, name: v, resizable: true, formatter }
return { key: v, name: v, resizable: true, formatter, header: v }
})
let columns
if (!queryType) {
@@ -179,82 +178,97 @@ const LogTable = ({ data = [], queryType }: Props) => {
}, [stringData])
return (
<>
{!queryType && (
<div
className="
w-full bg-scale-300 rounded
<section
className={'flex flex-1 flex-col ' + (!queryType ? 'shadow-lg' : '')}
style={{ maxHeight }}
>
{!queryType && (
<div>
<div
className="
w-full bg-scale-100 dark:bg-scale-300
rounded-tl rounded-tr
border-t
border-l
border-r
flex items-center justify-between
px-5 py-2
"
>
<div className="flex items-center gap-2">
{data && data.length ? (
<>
<span className="text-sm text-scale-1200">Query results</span>
<span className="text-sm text-scale-1100">{data && data.length}</span>
</>
) : (
<span className="text-xs text-scale-1200">Results will be shown below</span>
)}
</div>
<div className="flex items-center gap-2">
<Button type="default" icon={<IconEye />}>
Histogram
</Button>
<Button type="default" icon={<IconDownloadCloud />}>
Download
</Button>
</div>
</div>
)}
<section className="flex flex-1 flex-row" style={{ maxHeight }}>
<DataGrid
style={{ height: '100%' }}
className={`
flex-grow flex-1
${false ? '' : ' data-grid--simple-logs'}
`}
rowHeight={40}
headerRowHeight={0}
onSelectedCellChange={({ idx, rowIdx }) => {
if (!hasId) return
setFocusedLog(data[rowIdx] as LogData)
}}
noRowsFallback={
<div className="p-4">
<Typography.Text type="secondary" small className="font-mono">
No data returned from query
</Typography.Text>
>
<div className="flex items-center gap-2">
{data && data.length ? (
<>
<span className="text-sm text-scale-1200">Query results</span>
<span className="text-sm text-scale-1100">{data && data.length}</span>
</>
) : (
<span className="text-xs text-scale-1200">Results will be shown below</span>
)}
</div>
<div className="flex items-center gap-2">
<Button type="default" icon={<IconEye />}>
Histogram
</Button>
<Button type="default" icon={<IconDownloadCloud />}>
Download
</Button>
</div>
</div>
}
columns={columns as any}
rowClass={(r) => {
const row = r as LogData
let classes = []
classes.push(
`${row.id === focusedLog?.id ? '!bg-scale-400 rdg-row--focussed' : 'cursor-pointer'}`
)
return classes.join(' ')
}}
rows={logDataRows}
rowKeyGetter={(r) => {
if (!hasId) return Object.keys(r)[0]
const row = r as LogData
return row.id
}}
onRowClick={(r) => setFocusedLog(r)}
/>
{focusedLog && (
<div className="w-1/2 flex flex-col">
<LogSelection
onClose={() => setFocusedLog(null)}
log={focusedLog}
queryType={queryType}
/>
</div>
)}
<div className={'flex flex-row h-full ' + (!queryType ? 'border-l border-r' : '')}>
<DataGrid
style={{ height: '100%' }}
className={`
flex-grow flex-1
${!queryType ? 'data-grid--logs-explorer' : ' data-grid--simple-logs'}
`}
rowHeight={40}
headerRowHeight={0}
onSelectedCellChange={({ idx, rowIdx }) => {
if (!hasId) return
setFocusedLog(data[rowIdx] as LogData)
}}
noRowsFallback={
<div className="p-4">
<Typography.Text type="secondary" small className="font-mono">
No data returned from query
</Typography.Text>
</div>
}
columns={columns as any}
rowClass={(r) => {
const row = r as LogData
let classes = []
classes.push(
`${
row.id === focusedLog?.id ? '!bg-scale-400 rdg-row--focussed' : 'cursor-pointer'
}`
)
return classes.join(' ')
}}
rows={logDataRows}
rowKeyGetter={(r) => {
if (!hasId) return Object.keys(r)[0]
const row = r as LogData
return row.id
}}
onRowClick={(r) => setFocusedLog(r)}
/>
{focusedLog && (
<div className="w-1/2 flex flex-col">
<LogSelection
onClose={() => setFocusedLog(null)}
log={focusedLog}
queryType={queryType}
/>
</div>
)}
</div>
</section>
</>
)
@@ -15,6 +15,7 @@ export const TEMPLATES: LogTemplate[] = [
},
{
label: 'Commits By User',
description: 'Print which users made what commits on the database',
mode: 'custom',
searchString: `SELECT
p.user_name, count(*) as count
@@ -30,6 +31,7 @@ GROUP BY
},
{
label: 'Metadata IP',
description: 'Print all the IP addresses that used Supabase api',
mode: 'custom',
searchString: `SELECT timestamp, h.x_real_ip
FROM edge_logs
@@ -27,6 +27,7 @@ export interface LogData {
export interface LogTemplate {
label?: string
description?: string
mode: 'custom' | 'simple'
for?: string[]
searchString: string
@@ -0,0 +1,29 @@
import { Tabs } from '@supabase/ui'
import { useRouter } from 'next/router'
const LogsNavigation = () => {
const router = useRouter()
const activeRoute = router.pathname.split('/')[4]
const { ref } = router.query
const indexRoute = 'query'
return (
<Tabs
defaultActiveId="1"
type="underlined"
size="medium"
activeId={!activeRoute ? indexRoute : activeRoute}
onChange={(e: string) =>
router.push(`/project/${ref}/logs-explorer/${e === indexRoute ? '' : e}`)
}
>
<Tabs.Panel id="query" label="Query" />
{/* <Tabs.Panel id="recent" label={'Recent'} /> */}
<Tabs.Panel id="saved" label="Saved" />
<Tabs.Panel id="templates" label="Templates" />
</Tabs>
)
}
export default LogsNavigation
@@ -27,6 +27,9 @@ import PreviewFilterPanel from 'components/interfaces/Settings/Logs/PreviewFilte
import { LOGS_TABLES } from './Logs.constants'
import { Override } from './Logs.types'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import ShimmerLine from 'components/ui/ShimmerLine'
import LoadingOpacity from 'components/ui/LoadingOpacity'
/**
* Acts as a container component for the entire log display
@@ -128,7 +131,7 @@ export const LogsPreviewer: React.FC<Props> = ({ projectRef, queryType, override
}
return (
<div className="h-full flex flex-col flex-grow space-y-4">
<div className="h-full flex flex-col flex-grow gap-4">
<PreviewFilterPanel
isShowingEventChart={showChart}
onToggleEventChart={() => setShowChart(!showChart)}
@@ -172,13 +175,10 @@ export const LogsPreviewer: React.FC<Props> = ({ projectRef, queryType, override
) : null}
<div className="flex flex-col flex-grow relative">
{isLoading && <div className="logs-shimmering-loader w-full h-0.5"></div>}
<div
className={
'flex h-full flex-grow transition-opacity ' + (isLoading ? 'opacity-30' : 'opacity-100')
}
>
<ShimmerLine active={isLoading} />
<LoadingOpacity active={isLoading}>
<LogTable data={logData} queryType={queryType} />
</div>
</LoadingOpacity>
<div className="p-2">
<Button onClick={() => loadOlder()} icon={<IconRewind />} type="default">
Load older
@@ -3,7 +3,8 @@ 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'
import FunctionsNav from '../interfaces/Settings/Logs/LogsNav'
import LogsNavigation from 'components/interfaces/Settings/Logs/LogsNavigation'
const PageLayout = ({ children }: { children?: React.ReactNode }) => {
return (
@@ -11,7 +12,8 @@ const PageLayout = ({ children }: { children?: React.ReactNode }) => {
<div className="h-full flex flex-col flex-grow">
<div
className="
py-10
pt-10
pb-2
w-full mx-auto
transition-all
@@ -36,7 +38,7 @@ const PageLayout = ({ children }: { children?: React.ReactNode }) => {
</div>
<h1 className="text-2xl text-scale-1200">Logs Explorer</h1>
</div>
{/* <FunctionsNav /> */}
<LogsNavigation />
</div>
<div
// weird repetitive styling
+15
View File
@@ -0,0 +1,15 @@
import React from 'react'
const LoadingOpacity = ({ children, active }: { children?: React.ReactNode; active: boolean }) => {
return (
<div
className={
'flex h-full flex-grow transition-opacity ' + (active ? 'opacity-30' : 'opacity-100')
}
>
{children}
</div>
)
}
export default LoadingOpacity
+5
View File
@@ -0,0 +1,5 @@
const ShimmerLine = ({ active }: { active: boolean }) => {
return active ? <div className="logs-shimmering-loader w-full h-0.5"></div> : null
}
export default ShimmerLine
@@ -43,6 +43,8 @@ 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'
import ShimmerLine from 'components/ui/ShimmerLine'
import LoadingOpacity from 'components/ui/LoadingOpacity'
/**
* Acts as a container component for the entire log display
@@ -93,16 +95,22 @@ export const LogsExplorerPage: NextPage = () => {
}
const EditorControls = () => (
<div className="flex items-center gap-2">
<Button
type="default"
onClick={() => {
setEditorValue('')
setEditorId(uuidv4())
}}
>
Clear query
</Button>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Button
type="default"
onClick={() => {
setEditorValue('')
setEditorId(uuidv4())
}}
>
Clear query
</Button>
<Button type="default" disabled>
Save query
</Button>
</div>
<Button
type={editorValue ? 'alternative' : 'default'}
disabled={!editorValue}
@@ -117,7 +125,7 @@ export const LogsExplorerPage: NextPage = () => {
return (
<LogsExplorerLayout>
<div className="h-full flex flex-col flex-grow gap-4">
<div>
<div className="border rounded">
<LogPanel
isShowingEventChart={false}
onToggleEventChart={() => null}
@@ -138,7 +146,8 @@ export const LogsExplorerPage: NextPage = () => {
editorControls={<EditorControls />}
/>
<div className="min-h-[7rem] h-28">
<div className="min-h-[7rem] h-48">
<ShimmerLine active={isLoading} />
<CodeEditor
id={editorId}
language="pgsql"
@@ -149,19 +158,14 @@ export const LogsExplorerPage: NextPage = () => {
</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>
)}
<div className="flex flex-col flex-grow relative pb-8">
{/* <LogTable data={logData} isCustomQuery={true} /> */}
<LogTable data={logData} isCustomQuery={true} />
<LoadingOpacity active={isLoading}>
<div className="flex flex-grow h-full">
<LogTable data={logData} isCustomQuery={true} />
</div>
</LoadingOpacity>
{error && (
<div className="flex w-full h-full justify-center items-center mx-auto">
@@ -0,0 +1,49 @@
import React from 'react'
import { NextPage } from 'next'
import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import { IconCode, Badge } from '@supabase/ui'
import { withAuth } from 'hooks'
import { TEMPLATES } from 'components/interfaces/Settings/Logs'
import LogsExplorerLayout from 'components/layouts/LogsExplorerLayout/LogsExplorerLayout'
import CardButton from 'components/ui/CardButton'
export const LogsExplorerPage: NextPage = () => {
const router = useRouter()
const { ref } = router.query
return (
<LogsExplorerLayout>
<div>
<div className="grid grid-cols-3 gap-6">
{TEMPLATES.filter((template) => template.mode === 'custom').map((template) => {
return (
<CardButton
title={template.label}
icon={
<div
className="h-6 w-6 text-scale-100 flex items-center justify-center rounded
bg-scale-1200
transition-colors
duration-400
group-hover:bg-brand-900
group-hover:text-brand-1200
"
>
<div className="scale-100 group-hover:scale-110">
<IconCode size={12} strokeWidth={2} />
</div>
</div>
}
linkHref={`/project/${ref}/logs-explorer?q=${encodeURI(template.searchString)}`}
description={template.description}
/>
)
})}
</div>
</div>
</LogsExplorerLayout>
)
}
export default withAuth(observer(LogsExplorerPage))
+28
View File
@@ -26,6 +26,34 @@
}
}
.data-grid--logs-explorer {
.rdg-cell,
.rdg-cell span {
@apply text-scale-1100 font-normal #{!important};
border-left: none;
border-right: none;
}
.rdg-cell:first-child {
@apply pl-5;
}
.rdg-cell:last-child {
@apply pr-5;
}
.rdg-row {
&.rdg-row--focussed {
@apply border-r border-brand-900;
border-right-width: 4px;
}
&.rdg-row--focussed .rdg-cell,
&.rdg-row--focussed .rdg-cell span {
@apply text-scale-1200 font-semibold #{!important};
}
}
}
.syntax-highlight {
counter-reset: line-numbering;
font-family: Menlo, Monaco, monospace;