diff --git a/studio/components/interfaces/Settings/Logs/LogPanel.tsx b/studio/components/interfaces/Settings/Logs/LogPanel.tsx index d2e1b62979b..7f4d8eb7df1 100644 --- a/studio/components/interfaces/Settings/Logs/LogPanel.tsx +++ b/studio/components/interfaces/Settings/Logs/LogPanel.tsx @@ -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 = ({ onSelectTemplate, isShowingEventChart, onToggleEventChart, + + editorControls, }) => { const [search, setSearch] = useState('') const [to, setTo] = useState({ value: '', error: '' }) @@ -101,8 +106,15 @@ const LogPanel: FC = ({ const showReset = to.value !== '' || from.value !== '' return ( -
-
+
+
{!isCustomQuery && ( @@ -254,6 +266,7 @@ const LogPanel: FC = ({ )}
+ {editorControls}
) diff --git a/studio/components/interfaces/Settings/Logs/PreviewFilterPanel.tsx b/studio/components/interfaces/Settings/Logs/PreviewFilterPanel.tsx new file mode 100644 index 00000000000..590decbf4a3 --- /dev/null +++ b/studio/components/interfaces/Settings/Logs/PreviewFilterPanel.tsx @@ -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 = ({ + 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) => { + 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 ? ( +
+ } + loading={isLoading} + onClick={onRefresh} + > + Refresh + + ) : null + } + + const showFromReset = to.value !== '' + + return ( +
+
+
+ +
+ ) +} + +export default PreviewFilterPanel diff --git a/studio/components/layouts/LogsExplorerLayout/LogsExplorerLayout.tsx b/studio/components/layouts/LogsExplorerLayout/LogsExplorerLayout.tsx new file mode 100644 index 00000000000..99927d0b14d --- /dev/null +++ b/studio/components/layouts/LogsExplorerLayout/LogsExplorerLayout.tsx @@ -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 ( + +
+
+
+
+ +
+

Logs Explorer

+
+ {/* */} +
+
+ {children} +
+
+
+ ) +} + +export default withAuth(observer(PageLayout)) diff --git a/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx b/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx index ef988c39e1a..0586427113b 100644 --- a/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx +++ b/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx @@ -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: , + link: `/project/${ref}/logs-explorer`, + }, + ] + : []), ...(IS_PLATFORM ? [ { diff --git a/studio/pages/project/[ref]/logs-explorer/index.tsx b/studio/pages/project/[ref]/logs-explorer/index.tsx new file mode 100644 index 00000000000..6fa7b21f94c --- /dev/null +++ b/studio/pages/project/[ref]/logs-explorer/index.tsx @@ -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(uuidv4()) + const [editorValue, setEditorValue] = useState('') + + 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 = () => ( +
+ + +
+ ) + + return ( + +
+
+ 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={} + /> + +
+ setEditorValue(v || '')} + onInputRun={handleEditorSubmit} + /> +
+
+ +
+ {isLoading && ( +
+ +
+ )} + + + + {error && ( +
+ +
+ + + Sorry! An error occured when fetching data. + +
+ +
+
+ )} +
+
+
+ ) +} + +export default withAuth(observer(LogsExplorerPage)) diff --git a/studio/pages/project/[ref]/settings/logs/[type].tsx b/studio/pages/project/[ref]/settings/logs/[type].tsx index 73320beb67d..b1bd96e9d76 100644 --- a/studio/pages/project/[ref]/settings/logs/[type].tsx +++ b/studio/pages/project/[ref]/settings/logs/[type].tsx @@ -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 ( -
- + 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} />