diff --git a/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/FunctionLogsSelectionRender.tsx b/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/FunctionLogsSelectionRender.tsx index e5de7278831..c1dd5e69609 100644 --- a/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/FunctionLogsSelectionRender.tsx +++ b/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/FunctionLogsSelectionRender.tsx @@ -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) => { <>
Event message -
{ +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 diff --git a/studio/components/interfaces/Settings/Logs/Logs.helpers.ts b/studio/components/interfaces/Settings/Logs/Logs.helpers.ts index e76e774bcdc..9345977cfff 100644 --- a/studio/components/interfaces/Settings/Logs/Logs.helpers.ts +++ b/studio/components/interfaces/Settings/Logs/Logs.helpers.ts @@ -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) => { diff --git a/studio/components/interfaces/Settings/Logs/Logs.types.ts b/studio/components/interfaces/Settings/Logs/Logs.types.ts index 9d2d9d9fc6d..d53f0eed6b4 100644 --- a/studio/components/interfaces/Settings/Logs/Logs.types.ts +++ b/studio/components/interfaces/Settings/Logs/Logs.types.ts @@ -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 +} diff --git a/studio/components/interfaces/Settings/Logs/LogsFormatters.tsx b/studio/components/interfaces/Settings/Logs/LogsFormatters.tsx index 5179bea8e74..c7c3f4dc96b 100644 --- a/studio/components/interfaces/Settings/Logs/LogsFormatters.tsx +++ b/studio/components/interfaces/Settings/Logs/LogsFormatters.tsx @@ -140,6 +140,17 @@ export const SeverityFormatter = ({ value }: { value: string }) => { ) break + case 'warning': + return ( +
+
+ +
+ {value} +
+ ) + break + // All other responses default: return ( diff --git a/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx b/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx index bfeab6f0bf9..49b87b2e79a 100644 --- a/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx +++ b/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx @@ -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 = ({ projectRef, queryType }) => { +export const LogsPreviewer: React.FC = ({ 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 = ({ projectRef, queryType }) => { ? `where REGEXP_CONTAINS(event_message, '${filterObj.search_query}')` : '' } - ${filterSqlWhereBuilder(whereFilters, table).join('\n')}`, + ${filterSqlWhereBuilder(whereFilters, table, override).join('\n')}`, }) useEffect(() => { diff --git a/studio/pages/project/[ref]/functions/[id]/invocations.tsx b/studio/pages/project/[ref]/functions/[id]/invocations.tsx index 7198899cda1..97efe358221 100644 --- a/studio/pages/project/[ref]/functions/[id]/invocations.tsx +++ b/studio/pages/project/[ref]/functions/[id]/invocations.tsx @@ -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 ( - + ) } diff --git a/studio/pages/project/[ref]/functions/[id]/logs.tsx b/studio/pages/project/[ref]/functions/[id]/logs.tsx index f7b9a87c026..8d27ced48c0 100644 --- a/studio/pages/project/[ref]/functions/[id]/logs.tsx +++ b/studio/pages/project/[ref]/functions/[id]/logs.tsx @@ -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 ( - + ) } diff --git a/studio/pages/project/[ref]/functions/index.tsx b/studio/pages/project/[ref]/functions/index.tsx index 394573c3a70..ed882f1269a 100644 --- a/studio/pages/project/[ref]/functions/index.tsx +++ b/studio/pages/project/[ref]/functions/index.tsx @@ -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 (
- 2 functions deployed + {`${functions.length} function${ + functions.length > 1 ? 's' : '' + } deployed`} } size="tiny" />
- Email + NameTrigger + StatusCreated - Runtime - Memory + Last updated + Version } body={ <> - {PageState.functions.length > 0 && - PageState.functions.map((x: any) => )} + {functions.length > 0 && + functions.map((item: any) => )} } /> @@ -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 = () => (
@@ -169,6 +204,13 @@ const PageLayout = () => {
) + if (functions.isLoading) + return ( + + loading + + ) + if (pageState === 'empty') { return ( @@ -179,7 +221,7 @@ const PageLayout = () => { } else { return ( - + ) diff --git a/studio/pages/project/[ref]/functions/interfaces/FunctionLayout.tsx b/studio/pages/project/[ref]/functions/interfaces/FunctionLayout.tsx index 3008a59f575..abf598adba9 100644 --- a/studio/pages/project/[ref]/functions/interfaces/FunctionLayout.tsx +++ b/studio/pages/project/[ref]/functions/interfaces/FunctionLayout.tsx @@ -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 ( + + loading + + ) + } + + const item = functions.byId(id) + + const name = item?.name || '' + + console.log('item', item) + return (
@@ -51,9 +75,9 @@ const PageLayout = ({ children }: { children?: React.ReactNode }) => { -
hello-world
+
{name}
- +
= ({ _function }) => { - const is_functionConfirmed = _function.email_confirmed_at || _function.phone_confirmed_at +const FunctionsListItem: FC = ({ 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 ( { - router.push(`/project/${ref}/functions/hello-world`) + router.push(`/project/${ref}/functions/${item.id}`) }} >
- {_function.name} + {item.name}
@@ -46,20 +50,25 @@ const FunctionsListItem: FC = ({ _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}`} + + {item.status} + - {dayjs(_function.created_at).format('DD MMM, YYYY HH:mm')} + {dayjs(item.created_at).format('DD MMM, YYYY HH:mm')} - {_function.runtime} + + {dayjs(item.updated_at).format('DD MMM, YYYY HH:mm')} + - {_function.memory} MB + Version {item.version}
) diff --git a/studio/pages/project/[ref]/functions/interfaces/FunctionsNav.tsx b/studio/pages/project/[ref]/functions/interfaces/FunctionsNav.tsx index bf4142dfa72..2d50fe9f7ac 100644 --- a/studio/pages/project/[ref]/functions/interfaces/FunctionsNav.tsx +++ b/studio/pages/project/[ref]/functions/interfaces/FunctionsNav.tsx @@ -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}`) } > diff --git a/studio/stores/RootStore.ts b/studio/stores/RootStore.ts index 57cc645b3af..6fda81a866e 100644 --- a/studio/stores/RootStore.ts +++ b/studio/stores/RootStore.ts @@ -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: '', diff --git a/studio/stores/functions/ProjectFunctionsStore.ts b/studio/stores/functions/ProjectFunctionsStore.ts new file mode 100644 index 00000000000..c23725c5dba --- /dev/null +++ b/studio/stores/functions/ProjectFunctionsStore.ts @@ -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 } + } + } +}