mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 10:55:06 +03:00
chore: started UI changes on log explorer. adding improved loading state. added dedicated templates page
This commit is contained in:
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
|
||||
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user