mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Merge pull request #8112 from supabase/chore/show-upgrade-prompt-in-logs-explorer
Chore/show upgrade prompt in logs explorer
This commit is contained in:
10 files changed
+263
-158
No files matched your search
@@ -13,9 +13,10 @@ interface Props {
|
||||
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 selectedHelper = helpers.find((h) => h.text === newValue)
|
||||
|
||||
if (onChange && selectedHelper) {
|
||||
onChange({ to: selectedHelper.calcTo(), from: selectedHelper.calcFrom() })
|
||||
}
|
||||
@@ -28,6 +29,7 @@ const DatePickers: React.FC<Props> = ({ to, from, onChange, helpers }) => {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedHelper && helperValue !== selectedHelper.text) {
|
||||
setHelperValue(selectedHelper.text)
|
||||
|
||||
@@ -21,7 +21,7 @@ export const TEMPLATES: LogTemplate[] = [
|
||||
description: 'Count of commits made by users on the database',
|
||||
mode: 'custom',
|
||||
searchString: `select
|
||||
p.user_name,
|
||||
p.user_name,
|
||||
count(*) as count
|
||||
from postgres_logs
|
||||
left join unnest(metadata) as m on true
|
||||
@@ -38,7 +38,7 @@ group by
|
||||
description: 'List all IP addresses that used the Supabase API',
|
||||
mode: 'custom',
|
||||
searchString: `select
|
||||
cast(timestamp as datetime) as timestamp,
|
||||
cast(timestamp as datetime) as timestamp,
|
||||
h.x_real_ip
|
||||
from edge_logs
|
||||
left join unnest(metadata) as m on true
|
||||
@@ -52,8 +52,8 @@ where h.x_real_ip is not null
|
||||
label: 'Requests by Country',
|
||||
description: 'List all ISO 3166-1 alpha-2 country codes that used the Supabase API',
|
||||
mode: 'custom',
|
||||
searchString: `select
|
||||
cf.country,
|
||||
searchString: `select
|
||||
cf.country,
|
||||
count(*) as count
|
||||
from edge_logs
|
||||
left join unnest(metadata) as m on true
|
||||
@@ -71,11 +71,11 @@ order by
|
||||
mode: 'custom',
|
||||
description: 'List all Supabase API requests that are slow',
|
||||
searchString: `select
|
||||
cast(timestamp as datetime) as timestamp,
|
||||
cast(timestamp as datetime) as timestamp,
|
||||
event_message,
|
||||
r.origin_time
|
||||
from edge_logs
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(m.response) as r
|
||||
where
|
||||
r.origin_time > 1000
|
||||
@@ -90,11 +90,11 @@ limit 100
|
||||
description: 'List all Supabase API requests that responded witha 5XX status code',
|
||||
mode: 'custom',
|
||||
searchString: `select
|
||||
cast(timestamp as datetime) as timestamp,
|
||||
cast(timestamp as datetime) as timestamp,
|
||||
event_message,
|
||||
r.status_code
|
||||
from edge_logs
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(m.response) as r
|
||||
where
|
||||
r.status_code >= 500
|
||||
@@ -113,11 +113,11 @@ limit 100
|
||||
r.search as params,
|
||||
count(timestamp) as c
|
||||
from edge_logs
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(m.request) as r
|
||||
group by
|
||||
group by
|
||||
path,
|
||||
params
|
||||
params
|
||||
order by
|
||||
c desc
|
||||
limit 100
|
||||
@@ -132,7 +132,7 @@ limit 100
|
||||
cast(timestamp as datetime) as timestamp,
|
||||
event_message
|
||||
from edge_logs
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(m.request) as r
|
||||
where
|
||||
path like '%rest/v1%'
|
||||
@@ -453,14 +453,14 @@ export const PREVIEWER_DATEPICKER_HELPERS: DatetimeHelper[] = [
|
||||
calcTo: () => '',
|
||||
},
|
||||
{
|
||||
text: 'Last day',
|
||||
text: 'Last 24 hours',
|
||||
calcFrom: () => dayjs().subtract(1, 'day').startOf('day').toISOString(),
|
||||
calcTo: () => '',
|
||||
},
|
||||
]
|
||||
export const EXPLORER_DATEPICKER_HELPERS: DatetimeHelper[] = [
|
||||
{
|
||||
text: 'Last day',
|
||||
text: 'Last 24 hours',
|
||||
calcFrom: () => dayjs().subtract(1, 'day').startOf('day').toISOString(),
|
||||
calcTo: () => '',
|
||||
default: true,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Filters, LogData, LogsEndpointParams, LogsTableName, SQL_FILTER_TEMPLATES } from '.'
|
||||
import dayjs, { Dayjs } from 'dayjs'
|
||||
import { get } from 'lodash'
|
||||
import { StripeSubscription } from 'components/interfaces/Billing'
|
||||
|
||||
/**
|
||||
* Convert a micro timestamp from number/string to iso timestamp
|
||||
@@ -130,10 +131,10 @@ export const genDefaultQuery = (table: LogsTableName, filters: Filters) => {
|
||||
`
|
||||
|
||||
case 'postgres_logs':
|
||||
return `select postgres_logs.timestamp, id, event_message, parsed.error_severity from ${table}
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(m.parsed) as parsed
|
||||
${where}
|
||||
return `select postgres_logs.timestamp, id, event_message, parsed.error_severity from ${table}
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(m.parsed) as parsed
|
||||
${where}
|
||||
limit 100
|
||||
`
|
||||
|
||||
@@ -145,7 +146,7 @@ export const genDefaultQuery = (table: LogsTableName, filters: Filters) => {
|
||||
`
|
||||
|
||||
case 'function_edge_logs':
|
||||
return `select id, ${table}.timestamp, event_message, response.status_code, request.method, m.function_id, m.execution_time_ms, m.deployment_id, m.version from ${table}
|
||||
return `select id, ${table}.timestamp, event_message, response.status_code, 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
|
||||
@@ -156,7 +157,7 @@ export const genDefaultQuery = (table: LogsTableName, filters: Filters) => {
|
||||
default:
|
||||
return `select id, ${table}.timestamp, event_message from ${table}
|
||||
${where}
|
||||
limit 100
|
||||
limit 100
|
||||
`
|
||||
}
|
||||
}
|
||||
@@ -167,6 +168,18 @@ export const genDefaultQuery = (table: LogsTableName, filters: Filters) => {
|
||||
export const genSingleLogQuery = (table: LogsTableName, id: string) =>
|
||||
`select id, timestamp, event_message, metadata from ${table} where id = '${id}' limit 1`
|
||||
|
||||
/**
|
||||
* Determine if we should show the user an upgrade prompt while browsing logs
|
||||
*/
|
||||
export const maybeShowUpgradePrompt = (
|
||||
from: string | null | undefined,
|
||||
tierKey?: StripeSubscription['tier']["key"]
|
||||
) => {
|
||||
const day = Math.abs(dayjs().diff(dayjs(from), 'day'))
|
||||
|
||||
return (day > 1 && tierKey === 'FREE') || (day > 7 && tierKey === 'PRO') || day > 90 && tierKey === 'ENTERPRISE'
|
||||
}
|
||||
|
||||
export const genCountQuery = (table: string): string => `SELECT count(*) as count FROM ${table}`
|
||||
|
||||
/** calculates how much the chart start datetime should be offset given the current datetime filter params */
|
||||
@@ -209,11 +222,10 @@ SELECT
|
||||
FROM
|
||||
${table} t
|
||||
cross join unnest(t.metadata) as metadata
|
||||
${
|
||||
where
|
||||
${where
|
||||
? where + ` and t.timestamp > '${startOffset.toISOString()}'`
|
||||
: `where t.timestamp > '${startOffset.toISOString()}'`
|
||||
}
|
||||
}
|
||||
GROUP BY
|
||||
timestamp
|
||||
ORDER BY
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { IconAlertCircle, IconRewind, Button, Card, Input } from 'ui'
|
||||
import { IconRewind, Button } from 'ui'
|
||||
|
||||
import {
|
||||
LogTable,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
LogEventChart,
|
||||
Filters,
|
||||
ensureNoTimestampConflict,
|
||||
maybeShowUpgradePrompt,
|
||||
} from 'components/interfaces/Settings/Logs'
|
||||
import useLogsPreview from 'hooks/analytics/useLogsPreview'
|
||||
import PreviewFilterPanel from 'components/interfaces/Settings/Logs/PreviewFilterPanel'
|
||||
@@ -19,6 +20,9 @@ import PreviewFilterPanel from 'components/interfaces/Settings/Logs/PreviewFilte
|
||||
import { LOGS_TABLES } from './Logs.constants'
|
||||
import ShimmerLine from 'components/ui/ShimmerLine'
|
||||
import LoadingOpacity from 'components/ui/LoadingOpacity'
|
||||
import { useProjectSubscription } from 'hooks'
|
||||
import { useUpgradePrompt } from 'hooks/misc/useUpgradePrompt'
|
||||
import { StripeProduct } from 'components/interfaces/Billing'
|
||||
import UpgradePrompt from './UpgradePrompt'
|
||||
|
||||
/**
|
||||
@@ -46,8 +50,10 @@ export const LogsPreviewer: React.FC<Props> = ({
|
||||
tableName,
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const { s, ite, its } = router.query
|
||||
const { s, ite, its, ref } = router.query
|
||||
const [showChart, setShowChart] = useState(true)
|
||||
const { subscription } = useProjectSubscription(ref as string)
|
||||
const tier = subscription?.tier
|
||||
|
||||
const table = !tableName ? LOGS_TABLES[queryType] : tableName
|
||||
|
||||
@@ -56,6 +62,10 @@ export const LogsPreviewer: React.FC<Props> = ({
|
||||
{ loadOlder, setFilters, refresh, setParams },
|
||||
] = useLogsPreview(projectRef as string, table, filterOverride)
|
||||
|
||||
const { showUpgradePrompt, setShowUpgradePrompt } = useUpgradePrompt(
|
||||
params.iso_timestamp_start as string
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setFilters((prev) => ({ ...prev, search_query: s as string }))
|
||||
if (ite || its) {
|
||||
@@ -67,6 +77,16 @@ export const LogsPreviewer: React.FC<Props> = ({
|
||||
}
|
||||
}, [s, ite, its])
|
||||
|
||||
// Show the prompt on page load based on query params
|
||||
useEffect(() => {
|
||||
if (its) {
|
||||
const shouldShowUpgradePrompt = maybeShowUpgradePrompt(its as string, tier?.key)
|
||||
if (shouldShowUpgradePrompt) {
|
||||
setShowUpgradePrompt(!showUpgradePrompt)
|
||||
}
|
||||
}
|
||||
}, [its, tier])
|
||||
|
||||
const onSelectTemplate = (template: LogTemplate) => {
|
||||
setFilters((prev: any) => ({ ...prev, search_query: template.searchString }))
|
||||
}
|
||||
@@ -109,19 +129,25 @@ export const LogsPreviewer: React.FC<Props> = ({
|
||||
},
|
||||
})
|
||||
} else if (event === 'datepicker-change') {
|
||||
setParams((prev) => ({
|
||||
...prev,
|
||||
iso_timestamp_start: from || '',
|
||||
iso_timestamp_end: to || '',
|
||||
}))
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
its: from || '',
|
||||
ite: to || '',
|
||||
},
|
||||
})
|
||||
const shouldShowUpgradePrompt = maybeShowUpgradePrompt(from, tier?.key)
|
||||
|
||||
if (shouldShowUpgradePrompt) {
|
||||
setShowUpgradePrompt(!showUpgradePrompt)
|
||||
} else {
|
||||
setParams((prev) => ({
|
||||
...prev,
|
||||
iso_timestamp_start: from || '',
|
||||
iso_timestamp_end: to || '',
|
||||
}))
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
its: from || '',
|
||||
ite: to || '',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +224,9 @@ export const LogsPreviewer: React.FC<Props> = ({
|
||||
<Button onClick={loadOlder} icon={<IconRewind />} type="default">
|
||||
Load older
|
||||
</Button>
|
||||
<UpgradePrompt projectRef={projectRef} from={params.iso_timestamp_start || ''} />
|
||||
<div className="mt-2 flex flex-row justify-end">
|
||||
<UpgradePrompt show={showUpgradePrompt} setShowUpgradePrompt={setShowUpgradePrompt} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -51,11 +51,7 @@ const LogsQueryPanel: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="
|
||||
rounded
|
||||
rounded-bl-none rounded-br-none border border-panel-border-light bg-panel-header-light
|
||||
dark:border-panel-border-dark dark:bg-panel-header-dark
|
||||
|
||||
className=" rounded rounded-bl-none rounded-br-none border border-panel-border-light bg-panel-header-light dark:border-panel-border-dark dark:bg-panel-header-dark
|
||||
"
|
||||
>
|
||||
<div className="flex w-full items-center justify-between px-5 py-2">
|
||||
|
||||
@@ -1,101 +1,70 @@
|
||||
import Link from 'next/link'
|
||||
import dayjs from 'dayjs'
|
||||
import { useState } from 'react'
|
||||
import { Button, IconHelpCircle, IconLoader, Modal } from 'ui'
|
||||
import { Button, Modal } from 'ui'
|
||||
|
||||
import { TIER_QUERY_LIMITS } from '.'
|
||||
import { useProjectSubscription } from 'hooks'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
interface Props {
|
||||
projectRef: string
|
||||
from: string
|
||||
show: boolean
|
||||
setShowUpgradePrompt: (value: boolean) => void
|
||||
}
|
||||
|
||||
const UpgradePrompt: React.FC<Props> = ({ projectRef, from }) => {
|
||||
const [showHelperModal, setShowHelperModal] = useState(false)
|
||||
const { subscription, isLoading, isError } = useProjectSubscription(projectRef)
|
||||
|
||||
if (isLoading) return <IconLoader size={16} className="animate-spin" />
|
||||
if (isError) console.error('Error fetching project subscription')
|
||||
|
||||
const tier = subscription?.tier
|
||||
const queryLimit = TIER_QUERY_LIMITS[(tier?.key || 'FREE') as keyof typeof TIER_QUERY_LIMITS]
|
||||
|
||||
const fromValue = from ? dayjs(from) : dayjs()
|
||||
const fromMax = dayjs().startOf('day').subtract(queryLimit.value, queryLimit.unit)
|
||||
const isExceedingLimit = fromValue.isBefore(fromMax)
|
||||
const UpgradePrompt: React.FC<Props> = ({ show, setShowUpgradePrompt }) => {
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`flex flex-row items-center gap-3 px-2 py-1 text-xs transition-all ${
|
||||
isExceedingLimit
|
||||
? 'rounded border border-yellow-700 bg-yellow-200 font-semibold text-yellow-1100'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<span className="text-scale-1100">{`${queryLimit.text} retention`}</span>
|
||||
<IconHelpCircle
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
className="cursor-pointer text-scale-1100 transition hover:text-scale-1200"
|
||||
onClick={() => setShowHelperModal(true)}
|
||||
/>
|
||||
{queryLimit.promptUpgrade && (
|
||||
<Link href={`/project/${projectRef}/settings/billing`}>
|
||||
<Button size="tiny">Upgrade</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
hideFooter
|
||||
visible={showHelperModal}
|
||||
size="medium"
|
||||
header="Log retention"
|
||||
onCancel={() => setShowHelperModal(false)}
|
||||
>
|
||||
<div className="space-y-4 py-4">
|
||||
<Modal.Content>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm">
|
||||
Logs can be retained up to a duration of 3 months depending on the plan that your
|
||||
project is on. The table below shows an overview of the duration for which your logs
|
||||
will be retained for based on each plan.
|
||||
</p>
|
||||
<div className="rounded border border-scale-600 bg-scale-500">
|
||||
<div className="flex items-center px-4 pt-2 pb-1">
|
||||
<p className="w-[40%] text-sm text-scale-1100">Plan</p>
|
||||
<p className="w-[60%] text-sm text-scale-1100">Retention duration</p>
|
||||
<Modal
|
||||
hideFooter
|
||||
visible={show}
|
||||
closable
|
||||
size="medium"
|
||||
header="Log retention"
|
||||
onCancel={() => setShowUpgradePrompt(false)}
|
||||
>
|
||||
<div className="space-y-4 py-4">
|
||||
<Modal.Content>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm">
|
||||
Logs can be retained up to a duration of 3 months depending on the plan that your
|
||||
project is on.
|
||||
</p>
|
||||
<div className="border-scale-600 bg-scale-500 rounded border">
|
||||
<div className="flex items-center px-4 pt-2 pb-1">
|
||||
<p className="text-scale-1100 w-[40%] text-sm">Plan</p>
|
||||
<p className="text-scale-1100 w-[60%] text-sm">Retention duration</p>
|
||||
</div>
|
||||
<div className="py-1">
|
||||
<div className="flex items-center px-4 py-1">
|
||||
<p className="w-[40%] text-sm">Free</p>
|
||||
<p className="w-[60%] text-sm">{TIER_QUERY_LIMITS.FREE.text}</p>
|
||||
</div>
|
||||
<div className="py-1">
|
||||
<div className="flex items-center px-4 py-1">
|
||||
<p className="w-[40%] text-sm">Free</p>
|
||||
<p className="w-[60%] text-sm">{TIER_QUERY_LIMITS.FREE.text}</p>
|
||||
</div>
|
||||
<div className="flex items-center px-4 py-1">
|
||||
<p className="w-[40%] text-sm">Pro</p>
|
||||
<p className="w-[60%] text-sm">{TIER_QUERY_LIMITS.PRO.text}</p>
|
||||
</div>
|
||||
<div className="flex items-center px-4 py-1">
|
||||
<p className="w-[40%] text-sm">Enterprise</p>
|
||||
<p className="w-[60%] text-sm">{TIER_QUERY_LIMITS.ENTERPRISE.text}</p>
|
||||
</div>
|
||||
<div className="flex items-center px-4 py-1">
|
||||
<p className="w-[40%] text-sm">Pro</p>
|
||||
<p className="w-[60%] text-sm">{TIER_QUERY_LIMITS.PRO.text}</p>
|
||||
</div>
|
||||
<div className="flex items-center px-4 py-1">
|
||||
<p className="w-[40%] text-sm">Enterprise</p>
|
||||
<p className="w-[60%] text-sm">{TIER_QUERY_LIMITS.ENTERPRISE.text}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
<Modal.Seperator />
|
||||
<Modal.Content>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button block type="primary" onClick={() => setShowHelperModal(false)}>
|
||||
Understood
|
||||
</Button>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
<Modal.Seperator />
|
||||
<Modal.Content>
|
||||
<div className="flex justify-between">
|
||||
<Button type="default" onClick={() => setShowUpgradePrompt(false)}>
|
||||
Close
|
||||
</Button>
|
||||
|
||||
<Link href={`/project/${ref}/settings/billing`}>
|
||||
<Button size="tiny">Upgrade</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useProjectSubscription } from 'hooks'
|
||||
import { useRouter } from 'next/router'
|
||||
import { maybeShowUpgradePrompt } from 'components/interfaces/Settings/Logs'
|
||||
import { StripeProduct } from 'components/interfaces/Billing'
|
||||
|
||||
export const useUpgradePrompt = (from: string) => {
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
const { subscription } = useProjectSubscription(ref as string)
|
||||
const tier = subscription?.tier
|
||||
const [showUpgradePrompt, setShowUpgradePrompt] = useState(false)
|
||||
|
||||
const shouldShowUpgradePrompt = maybeShowUpgradePrompt(from, tier?.key)
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldShowUpgradePrompt) {
|
||||
setShowUpgradePrompt(true)
|
||||
}
|
||||
}, [from])
|
||||
|
||||
return { showUpgradePrompt, setShowUpgradePrompt, shouldShowUpgradePrompt }
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Input, Modal, Form, Button } from 'ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { useProjectSubscription, useStore } from 'hooks'
|
||||
import useLogsQuery from 'hooks/analytics/useLogsQuery'
|
||||
import { NextPageWithLayout, UserContent } from 'types'
|
||||
import { uuidv4 } from 'lib/helpers'
|
||||
@@ -21,8 +21,11 @@ import {
|
||||
LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD,
|
||||
LogTable,
|
||||
LogTemplate,
|
||||
maybeShowUpgradePrompt,
|
||||
TEMPLATES,
|
||||
} from 'components/interfaces/Settings/Logs'
|
||||
import { useUpgradePrompt } from 'hooks/misc/useUpgradePrompt'
|
||||
import { StripeProduct } from 'components/interfaces/Billing'
|
||||
import UpgradePrompt from 'components/interfaces/Settings/Logs/UpgradePrompt'
|
||||
|
||||
export const LogsExplorerPage: NextPageWithLayout = () => {
|
||||
@@ -33,6 +36,8 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
|
||||
const [saveModalOpen, setSaveModalOpen] = useState<boolean>(false)
|
||||
const [warnings, setWarnings] = useState<LogsWarning[]>([])
|
||||
const { content } = useStore()
|
||||
const { subscription } = useProjectSubscription(ref as string)
|
||||
const tier = subscription?.tier
|
||||
|
||||
const [{ params, logData, error, isLoading }, { changeQuery, runQuery, setParams }] =
|
||||
useLogsQuery(ref as string, {
|
||||
@@ -40,6 +45,10 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
|
||||
iso_timestamp_end: ite ? (ite as string) : undefined,
|
||||
})
|
||||
|
||||
const { showUpgradePrompt, setShowUpgradePrompt } = useUpgradePrompt(
|
||||
params.iso_timestamp_start as string
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// on mount, set initial values
|
||||
if (q) {
|
||||
@@ -65,6 +74,16 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
|
||||
setWarnings(newWarnings)
|
||||
}, [editorValue, params.iso_timestamp_start, params.iso_timestamp_end])
|
||||
|
||||
// Show the prompt on page load based on query params
|
||||
useEffect(() => {
|
||||
if (its) {
|
||||
const shouldShowUpgradePrompt = maybeShowUpgradePrompt(its as string, tier?.key)
|
||||
if (shouldShowUpgradePrompt) {
|
||||
setShowUpgradePrompt(!showUpgradePrompt)
|
||||
}
|
||||
}
|
||||
}, [its, tier])
|
||||
|
||||
const onSelectTemplate = (template: LogTemplate) => {
|
||||
setEditorValue(template.searchString)
|
||||
changeQuery(template.searchString)
|
||||
@@ -110,15 +129,21 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
|
||||
}
|
||||
|
||||
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 || '' },
|
||||
})
|
||||
const shouldShowUpgradePrompt = maybeShowUpgradePrompt(from, tier?.key)
|
||||
|
||||
if (shouldShowUpgradePrompt) {
|
||||
setShowUpgradePrompt(!showUpgradePrompt)
|
||||
} else {
|
||||
setParams((prev) => ({
|
||||
...prev,
|
||||
iso_timestamp_start: from || '',
|
||||
iso_timestamp_end: to || '',
|
||||
}))
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...router.query, its: from || '', ite: to || '' },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -139,7 +164,6 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
|
||||
isLoading={isLoading}
|
||||
warnings={warnings}
|
||||
/>
|
||||
|
||||
<div className="h-48 min-h-[7rem]">
|
||||
<ShimmerLine active={isLoading} />
|
||||
<CodeEditor
|
||||
@@ -158,7 +182,7 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
|
||||
</div>
|
||||
</LoadingOpacity>
|
||||
<div className="mt-2 flex flex-row justify-end">
|
||||
<UpgradePrompt projectRef={ref as string} from={params.iso_timestamp_start || ''} />
|
||||
<UpgradePrompt show={showUpgradePrompt} setShowUpgradePrompt={setShowUpgradePrompt} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -393,3 +393,28 @@ test('filters accept filterOverride', async () => {
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('myvalue'))
|
||||
})
|
||||
})
|
||||
|
||||
describe.each(['FREE', 'PRO', 'ENTERPRISE'])('upgrade modal for %s', (key) => {
|
||||
beforeEach(() => {
|
||||
useProjectSubscription.mockReturnValue({
|
||||
subscription: {
|
||||
tier: {
|
||||
supabase_prod_id: `tier_${key.toLocaleLowerCase()}`,
|
||||
key,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
test('based on query params', async () => {
|
||||
const router = defaultRouterMock()
|
||||
router.query = {
|
||||
...router.query,
|
||||
q: 'some_query',
|
||||
its: dayjs().subtract(4, 'months').toISOString(),
|
||||
ite: dayjs().toISOString(),
|
||||
}
|
||||
useRouter.mockReturnValue(router)
|
||||
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
|
||||
await screen.findByText('Log retention') // assert modal title is present
|
||||
})
|
||||
})
|
||||
@@ -7,6 +7,8 @@ import userEvent from '@testing-library/user-event'
|
||||
import { logDataFixture } from '../../fixtures'
|
||||
import { clickDropdown } from 'tests/helpers'
|
||||
import dayjs from 'dayjs'
|
||||
import { useProjectSubscription } from 'hooks'
|
||||
import UpgradePrompt from 'components/interfaces/Settings/Logs/UpgradePrompt'
|
||||
|
||||
const defaultRouterMock = () => {
|
||||
const router = jest.fn()
|
||||
@@ -137,21 +139,6 @@ test('custom sql querying', async () => {
|
||||
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 = {
|
||||
@@ -164,3 +151,42 @@ test('query warnings', async () => {
|
||||
render(<LogsExplorerPage />)
|
||||
await screen.findByText('1 warning')
|
||||
})
|
||||
|
||||
describe.each(['FREE', 'PRO', 'ENTERPRISE'])('upgrade modal for %s', (key) => {
|
||||
beforeEach(() => {
|
||||
useProjectSubscription.mockReturnValue({
|
||||
subscription: {
|
||||
tier: {
|
||||
supabase_prod_id: `tier_${key.toLocaleLowerCase()}`,
|
||||
key,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
test('based on query params', async () => {
|
||||
const router = defaultRouterMock()
|
||||
router.query = {
|
||||
...router.query,
|
||||
q: 'some_query',
|
||||
its: dayjs().subtract(5, 'month').toISOString(),
|
||||
ite: dayjs().toISOString(),
|
||||
}
|
||||
useRouter.mockReturnValue(router)
|
||||
render(<LogsExplorerPage />)
|
||||
await screen.findByText(/Log retention/) // assert modal title is present
|
||||
})
|
||||
|
||||
test('based on datepicker helpers', async () => {
|
||||
render(<LogsExplorerPage />)
|
||||
// click on the dropdown
|
||||
clickDropdown(await screen.findByText('Last 24 hours'))
|
||||
userEvent.click(await screen.findByText('Last 3 days'))
|
||||
|
||||
// only free tier will show modal
|
||||
if (key === 'FREE') {
|
||||
await screen.findByText('Log retention') // assert modal title is present
|
||||
} else {
|
||||
await expect(screen.findByText('Log retention')).rejects.toThrow()
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user