chore: log pages for functions now show the correct logs for a specific function

This commit is contained in:
Jonathan Summers-Muir authored and TzeYiing committed 2022-03-30 14:39:18 +02:00
1 parent 9f6d82fd12
commit 0834a2cdda
14 files changed
+381 -71

No files matched your search

@@ -3,9 +3,6 @@ import { jsonSyntaxHighlight, ResponseCodeFormatter, SeverityFormatter } from '.
const FunctionLogsSelectionRender = ({ log }: any) => {
const request = log?.request
const response = log?.response
const requestUrl = request?.url
const host = request?.host
const DetailedRow = ({
label,
@@ -30,7 +27,6 @@ const FunctionLogsSelectionRender = ({ log }: any) => {
<>
<div className="px-5">
<span className="text-scale-900 text-sm col-span-4">Event message</span>
<div
className="text-xs text-wrap font-mono text-scale-1200 mt-2"
dangerouslySetInnerHTML={{
@@ -177,7 +177,11 @@ export const LOG_TYPE_LABEL_MAPPING: { [k: string]: string } = {
database: 'Database',
}
export const genDefaultQuery = (table: LogsTableName, where: string | undefined) => {
export const genDefaultQuery = (
table: LogsTableName,
where: string | undefined,
idFilter?: string
) => {
switch (table) {
case 'edge_logs':
return `SELECT id, timestamp, event_message, metadata, request, response, request.method, request.path, response.status_code
@@ -1,18 +1,32 @@
import { LogsTableName, SQL_FILTER_TEMPLATES } from './Logs.constants'
import { FilterObject } from './Logs.types'
import { FilterObject, Override } from './Logs.types'
export function filterSqlWhereBuilder(
filters: FilterObject | any,
table: LogsTableName,
override: Override
) {
let count = 0
// remove any filter arrays that are empty
const filtersSanitized: any = Object.values(filters).filter((x: any) => x && x.length > 0)
const keys = Object.keys(filters)
console.log('override first', override)
const hasOverride = override && override.value && override.key
// build array of where statements
// first item is `where` if there are any filters
let whereArray: string[] = filtersSanitized.length > 0 ? ['where'] : []
let whereArray: string[] = filtersSanitized.length > 0 || hasOverride ? ['where'] : []
if (filtersSanitized.length === 0 && !hasOverride) return whereArray
if (override.key && override) {
console.log('override', override)
count = +1
const sql = `${override.key}='${override.value}'`
whereArray.push(sql)
}
if (filtersSanitized.length === 0) return whereArray
@@ -20,7 +34,6 @@ export function filterSqlWhereBuilder(
// do not parse empty key
if (!x) return
let count = 0
const filterKeyArray: string[] = []
filters[x].map((value: string, i: number) => {
@@ -21,8 +21,8 @@ export interface LogData {
id: string
timestamp: number
event_message: string
metadata: Metadata,
[other: string]: unknown;
metadata: Metadata
[other: string]: unknown
}
export interface LogTemplate {
@@ -64,3 +64,8 @@ export interface FilterObject {
// `te` for timestamp start value.
te?: string
}
export type Override = {
key: string
value: string | string[] | undefined
}
@@ -140,6 +140,17 @@ export const SeverityFormatter = ({ value }: { value: string }) => {
)
break
case 'warning':
return (
<div className="flex items-center h-full gap-1">
<div className=" p-0.5 rounded !text-amber-900">
<IconAlertCircle size={14} strokeWidth={2} />
</div>
<span className="!text-amber-900 !block titlecase">{value}</span>
</div>
)
break
// All other responses
default:
return (
@@ -25,6 +25,7 @@ import useLogsPreview from 'hooks/analytics/useLogsPreview'
import PreviewFilterPanel from 'components/interfaces/Settings/Logs/PreviewFilterPanel'
import { LOGS_TABLES } from './Logs.constants'
import { Override } from './Logs.types'
/**
* Acts as a container component for the entire log display
@@ -40,16 +41,15 @@ import { LOGS_TABLES } from './Logs.constants'
interface Props {
projectRef: string
queryType: QueryType
override: Override
}
export const LogsPreviewer: React.FC<Props> = ({ projectRef, queryType }) => {
export const LogsPreviewer: React.FC<Props> = ({ projectRef, queryType, override }) => {
const router = useRouter()
const { s, te, ts } = router.query
const [showChart, setShowChart] = useState(true)
const [whereFilters, dispatchWhereFilters] = useReducer(filterReducer, {})
// const table = queryType === 'api' ? LogsTableName.EDGE : LogsTableName.POSTGRES
console.log('queryType', queryType)
const table = LOGS_TABLES[queryType]
@@ -63,7 +63,7 @@ export const LogsPreviewer: React.FC<Props> = ({ projectRef, queryType }) => {
? `where REGEXP_CONTAINS(event_message, '${filterObj.search_query}')`
: ''
}
${filterSqlWhereBuilder(whereFilters, table).join('\n')}`,
${filterSqlWhereBuilder(whereFilters, table, override).join('\n')}`,
})
useEffect(() => {
@@ -37,13 +37,17 @@ import FunctionLayout from '../interfaces/FunctionLayout'
*/
export const LogPage: NextPage = () => {
const router = useRouter()
const { ref, type } = router.query
const { ref, type, id } = router.query
const title = `Logs - ${LOG_TYPE_LABEL_MAPPING[type as keyof typeof LOG_TYPE_LABEL_MAPPING]}`
return (
<FunctionLayout title={title}>
<LogsPreviewer projectRef={ref as string} queryType={'fn_edge'} />
<LogsPreviewer
projectRef={ref as string}
queryType={'fn_edge'}
override={{ key: 'metadata.function_id', value: id }}
/>
</FunctionLayout>
)
}
@@ -26,7 +26,7 @@ import React from 'react'
import { NextPage } from 'next'
import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import { withAuth } from 'hooks'
import { useStore, withAuth } from 'hooks'
import { SettingsLayout } from 'components/layouts/'
import { LOG_TYPE_LABEL_MAPPING, QueryType } from 'components/interfaces/Settings/Logs'
import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer'
@@ -37,13 +37,21 @@ import FunctionLayout from '../interfaces/FunctionLayout'
*/
export const LogPage: NextPage = () => {
const router = useRouter()
const { ref, type } = router.query
const { ref, type, id } = router.query
const { functions } = useStore()
console.log('id', id)
const title = `Logs - ${LOG_TYPE_LABEL_MAPPING[type as keyof typeof LOG_TYPE_LABEL_MAPPING]}`
return (
<FunctionLayout title={title}>
<LogsPreviewer projectRef={ref as string} queryType={'functions'} />
<LogsPreviewer
projectRef={ref as string}
queryType={'functions'}
override={{ key: 'metadata.function_id', value: id }}
/>
</FunctionLayout>
)
}
+77 -35
View File
@@ -1,5 +1,5 @@
import { observer } from 'mobx-react-lite'
import { withAuth } from 'hooks'
import { useStore, withAuth } from 'hooks'
import FunctionsLayout from './interfaces/FunctionsLayout'
import {
@@ -9,11 +9,18 @@ import {
IconGlobe,
IconSearch,
Input,
Loading,
} from '@supabase/ui'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import Table from 'components/to-be-cleaned/Table'
import { memoryUsage } from 'process'
import FunctionsListItem from './interfaces/FunctionsListItem'
import { useRouter } from 'next/router'
import { post } from 'lib/common/fetch'
import { API_URL, PROJECT_STATUS } from 'lib/constants'
import { useProjectFunctionsStore } from 'stores/projectFunctionsStore'
import { toJS } from 'mobx'
const EmptyFunctions = () => {
return (
@@ -103,54 +110,67 @@ const EmptyFunctions = () => {
)
}
const FunctionsList = () => {
const PageState = {
functions: [
{
name: 'hello-world',
id: 1,
runtime: 'Node14',
memory: 256,
created_at: 1646524256,
},
{
name: 'charge',
id: 2,
runtime: 'Node14',
memory: 256,
created_at: 1646524256,
},
{
name: 'projects',
id: 3,
runtime: 'Node14',
memory: 512,
created_at: 1646524256,
},
],
}
interface Function {
id: string
slug: string
name: string
version: 9
status: 'ACTIVE' | 'INACTIVE' | 'THROTTLE'
created_at: number
updated_at: number
}
const FunctionsList = ({ functions }: { functions: Function[] }) => {
// const PageState = {
// functions: [
// {
// name: 'hello-world',
// id: 1,
// runtime: 'Node14',
// memory: 256,
// created_at: 1646524256,
// },
// {
// name: 'charge',
// id: 2,
// runtime: 'Node14',
// memory: 256,
// created_at: 1646524256,
// },
// {
// name: 'projects',
// id: 3,
// runtime: 'Node14',
// memory: 512,
// created_at: 1646524256,
// },
// ],
// }
return (
<div className="flex flex-col gap-3">
<div className="flex justify-between items-center">
<span className="text-sm text-scale-900">2 functions deployed</span>
<span className="text-sm text-scale-900">{`${functions.length} function${
functions.length > 1 ? 's' : ''
} deployed`}</span>
<Input icon={<IconSearch size={14} />} size="tiny" />
</div>
<div>
<Table
head={
<>
<Table.th>Email</Table.th>
<Table.th>Name</Table.th>
<Table.th>Trigger</Table.th>
<Table.th>Status</Table.th>
<Table.th className="hidden 2xl:table-cell">Created</Table.th>
<Table.th className="hidden xl:table-cell">Runtime</Table.th>
<Table.th className="hidden lg:table-cell">Memory</Table.th>
<Table.th className="hidden 2xl:table-cell">Last updated</Table.th>
<Table.th className="hidden 2xl:table-cell">Version</Table.th>
</>
}
body={
<>
{PageState.functions.length > 0 &&
PageState.functions.map((x: any) => <FunctionsListItem key={x.id} _function={x} />)}
{functions.length > 0 &&
functions.map((item: any) => <FunctionsListItem key={item.id} function={item} />)}
</>
}
/>
@@ -160,8 +180,23 @@ const FunctionsList = () => {
}
const PageLayout = () => {
const router = useRouter()
const { ui, functions } = useStore()
const [pageState, setPageState] = useState('list')
const project = ui.selectedProject
useEffect(() => {
if (project && project.status === PROJECT_STATUS.INACTIVE) {
post(`${API_URL}/projects/${ui.selectedProject}/restore`, {})
}
}, [project])
useEffect(() => {
functions.load()
}, [ui.selectedProject])
const Selection = () => (
<div>
<Button onClick={() => setPageState('empty')}>empty list</Button>
@@ -169,6 +204,13 @@ const PageLayout = () => {
</div>
)
if (functions.isLoading)
return (
<FunctionsLayout>
<Loading active={true}>loading</Loading>
</FunctionsLayout>
)
if (pageState === 'empty') {
return (
<FunctionsLayout>
@@ -179,7 +221,7 @@ const PageLayout = () => {
} else {
return (
<FunctionsLayout>
<FunctionsList />
<FunctionsList functions={functions.list()} />
<Selection />
</FunctionsLayout>
)
@@ -1,12 +1,36 @@
import BaseLayout from 'components/layouts'
import { observer } from 'mobx-react-lite'
import { useStore, withAuth } from 'hooks'
import { IconCode, IconSlash, Tabs } from '@supabase/ui'
import { IconCode, IconSlash, Loading, Tabs } from '@supabase/ui'
import { Tab } from '@headlessui/react'
import FunctionsNav from './FunctionsNav'
import React from 'react'
import React, { useEffect } from 'react'
import { useRouter } from 'next/router'
const PageLayout = ({ children }: { children?: React.ReactNode }) => {
const { functions, ui } = useStore()
const router = useRouter()
const { id } = router.query
useEffect(() => {
functions.load()
}, [ui.selectedProject])
if (!functions.isInitialized) {
return (
<BaseLayout>
<Loading active={true}>loading</Loading>
</BaseLayout>
)
}
const item = functions.byId(id)
const name = item?.name || ''
console.log('item', item)
return (
<BaseLayout>
<div className="h-full flex flex-col flex-grow py-10">
@@ -51,9 +75,9 @@ const PageLayout = ({ children }: { children?: React.ReactNode }) => {
<path d="M16 3.549L7.12 20.600"></path>
</svg>
</span>
<h5 className="text-2xl text-scale-1200">hello-world</h5>
<h5 className="text-2xl text-scale-1200">{name}</h5>
</div>
<FunctionsNav />
<FunctionsNav item={item} />
</div>
<div
// weird repetative styling
@@ -13,28 +13,32 @@ import Table from 'components/to-be-cleaned/Table'
import SimpleCodeBlock from 'components/to-be-cleaned/SimpleCodeBlock'
import router, { useRouter } from 'next/router'
import { useStore } from 'hooks'
// import _functionDropdown from './_functionDropdown'
interface Props {
_function: any
function: any
}
const FunctionsListItem: FC<Props> = ({ _function }) => {
const is_functionConfirmed = _function.email_confirmed_at || _function.phone_confirmed_at
const FunctionsListItem: FC<Props> = ({ function: item }) => {
const { ui } = useStore()
console.log('_function', item)
const is_functionConfirmed = item.email_confirmed_at || item.phone_confirmed_at
const router = useRouter()
const { ref } = router.query
const ref = ui?.selectedProject?.ref
return (
<Table.tr
key={_function.id}
key={item.id}
onClick={() => {
router.push(`/project/${ref}/functions/hello-world`)
router.push(`/project/${ref}/functions/${item.id}`)
}}
>
<Table.td className="whitespace-nowrap">
<div className="flex items-center gap-2">
<span className="text-base text-scale-1200">{_function.name}</span>
<span className="text-base text-scale-1200">{item.name}</span>
</div>
</Table.td>
<Table.td className="whitespace-nowrap flex flex-col gap-0">
@@ -46,20 +50,25 @@ const FunctionsListItem: FC<Props> = ({ _function }) => {
className="text-xs text-scale-900 flex gap-2 items-center
font-mono"
>
https://adlnssnsfsfsfdf.supabase.com/functions/{_function.name}
{`https://${ref}.supabase.com/functions/${item.name}`}
<IconClipboard size={12} />
</span>
</Table.td>
<Table.td className="hidden 2xl:table-cell">
<span className="text-scale-1100">{item.status}</span>
</Table.td>
<Table.td className="hidden 2xl:table-cell">
<span className="text-scale-1100">
{dayjs(_function.created_at).format('DD MMM, YYYY HH:mm')}
{dayjs(item.created_at).format('DD MMM, YYYY HH:mm')}
</span>
</Table.td>
<Table.td className="hidden 2xl:table-cell">
<span className="text-scale-1100">{_function.runtime}</span>
<span className="text-scale-1100">
{dayjs(item.updated_at).format('DD MMM, YYYY HH:mm')}
</span>
</Table.td>
<Table.td className="hidden 2xl:table-cell">
<span className="text-scale-1100">{_function.memory} MB</span>
<span className="text-scale-1100">Version {item.version}</span>
</Table.td>
</Table.tr>
)
@@ -1,7 +1,7 @@
import { Tabs } from '@supabase/ui'
import { useRouter } from 'next/router'
const FunctionsNav = () => {
const FunctionsNav = ({ item }) => {
const router = useRouter()
const activeRoute = router.pathname.split('/')[5]
const { ref } = router.query
@@ -14,7 +14,7 @@ const FunctionsNav = () => {
size="medium"
activeId={!activeRoute ? 'metrics' : activeRoute}
onChange={(e: string) =>
router.push(`/project/${ref}/functions/hello-world/${e === 'metrics' ? '' : e}`)
router.push(`/project/${ref}/functions/${item.id}/${e === 'metrics' ? '' : e}`)
}
>
<Tabs.Panel id="metrics" label="Metrics" />
+6
View File
@@ -3,6 +3,7 @@ import AppStore, { IAppStore } from './app/AppStore'
import MetaStore, { IMetaStore } from './pgmeta/MetaStore'
import UiStore, { IUiStore } from './UiStore'
import ProjectContentStore, { IProjectContentStore } from './content/ProjectContentStore'
import ProjectFunctionsStore, { IProjectFunctionsStore } from './functions/ProjectFunctionsStore'
// Temporary disable mobx warnings
// TODO: need to remove this after refactoring old stores.
@@ -13,6 +14,7 @@ configure({
export interface IRootStore {
ui: IUiStore
content: IProjectContentStore
functions: IProjectFunctionsStore
meta: IMetaStore
app: IAppStore
setProjectRef: (value?: string) => void
@@ -21,12 +23,14 @@ export interface IRootStore {
export class RootStore implements IRootStore {
ui: IUiStore
content: IProjectContentStore
functions: IProjectFunctionsStore
meta: IMetaStore
app: IAppStore
constructor() {
this.ui = new UiStore(this)
this.content = new ProjectContentStore(this, { projectRef: '' })
this.functions = new ProjectFunctionsStore(this, { projectRef: '' })
this.meta = new MetaStore(this, {
projectRef: '',
connectionString: '',
@@ -46,12 +50,14 @@ export class RootStore implements IRootStore {
(selectedProject) => {
if (selectedProject) {
this.content = new ProjectContentStore(this, { projectRef: selectedProject.ref })
this.functions = new ProjectFunctionsStore(this, { projectRef: selectedProject.ref })
this.meta = new MetaStore(this, {
projectRef: selectedProject.ref,
connectionString: selectedProject.connectionString ?? '',
})
} else {
this.content = new ProjectContentStore(this, { projectRef: '' })
this.functions = new ProjectFunctionsStore(this, { projectRef: '' })
this.meta = new MetaStore(this, {
projectRef: '',
connectionString: '',
@@ -0,0 +1,188 @@
import { makeAutoObservable, toJS } from 'mobx'
import { keyBy } from 'lodash'
import { get, post, patch, delete_ } from 'lib/common/fetch'
import { UserContent, UserContentMap } from 'types'
import { IRootStore } from '../RootStore'
// [Joshen] This will be the new ProjectContentStore
// but use the one under the stores folder projectContentStore first while we transition
export interface IProjectFunctionsStore {
isLoading: boolean
isInitialized: boolean
error: any
load: () => void
list: (filter?: any) => any[]
byId: (id?: any) => []
reports: (filter?: any) => any[]
sqlSnippets: (filter?: any) => any[]
}
export default class ProjectFunctionsStore implements IProjectFunctionsStore {
rootStore: IRootStore
STATES = {
INITIAL: 'initial',
LOADING: 'loading',
ERROR: 'error',
LOADED: 'loaded',
}
baseUrl: string
data: UserContentMap = []
state = this.STATES.INITIAL
error = null
constructor(rootStore: IRootStore, options: { projectRef: string }) {
const { projectRef } = options
this.rootStore = rootStore
this.baseUrl = `${process.env.NEXT_PUBLIC_API_ADMIN_URL}/projects/${projectRef}/functions`
makeAutoObservable(this)
}
get isLoading() {
return this.state === this.STATES.INITIAL || this.state === this.STATES.LOADING
}
get isInitialized() {
return this.state !== this.STATES.INITIAL
}
async fetchData() {
const headers = {
'Content-Type': 'application/json',
}
const response = await get(this.baseUrl, { headers })
if (response.error) {
throw response.error
}
this.data = keyBy(response, 'id')
return response
}
async load() {
let { LOADING, ERROR, LOADED } = this.STATES
try {
this.error = null
this.state = LOADING
await this.fetchData()
this.state = LOADED
return this.data
} catch (e: any) {
console.error('Failed to load project content', e.message)
this.error = e
this.state = ERROR
}
}
async loadPersistentData() {}
async loadRemotePersistentData(userId: any) {
const sqlSnippets = this.sqlSnippets((x: any) => x.owner_id === userId)
}
list(filter?: any) {
const arr = Object.values(this.data)
if (!!filter) {
return arr.filter(filter).sort((a, b) => a.name.localeCompare(b.name))
} else {
return arr.sort((a, b) => a.name.localeCompare(b.name))
}
}
byId(id: any) {
// console.log('this.data', toJS(this.data))
// return toJS(this.data).find((x: any) => x.id === id)
return toJS(this.data)[id]
}
reports(filter?: any) {
let arr = Object.values(this.data)
if (!!filter) {
return arr
.filter((x) => x.type == 'report')
.filter(filter)
.sort((a, b) => a.name.localeCompare(b.name))
} else {
return arr.filter((x) => x.type == 'report').sort((a, b) => a.name.localeCompare(b.name))
}
}
sqlSnippets(filter?: any) {
let arr = Object.values(this.data)
if (!!filter) {
const arr_filtered = arr
.filter((x) => x.type == 'sql')
.filter(filter)
.sort((a, b) => a.name.localeCompare(b.name))
return arr_filtered
} else {
const arr_filtered = arr
.filter((x) => x.type == 'sql')
.sort((a, b) => a.name.localeCompare(b.name))
return arr_filtered
}
}
async create(payload: any) {
try {
const headers = {
'Content-Type': 'application/json',
}
const created = await post(this.baseUrl, payload, { headers })
if (created.error) throw created.error
this.data[created['id']] = created
return { data: created, error: null }
} catch (error) {
return { data: null, error }
}
}
async update(id: any, updates: any, type: UserContent['type']) {
try {
const headers = {
'Content-Type': 'application/json',
}
let payload = {
...updates,
type,
id,
}
const url = `${this.baseUrl}?id=${id}`
const updated = await patch(url, payload, { headers })
if (updated.error) throw updated.error
this.data[id] = updated
return { data: updated, error: null }
} catch (error) {
return { data: null, error }
}
}
async updateSql(id: any, updates: any) {
return this.update(id, updates, 'sql')
}
async updateReport(id: any, updates: any) {
return this.update(id, updates, 'report')
}
async del(id: any) {
try {
const headers = {
'Content-Type': 'application/json',
}
const url = `${this.baseUrl}?id=${id}`
const deleted = await delete_(url, {}, { headers })
if (deleted.error) throw deleted.error
delete this.data[id]
return { data: true, error: null }
} catch (error) {
return { data: false, error }
}
}
}