Merge branch 'master' into patch-3

This commit is contained in:
aar2dee2 committed 2023-09-13 10:01:59 +05:30
commit f3716668d0
28 files changed
+539 -199

No files matched your search

-4
View File
@@ -8,7 +8,6 @@ import Head from 'next/head'
import { PropsWithChildren, memo } from 'react'
import Footer from '~/components/Navigation/Footer'
import { menuState, useMenuLevelId, useMenuMobileOpen } from '~/hooks/useMenuState'
import { Announcement, LW8CountdownBanner } from 'ui'
const levelsData = {
home: {
@@ -327,9 +326,6 @@ const SiteLayout = ({ children }: PropsWithChildren<{}>) => {
<title>Supabase Docs</title>
</Head>
<main>
<Announcement>
<LW8CountdownBanner />
</Announcement>
<div className="flex flex-row h-screen">
<NavContainer />
<Container>
+15 -6
View File
@@ -3,10 +3,17 @@ import Link from 'next/link'
import { Badge } from 'ui'
import { ArrowNarrowRightIcon } from '@heroicons/react/outline'
const AnnouncementBadge = () => (
interface Props {
url: string
announcement: string
badge?: string
target?: '_self' | '_blank'
}
const AnnouncementBadge = ({ url, announcement, badge, target = '_self' }: Props) => (
<div className="w-full max-w-xl flex justify-center opacity-0 !animate-[fadeIn_0.5s_cubic-bezier(0.25,0.25,0,1)_0.5s_both]">
<Link href="/launch-week">
<Link href={url}>
<a
target={target}
className="
group
relative
@@ -26,10 +33,12 @@ const AnnouncementBadge = () => (
focus:outline-none focus:ring-brand-600 focus:ring-2 focus:rounded-full
"
>
<Badge color="brand" size="large" className="py-1">
Explore
</Badge>
<span className="text-foreground">Launch Week 8 announcements</span>
{badge && (
<Badge color="brand" size="large" className="py-1">
{badge}
</Badge>
)}
<span className="text-foreground">{announcement}</span>
<ArrowNarrowRightIcon className="h-4 ml-2 -translate-x-1 transition-transform group-hover:translate-x-0" />
<div
className="absolute inset-0 -z-10 bg-gradient-to-br
+4 -29
View File
@@ -8,7 +8,7 @@ import SectionContainer from '~/components/Layouts/SectionContainer'
import HeroFrameworks from './HeroFrameworks'
import styles from './hero.module.css'
import Image from 'next/image'
import AnnouncementBadge from '../Announcement/Badge'
// import AnnouncementBadge from '../Announcement/Badge'
const Hero = () => {
const router = useRouter()
@@ -19,7 +19,7 @@ const Hero = () => {
return (
<div className="relative -mt-[65px]">
<SectionContainer className="pt-8 md:pt-16 lg:pt-24 overflow-hidden">
<SectionContainer className="pt-8 md:pt-16 overflow-hidden">
<div className="relative">
<div className="mx-auto">
<div className="mx-auto max-w-2xl lg:col-span-6 lg:flex lg:items-center justify-center text-center">
@@ -30,9 +30,9 @@ const Hero = () => {
].join(' ')}
>
<div className="flex flex-col items-center">
<div className="z-40 w-full flex justify-center mb-8 lg:mb-8">
{/* <div className="z-40 w-full flex justify-center mb-8 lg:mb-8">
<AnnouncementBadge />
</div>
</div> */}
<h1 className="text-scale-1200 text-4xl sm:text-5xl sm:leading-none lg:text-7xl">
<span className="block text-[#F4FFFA00] bg-clip-text bg-gradient-to-b from-scale-1200 to-scale-1200 dark:to-scale-1100">
Build in a weekend
@@ -80,32 +80,7 @@ const Hero = () => {
</div>
</div>
</div>
<div className="absolute -top-2 md:top-0 -left-10 -right-10 md:left-0 md:right-0 h-[500px] lg:h-[550px] z-0 flex items-center justify-center">
<span className="absolute inset-0">
<Image
src="/images/launchweek/8/stars.svg"
alt="stars background"
layout="fill"
objectFit="cover"
className="opacity-70"
draggable={false}
/>
</span>
</div>
</SectionContainer>
<div className="absolute w-full max-w-[1600px] mx-auto h-[500px] lg:h-[750px] inset-0 z-0 flex items-center justify-center">
<Image
src="/images/launchweek/8/LW8-gradient.png"
alt="Launch Week 8 purple background gradient"
className="not-sr-only"
layout="fill"
objectFit="cover"
objectPosition="top"
priority
draggable={false}
/>
</div>
<div className="absolute top-0 left-0 right-0 h-screen bg-gradient-to-b from-[#020405] to-transparent -z-10" />
</div>
)
}
+1 -1
View File
@@ -32,7 +32,7 @@ const Developers = () => {
stroke="currentColor"
aria-hidden="true"
>
<path stroke-linecap="round" strokeLinejoin="round" strokeWidth="1.5" d={icon} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d={icon} />
</svg>
)}
{Svg && <Svg />}
+1 -5
View File
@@ -2,8 +2,7 @@ import React, { useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import Announcement from '~/components/Announcement/Announcement'
import { Button, Badge, IconStar, IconChevronDown, LW8CountdownBanner } from 'ui'
import { Button, Badge, IconStar, IconChevronDown } from 'ui'
import FlyOut from '~/components/UI/FlyOut'
import Transition from 'lib/Transition'
@@ -198,9 +197,6 @@ const Nav = () => {
return (
<>
<Announcement link="/launch-week">
<LW8CountdownBanner />
</Announcement>
<div className="sticky top-0 z-40 transform" style={{ transform: 'translate3d(0,0,999px)' }}>
<div
className={[
+15 -15
View File
@@ -1,4 +1,19 @@
[
{
"type": "Announcement",
"title": "Supabase Beta August 2023",
"description": "Launch Week 8 review and more things we shipped 🚀",
"imgUrl": "/images/blog/2023-09-07-beta-update-august-2023/monthly-update-august-2023.jpg",
"logoUrl": "/images/blog/2023-09-07-beta-update-august-2023/monthly-update-august-2023.jpg",
"organization": "Supabase",
"url": "/blog/beta-update-august-2023",
"postMeta": {
"name": "Ant Wilson",
"publishDate": "Sep 08, 2023",
"readLength": 5
},
"ctaText": "Learn more"
},
{
"type": "Announcement",
"title": "Launch Week 8 Hackathon Winners",
@@ -13,20 +28,5 @@
"readLength": 3
},
"ctaText": "See the winners"
},
{
"type": "Announcement",
"title": "Supabase Launch Week 8",
"description": "One new feature (or more) every day during a week.",
"imgUrl": "/images/launchweek/8/lw8-og.jpg",
"logoUrl": "/images/launchweek/8/lw8-og.jpg",
"organization": "Supabase",
"url": "/launch-week",
"postMeta": {
"name": "Paul Copplestone",
"publishDate": "Aug 07, 2023",
"readLength": 5
},
"ctaText": "Learn more"
}
]
+5 -5
View File
@@ -51,8 +51,8 @@ const Partners = () => {
<path
d="M91 1C120.005 1 145.841 14.5702 162.505 35.708"
stroke="url(#paint0_linear_4766_6117)"
stroke-width="2"
stroke-linecap="round"
strokeWidth="2"
strokeLinecap="round"
/>
<defs>
<linearGradient
@@ -63,9 +63,9 @@ const Partners = () => {
y2="21.5"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#1CF7C3" stop-opacity="0" />
<stop offset="0.510417" stop-color="#1CF7C3" />
<stop offset="1" stop-color="#1CF7C3" stop-opacity="0" />
<stop stopColor="#1CF7C3" stopOpacity="0" />
<stop offset="0.510417" stopColor="#1CF7C3" />
<stop offset="1" stopColor="#1CF7C3" stopOpacity="0" />
</linearGradient>
</defs>
</svg>
+9 -40
View File
@@ -14,6 +14,7 @@ import { useTheme } from 'common/Providers'
import ComputePricingModal from '~/components/Pricing/ComputePricingModal'
import { plans } from 'shared-data/plans'
import { ArrowNarrowRightIcon } from '@heroicons/react/outline'
import AnnouncementBadge from '../../components/Announcement/Badge'
export default function IndexPage() {
const router = useRouter()
@@ -151,7 +152,7 @@ export default function IndexPage() {
/>
<div>
<div className="relative z-10 py-16 lg:py-28">
<div className="relative z-10 py-16 lg:py-20">
<div className="mx-auto max-w-7xl px-4 text-center sm:px-6 lg:px-8">
<div className="mx-auto max-w-3xl space-y-2 lg:max-w-none">
<h1 className="text-brand text-base">Pricing</h1>
@@ -159,45 +160,13 @@ export default function IndexPage() {
<p className="p text-lg">
Start building for free, collaborate with a team, then scale to millions of users.
</p>
<div className="w-full flex justify-center items-center opacity-0 !animate-[fadeIn_0.5s_cubic-bezier(0.25,0.25,0,1)_0.5s_both]">
<Link href="/blog/organization-based-billing" passHref>
<a
target="_blank"
className="
group
relative
flex flex-row
items-center
pr-3 p-1
text-sm
w-auto
gap-2
text-left
rounded-full
bg-opacity-20
border
border-background-surface-100
hover:border-background-surface-300
overflow-hidden
focus:outline-none focus:ring-brand-600 focus:ring-2 focus:rounded-full
"
>
<Badge color="brand" size="large" className="py-1">
Update
</Badge>
<span className="text-foreground">Changes to how we bill</span>
<ArrowNarrowRightIcon className="h-4 ml-2 -translate-x-1 transition-transform group-hover:translate-x-0" />
<div
className="absolute inset-0 -z-10 bg-gradient-to-br
opacity-70
overflow-hidden rounded-full
from-background-surface-100
to-background-surface-300
backdrop-blur-md
"
/>
</a>
</Link>
<div className="w-full inline-flex justify-center items-center pt-3 pb-6">
<AnnouncementBadge
url="/blog/organization-based-billing"
badge="Update"
announcement="Changes to how we bill"
target="_blank"
/>
</div>
</div>
</div>
+1 -1
View File
@@ -94,7 +94,7 @@ export const plans: PricingInformation[] = [
'Daily backups stored for 14 days',
'Standardised Security Questionnaire',
'SOC2',
'HIPAA',
'HIPAA available as paid add-on',
'SSO for Supabase Dashboard',
'Priority email support & SLAs',
'28-day log retention',
+2 -2
View File
@@ -469,8 +469,8 @@ export const pricing: Pricing = {
plans: {
free: false,
pro: false,
team: true,
enterprise: true,
team: 'Available as paid add-on',
enterprise: 'Available as paid add-on',
},
tooltips: {
main: 'Available as a paid add-on on Team plan and above.',
@@ -228,6 +228,52 @@ export const PRESET_CONFIG: Record<Presets, PresetConfig> = {
title: '',
queries: {},
},
[Presets.STORAGE]: {
title: 'Storage',
queries: {
cacheHitRate: {
queryType: 'logs',
// storage report does not perform any filtering
sql: (_filters) => `
-- cache-hit-rate
SELECT
timestamp_trunc(timestamp, hour) as timestamp,
countif( h.cf_cache_status in ('HIT', 'STALE', 'REVALIDATED', 'UPDATING') ) as hit_count,
countif( h.cf_cache_status in ('MISS', 'NONE/UNKNOWN', 'EXPIRED', 'BYPASS', 'DYNAMIC') ) as miss_count
from edge_logs f
cross join unnest(f.metadata) as m
cross join unnest(m.request) as r
cross join unnest(m.response) as res
cross join unnest(res.headers) as h
where starts_with(r.path, '/storage/v1/object') and r.method = 'GET'
group by timestamp
order by timestamp desc
`,
},
topCacheMisses: {
queryType: 'logs',
// storage report does not perform any filtering
sql: (_filters) => `
-- top-cache-misses
SELECT
r.path as path,
r.search as search,
count(id) as count
from edge_logs f
cross join unnest(f.metadata) as m
cross join unnest(m.request) as r
cross join unnest(m.response) as res
cross join unnest(res.headers) as h
where starts_with(r.path, '/storage/v1/object')
and r.method = 'GET'
and h.cf_cache_status in ('MISS', 'NONE/UNKNOWN', 'EXPIRED', 'BYPASS', 'DYNAMIC')
group by path, search
order by count desc
limit 12
`,
},
},
},
[Presets.QUERY_PERFORMANCE]: {
title: 'Query performance',
queries: {
@@ -3,6 +3,7 @@ import { DEFAULT_QUERY_PARAMS } from './Reports.constants'
export enum Presets {
API = 'api',
STORAGE = 'storage',
AUTH = 'auth',
QUERY_PERFORMANCE = 'query_performance',
}
@@ -0,0 +1,86 @@
import { TextFormatter } from 'components/interfaces/Settings/Logs/LogsFormatters'
import Table from 'components/to-be-cleaned/Table'
import StackedBarChart from 'components/ui/Charts/StackedBarChart'
import { Fragment } from 'react'
import { ReportWidgetProps, ReportWidgetRendererProps } from '../ReportWidget'
export const CacheHitRateChartRenderer = (
props: ReportWidgetProps<{
timestamp: string
hit_count: number
miss_count: number
}>
) => {
const stackedData = props.data.flatMap((datum) => [
{
timestamp: datum.timestamp,
count: datum.hit_count,
type: 'hit',
},
{
timestamp: datum.timestamp,
count: datum.miss_count,
type: 'miss',
},
])
return (
<StackedBarChart
hideHeader
variant="percentages"
data={stackedData}
xAxisKey="timestamp"
yAxisKey="count"
stackKey="type"
stackColors={['brand', 'amber']}
/>
)
}
export const TopCacheMissesRenderer = (
props: ReportWidgetRendererProps<{
path: string
search: string
count: number
}>
) => {
if (props.data.length === 0) return null
const headerClasses = '!text-xs !py-2 p-0 font-bold !bg-scale-400'
const cellClasses = '!text-xs !py-2'
return (
<>
<h4>Top Cache Misses</h4>
<Table
head={
<>
<Table.th className={headerClasses}>Request</Table.th>
<Table.th className={headerClasses + ' text-right'}>Count</Table.th>
</>
}
body={
<>
{props.data.map((datum) => (
<Fragment key={datum.path + (datum.search || '')}>
<Table.tr className="p-0">
<Table.td className={[cellClasses].join(' ')}>
<div className=" truncate max-w-sm lg:max-w-lg">
<TextFormatter className="text-scale-1100" value={datum.path} />
<TextFormatter
className="max-w-sm text-scale-900 truncate "
value={decodeURIComponent(datum.search || '')}
/>
</div>
</Table.td>
<Table.td className={[cellClasses, 'text-right align-top'].join(' ')}>
{datum.count}
</Table.td>
</Table.tr>
</Fragment>
))}
</>
}
/>
</>
)
}
@@ -2,7 +2,17 @@ import type { PostgresColumn, PostgresSchema, PostgresTable } from '@supabase/po
import { Dictionary } from 'components/grid'
import { find, get, isEmpty, sortBy } from 'lodash'
import { useEffect, useState } from 'react'
import { IconDatabase, IconHelpCircle, Input, Listbox, SidePanel } from 'ui'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
IconAlertTriangle,
IconDatabase,
IconHelpCircle,
Input,
Listbox,
SidePanel,
} from 'ui'
import InformationBox from 'components/ui/InformationBox'
import { FOREIGN_KEY_DELETION_ACTION } from 'data/database/database-query-constants'
@@ -153,6 +163,8 @@ const ForeignKeySelector = ({
resolve()
}
const matchingColumnTypes = selectedColumn?.format === column?.format
return (
<SidePanel
key="ForeignKeySelector"
@@ -173,6 +185,8 @@ const ForeignKeySelector = ({
customFooter={
<ActionBar
backButtonLabel="Cancel"
// if the type of the two columns don't match, disable the save button
disableApply={!matchingColumnTypes}
applyButtonLabel="Save"
closePanel={closePanel}
applyFunction={onSaveChanges}
@@ -285,6 +299,31 @@ const ForeignKeySelector = ({
))}
</Listbox>
)}
{!matchingColumnTypes && (
<Alert_Shadcn_ variant="warning">
<IconAlertTriangle strokeWidth={2} />
<AlertTitle_Shadcn_>The column types don't match</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_ className="leading-6">
<span>The referenced column</span>
{column?.name && <span className="text-code">{column.name}</span>}
{column?.format ? (
<>
<span> is of type </span>
<span className="text-code">{column.format}</span>
</>
) : (
<span> has no type</span>
)}
<span> while the selected foreign column </span>
<span className="text-code">
{selectedTable?.name}.{selectedColumn?.name}
</span>
<span> has </span>
<span className="text-code">{selectedColumn?.data_type}</span>type. These two
columns can't be referenced until they are of the same type.
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
)}
<SidePanel.Separator />
<InformationBox
icon={<IconHelpCircle size="large" strokeWidth={1.5} />}
@@ -4,7 +4,7 @@ import { PropsWithChildren } from 'react'
import ProductMenu from 'components/ui/ProductMenu'
import { useSelectedProject, withAuth } from 'hooks'
import ProjectLayout from '../'
import { generateReportsMenu } from './ReportsMenu.utils'
import { useFlag } from 'hooks'
interface ReportsLayoutProps {
title?: string
@@ -15,12 +15,61 @@ const ReportsLayout = ({ title, children }: PropsWithChildren<ReportsLayoutProps
const project = useSelectedProject()
const page = router.pathname.split('/')[4] || ''
const ref = project?.ref ?? 'default'
const storageReportFlag = useFlag('storageReport')
const menuItems = [
{
title: '',
items: [
{
name: 'Custom reports',
key: '',
url: `/project/${ref}/reports`,
items: [],
},
],
},
{
items: [
{
name: 'API',
key: 'api-overview',
url: `/project/${ref}/reports/api-overview`,
items: [],
},
...(storageReportFlag
? [
{
name: 'Storage',
key: 'storage',
url: `/project/${ref}/reports/storage`,
items: [],
label: 'NEW',
},
]
: []),
{
name: 'Database',
key: 'database',
url: `/project/${ref}/reports/database`,
items: [],
},
{
name: 'Query Performance',
key: 'query-performance',
url: `/project/${ref}/reports/query-performance`,
items: [],
},
],
},
]
return (
<ProjectLayout
title={title}
product="Reports"
productMenu={<ProductMenu page={page} menu={generateReportsMenu(project)} />}
productMenu={<ProductMenu page={page} menu={menuItems} />}
>
<main style={{ maxHeight: '100vh' }} className="flex-1 overflow-y-auto">
{children}
@@ -1,44 +0,0 @@
import { Project } from 'types'
import { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types'
export const generateReportsMenu = (project?: Project): ProductMenuGroup[] => {
const ref = project?.ref ?? 'default'
return [
{
title: '',
items: [
{
name: 'Custom reports',
key: '',
url: `/project/${ref}/reports`,
items: [],
},
],
},
{
items: [
{
name: 'API',
key: 'api-overview',
url: `/project/${ref}/reports/api-overview`,
items: [],
label: 'NEW',
},
{
name: 'Database',
key: 'database',
url: `/project/${ref}/reports/database`,
items: [],
},
{
name: 'Query Performance',
key: 'query-performance',
url: `/project/${ref}/reports/query-performance`,
items: [],
label: 'NEW',
},
],
},
]
}
@@ -1,11 +1,21 @@
import { Form, Modal, Input, Button } from 'ui'
import dayjs from 'dayjs'
import { observer } from 'mobx-react-lite'
import { Button, Form, Input, Listbox, Modal } from 'ui'
import { DATETIME_FORMAT } from 'lib/constants'
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
export interface CustomExpiryModalProps {
onCopyUrl: (name: string, url: string) => void
}
const unitMap = {
days: 3600 * 24,
weeks: 3600 * 24 * 7,
months: 3600 * 24 * 30,
years: 3600 * 24 * 365,
}
const CustomExpiryModal = ({ onCopyUrl }: CustomExpiryModalProps) => {
const storageExplorerStore = useStorageStore()
const { getFileUrl, selectedFileCustomExpiry, setSelectedFileCustomExpiry } = storageExplorerStore
@@ -25,34 +35,57 @@ const CustomExpiryModal = ({ onCopyUrl }: CustomExpiryModalProps) => {
>
<Form
validateOnBlur
initialValues={{ expiresIn: '' }}
initialValues={{ expiresIn: '', units: 'days' }}
onSubmit={async (values: any, { setSubmitting }: any) => {
setSubmitting(true)
onCopyUrl(
selectedFileCustomExpiry.name,
await getFileUrl(selectedFileCustomExpiry, values.expiresIn)
await getFileUrl(
selectedFileCustomExpiry,
values.expiresIn * unitMap[values.units as 'days' | 'weeks' | 'months' | 'years']
)
)
setSubmitting(false)
onClose()
}}
validate={(values: any) => {
const errors: any = {}
if (values.expiresIn !== '' && values.expiresIn <= 0)
if (values.expiresIn !== '' && values.expiresIn <= 0) {
errors.expiresIn = 'Expiry duration cannot be less than 0'
}
return errors
}}
>
{({ values, isSubmitting }: { values: any; isSubmitting: boolean }) => (
<>
<div className="py-6">
<div className="pt-4 pb-2">
<Modal.Content>
<Input
disabled={isSubmitting}
type="number"
id="expiresIn"
label="Enter the duration for which the URL will be valid:"
actions={<p className="text-sm text-scale-1000 mr-2">seconds</p>}
/>
<p className="text-sm text-light mb-2">
Enter the duration for which the URL will be valid for:
</p>
<div className="flex items-center space-x-2">
<Input disabled={isSubmitting} type="number" id="expiresIn" className="w-full" />
<Listbox id="units" className="w-[150px]">
<Listbox.Option id="days" label="days" value="days">
days
</Listbox.Option>
<Listbox.Option id="weeks" label="weeks" value="weeks">
weeks
</Listbox.Option>
<Listbox.Option id="months" label="months" value="months">
months
</Listbox.Option>
<Listbox.Option id="years" label="years" value="years">
years
</Listbox.Option>
</Listbox>
</div>
{values.expiresIn !== '' && (
<p className="text-sm text-light mt-2">
URL will expire on{' '}
{dayjs().add(values.expiresIn, values.units).format(DATETIME_FORMAT)}
</p>
)}
</Modal.Content>
</div>
<Modal.Separator />
@@ -409,6 +409,7 @@ const FileExplorerRow = ({
/>
) : (
<Dropdown
modal={false}
side="bottom"
align="end"
overlay={[
+28 -21
View File
@@ -10,28 +10,35 @@ export const CHART_COLORS = {
}
// refer to packages/ui/radix-colors.js for full list of colors
export const STACK_COLORS = [
['brand', 9],
['mint', 9],
['blue', 9],
['sky', 9],
['lime', 9],
['yellow', 9],
['orange', 9],
].map(([color, n]) => ({
lighter: `var(--colors-${color}${(n as number) - 1})`,
base: `var(--colors-${color}${n})`,
darker: `var(--colors-${color}${(n as number) + 1})`,
}))
export type ValidStackColor =
| 'brand'
| 'blue'
| 'red'
| 'yellow'
| 'green'
| 'slate'
| 'indigo'
| 'tomato'
| 'orange'
| 'amber'
export const USAGE_COLORS = {
200: 'hsl(var(--brand-default))',
201: 'hsl(var(--brand-500))',
400: 'var(--colors-amber9)',
401: 'var(--colors-amber8)',
404: 'var(--colors-amber7)',
500: 'var(--colors-red9)',
}
export const genStackColorScales = (colors: ValidStackColor[]) =>
colors.map((color) => {
// override default base scale for certain colors that do not have good contrast
const scale =
(
{
slate: 11,
} as any
)[color] ?? 9
return {
lighter: `var(--colors-${color}${(scale as number) - 1})`,
base: `var(--colors-${color}${scale})`,
darker: `var(--colors-${color}${(scale as number) + 1})`,
}
})
export const DEFAULT_STACK_COLORS : ValidStackColor[] = ['brand', 'slate', 'blue', 'yellow', 'indigo']
export enum DateTimeFormats {
FULL = 'MMM D, YYYY, hh:mma',
@@ -1,9 +1,7 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import { ComponentMeta } from '@storybook/react'
import dayjs from 'dayjs'
import Chart from './Sparkline'
import { StackedChartProps } from './Charts.types'
import { STACK_COLORS } from './Charts.constants'
export default {
title: 'Charts/Sparkline',
@@ -3,7 +3,7 @@ import { ComponentStory, ComponentMeta } from '@storybook/react'
import dayjs from 'dayjs'
import Chart from './StackedBarChart'
import { StackedChartProps } from './Charts.types'
import { STACK_COLORS } from './Charts.constants'
import { DEFAULT_STACK_COLORS, genStackColorScales } from './Charts.constants'
export default {
title: 'Charts/StackedBarChart',
@@ -38,7 +38,7 @@ export const StackedBarChart = () => (
<div className="flex flex-col gap-4 ">
<ExampleName>Default Stacked Chart Colors</ExampleName>
<div className="flex flex-row ">
{STACK_COLORS.map((c) => (
{genStackColorScales(DEFAULT_STACK_COLORS).map((c) => (
<div className="w-10 h-10" key={c.base} style={{ background: c.base }}></div>
))}
</div>
@@ -1,7 +1,7 @@
import { useState } from 'react'
import { BarChart, Bar, XAxis, Tooltip, Legend, Cell, TooltipProps } from 'recharts'
import ChartHeader from './ChartHeader'
import { CHART_COLORS, STACK_COLORS, DateTimeFormats } from './Charts.constants'
import { CHART_COLORS, DateTimeFormats, DEFAULT_STACK_COLORS, genStackColorScales, ValidStackColor } from './Charts.constants'
import { CommonChartProps } from './Charts.types'
import { timestampFormatter, useChartSize, useStacked } from './Charts.utils'
import { precisionFormatter } from './Charts.utils'
@@ -16,6 +16,7 @@ interface Props extends CommonChartProps<any> {
displayDateInUtc?: boolean
hideLegend?: boolean
hideHeader?: boolean
stackColors?: ValidStackColor[]
}
const StackedBarChart: React.FC<Props> = ({
size,
@@ -33,6 +34,7 @@ const StackedBarChart: React.FC<Props> = ({
displayDateInUtc,
hideLegend = false,
hideHeader = false,
stackColors = DEFAULT_STACK_COLORS
}) => {
const { Container } = useChartSize(size)
const { dataKeys, stackedData, percentagesStackedData } = useStacked({
@@ -44,6 +46,7 @@ const StackedBarChart: React.FC<Props> = ({
})
const [focusDataIndex, setFocusDataIndex] = useState<number | null>(null)
if (!data || data.length === 0) return <NoDataPlaceholder size={size} />
const stackColorScales = genStackColorScales(stackColors)
return (
<div className="w-full">
{!hideHeader && (
@@ -94,7 +97,7 @@ const StackedBarChart: React.FC<Props> = ({
dataKey={datum}
type="monotone"
legendType="circle"
fill={STACK_COLORS[stackIndex].base}
fill={stackColorScales[stackIndex].base}
stackId={1}
animationDuration={300}
maxBarSize={48}
+3 -1
View File
@@ -178,7 +178,9 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) {
return res.json(debugSqlResult)
} catch (error) {
console.error(
`AI SQL editing failed: ${isError(error) ? error.message : 'An unknown error occurred'}`
`AI SQL editing failed: ${
isError(error) ? error.message : 'An unknown error occurred'
}, sqlResponseString: ${sqlResponseString}`
)
return res.status(500).json({
+3 -1
View File
@@ -170,7 +170,9 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) {
return res.json(editSqlResult)
} catch (error) {
console.error(
`AI SQL editing failed: ${isError(error) ? error.message : 'An unknown error occurred'}`
`AI SQL editing failed: ${
isError(error) ? error.message : 'An unknown error occurred'
}, sqlResponseString: ${sqlResponseString}`
)
return res.status(500).json({
+3 -1
View File
@@ -158,7 +158,9 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) {
return res.json(generateSqlResult)
} catch (error) {
console.error(
`AI SQL editing failed: ${isError(error) ? error.message : 'An unknown error occurred'}`
`AI SQL editing failed: ${
isError(error) ? error.message : 'An unknown error occurred'
}, sqlResponseString: ${sqlResponseString}`
)
return res.status(500).json({
+3 -1
View File
@@ -136,7 +136,9 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) {
return res.json(generateTitleResult)
} catch (error) {
console.error(
`AI SQL editing failed: ${isError(error) ? error.message : 'An unknown error occurred'}`
`AI SQL editing failed: ${
isError(error) ? error.message : 'An unknown error occurred'
}, titleResponseString: ${titleResponseString}`
)
return res.status(500).json({
@@ -0,0 +1,126 @@
import { observer } from 'mobx-react-lite'
import { NextPageWithLayout } from 'types'
import { ReportsLayout } from 'components/layouts'
import {
PRESET_CONFIG,
REPORTS_DATEPICKER_HELPERS,
} from 'components/interfaces/Reports/Reports.constants'
import ReportWidget from 'components/interfaces/Reports/ReportWidget'
import { queriesFactory } from 'components/interfaces/Reports/Reports.utils'
import {
CacheHitRateChartRenderer,
TopCacheMissesRenderer,
} from 'components/interfaces/Reports/renderers/StorageRenderers'
import { useEffect, useMemo } from 'react'
import ReportHeader from 'components/interfaces/Reports/ReportHeader'
import { DatePickerToFrom, LogsEndpointParams } from 'components/interfaces/Settings/Logs'
import { useParams } from 'common'
import ShimmerLine from 'components/ui/ShimmerLine'
import ReportPadding from 'components/interfaces/Reports/ReportPadding'
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
import DatePickers from 'components/interfaces/Settings/Logs/Logs.DatePickers'
export const StorageReport: NextPageWithLayout = () => {
const { ref: projectRef } = useParams()
const report = useStorageReport()
const { data: subscription } = useProjectSubscriptionV2Query({ projectRef })
const plan = subscription?.plan
const handleDatepickerChange = ({ from, to }: DatePickerToFrom) => {
report.mergeParams({
iso_timestamp_start: from || '',
iso_timestamp_end: to || '',
})
}
const datepickerHelpers = useMemo(
() =>
REPORTS_DATEPICKER_HELPERS.map((helper, index) => ({
...helper,
disabled: (index > 0 && plan?.id === 'free') || (index > 1 && plan?.id !== 'pro'),
})),
[]
)
return (
<ReportPadding>
<ReportHeader title="Storage" isLoading={report.isLoading} onRefresh={report.refresh} />
<div className="w-full flex flex-col gap-1">
<div>
<DatePickers
onChange={handleDatepickerChange}
to={report.params.cacheHitRate.iso_timestamp_end || ''}
from={report.params.cacheHitRate.iso_timestamp_start || ''}
helpers={datepickerHelpers}
/>
</div>
<div className="h-2 w-full">
<ShimmerLine active={report.isLoading} />
</div>
</div>
<ReportWidget
isLoading={report.isLoading}
params={report.params.cacheHitRate}
title="Request Caching"
tooltip="The number of storage requests that are cached at the edge level. A higher number of hits is better."
data={report.data.cacheHitRate || []}
renderer={CacheHitRateChartRenderer}
append={TopCacheMissesRenderer}
appendProps={{ data: report.data.topCacheMisses || [] }}
/>
</ReportPadding>
)
}
// hook to fetch data
const useStorageReport = () => {
const { ref: projectRef } = useParams()
const queryHooks = queriesFactory<keyof typeof PRESET_CONFIG.storage.queries>(
PRESET_CONFIG.storage.queries,
projectRef ?? 'default'
)
const cacheHitRate = queryHooks.cacheHitRate()
const topCacheMisses = queryHooks.topCacheMisses()
const activeHooks = [cacheHitRate, topCacheMisses]
const handleRefresh = async () => {
activeHooks.forEach((hook) => hook.runQuery())
}
const handleSetParams = (params: Partial<LogsEndpointParams>) => {
activeHooks.forEach((hook) => {
hook.setParams?.((prev: LogsEndpointParams) => ({ ...prev, ...params }))
})
}
useEffect(()=>{
if (cacheHitRate.changeQuery) {
cacheHitRate.changeQuery(PRESET_CONFIG.storage.queries.cacheHitRate.sql([]))
}
if (topCacheMisses.changeQuery) {
topCacheMisses.changeQuery(PRESET_CONFIG.storage.queries.topCacheMisses.sql([]))
}
}, [])
const isLoading = activeHooks.some((hook) => hook.isLoading)
return {
data: {
cacheHitRate: cacheHitRate.logData,
topCacheMisses: topCacheMisses.logData,
},
params: {
cacheHitRate: cacheHitRate.params,
topCacheMisses: topCacheMisses.params,
},
mergeParams: handleSetParams,
isLoading,
refresh: handleRefresh,
}
}
StorageReport.getLayout = (page) => <ReportsLayout>{page}</ReportsLayout>
export default observer(StorageReport)
@@ -0,0 +1,42 @@
import { get } from 'lib/common/fetch'
import { render } from '../../../helpers'
import { waitFor, screen } from '@testing-library/react'
import { StorageReport } from 'pages/project/[ref]/reports/storage'
import userEvent from '@testing-library/user-event'
beforeEach(() => {
// reset mocks between tests
get.mockReset()
get.mockImplementation(async (_url) => [{ result: [] }])
})
test(`static elements`, async () => {
render(<StorageReport />)
await screen.findByText('Request Caching')
await screen.findByText(/Last 24 hours/)
await screen.findByText(/Custom/)
})
test('refresh button', async () => {
render(<StorageReport />)
await waitFor(() => expect(get).toBeCalled())
get.mockReset()
userEvent.click(await screen.findByText(/Refresh/))
await waitFor(() => expect(get).toBeCalled())
})
test('append - top cache misses', async () => {
get.mockImplementation(async (url) => {
if (decodeURIComponent(url).includes('misses')) {
return {
result: [{ path: 'mypath', search: 'some-query', count: 22 }],
}
}
return { result: [{ timestamp: new Date().toISOString(), miss_count: 123, hit_count: 123 }] }
})
render(<StorageReport />)
await waitFor(() => expect(get).toBeCalled())
await screen.findAllByText(/mypath/)
await screen.findAllByText(/some\-query/)
await screen.findAllByText(/22/)
})