mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 21:59:16 +03:00
adjusted tier upgrade prompt to be based on project tier
This commit is contained in:
1 parent
8992cf1127
commit
6ed5a302bb
1 file changed
+376
@@ -0,0 +1,376 @@
|
||||
import useSWR, { KeyLoader } from 'swr'
|
||||
import React, { useEffect, useRef, 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,
|
||||
} 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,
|
||||
} 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 useProjectSubscription from 'hooks/misc/useProjectSubscription'
|
||||
import Link from 'next/link'
|
||||
|
||||
/**
|
||||
* 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 LogPage: NextPage = () => {
|
||||
const router = useRouter()
|
||||
const { ref, type, q, s, te } = router.query
|
||||
const [editorId, setEditorId] = useState<string>(uuidv4())
|
||||
const [editorValue, setEditorValue] = useState('')
|
||||
const [showChart, setShowChart] = useState(true)
|
||||
const [mode, setMode] = useState<'simple' | 'custom'>('simple')
|
||||
const [latestRefresh, setLatestRefresh] = useState<string>(new Date().toISOString())
|
||||
const [params, setParams] = useState({
|
||||
type: '',
|
||||
search_query: '',
|
||||
sql: '',
|
||||
where: '',
|
||||
timestamp_start: '',
|
||||
timestamp_end: '',
|
||||
})
|
||||
const title = `Logs - ${LOG_TYPE_LABEL_MAPPING[type as keyof typeof LOG_TYPE_LABEL_MAPPING]}`
|
||||
const checkIfSelectQuery = (value: string) =>
|
||||
value.toLowerCase().includes('select') ? true : false
|
||||
const isSelectQuery = checkIfSelectQuery(editorValue)
|
||||
const [sub] = useProjectSubscription(ref as string)
|
||||
const tier = sub ? sub?.tier.key : 'FREE'
|
||||
const tierName = sub ? sub?.tier.name : 'Free tier'
|
||||
const tierQueryWindow = {
|
||||
FREE: '1 day',
|
||||
PRO: '7 days',
|
||||
PAYG: '3 months',
|
||||
}[tier]
|
||||
|
||||
useEffect(() => {
|
||||
setParams({ ...params, type: type as string })
|
||||
}, [type])
|
||||
|
||||
useEffect(() => {
|
||||
// on mount, set initial values
|
||||
if (q) {
|
||||
onSelectTemplate({
|
||||
mode: 'custom',
|
||||
searchString: q as string,
|
||||
})
|
||||
} else if (s) {
|
||||
onSelectTemplate({
|
||||
mode: 'simple',
|
||||
searchString: s as string,
|
||||
})
|
||||
}
|
||||
if (te) {
|
||||
setParams((prev) => ({ ...prev, timestamp_end: te as string }))
|
||||
} else {
|
||||
setParams((prev) => ({ ...prev, timestamp_end: '' }))
|
||||
}
|
||||
}, [])
|
||||
|
||||
const genQueryParams = (params: { [k: string]: string }) => {
|
||||
// remove keys which are empty strings, null, or undefined
|
||||
for (const k in params) {
|
||||
const v = params[k]
|
||||
if (v === null || v === '' || isUndefined(v)) {
|
||||
delete params[k]
|
||||
}
|
||||
}
|
||||
const qs = new URLSearchParams(params).toString()
|
||||
return qs
|
||||
}
|
||||
// handle log fetching
|
||||
const getKeyLogs: SWRInfiniteKeyLoader = (_pageIndex: number, prevPageData) => {
|
||||
let queryParams
|
||||
// if prev page data is 100 items, could possibly have more records that are not yet fetched within this interval
|
||||
if (prevPageData === null) {
|
||||
// reduce interval window limit by using the timestamp of the last log
|
||||
queryParams = genQueryParams(params)
|
||||
} else if ((prevPageData?.data ?? []).length === 0) {
|
||||
// no rows returned, indicates that no more data to retrieve and append.
|
||||
return null
|
||||
} else {
|
||||
const len = prevPageData.data.length
|
||||
const { timestamp: tsLimit }: LogData = prevPageData.data[len - 1]
|
||||
// create new key from params
|
||||
queryParams = genQueryParams({ ...params, timestamp_end: String(tsLimit) })
|
||||
}
|
||||
|
||||
const logUrl = `${API_URL}/projects/${ref}/logs?${queryParams}`
|
||||
return logUrl
|
||||
}
|
||||
const {
|
||||
data = [],
|
||||
error: swrError,
|
||||
isValidating,
|
||||
mutate,
|
||||
size,
|
||||
setSize,
|
||||
} = useSWRInfinite<Logs>(getKeyLogs, get, { revalidateOnFocus: false })
|
||||
let logData: LogData[] = []
|
||||
let error: null | string = swrError ? swrError.message : null
|
||||
data.forEach((response: Logs) => {
|
||||
if (!error && response && response.data) {
|
||||
logData = [...logData, ...response.data]
|
||||
}
|
||||
if (!error && response && response.error) {
|
||||
error = response.error
|
||||
}
|
||||
})
|
||||
|
||||
const countUrl = `${API_URL}/projects/${ref}/logs?${genQueryParams({
|
||||
...params,
|
||||
count: String(true),
|
||||
period_start: String(latestRefresh),
|
||||
})}`
|
||||
const { data: countData } = useSWR<Count>(countUrl, get, { refreshInterval: 5000 })
|
||||
const newCount = countData?.data?.[0]?.count ?? 0
|
||||
|
||||
const handleRefresh = () => {
|
||||
setLatestRefresh(new Date().toISOString())
|
||||
setParams({ ...params, timestamp_end: '' })
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
te: undefined,
|
||||
},
|
||||
})
|
||||
setSize(1)
|
||||
}
|
||||
|
||||
const handleModeToggle = () => {
|
||||
if (mode === 'simple') {
|
||||
setMode('custom')
|
||||
// setWhere(DEFAULT_QUERY)
|
||||
} else {
|
||||
setMode('simple')
|
||||
}
|
||||
}
|
||||
|
||||
const onSelectTemplate = (template: LogTemplate) => {
|
||||
setMode(template.mode)
|
||||
if (template.mode === 'simple') {
|
||||
setParams((prev) => ({ ...prev, search_query: template.searchString, sql: '', where: '' }))
|
||||
} else {
|
||||
setEditorValue(template.searchString)
|
||||
setParams((prev) => ({
|
||||
...prev,
|
||||
where: checkIfSelectQuery(template.searchString)
|
||||
? ''
|
||||
: cleanEditorValue(template.searchString),
|
||||
sql: checkIfSelectQuery(template.searchString)
|
||||
? cleanEditorValue(template.searchString)
|
||||
: '',
|
||||
search_query: '',
|
||||
timestamp_end: '',
|
||||
}))
|
||||
setEditorId(uuidv4())
|
||||
}
|
||||
}
|
||||
const handleEditorSubmit = () => {
|
||||
setParams((prev) => ({
|
||||
...prev,
|
||||
where: isSelectQuery ? '' : cleanEditorValue(editorValue),
|
||||
sql: isSelectQuery ? cleanEditorValue(editorValue) : '',
|
||||
search_query: '',
|
||||
}))
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
q: editorValue,
|
||||
s: undefined,
|
||||
te: undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
const handleSearch: LogSearchCallback = ({ query, from, fromMicro }) => {
|
||||
const unixMicro = fromMicro ? fromMicro : dayjs(from).valueOf() * 1000
|
||||
setParams((prev) => ({
|
||||
...prev,
|
||||
search_query: query || '',
|
||||
timestamp_end: unixMicro ? String(unixMicro) : '',
|
||||
where: '',
|
||||
sql: '',
|
||||
}))
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
q: undefined,
|
||||
s: query || '',
|
||||
te: unixMicro,
|
||||
},
|
||||
})
|
||||
setEditorValue('')
|
||||
}
|
||||
const cleanEditorValue = (value: string) => {
|
||||
if (typeof value !== 'string') return value
|
||||
return value.replace(/\n/g, ' ')
|
||||
}
|
||||
return (
|
||||
<SettingsLayout title={title}>
|
||||
<div className="h-full flex flex-col flex-grow">
|
||||
<LogPanel
|
||||
isShowingEventChart={showChart}
|
||||
onToggleEventChart={() => setShowChart(!showChart)}
|
||||
isCustomQuery={mode === 'custom'}
|
||||
isLoading={isValidating}
|
||||
newCount={newCount}
|
||||
templates={TEMPLATES.filter((template) => template.for?.includes(type as string))}
|
||||
onRefresh={handleRefresh}
|
||||
onSearch={handleSearch}
|
||||
defaultSearchValue={params.search_query}
|
||||
defaultFromValue={
|
||||
params.timestamp_end ? dayjs(Number(params.timestamp_end) / 1000).toISOString() : ''
|
||||
}
|
||||
onCustomClick={handleModeToggle}
|
||||
onSelectTemplate={onSelectTemplate}
|
||||
/>
|
||||
{mode === 'custom' && (
|
||||
<React.Fragment>
|
||||
<div className="min-h-[7rem] h-28">
|
||||
<CodeEditor
|
||||
id={editorId}
|
||||
language="pgsql"
|
||||
defaultValue={editorValue}
|
||||
onInputChange={(v) => setEditorValue(v || '')}
|
||||
onInputRun={handleRefresh}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-end items-center px-2 py-1 w-full">
|
||||
<div className="flex flex-row gap-x-2 justify-end p-2">
|
||||
{editorValue && (
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() => {
|
||||
setEditorValue('')
|
||||
setEditorId(uuidv4())
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
<Button type={editorValue ? 'secondary' : 'text'} onClick={handleEditorSubmit}>
|
||||
Run
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{showChart && mode !== 'custom' && (
|
||||
<div>
|
||||
<LogEventChart
|
||||
data={!isValidating ? logData : undefined}
|
||||
onBarClick={(timestampMicro) => {
|
||||
handleSearch({ query: params.search_query, fromMicro: timestampMicro })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col flex-grow relative">
|
||||
{isValidating && (
|
||||
<div
|
||||
className={[
|
||||
'absolute top-0 w-full h-full bg-gray-800 flex items-center justify-center',
|
||||
`${isValidating ? 'bg-opacity-75 z-50' : ''}`,
|
||||
].join(' ')}
|
||||
>
|
||||
<IconLoader className="animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="flex w-full h-full justify-center items-center mx-auto">
|
||||
<Card className="flex flex-col gap-y-2 w-1/3">
|
||||
<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>
|
||||
<details className="cursor-pointer">
|
||||
<summary>
|
||||
<Typography.Text type="secondary">Error Message</Typography.Text>
|
||||
</summary>
|
||||
<Typography.Text className="block whitespace-pre-wrap" small code type="warning">
|
||||
{JSON.stringify(error, null, 2)}
|
||||
</Typography.Text>
|
||||
</details>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
<LogTable data={logData} isCustomQuery={mode === 'custom'} />
|
||||
{/* Footer section of log ui, appears below table */}
|
||||
<div className="p-2 flex flex-row gap-2 justify-between w-full">
|
||||
{!isSelectQuery && (
|
||||
<Button
|
||||
// trigger page increase
|
||||
onClick={() => setSize(size + 1)}
|
||||
icon={<IconRewind />}
|
||||
type="secondary"
|
||||
>
|
||||
Load older
|
||||
</Button>
|
||||
)}
|
||||
<InformationBox
|
||||
className="shrink text-grey-700"
|
||||
block={false}
|
||||
size="tiny"
|
||||
icon={<IconInfo size="tiny" />}
|
||||
title={
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<Typography.Text>
|
||||
{`${tierName} is limited to a ${tierQueryWindow} query window`}
|
||||
</Typography.Text>
|
||||
{tier !== 'PAYG' && (
|
||||
<Link href={`/project/${ref}/settings/billing`}>
|
||||
<Button size="tiny">Upgrade</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
hideCollapse
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default withAuth(observer(LogPage))
|
||||
Reference in new issue
Block a user