From 6ed5a302bb40b6a08ef99126b2def8ba2d9ba75b Mon Sep 17 00:00:00 2001 From: TzeYiing Date: Wed, 2 Mar 2022 19:06:39 +0800 Subject: [PATCH] adjusted tier upgrade prompt to be based on project tier --- .../project/[ref]/settings/logs/[type].tsx | 376 ++++++++++++++++++ 1 file changed, 376 insertions(+) create mode 100644 studio/pages/project/[ref]/settings/logs/[type].tsx diff --git a/studio/pages/project/[ref]/settings/logs/[type].tsx b/studio/pages/project/[ref]/settings/logs/[type].tsx new file mode 100644 index 00000000000..a27a14be23b --- /dev/null +++ b/studio/pages/project/[ref]/settings/logs/[type].tsx @@ -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(uuidv4()) + const [editorValue, setEditorValue] = useState('') + const [showChart, setShowChart] = useState(true) + const [mode, setMode] = useState<'simple' | 'custom'>('simple') + const [latestRefresh, setLatestRefresh] = useState(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(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(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 ( + +
+ 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' && ( + +
+ setEditorValue(v || '')} + onInputRun={handleRefresh} + /> +
+
+
+ {editorValue && ( + + )} + +
+
+
+ )} + {showChart && mode !== 'custom' && ( +
+ { + handleSearch({ query: params.search_query, fromMicro: timestampMicro }) + }} + /> +
+ )} +
+ {isValidating && ( +
+ +
+ )} + {error && ( +
+ +
+ + + Sorry! An error occured when fetching data. + +
+
+ + Error Message + + + {JSON.stringify(error, null, 2)} + +
+
+
+ )} + + {/* Footer section of log ui, appears below table */} +
+ {!isSelectQuery && ( + + )} + } + title={ +
+ + {`${tierName} is limited to a ${tierQueryWindow} query window`} + + {tier !== 'PAYG' && ( + + + + )} +
+ } + hideCollapse + /> +
+
+
+
+ ) +} + +export default withAuth(observer(LogPage))