Pull master and resolve conflicts

This commit is contained in:
Joshen Lim committed 2022-04-21 15:09:32 +08:00
commit 67fe855a4e
32 files changed
+976 -515

No files matched your search

+4 -4
View File
@@ -10,10 +10,10 @@
- [x] ஹோஸ்ட் செய்யப்பட்ட பூஸ்ட்ஜீஆர்இஎஸ் தரவுத்தளம். [ஆவணம்](https://supabase.com/docs/guides/database)
- [x] அங்கீகாரம் மற்றும் அங்கீகாரம்.. [ஆவணம்](https://supabase.com/docs/guides/auth)
- [x] தானாக உருவாக்கப்பட்ட ஏபிஐ கள்.
- [x] தானாக உருவாக்கப்பட்ட ஏபிஐ கள்.
- [x] ராஸ்ட். [ஆவணம்](https://supabase.com/docs/guides/api#rest-api)
- [x] நிகழ்நேர சந்தாக்கள். [ஆவணம்](https://supabase.com/docs/guides/api#realtime-api)
- [x] கிராப்க்யூஎல் (பீட்டா). [ஆவணம்](https://supabase.com/docs/guides/api#graphql-api)
- [x] கிராப்க்யூஎல் (பீட்டா). [ஆவணம்](https://supabase.com/docs/guides/api#graphql-api)
- [x] செயல்பாடுகள்.
- [x] தரவுத்தளம் செயல்பாடுகள். [ஆவணம்](https://supabase.com/docs/guides/database/functions)
- [x] விளிம்பு செயல்பாடுகள் [ஆவணம்](https://supabase.com/docs/guides/functions)
@@ -45,7 +45,7 @@
நாங்கள் தற்போது பொது பீட்டாவில் இருக்கிறோம். முக்கிய துப்பிப்புகளைப் பற்றி அறிவிக்க இந்த ரெப்போவின் "வெளியீடுகளைப்" பாருங்கள்.
<kbd><img src="https://gitcdn.link/repo/supabase/supabase/master/web/static/watch-repo.gif" alt="இந்த ரெப்போவைப் பாருங்கள்"/></kbd>
---
## இது எவ்வாறு செயல்படுகிறது
@@ -182,8 +182,8 @@
</table>
## மொழிபெயர்ப்புகள்
- [மொழிபெயர்ப்புகளின் பட்டியல்](/i18n/languages.md) <!--- Keep only this -->
- [மொழிபெயர்ப்புகளின் பட்டியல்](/i18n/languages.md) <!--- Keep only this -->
## ஸ்பான்சர்கள்
@@ -1,15 +1,6 @@
import dayjs from 'dayjs'
import { useEffect, useState, useMemo } from 'react'
import {
Alert,
Button,
IconDownloadCloud,
IconEye,
IconEyeOff,
IconLoader,
Input,
Typography,
} from '@supabase/ui'
import { Alert, Button, IconEye, IconEyeOff, Input } from '@supabase/ui'
import DataGrid from '@supabase/react-data-grid'
import LogSelection from './LogSelection'
@@ -262,52 +253,69 @@ const LogTable = ({
!isLoading ? (
<>
<div className="py-4 w-full h-full flex-col space-y-12">
<div
className={[
'transition-all duration-500 delay-200',
'flex flex-col items-center gap-6 text-center',
'mt-16 opacity-100 scale-100 justify-center',
].join(' ')}
>
<div className="flex flex-col gap-1">
<div className="relative border border-scale-600 border-dashed dark:border-scale-400 w-32 h-4 rounded px-2 flex items-center"></div>
<div className="relative border border-scale-600 border-dashed dark:border-scale-400 w-32 h-4 rounded px-2 flex items-center">
<div className="absolute right-1 -bottom-4">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="2"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{!error && (
<div
className={`transition-all
duration-500
delay-200
flex
flex-col
items-center
gap-6
text-center
mt-16
opacity-100
scale-100
justify-center
`}
>
<>
<div className="flex flex-col gap-1">
<div className="relative border border-scale-600 border-dashed dark:border-scale-400 w-32 h-4 rounded px-2 flex items-center"></div>
<div className="relative border border-scale-600 border-dashed dark:border-scale-400 w-32 h-4 rounded px-2 flex items-center">
<div className="absolute right-1 -bottom-4">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="2"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
</div>
</div>
</div>
<div className="flex flex-col gap-1 px-5">
<h3 className="text-lg text-scale-1200">No results</h3>
<p className="text-sm text-scale-900">
Try another search, or adjusting the filters
</p>
</div>
</>
</div>
<div className="flex flex-col gap-1 px-5">
<h3 className="text-lg text-scale-1200">No results</h3>
<p className="text-sm text-scale-900">
Try another search, or adjusting the filters
</p>
</div>
</div>
)}
{error && (
<div className="flex justify-center px-5">
<Alert
variant="danger"
title="Sorry! An error occured when fetching data."
withIcon
className="max-w-xl"
className="w-1/2"
>
<Input.TextArea
size="small"
value={JSON.stringify(error, null, 2)}
size="tiny"
value={
typeof error === 'string' ? error : JSON.stringify(error, null, 2)
}
borderless
className="font-mono w-full mt-4"
copy
@@ -1,35 +1,41 @@
import { Button, Dropdown, IconClock } from '@supabase/ui'
import { Alert, Button, Dropdown, IconClock } from '@supabase/ui'
import { DatePicker } from 'components/ui/DatePicker'
import { useState, useEffect } from 'react'
import dayjs from 'dayjs'
import { useEffect, useState } from 'react'
import { DatetimeHelper, getDefaultHelper, LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD } from '.'
type ToFrom = { to: string; from: string }
interface Props {
to?: string
from?: string
onChange: ({ to, from }: ToFrom) => void
to: string
from: string
onChange: React.ComponentProps<typeof DatePicker>["onChange"]
helpers: DatetimeHelper[]
}
const HELPERS = [
{ text: 'Last hour', value: '1_hour' },
{ text: 'Last 3 hours', value: '3_hour' },
{ text: 'Last day', value: '1_day' },
]
const DEFAULT_HELPER_VALUE = HELPERS[0].value
const DatePickers: React.FC<Props> = ({ to, from, onChange }) => {
const [helperValue, setHelperValue] = useState<string>('')
const DatePickers: React.FC<Props> = ({ to, from, onChange, helpers }) => {
const defaultHelper = getDefaultHelper(helpers)
const [helperValue, setHelperValue] = useState<string>(to || from ? '' : defaultHelper.text)
const handleHelperChange = (newValue: string) => {
setHelperValue(newValue)
const toDate = new Date()
const fromDate = {
'1_hour': new Date(toDate.getTime() - 60 * 60 * 1000),
'3_hour': new Date(toDate.getTime() - 3 * 60 * 60 * 1000),
'1_day': new Date(toDate.getTime() - 24 * 60 * 60 * 1000),
}[newValue]
if (onChange) onChange({ to: toDate.toISOString(), from: fromDate?.toISOString() as string })
const selectedHelper = helpers.find((h) => h.text === newValue)
if (onChange && selectedHelper) {
onChange({ to: selectedHelper.calcTo(), from: selectedHelper.calcFrom() })
}
}
const selectedHelper = HELPERS.find((h) => h.value === (helperValue || DEFAULT_HELPER_VALUE))
const selectedHelper = helpers.find((helper) => {
if (to === helper.calcTo() && from === helper.calcFrom()) {
return true
} else if (helper.text === helperValue) {
return true
} else {
return false
}
})
useEffect(() => {
if (selectedHelper && helperValue !== selectedHelper.text) {
setHelperValue(selectedHelper.text)
}
}, [selectedHelper])
return (
<div className="flex items-center">
<Dropdown
@@ -38,12 +44,9 @@ const DatePickers: React.FC<Props> = ({ to, from, onChange }) => {
align="start"
overlay={
<>
<Dropdown.RadioGroup
onChange={handleHelperChange}
value={helperValue || DEFAULT_HELPER_VALUE}
>
{HELPERS.map((helper) => (
<Dropdown.Radio key={helper.value} value={helper.value}>
<Dropdown.RadioGroup onChange={handleHelperChange} value={selectedHelper?.text || ''}>
{helpers.map((helper) => (
<Dropdown.Radio key={helper.text} value={helper.text}>
{helper.text}
</Dropdown.Radio>
))}
@@ -57,18 +60,27 @@ const DatePickers: React.FC<Props> = ({ to, from, onChange }) => {
icon={<IconClock size={12} />}
className="rounded-r-none"
>
{selectedHelper?.text}
{selectedHelper?.text || defaultHelper.text}
</Button>
</Dropdown>
<DatePicker
triggerButtonClassName="rounded-l-none"
triggerButtonType={helperValue ? 'default' : 'secondary'}
onChange={(value: ToFrom) => {
triggerButtonType={selectedHelper ? 'default' : 'secondary'}
onChange={(value) => {
setHelperValue('')
if (onChange) onChange(value)
}}
to={!helperValue ? to : undefined}
from={!helperValue ? from : undefined}
renderFooter={({ to, from }) => {
if (to && from && Math.abs(dayjs(from).diff(dayjs(to), 'day')) > LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD) {
return (
<Alert title={""} variant="warning" className="mx-3 pl-2 pr-2 pt-1 pb-2">
Large ranges may result in memory errors for big projects.
</Alert>
)
}
}}
/>
</div>
)
@@ -1,4 +1,7 @@
import { FilterTableSet, LogTemplate } from '.'
import dayjs from 'dayjs'
import { DatetimeHelper, FilterTableSet, LogTemplate } from '.'
export const LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD = 4
export const TEMPLATES: LogTemplate[] = [
{
@@ -185,10 +188,8 @@ export const LOG_TYPE_LABEL_MAPPING: { [k: string]: string } = {
database: 'Database',
}
const _SQL_FILTER_COMMON = {
'search_query': (value: string) => `regexp_contains(event_message, '${value}')`
search_query: (value: string) => `regexp_contains(event_message, '${value}')`,
}
export const SQL_FILTER_TEMPLATES: any = {
@@ -424,3 +425,43 @@ export const LOGS_TAILWIND_CLASSES = {
log_selection_x_padding: 'px-8',
space_y: 'px-6',
}
export const PREVIEWER_DATEPICKER_HELPERS: DatetimeHelper[] = [
{
text: 'Last hour',
calcFrom: () => dayjs().subtract(1, 'hour').startOf('hour').toISOString(),
calcTo: () => '',
},
{
text: 'Last 3 hours',
calcFrom: () => dayjs().subtract(3, 'hour').startOf('hour').toISOString(),
calcTo: () => '',
},
{
text: 'Last day',
calcFrom: () => dayjs().subtract(1, 'day').startOf('day').toISOString(),
calcTo: () => '',
default: true,
},
]
export const EXPLORER_DATEPICKER_HELPERS: DatetimeHelper[] = [
{
text: 'Last day',
calcFrom: () => dayjs().subtract(1, 'day').startOf('day').toISOString(),
calcTo: () => '',
default: true,
},
{
text: 'Last 3 days',
calcFrom: () => dayjs().subtract(3, 'day').startOf('day').toISOString(),
calcTo: () => '',
},
{
text: 'Last 7 days',
calcFrom: () => dayjs().subtract(7, 'day').startOf('day').toISOString(),
calcTo: () => '',
},
]
export const getDefaultHelper = (helpers: DatetimeHelper[]) =>
helpers.find((helper) => helper.default) || helpers[0]
@@ -1,19 +1,29 @@
import React from "react"
interface Metadata {
[key: string]: string | number | Object | Object[]
}
export type LogSearchCallback = (filters: { query: string; to?: string; from?: string; fromMicro?: number, toMicro?: number }) => void
export type DatePickerToFrom = { to: string | null; from: string | null }
export type LogSearchCallback = (
filters: {
query: string
} & DatePickerToFrom
) => void
export interface LogsWarning {
text: string | React.ReactNode
link?: string
linkText?: string
}
export interface LogsEndpointParams {
// project ref
project: string
// micro unix timestamp
timestamp_start?: string
// micro timestamp
timestamp_end?: string
period_start?: string
period_end?: string
sql: string
rawSql: string
iso_timestamp_start?: string
iso_timestamp_end?: string
sql?: string
rawSql?: string
}
export interface LogData {
@@ -65,7 +75,6 @@ export interface FilterObject {
te?: string
}
export interface FilterSet {
label: string
key: string
@@ -91,3 +100,10 @@ export type Override = {
key: string
value: string | string[] | undefined
}
export interface DatetimeHelper {
text: string
calcTo: () => string
calcFrom: () => string
default?: boolean
}
@@ -1,37 +1,45 @@
import { get } from "lodash"
import { Filters, LogsTableName, SQL_FILTER_TEMPLATES } from "."
import { Filters, LogsTableName, SQL_FILTER_TEMPLATES } from '.'
import dayjs from 'dayjs'
import { get } from 'lodash'
/**
* Convert a micro timestamp from number/string to iso timestamp
*/
export const unixMicroToIsoTimestamp = (unix: string | number): string => {
return dayjs.unix(Number(unix) / 1000).toISOString()
}
/**
* Recursively retrieve all nested object key paths.
*
*
* TODO: move to utils
*
*
* @param obj any object
* @param parent a string representing the parent key
* @returns string[] all dot paths for keys.
*/
const getDotKeys = (obj: { [k: string]: unknown }, parent?: string): string[] => {
const keys = Object.keys(obj).filter(k => obj[k])
return keys.flatMap(k => {
const currKey = parent ? `${parent}.${k}` : k
if (typeof obj[k] === 'object') {
return getDotKeys(obj[k] as any, currKey)
} else {
return [currKey]
}
})
const keys = Object.keys(obj).filter((k) => obj[k])
return keys.flatMap((k) => {
const currKey = parent ? `${parent}.${k}` : k
if (typeof obj[k] === 'object') {
return getDotKeys(obj[k] as any, currKey)
} else {
return [currKey]
}
})
}
/**
* Root keys in the filter object are considered to be AND filters.
* Nested keys under a root key are considered to be OR filters.
*
*
* For example:
* ```
* {my_value: 'something', nested: {id: 123, test: 123 }}
* ```
* This would be converted into `WHERE (my_value = 'something') and (id = 123 or test = 123)
*
*
* The template of the filter determines the actual filter statement. If no template is provided, a generic equality statement will be used.
* This only applies for root keys of the filter.
* For example:
@@ -39,60 +47,71 @@ const getDotKeys = (obj: { [k: string]: unknown }, parent?: string): string[] =>
* {'my.nested.value': 123}
* ```
* with no template, it will be converted into `WHERE (my.nested.value = 123)
*
* @returns a whewhere statement with WHERE clause.
*
* @returns a where statement with WHERE clause.
*/
const _genWhereStatement = (table: LogsTableName, filters: Filters) => {
const keys = Object.keys(filters)
const filterTemplates = SQL_FILTER_TEMPLATES[table]
const _resolveTemplateToStatement = (dotKey: string): string | null => {
const template = filterTemplates[dotKey]
const value = get(filters, dotKey)
if (value !== undefined && typeof template === 'function') {
return template(value)
} else if (template === undefined) {
// resolve unknwon filters (possibly from filter overrides)
// no template, set a default
if (typeof value === 'string') {
return `${dotKey} = '${value}'`
} else {
return `${dotKey} = ${value}`
}
} else if (value === undefined && typeof template === 'function') {
return null
} else {
return template
}
}
const statement = keys.map(rootKey => {
if (typeof filters[rootKey] === 'object') {
// join all statements with an OR
const nestedStatement = getDotKeys(filters[rootKey] as Filters, rootKey).map(_resolveTemplateToStatement).filter(Boolean).join(" or ")
return `(${nestedStatement})`
} else {
const nestedStatement = _resolveTemplateToStatement(rootKey)
if (nestedStatement === null) return null
return `(${nestedStatement})`
}
}).filter(Boolean)
// join all root statements with AND
.join(" and ")
if (statement) {
return 'where ' + statement
const keys = Object.keys(filters)
const filterTemplates = SQL_FILTER_TEMPLATES[table]
const _resolveTemplateToStatement = (dotKey: string): string | null => {
const template = filterTemplates[dotKey]
const value = get(filters, dotKey)
if (value !== undefined && typeof template === 'function') {
return template(value)
} else if (template === undefined) {
// resolve unknwon filters (possibly from filter overrides)
// no template, set a default
if (typeof value === 'string') {
return `${dotKey} = '${value}'`
} else {
return `${dotKey} = ${value}`
}
} else if (value === undefined && typeof template === 'function') {
return null
} else if (template && value === false) {
// template present, but value is false
return null
} else {
return ''
return template
}
}
const statement = keys
.map((rootKey) => {
if (typeof filters[rootKey] === 'object') {
// join all statements with an OR
const nestedStatements = getDotKeys(filters[rootKey] as Filters, rootKey)
.map(_resolveTemplateToStatement)
.filter(Boolean)
if (nestedStatements.length > 0) {
return `(${nestedStatements.join(' or ')})`
} else {
return null
}
} else {
const nestedStatement = _resolveTemplateToStatement(rootKey)
if (nestedStatement === null) return null
return `(${nestedStatement})`
}
})
.filter(Boolean)
// join all root statements with AND
.join(' and ')
if (statement) {
return 'where ' + statement
} else {
return ''
}
}
export const genDefaultQuery = (table: LogsTableName, filters: Filters) => {
const where = _genWhereStatement(table, filters)
const where = _genWhereStatement(table, filters)
switch (table) {
case 'edge_logs':
return `select id, timestamp, event_message, metadata, request, response, request.method, request.path, response.status_code
switch (table) {
case 'edge_logs':
return `select id, timestamp, event_message, metadata, request, response, request.method, request.path, response.status_code
from ${table}
cross join unnest(metadata) as m
cross join unnest(m.request) as request
@@ -100,27 +119,27 @@ export const genDefaultQuery = (table: LogsTableName, filters: Filters) => {
${where}
limit 100
`
break
break
case 'postgres_logs':
return `select postgres_logs.timestamp, id, event_message, metadata, metadataparsed.error_severity from ${table}
case 'postgres_logs':
return `select postgres_logs.timestamp, id, event_message, metadata, metadataparsed.error_severity from ${table}
cross join unnest(metadata) as m
cross join unnest(m.parsed) as metadataparsed
${where}
limit 100
`
break
break
case 'function_logs':
return `select id, ${table}.timestamp, event_message, metadata.event_type, metadata.function_id, metadata.level, metadata from ${table}
case 'function_logs':
return `select id, ${table}.timestamp, event_message, metadata.event_type, metadata.function_id, metadata.level, metadata from ${table}
cross join unnest(metadata) as metadata
${where}
limit 100
`
break
break
case 'function_edge_logs':
return `select id, ${table}.timestamp, event_message, response.status_code, response, request, request.method, m.function_id, m.execution_time_ms, m.deployment_id, m.version from ${table}
case 'function_edge_logs':
return `select id, ${table}.timestamp, event_message, response.status_code, response, request, request.method, m.function_id, m.execution_time_ms, m.deployment_id, m.version from ${table}
cross join unnest(metadata) as m
cross join unnest(m.response) as response
cross join unnest(m.request) as request
@@ -128,8 +147,8 @@ export const genDefaultQuery = (table: LogsTableName, filters: Filters) => {
limit 100
`
default:
return ""
break
}
}
default:
return ''
break
}
}
@@ -1,4 +1,4 @@
import React, { useEffect, useReducer, useState } from 'react'
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { Typography, IconAlertCircle, IconRewind, Button, Card, Input } from '@supabase/ui'
@@ -10,15 +10,13 @@ import {
LogsTableName,
QueryType,
LogEventChart,
FilterObject,
Filters,
unixMicroToIsoTimestamp,
} from 'components/interfaces/Settings/Logs'
import dayjs from 'dayjs'
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'
import ShimmerLine from 'components/ui/ShimmerLine'
import LoadingOpacity from 'components/ui/LoadingOpacity'
@@ -47,29 +45,26 @@ export const LogsPreviewer: React.FC<Props> = ({
tableName,
}) => {
const router = useRouter()
const { s, te, ts } = router.query
const { s, ite, its } = router.query
const [showChart, setShowChart] = useState(true)
const table = !tableName ? LOGS_TABLES[queryType] : tableName
const [
{ error, logData, params, newCount, filters, isLoading, oldestTimestamp },
{ loadOlder, setFilters, refresh, setTo, setFrom },
{ error, logData, params, newCount, filters, isLoading },
{ loadOlder, setFilters, refresh, setParams },
] = useLogsPreview(projectRef as string, table, filterOverride)
useEffect(() => {
setFilters((prev) => ({ ...prev, search_query: s as string }))
if (te) {
setTo(te as string)
} else {
setTo('')
if (ite || its) {
setParams((prev) => ({
...prev,
iso_timestamp_start: (its || '') as string,
iso_timestamp_end: (ite || '') as string,
}))
}
if (ts) {
setFrom(ts as string)
} else {
setFrom('')
}
}, [s, te, ts])
}, [s, ite, its])
const onSelectTemplate = (template: LogTemplate) => {
setFilters((prev: any) => ({ ...prev, search_query: template.searchString }))
@@ -81,33 +76,27 @@ export const LogsPreviewer: React.FC<Props> = ({
pathname: router.pathname,
query: {
...router.query,
te: undefined,
ts: undefined,
ite: undefined,
its: undefined,
// ...whereFilters,
},
})
}
const handleSearch: LogSearchCallback = async ({ query, to, from, fromMicro, toMicro }) => {
let toValue, fromValue
if (to || toMicro) {
toValue = toMicro ? toMicro : dayjs(to).valueOf() * 1000
setTo(String(toValue))
}
if (from || fromMicro) {
fromValue = fromMicro ? fromMicro : dayjs(from).valueOf() * 1000
setFrom(String(fromValue))
}
setFilters((prev) => ({ ...prev, search_query: query || '' }))
const handleSearch: LogSearchCallback = async ({ query = '', to, from }) => {
setParams((prev) => ({
...prev,
iso_timestamp_start: from || prev.iso_timestamp_start || '',
iso_timestamp_end: to || prev.iso_timestamp_end || '',
}))
setFilters((prev) => ({ ...prev, search_query: query }))
router.push({
pathname: router.pathname,
query: {
...router.query,
s: query || '',
ts: fromValue,
te: toValue,
its: from || its || '',
ite: to || ite || '',
},
})
}
@@ -123,18 +112,16 @@ export const LogsPreviewer: React.FC<Props> = ({
onRefresh={handleRefresh}
onSearch={handleSearch}
defaultSearchValue={filters.search_query as string}
defaultToValue={
params.timestamp_end ? dayjs(Number(params.timestamp_end) / 1000).toISOString() : ''
}
defaultFromValue={
params.timestamp_start
? dayjs(Number(params.timestamp_start) / 1000).toISOString()
: oldestTimestamp
? dayjs(Number(oldestTimestamp) / 1000).toISOString()
: ''
}
defaultToValue={params.iso_timestamp_end}
defaultFromValue={params.iso_timestamp_start}
onExploreClick={() => {
router.push(`/project/${projectRef}/logs-explorer?q=${encodeURIComponent(params.rawSql)}`)
router.push(
`/project/${projectRef}/logs-explorer?q=${encodeURIComponent(
params.rawSql || ''
)}&its=${encodeURIComponent(params.iso_timestamp_start || '')}&ite=${encodeURIComponent(
params.iso_timestamp_end || ''
)}`
)
}}
onSelectTemplate={onSelectTemplate}
filters={filters}
@@ -157,7 +144,8 @@ export const LogsPreviewer: React.FC<Props> = ({
<LogEventChart
data={!isLoading ? logData : undefined}
onBarClick={(timestampMicro) => {
handleSearch({ query: filters.search_query as string, toMicro: timestampMicro })
const to = unixMicroToIsoTimestamp(timestampMicro)
handleSearch({ query: filters.search_query as string, to, from: null })
}}
/>
)}
@@ -1,6 +1,24 @@
import { Button, Dropdown, Typography, IconChevronDown, IconPlay } from '@supabase/ui'
import {
Button,
Dropdown,
Typography,
IconChevronDown,
IconPlay,
Badge,
Popover,
Alert,
} from '@supabase/ui'
import Flag from 'components/ui/Flag/Flag'
import { LogsTableName, LOGS_SOURCE_DESCRIPTION, LogTemplate } from '.'
import {
EXPLORER_DATEPICKER_HELPERS,
LogsTableName,
LogsWarning,
LOGS_SOURCE_DESCRIPTION,
LogTemplate,
} from '.'
import DatePickers from './Logs.DatePickers'
import Link from 'next/link'
interface Props {
templates?: LogTemplate[]
onSelectTemplate: (template: LogTemplate) => void
@@ -10,6 +28,10 @@ interface Props {
onSave?: () => void
hasEditorValue: boolean
isLoading: boolean
onDateChange: React.ComponentProps<typeof DatePickers>['onChange']
defaultTo: string
defaultFrom: string
warnings: LogsWarning[]
}
const LogsQueryPanel: React.FC<Props> = ({
@@ -21,6 +43,10 @@ const LogsQueryPanel: React.FC<Props> = ({
onSave,
onSelectSource,
isLoading,
defaultFrom,
defaultTo,
onDateChange,
warnings,
}) => (
<div
className="
@@ -63,6 +89,41 @@ bg-panel-header-light dark:bg-panel-header-dark
Templates
</Button>
</Dropdown>
<DatePickers
to={defaultTo}
from={defaultFrom}
onChange={onDateChange}
helpers={EXPLORER_DATEPICKER_HELPERS}
/>
<div className="overflow-hidden">
<div
className={` transition-all duration-300 ${
warnings.length > 0 ? 'opacity-100' : 'invisible w-0 h-0 opacity-0'
}`}
>
<Popover
portalled
overlay={
<Alert variant="warning" title="">
<div className="flex flex-col gap-3">
{warnings.map((warning, index) => (
<p key={index}>
{warning.text}{' '}
{warning.link && (
<Link href={warning.link}>{warning.linkText || 'View'}</Link>
)}
</p>
))}
</div>
</Alert>
}
>
<Badge color="yellow">
{warnings.length} {warnings.length > 1 ? 'warnings' : 'warning'}
</Badge>
</Popover>
</div>
</div>
</div>
<div>
<div className="flex items-center gap-4">
@@ -9,7 +9,7 @@ import {
IconEye,
IconEyeOff,
} from '@supabase/ui'
import { Filters, LogSearchCallback, LogTemplate } from '.'
import { Filters, LogSearchCallback, LogTemplate, PREVIEWER_DATEPICKER_HELPERS } from '.'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import { FILTER_OPTIONS, LogsTableName } from './Logs.constants'
@@ -101,9 +101,8 @@ const PreviewFilterPanel: FC<Props> = ({
Refresh
</Button>
)
const handleSearch = (partial: Partial<{ query: string; to: string; from: string }>) =>
onSearch({ query: search, ...partial })
const handleSearch = (partial: Partial<Parameters<LogSearchCallback>[0]>) =>
onSearch({ query: search, to: partial?.to || null, from: partial?.from || null })
return (
<div
@@ -147,11 +146,10 @@ const PreviewFilterPanel: FC<Props> = ({
</form>
<DatePickers
onChange={async (e: { to: string; from: string }) => {
handleSearch(e)
}}
onChange={handleSearch}
to={defaultToValue}
from={defaultFromValue}
helpers={PREVIEWER_DATEPICKER_HELPERS}
/>
<div>
+121 -232
View File
@@ -1,9 +1,5 @@
import React, { useState, forwardRef, useEffect } from 'react'
import React, { useState, useEffect } from 'react'
import DatePicker from 'react-datepicker'
// import dayjs from 'dayjs'
import {
Button,
Popover,
@@ -11,33 +7,26 @@ import {
IconChevronRight,
IconArrowRight,
IconCalendar,
IconGlobe,
Input,
Toggle,
Select,
} from '@supabase/ui'
import { format } from 'date-fns'
import TimeSplitInput from './TimeSplitInput'
// import DateSplitInput from './DateSplitInput'
import DividerSlash from './DividerSlash'
import dayjs from 'dayjs'
import { ButtonProps } from '@supabase/ui/dist/cjs/components/Button/Button'
import { DatePickerToFrom } from 'components/interfaces/Settings/Logs'
var utc = require('dayjs/plugin/utc')
var timezone = require('dayjs/plugin/timezone') // dependent on utc plugin
dayjs.extend(utc)
dayjs.extend(timezone)
interface RootProps {
onChange?: ({ to, from }: { to: string; from: string }) => void
export interface DatePickerProps {
onChange?: (args: DatePickerToFrom) => void
to?: string
from?: string
triggerButtonType?: ButtonProps['type']
triggerButtonClassName?: string
renderFooter?: (args: DatePickerToFrom) => React.ReactNode | void
}
const START_DATE_DEFAULT = new Date()
@@ -52,24 +41,31 @@ function _DatePicker({
onChange,
triggerButtonType = 'default',
triggerButtonClassName = '',
}: RootProps) {
renderFooter = () => null,
}: DatePickerProps) {
const [open, setOpen] = useState<boolean>(false)
const [showTime, setShowTime] = useState<boolean>(true)
const [appliedStartDate, setAppliedStartDate] = useState<any>(null)
const [appliedEndDate, setAppliedEndDate] = useState<any>(null)
const [startDate, setStartDate] = useState<any>(START_DATE_DEFAULT)
const [endDate, setEndDate] = useState<any>(END_DATE_DEFAULT)
const [appliedStartDate, setAppliedStartDate] = useState<null | Date>(null)
const [appliedEndDate, setAppliedEndDate] = useState<null | Date>(null)
const [startDate, setStartDate] = useState<Date | null>(START_DATE_DEFAULT)
const [endDate, setEndDate] = useState<Date | null>(END_DATE_DEFAULT)
const [startTime, setStartTime] = useState<any>(START_TIME_DEFAULT)
const [endTime, setEndTime] = useState<any>(END_TIME_DEFAULT)
useEffect(() => {
if (!to || !from) {
if (!from) {
setAppliedStartDate(null)
} else if (from !== appliedStartDate?.toISOString()) {
const start = dayjs(from).toDate()
setAppliedStartDate(start)
setStartDate(start)
}
if (!to) {
setAppliedEndDate(null)
} else if (to !== appliedEndDate?.toISOString()) {
const end = dayjs(to).toDate()
setAppliedEndDate(end)
setEndDate(end)
}
}, [to, from])
@@ -113,168 +109,77 @@ function _DatePicker({
setAppliedStartDate(null)
setAppliedEndDate(null)
}
const DEFAULT_DATE_FORMAT = 'ddd MMM YYYY HH:mm:ss'
return (
<>
<Popover
open={open}
onOpenChange={(e) => setOpen(e)}
size="small"
align="center"
side="bottom"
header={
<>
<div className="flex justify-between items-stretch py-2">
<div className="grow flex flex-col gap-1">
<TimeSplitInput
type="start"
startTime={startTime}
endTime={endTime}
time={startTime}
setTime={setStartTime}
setStartTime={setStartTime}
setEndTime={setEndTime}
startDate={startDate}
endDate={endDate}
/>
</div>
<div
className={`
<Popover
open={open}
onOpenChange={(e) => setOpen(e)}
size="small"
align="center"
side="bottom"
header={
<>
<div className="flex justify-between items-stretch py-2">
<div className="grow flex flex-col gap-1">
<TimeSplitInput
type="start"
startTime={startTime}
endTime={endTime}
time={startTime}
setTime={setStartTime}
setStartTime={setStartTime}
setEndTime={setEndTime}
startDate={startDate}
endDate={endDate}
/>
</div>
<div
className={`
w-12
flex
items-center
justify-center
text-scale-900
`}
>
<IconArrowRight strokeWidth={1.5} size={14} />
</div>
<div className="grow flex flex-col gap-1">
<TimeSplitInput
type="end"
startTime={startTime}
endTime={endTime}
time={endTime}
setTime={setEndTime}
setStartTime={setStartTime}
setEndTime={setEndTime}
startDate={startDate}
endDate={endDate}
/>
</div>
>
<IconArrowRight strokeWidth={1.5} size={14} />
</div>
</>
}
overlay={
<>
<div className="px-3 py-4">
<DatePicker
selected={startDate}
onChange={(dates) => {
handleDatePickerChange(dates)
}}
dateFormat="MMMM d, yyyy h:mm aa"
<div className="grow flex flex-col gap-1">
<TimeSplitInput
type="end"
startTime={startTime}
endTime={endTime}
time={endTime}
setTime={setEndTime}
setStartTime={setStartTime}
setEndTime={setEndTime}
startDate={startDate}
endDate={endDate}
selectsRange
// showTimeSelect
// nextMonthButtonLabel=">"
// previousMonthButtonLabel="<"
// popperClassName="react-datepicker-left"
// customInput={<ButtonInput />}
// dateFormat={DEFAULT_DATE_FORMAT}
// dateFormat={(locale, date) => dayjs(date).format('MM-YYYY')}
inline
dayClassName={() => 'cursor-pointer'}
renderCustomHeader={({
date,
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}) => (
<div className="flex items-center justify-between px-2 py-2">
<div className="flex items-center justify-between w-full">
<button
onClick={decreaseMonth}
disabled={prevMonthButtonDisabled}
type="button"
className={`
${prevMonthButtonDisabled && 'cursor-not-allowed opacity-50'}
text-scale-1100 hover:text-scale-1200 focus:outline-none
`}
>
<IconChevronLeft size={16} strokeWidth={2} />
</button>
<span className="text-sm text-scale-1100">{format(date, 'MMMM yyyy')}</span>
<button
onClick={increaseMonth}
disabled={nextMonthButtonDisabled}
type="button"
className={`
${nextMonthButtonDisabled && 'cursor-not-allowed opacity-50'}
text-scale-1100 hover:text-scale-1200 focus:outline-none
`}
>
<IconChevronRight size={16} strokeWidth={2} />
</button>
</div>
</div>
)}
/>
</div>
<Popover.Seperator />
<div className="flex items-center justify-end gap-2 py-2 pb-4 px-3">
<Button type="default" onClick={() => handleClear()}>
Clear
</Button>
<Button onClick={() => handleSubmit()}>Apply</Button>
</div>
</>
}
>
<Button
type={triggerButtonType}
as="span"
icon={<IconCalendar />}
className={triggerButtonClassName}
>
{/* Custom */}
{appliedStartDate && appliedEndDate && appliedStartDate !== appliedEndDate ? (
<>
{format(new Date(appliedStartDate), 'dd MMM')} -{' '}
{format(new Date(appliedEndDate), 'dd MMM')}
</>
) : appliedStartDate || appliedEndDate ? (
format(new Date(appliedStartDate || appliedEndDate), 'dd MMM')
) : (
'Custom'
)}
</Button>
</Popover>
{/* <div className="relative w-40">
<DatePicker
selected={endDate}
onChange={(date) => setEndDate(date)}
selectsEnd
startDate={startDate}
endDate={endDate}
nextMonthButtonLabel=">"
previousMonthButtonLabel="<"
popperClassName="react-datepicker-right"
customInput={<ButtonInput />}
renderCustomHeader={({
date,
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}) => (
<>
</div>
</>
}
overlay={
<>
<div className="px-3 py-4">
<DatePicker
selected={startDate}
onChange={(dates) => {
handleDatePickerChange(dates)
}}
dateFormat="MMMM d, yyyy h:mm aa"
startDate={startDate}
endDate={endDate}
selectsRange
inline
dayClassName={() => 'cursor-pointer'}
renderCustomHeader={({
date,
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}) => (
<div className="flex items-center justify-between px-2 py-2">
<div className="flex items-center justify-between w-full">
<button
@@ -282,27 +187,19 @@ function _DatePicker({
disabled={prevMonthButtonDisabled}
type="button"
className={`
${
prevMonthButtonDisabled &&
'cursor-not-allowed opacity-50'
}
${prevMonthButtonDisabled && 'cursor-not-allowed opacity-50'}
text-scale-1100 hover:text-scale-1200 focus:outline-none
`}
>
<IconChevronLeft size={16} strokeWidth={2} />
</button>
<span className="text-sm text-scale-1100">
{format(date, 'MMMM yyyy')}
</span>
<span className="text-sm text-scale-1100">{format(date, 'MMMM yyyy')}</span>
<button
onClick={increaseMonth}
disabled={nextMonthButtonDisabled}
type="button"
className={`
${
nextMonthButtonDisabled &&
'cursor-not-allowed opacity-50'
}
${nextMonthButtonDisabled && 'cursor-not-allowed opacity-50'}
text-scale-1100 hover:text-scale-1200 focus:outline-none
`}
>
@@ -310,51 +207,43 @@ function _DatePicker({
</button>
</div>
</div>
</>
)}
/>
</div>
</div>
<div className="flex items-center justify-center max-w-2xl py-20 mx-auto space-x-4">
<span className="font-medium text-gray-900">Default Components:</span>
<div className="relative w-40">
<DatePicker
selected={startDate}
onChange={(date) => setStartDate(date)}
selectsStart
startDate={startDate}
endDate={endDate}
nextMonthButtonLabel=">"
previousMonthButtonLabel="<"
popperClassName="react-datepicker-left"
/>
</div>
<div className="relative w-40">
<DatePicker
selected={endDate}
onChange={(date) => setEndDate(date)}
selectsEnd
startDate={startDate}
endDate={endDate}
nextMonthButtonLabel=">"
previousMonthButtonLabel="<"
popperClassName="react-datepicker-right"
/>
</div> */}
</>
)}
/>
</div>
{renderFooter({
from: startDate?.toISOString() || null,
to: endDate?.toISOString() || null,
})}
<Popover.Seperator />
<div className="flex items-center justify-end gap-2 py-2 pb-4 px-3">
<Button type="default" onClick={() => handleClear()}>
Clear
</Button>
<Button onClick={() => handleSubmit()}>Apply</Button>
</div>
</>
}
>
<Button
type={triggerButtonType}
as="span"
icon={<IconCalendar />}
className={triggerButtonClassName}
>
{/* Custom */}
{appliedStartDate && appliedEndDate && appliedStartDate !== appliedEndDate ? (
<>
{format(new Date(appliedStartDate), 'dd MMM')} -{' '}
{format(new Date(appliedEndDate), 'dd MMM')}
</>
) : appliedStartDate || appliedEndDate ? (
format(new Date((appliedStartDate || appliedEndDate)!), 'dd MMM')
) : (
'Custom'
)}
</Button>
</Popover>
)
}
const ButtonInput = forwardRef(({ value, onClick }: any, ref): any => (
<button
onClick={onClick}
// @ts-ignore
ref={ref}
type="button"
className="inline-flex justify-start w-full px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-0 focus:ring-blue-500"
>
{value}
</button>
))
export default _DatePicker
@@ -1 +1,2 @@
export { default as DatePicker } from './DatePicker'
export type {DatePickerProps} from "./DatePicker"
+13 -10
View File
@@ -5,17 +5,22 @@ import {
genCountQuery,
genDefaultQuery,
genQueryParams,
getDefaultHelper,
LogData,
Logs,
LogsEndpointParams,
LogsTableName,
PREVIEWER_DATEPICKER_HELPERS,
} from 'components/interfaces/Settings/Logs'
import { Dispatch, SetStateAction, useEffect, useState } from 'react'
import useSWR from 'swr'
import useSWRInfinite, { SWRInfiniteKeyLoader } from 'swr/infinite'
import { API_URL } from 'lib/constants'
import { get } from 'lib/common/fetch'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
dayjs.extend(utc)
interface Data {
logData: LogData[]
error: string | Object | null
@@ -30,14 +35,14 @@ interface Handlers {
loadOlder: () => void
refresh: () => void
setFilters: (filters: Filters | ((previous: Filters) => Filters)) => void
setFrom: (value: string) => void
setTo: (value: string) => void
setParams: Dispatch<SetStateAction<LogsEndpointParams>>
}
function useLogsPreview(
projectRef: string,
table: LogsTableName,
filterOverride?: Filters
): [Data, Handlers] {
const defaultHelper = getDefaultHelper(PREVIEWER_DATEPICKER_HELPERS)
const [latestRefresh, setLatestRefresh] = useState<string>(new Date().toISOString())
const [filters, setFilters] = useState<Filters>({ ...filterOverride })
@@ -46,10 +51,8 @@ function useLogsPreview(
project: projectRef,
sql: '',
rawSql: '',
period_start: '',
period_end: '',
timestamp_start: '',
timestamp_end: '',
iso_timestamp_start: defaultHelper.calcFrom(),
iso_timestamp_end: defaultHelper.calcTo(),
})
useEffect(() => {
@@ -79,8 +82,9 @@ function useLogsPreview(
} else {
const len = prevPageData.result.length
const { timestamp: tsLimit }: LogData = prevPageData.result[len - 1]
const isoTsLimit = dayjs.utc(Number(tsLimit / 1000)).toISOString()
// create new key from params
queryParams = genQueryParams({ ...params, timestamp_end: String(tsLimit) } as any)
queryParams = genQueryParams({ ...params, iso_timestamp_end: isoTsLimit } as any)
}
return `${API_URL}/projects/${projectRef}/analytics/endpoints/logs.all?${queryParams}`
}
@@ -105,7 +109,7 @@ function useLogsPreview(
return `${API_URL}/projects/${projectRef}/analytics/endpoints/logs.all?${genQueryParams({
...params,
sql: genCountQuery(table),
period_start: String(latestRefresh),
iso_timestamp_start: latestRefresh,
} as any)}`
}
@@ -157,11 +161,10 @@ function useLogsPreview(
oldestTimestamp: oldestTimestamp ? String(oldestTimestamp) : undefined,
},
{
setFrom: (value) => setParams((prev) => ({ ...prev, timestamp_start: value })),
setTo: (value) => setParams((prev) => ({ ...prev, timestamp_end: value })),
setFilters: handleSetFilters,
refresh,
loadOlder: () => setSize((prev) => prev + 1),
setParams,
},
]
}
+15 -7
View File
@@ -1,9 +1,13 @@
import { cleanQuery, genQueryParams } from 'components/interfaces/Settings/Logs'
import {
cleanQuery,
EXPLORER_DATEPICKER_HELPERS,
genQueryParams,
getDefaultHelper,
} from 'components/interfaces/Settings/Logs'
import { Dispatch, SetStateAction, useState } from 'react'
import { LogsEndpointParams, Logs, LogData } from 'components/interfaces/Settings/Logs/Logs.types'
import useSWRInfinite, { SWRInfiniteKeyLoader } from 'swr/infinite/dist/infinite'
import { API_URL } from 'lib/constants'
import useSWR, { mutate } from 'swr'
import useSWR from 'swr'
import { get } from 'lib/common/fetch'
interface Data {
params: LogsEndpointParams
@@ -14,18 +18,20 @@ interface Data {
interface Handlers {
changeQuery: (newQuery?: string) => void
runQuery: () => void
setParams: Dispatch<SetStateAction<LogsEndpointParams>>
}
const useLogsQuery = (
projectRef: string,
initialParams: Partial<LogsEndpointParams> = {}
): [Data, Handlers] => {
const defaultHelper = getDefaultHelper(EXPLORER_DATEPICKER_HELPERS)
const [params, setParams] = useState<LogsEndpointParams>({
project: projectRef,
sql: '',
rawSql: '',
// timestamp_start: '',
// timestamp_end: '',
iso_timestamp_start: defaultHelper.calcFrom(),
iso_timestamp_end: defaultHelper.calcTo(),
...initialParams,
})
@@ -36,7 +42,9 @@ const useLogsQuery = (
isValidating: isLoading,
mutate,
} = useSWR<Logs>(
params.sql ? `${API_URL}/projects/${projectRef}/analytics/endpoints/logs.all?${queryParams}` : null,
params.sql
? `${API_URL}/projects/${projectRef}/analytics/endpoints/logs.all?${queryParams}`
: null,
get,
{ revalidateOnFocus: false }
)
@@ -51,7 +59,7 @@ const useLogsQuery = (
return [
{ params, isLoading, logData: data?.result ? data?.result : [], error },
{ changeQuery, runQuery: () => mutate() },
{ changeQuery, runQuery: () => mutate(), setParams },
]
}
export default useLogsQuery
@@ -2,12 +2,15 @@ import React, { useEffect, useState } from 'react'
import { NextPage } from 'next'
import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import { Typography, IconAlertCircle, Card, Input, Alert, Modal, Form, Button } from '@supabase/ui'
import { Input, Modal, Form, Button } from '@supabase/ui'
import { useStore, withAuth } from 'hooks'
import CodeEditor from 'components/ui/CodeEditor'
import {
DatePickerToFrom,
LogsQueryPanel,
LogsTableName,
LogsWarning,
LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD,
LogTable,
LogTemplate,
TEMPLATES,
@@ -17,37 +20,58 @@ import useLogsQuery from 'hooks/analytics/useLogsQuery'
import { LogsExplorerLayout } from 'components/layouts'
import ShimmerLine from 'components/ui/ShimmerLine'
import LoadingOpacity from 'components/ui/LoadingOpacity'
import { LogSqlSnippets, UserContent } from 'types'
import { UserContent } from 'types'
import toast from 'react-hot-toast'
import dayjs from 'dayjs'
export const LogsExplorerPage: NextPage = () => {
const router = useRouter()
const { ref, q } = router.query
const { ref, q, ite, its } = router.query
const [editorId, setEditorId] = useState<string>(uuidv4())
const [editorValue, setEditorValue] = useState<string>('')
const [saveModalOpen, setSaveModalOpen] = useState<boolean>(false)
const [warnings, setWarnings] = useState<LogsWarning[]>([])
const { content } = useStore()
const [{ logData, error, isLoading }, { changeQuery, runQuery }] = useLogsQuery(ref as string)
const [{ params, logData, error, isLoading }, { changeQuery, runQuery, setParams }] =
useLogsQuery(ref as string, {
iso_timestamp_start: (its || '') as string,
iso_timestamp_end: (ite || '') as string,
})
useEffect(() => {
// on mount, set initial values
if (q !== undefined && q !== '') {
changeQuery(q as string)
runQuery()
if (q) {
onSelectTemplate({
mode: 'custom',
searchString: q as string,
})
}
}, [q])
}, [])
useEffect(() => {
let newWarnings = []
const start = params.iso_timestamp_start ? dayjs(params.iso_timestamp_start) : dayjs()
const end = params.iso_timestamp_end ? dayjs(params.iso_timestamp_end) : dayjs()
const daysDiff = Math.abs(start.diff(end, 'days'))
if (
editorValue &&
!editorValue.includes('limit') &&
daysDiff > LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD
) {
newWarnings.push({ text: 'When querying large date ranges, include a LIMIT clause.' })
}
setWarnings(newWarnings)
}, [editorValue, params.iso_timestamp_start, params.iso_timestamp_end])
const onSelectTemplate = (template: LogTemplate) => {
setEditorValue(template.searchString)
changeQuery(template.searchString)
setEditorId(uuidv4())
runQuery()
router.push({
pathname: router.pathname,
query: { ...router.query, q: template.searchString },
})
}
const handleRun = () => {
@@ -74,11 +98,26 @@ export const LogsExplorerPage: NextPage = () => {
setSaveModalOpen(!saveModalOpen)
}
const handleDateChange = ({ to, from }: DatePickerToFrom) => {
setParams((prev) => ({
...prev,
iso_timestamp_start: from || '',
iso_timestamp_end: to || '',
}))
router.push({
pathname: router.pathname,
query: { ...router.query, its: from || '', ite: to || '' },
})
}
return (
<LogsExplorerLayout>
<div className="h-full flex flex-col flex-grow gap-4">
<div className="border rounded">
<LogsQueryPanel
defaultFrom={params.iso_timestamp_start || ''}
defaultTo={params.iso_timestamp_end || ''}
onDateChange={handleDateChange}
onSelectSource={handleInsertSource}
onClear={handleClear}
onRun={handleRun}
@@ -87,6 +126,7 @@ export const LogsExplorerPage: NextPage = () => {
onSelectTemplate={onSelectTemplate}
onSave={handleOnSave}
isLoading={isLoading}
warnings={warnings}
/>
<div className="min-h-[7rem] h-48">
@@ -63,3 +63,17 @@ test('toggle histogram', async () => {
userEvent.click(toggle)
expect(mockFn).toBeCalled()
})
test('error message handling', async () => {
const {rerender} = render(<LogTable error="some \nstring" />)
await expect(screen.findByText("some \nstring")).rejects.toThrow()
await screen.findByDisplayValue(/some/)
await screen.findByDisplayValue(/string/)
rerender(<LogTable error={{my_error: "some \nstring"}} />)
await screen.findByText(/some \\nstring/)
await screen.findByText(/some/)
await screen.findByText(/string/)
await screen.findByText(/my_error/)
})
@@ -0,0 +1,19 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { PREVIEWER_DATEPICKER_HELPERS } from 'components/interfaces/Settings/Logs'
import DatePickers from 'components/interfaces/Settings/Logs/Logs.DatePickers'
import dayjs from 'dayjs'
test('renders warning', async () => {
const from = dayjs().subtract(60, 'days')
const to = dayjs()
render(
<DatePickers
helpers={PREVIEWER_DATEPICKER_HELPERS}
to={to.toISOString()}
from={from.toISOString()}
/>
)
userEvent.click(await screen.findByText(RegExp(from.format('DD MMM'))))
await screen.findByText(/memory errors/)
})
+26 -3
View File
@@ -1,6 +1,19 @@
import { genDefaultQuery, LogsTableName } from 'components/interfaces/Settings/Logs'
import {
genDefaultQuery,
LogsTableName,
SQL_FILTER_TEMPLATES,
} from 'components/interfaces/Settings/Logs'
describe.each(Object.values(LogsTableName))('%s', (table) => {
const templates = SQL_FILTER_TEMPLATES[table]
const stringTemplateKey = Object.keys(templates).find((key) => {
if (typeof templates[key] === 'string' && key.split('.').length === 2) {
return true
} else {
false
}
})
const [root, child] = (stringTemplateKey || '').split('.')
describe.each([
{ filter: { search_query: '' } },
{ filter: { search_query: undefined } },
@@ -27,15 +40,25 @@ describe.each(Object.values(LogsTableName))('%s', (table) => {
filter: { 'override.nested': 'something' },
contains: ['override.nested', 'something'],
},
])('generates sql for filter $filter', ({ filter, contains = [] }) => {
// check for when string templates are set to false.
...(stringTemplateKey
? [
{
filter: { search_query: 'some-search-query', [root]: { [child]: false } },
excludes: [templates[stringTemplateKey], '()'],
contains: ['some-search-query'],
},
]
: []),
])('generates sql for filter $filter', ({ filter, contains = [], excludes = [] }) => {
test('generates query correctly', async () => {
const generated = genDefaultQuery(table, filter)
// should not contain functions
expect(generated).not.toMatch(/function[ A-Za-z_-]+\(.+\).+\{.+return.+\}/)
expect(generated).not.toMatch(/\=\>|\$\{.+\}/)
expect(generated).not.toContain('return')
contains.forEach((str) => expect(generated).toContain(str))
excludes.forEach((str) => expect(generated).not.toContain(str))
})
})
})
@@ -170,40 +170,36 @@ test('te= query param will populate the timestamp to input', async () => {
// get time 20 mins before
const newDate = new Date()
newDate.setMinutes(new Date().getMinutes() - 20)
const isoString = newDate.toISOString()
const unixMicro = newDate.getTime() * 1000 //microseconds
const iso = newDate.toISOString()
const router = defaultRouterMock()
router.query = { ...router.query, te: unixMicro }
router.query = { ...router.query, ite: iso }
useRouter.mockReturnValue(router)
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
await waitFor(() => {
expect(get).toHaveBeenCalledWith(
expect.stringContaining(`timestamp_end=${encodeURIComponent(unixMicro)}`)
expect.stringContaining(`iso_timestamp_end=${encodeURIComponent(iso)}`)
)
})
userEvent.click(await screen.findByText('Custom'))
expect(get).toHaveBeenCalledWith(expect.stringContaining('timestamp_end=' + unixMicro))
})
test('ts= query param will populate the timestamp from input', async () => {
// get time 20 mins before
const newDate = new Date()
newDate.setMinutes(new Date().getMinutes() - 20)
const unixMicro = newDate.getTime() * 1000 //microseconds
const iso = newDate.toISOString()
const router = defaultRouterMock()
router.query = { ...router.query, ts: unixMicro }
router.query = { ...router.query, its: iso }
useRouter.mockReturnValue(router)
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
await waitFor(() => {
expect(get).toHaveBeenCalledWith(
expect.stringContaining(`timestamp_start=${encodeURIComponent(unixMicro)}`)
expect.stringContaining(`iso_timestamp_start=${encodeURIComponent(iso)}`)
)
})
userEvent.click(await screen.findByText('Custom'))
await screen.findByText(new RegExp(newDate.getFullYear()))
await screen.findByText(/Apply/)
expect(get).toHaveBeenCalledWith(expect.stringContaining('timestamp_start=' + unixMicro))
})
test('load older btn will fetch older logs', async () => {
@@ -284,14 +280,12 @@ test('bug: nav backwards with params change results in ui changing', async () =>
await screen.findByDisplayValue('simple-query')
})
test("bug: nav to explorer preserves newlines", async ()=>{
render(
<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />
)
test('bug: nav to explorer preserves newlines', async () => {
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
const router = useRouter()
userEvent.click(await screen.findByText(/Explore/))
await expect(router.push).toBeCalledWith(expect.stringContaining(encodeURIComponent("\n")))
}
await expect(router.push).toBeCalledWith(expect.stringContaining(encodeURIComponent('\n')))
})
test('filters alter generated query', async () => {
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
userEvent.click(await screen.findByRole('button', { name: 'Status' }))
@@ -14,7 +14,7 @@ test.todo('templates')
test('run and clear', async () => {
const mockRun = jest.fn()
const mockClear = jest.fn()
render(<LogsQueryPanel onRun={mockRun} onClear={mockClear} hasEditorValue/>)
render(<LogsQueryPanel warnings={[]} onRun={mockRun} onClear={mockClear} hasEditorValue />)
await expect(screen.findByPlaceholderText(/Search/)).rejects.toThrow()
const run = await screen.findByText(/Run/)
userEvent.click(run)
@@ -22,5 +22,4 @@ test('run and clear', async () => {
const clear = await screen.findByText(/Clear/)
userEvent.click(clear)
expect(mockClear).toBeCalled()
})
@@ -50,10 +50,10 @@ test('Manual refresh', async () => {
})
test('Datepicker dropdown', async () => {
render(<PreviewFilterPanel />)
clickDropdown(await screen.findByText(/Last hour/))
clickDropdown(await screen.findByText(/Last day/))
userEvent.click(await screen.findByText(/Last 3 hours/))
await screen.findByText(/Last 3 hours/)
await expect(screen.findByText(/Last hour/)).rejects.toThrow()
await expect(screen.findByText(/Last day/)).rejects.toThrow()
})
test.todo('timestamp to/from filter default value')
+53 -3
View File
@@ -62,7 +62,8 @@ const LogsExplorerPage = (props) => (
import { render, fireEvent, waitFor, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { logDataFixture } from '../../fixtures'
import { clickDropdown } from 'tests/helpers'
import dayjs from 'dayjs'
beforeEach(() => {
// reset mocks between tests
get.mockReset()
@@ -80,7 +81,7 @@ test('can display log data', async () => {
}),
],
})
const {container} = render(<LogsExplorerPage />)
const { container } = render(<LogsExplorerPage />)
let editor = container.querySelector('.monaco-editor')
await waitFor(() => {
editor = container.querySelector('.monaco-editor')
@@ -90,7 +91,7 @@ test('can display log data', async () => {
userEvent.type(editor, 'select \ncount(*) as my_count \nfrom edge_logs')
userEvent.click(await screen.findByText(/Run/))
const row = await screen.findByText("some-event-happened")
const row = await screen.findByText('some-event-happened')
userEvent.click(row)
await screen.findByText(/something_value/)
})
@@ -105,6 +106,27 @@ test('q= query param will populate the query input', async () => {
expect(get).toHaveBeenCalledWith(expect.stringContaining('sql=some_query'))
})
})
test('ite= and its= query param will populate the datepicker', async () => {
const router = defaultRouterMock()
const start = dayjs().subtract(1, 'day')
const end = dayjs()
router.query = {
...router.query,
type: 'api',
q: 'some_query',
its: start.toISOString(),
ite: end.toISOString(),
}
useRouter.mockReturnValue(router)
render(<LogsExplorerPage />)
// should populate editor with the query param
await waitFor(() => {
expect(get).toHaveBeenCalledWith(
expect.stringContaining(encodeURIComponent(start.toISOString()))
)
expect(get).toHaveBeenCalledWith(expect.stringContaining(encodeURIComponent(end.toISOString())))
})
})
test('custom sql querying', async () => {
get.mockImplementation((url) => {
@@ -155,3 +177,31 @@ test('custom sql querying', async () => {
// should not see chronological features
await expect(screen.findByText(/Load older/)).rejects.toThrow()
})
test('datepicker interaction updates query params', async () => {
render(<LogsExplorerPage />)
clickDropdown(await screen.findByText(/Last day/))
userEvent.click(await screen.findByText(/Last 3 days/))
const router = useRouter()
expect(router.push).toBeCalledWith(
expect.objectContaining({
query: expect.objectContaining({
its: expect.any(String),
}),
})
)
})
test('query warnings', async () => {
const router = defaultRouterMock()
router.query = {
...router.query,
q: 'some_query',
its: dayjs().subtract(10, 'days').toISOString(),
ite: dayjs().toISOString(),
}
useRouter.mockReturnValue(router)
render(<LogsExplorerPage />)
await screen.findByText('1 warning')
})
@@ -0,0 +1,218 @@
---
title: Works With Supabase - announcing our Partner Gallery
description: Introducing our Partner Gallery - open source and made with Supabase.
author: alaister,shanerice,thor_schaeff
image: partner-gallery/partner-showcase-thumb.png
thumb: partner-gallery/partner-showcase-thumb.png
tags:
- community
- partnerships
date: '2022-04-20'
toc_depth: 3
---
# Introducing our Partner Gallery - open source and made with Supabase
Supabase is a collaborative effort across many open-source projects, and we’re incredibly fortunate that you all have rallied around us to make Supabase what it is today.
To keep track of all the great tools and people that “Work With Supabase”, we’ve built a [Partner Gallery](/partners) that showcases integration partners who are extending how Supabase works, as well as experts who are certified to help you build new, exciting things.
![partner-gallery-screenshot.png](/images/blog/partner-gallery/partner-gallery-screenshot.png)
As with anything at Supabase, the Partner Gallery is built on top of the [Supabase stack](/docs/architecture) and is, of course, open-source. If you’re looking to build a partner gallery yourself, feel free to use our template as a start: [github.com/supabase-community/partner-gallery-example](https://github.com/supabase-community/partner-gallery-example). It’s packed with goodies like [Postgres Full Text Search](/docs/guides/database/full-text-search), `next-image` with Supabase Storage, and sending emails based on an insert trigger with Supabase Edge Functions.
# Partner showcase - new integrations and expert services
<iframe
className="w-full"
width="700"
height="350"
src="https://www.youtube-nocookie.com/embed/videoseries?list=PL5S4mPUpp4OtKHdiuNkt8GewVo3dc-oHW"
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
## Expert services
Some agencies and developer experts have specialized experience building with Supabase. If you’re looking for the right partner to bring your application to life, check out the experts on [supabase.com/partners/experts](/partners/experts).
If you have expertise developing with Supabase and want to offer your expert services, you can [apply to become a partner today](/partners/integrations#become-a-partner).
Please join us in welcoming our first two experts!
### Morrow
<iframe
className="w-full"
width="700"
height="350"
src="https://www.youtube-nocookie.com/embed/uPN5BbIuJw8"
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
Morrow is an app development agency headquartered in Bristol, UK. Their mission is to consult, build and support cutting-edge innovation.
Their specialisms are working with Supabase, React Native, Netlify and Swell to produce bespoke solutions that are robust and beautiful.
Morrow was awarded UK App Dev Agency of the Year 2022.
### Quinence
![Quinence](/images/blog/partner-gallery/expert-quinence.png)
Quinence is a boutique product development studio from Singapore.
They build digital products that delight. You give them an idea, and they can deliver the complete experience with hyper-scalable infrastructure.
## Integrations
There are so many fantastic tools out there making it easier than ever to build stunning applications or helping you with a specific workflow. We’re ecstatic that many of them chose to integrate with Supabase to give you even more superpowers.
We’re starting off with a selection across auth, developer tools, and low-code tools. If you’ve built a tool that integrates with Supabase, we’d love to [hear from you](/partners/integrations#become-a-partner)!
## Auth
### Clerk
<iframe
className="w-full"
width="700"
height="350"
src="https://www.youtube-nocookie.com/embed/vLSynn8_SN4"
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
Clerk authenticates users, manages session tokens, and provides user management functionality that can be used in combination with the authorization logic available in Supabase through PostgreSQL Row Level Security (RLS) policies.
### Stytch
![Stytch](/images/blog/partner-gallery/integration-stytch.png)
Stytch provides an all-in-one platform for passwordless auth. Stytch makes it easy for you to embed passwordless solutions into your websites and apps for better security, better conversion rates, and a better end user experience.
## DevTools
### Prisma
<iframe
className="w-full"
width="700"
height="350"
src="https://www.youtube-nocookie.com/embed/YjvQsNEOs60"
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
Prisma is an open source next-generation ORM. It consists of the following parts:
- Prisma Client: Auto-generated and type-safe query builder for Node.js & TypeScript.
- Prisma Migrate: Migration system.
- Prisma Studio: GUI to view and edit data in your database.
### Snaplet
<iframe
className="w-full"
width="700"
height="350"
src="https://www.youtube-nocookie.com/embed/64Z6Ku0t334"
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
Snaplet is a developer tool that copies a Postgres database, transforming personal information, so that you can safely code against actual data. This functionality makes it possible to easily achieve environment parity in Supabase.
### Vercel
![Vercel](/images/blog/partner-gallery/integration-vercel.png)
Vercel enables developers to build and publish wonderful things. They build products for developers and designers. And those who aspire to become one. This makes it the perfect tool for building web apps with Supabase.
### pgMustard
<iframe
className="w-full"
width="700"
height="350"
src="https://www.youtube-nocookie.com/embed/IGYcN1Hvc84"
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
pgMustard speeds up your journey from knowing which query is a problem to working out what can be done about it.
Reading EXPLAIN ANALYZE output can be tough.
Even if you know how, Postgres includes so much useful information, it is easy to miss something important. And for queries that do a lot, the plans are even more useful-yet-time-consuming.
## Low-Code
### Clutch
<iframe
className="w-full"
width="700"
height="350"
src="https://www.youtube-nocookie.com/embed/ZZE9BabdTxE"
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
What can I achieve with Clutch & Supabase? Anything you can think of where a database would be necessary for your project. Examples include building a blog, real-time chat app, booking platform, and todo list, Supabase, and Clutch provides all the necessary tools to build your projects quickly and efficiently.
### Appsmith
<iframe
className="w-full"
width="700"
height="350"
src="https://www.youtube-nocookie.com/embed/SfAxGVjJ6Q0"
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
Appsmith is an open-source framework for building internal tools. It lets you drag-and-drop UI components to build pages, connect to any API, database or GraphQL source and write logic with JavaScript objects.
### Draftbit
![Draftbit](/images/blog/partner-gallery/integration-draftbit.png)
Draftbit is a "pro-code" low-code mobile app building platform. Draftbit exports React Native source code that is 100% run on open-source languages and libraries.
### Plasmic
<iframe
className="w-full"
width="700"
height="350"
src="https://www.youtube-nocookie.com/embed/nFWUCNbCFhI"
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
By connecting Supabase with Plasmic, a visual builder for the web, you can build data-backed applications without code. While many users leverage Plasmic to quickly launch and iterate on landing pages, Plasmic can be used as a general-purpose visual builder for React, which can be used to design and implement fully featured read-write applications.
### Teta
<iframe
className="w-full"
width="700"
height="350"
src="https://www.youtube-nocookie.com/embed/FAqQxT-peuc"
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
Teta is an online cooperative app builder. It allows designers and developers to collaborate in real-time on the design and development of mobile applications, through a convenient visual interface that allows the export of Flutter code.
+16
View File
@@ -120,5 +120,21 @@
"position": "Engineering",
"author_url": "https://github.com/dthyresson",
"author_image_url": "https://github.com/dthyresson.png"
},
{
"author_id": "shanerice",
"author": "Shane Rice",
"username": "shanerice",
"position": "Marketing",
"author_url": "https://github.com/shanerice",
"author_image_url": "https://github.com/shanerice.png"
},
{
"author_id": "alaister",
"author": "Alaister Young",
"username": "alaister",
"position": "Engineering",
"author_url": "https://github.com/alaister",
"author_image_url": "https://github.com/alaister.png"
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 707 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 831 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

+10 -2
View File
@@ -5,9 +5,17 @@
<link>https://supabase.com</link>
<description>Latest news from Supabase</description>
<language>en</language>
<lastBuildDate>Sun, 17 Apr 2022 16:00:00 GMT</lastBuildDate>
<lastBuildDate>Tue, 19 Apr 2022 16:00:00 GMT</lastBuildDate>
<atom:link href="https://supabase.com/blog/rss.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://supabase.com/blog/2022/04/20/partner-gallery-works-with-supabase</guid>
<title>Works With Supabase - announcing our Partner Gallery</title>
<link>https://supabase.com/blog/2022/04/20/partner-gallery-works-with-supabase</link>
<description>Introducing our Partner Gallery - open source and made with Supabase.</description>
<pubDate>Tue, 19 Apr 2022 16:00:00 GMT</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/2022/04/18/bring-the-func-hackathon-winners</guid>
<title>Bring the Func Hackathon Winners 2022</title>
@@ -15,7 +23,7 @@
<description>Celebrating many amazing OSS Hackathon projects using GraphQL and Edge Functions.</description>
<pubDate>Sun, 17 Apr 2022 16:00:00 GMT</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/2022/04/15/beta-update-march-2022</guid>
<title>Supabase Beta March 2022</title>
+36
View File
@@ -613,4 +613,40 @@
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/blog/2022/04/01/hackathon-bring-the-func</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/blog/2022/04/01/supabase-realtime-with-multiplayer-features</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/blog/2022/04/01/supabrew</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/blog/2022/04/15/beta-update-march-2022</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/blog/2022/04/18/bring-the-func-hackathon-winners</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/blog/2022/04/20/partner-gallery-works-with-supabase</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
</urlset>