Merge pull request #14418 from supabase/master

Prod deploy dashboard
This commit is contained in:
Joshen Lim authored and GitHub committed 2023-05-17 15:23:55 +08:00
commit 5f4e87fc12
100 files changed
+1170 -937

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
name: UI Tests
on:
pull_request:
branches: [master]
paths:
- 'packages/ui/**'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install deps
run: npm ci
- name: Run tests
run: npm run test:ui
+125 -125
View File
@@ -37,8 +37,8 @@ const DocsCoverLogo = () => {
transition={pathMotionConfig.transition as any}
d="M124.951 5.03906V224.602"
stroke="url(#paint0_radial_0_1)"
stroke-opacity="0.5"
stroke-width="0.7"
strokeOpacity="0.5"
strokeWidth="0.7"
/>
<m.path
initial={pathMotionConfig.initial}
@@ -46,10 +46,10 @@ const DocsCoverLogo = () => {
transition={pathMotionConfig.transition as any}
d="M125.25 191.383C167.828 191.383 202.344 156.867 202.344 114.289C202.344 71.7115 167.828 37.1953 125.25 37.1953C82.6724 37.1953 48.1562 71.7115 48.1562 114.289C48.1562 156.867 82.6724 191.383 125.25 191.383Z"
stroke="var(--colors-scale11)"
stroke-opacity="0.1"
stroke-width="0.7"
stroke-miterlimit="10"
stroke-linejoin="bevel"
strokeOpacity="0.1"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
@@ -57,10 +57,10 @@ const DocsCoverLogo = () => {
transition={pathMotionConfig.transition as any}
d="M33.7782 58.8377C48.6666 58.8377 60.7361 46.7682 60.7361 31.8798C60.7361 16.9913 48.6666 4.92188 33.7782 4.92188C18.8898 4.92188 6.82031 16.9913 6.82031 31.8798C6.82031 46.7682 18.8898 58.8377 33.7782 58.8377Z"
stroke="url(#paint1_linear_0_1)"
stroke-opacity="0.5"
stroke-width="0.7"
stroke-miterlimit="10"
stroke-linejoin="bevel"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
@@ -68,10 +68,10 @@ const DocsCoverLogo = () => {
transition={pathMotionConfig.transition as any}
d="M33.7782 223.664C48.6666 223.664 60.7361 211.594 60.7361 196.706C60.7361 181.817 48.6666 169.748 33.7782 169.748C18.8898 169.748 6.82031 181.817 6.82031 196.706C6.82031 211.594 18.8898 223.664 33.7782 223.664Z"
stroke="url(#paint2_linear_0_1)"
stroke-opacity="0.5"
stroke-width="0.7"
stroke-miterlimit="10"
stroke-linejoin="bevel"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
@@ -79,10 +79,10 @@ const DocsCoverLogo = () => {
transition={pathMotionConfig.transition as any}
d="M216.724 58.8377C231.612 58.8377 243.681 46.7682 243.681 31.8798C243.681 16.9913 231.612 4.92188 216.724 4.92188C201.835 4.92188 189.766 16.9913 189.766 31.8798C189.766 46.7682 201.835 58.8377 216.724 58.8377Z"
stroke="url(#paint3_linear_0_1)"
stroke-opacity="0.5"
stroke-width="0.7"
stroke-miterlimit="10"
stroke-linejoin="bevel"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
@@ -90,36 +90,36 @@ const DocsCoverLogo = () => {
transition={pathMotionConfig.transition as any}
d="M216.724 223.664C231.612 223.664 243.681 211.594 243.681 196.706C243.681 181.817 231.612 169.748 216.724 169.748C201.835 169.748 189.766 181.817 189.766 196.706C189.766 211.594 201.835 223.664 216.724 223.664Z"
stroke="url(#paint4_linear_0_1)"
stroke-opacity="0.5"
stroke-width="0.7"
stroke-miterlimit="10"
stroke-linejoin="bevel"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
animate={pathMotionConfig.animate}
transition={pathMotionConfig.transition as any}
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M209.29 226.982H40.6907C18.7665 226.982 1 209.51 1 187.949V40.0333C1 18.4722 18.7665 1 40.6907 1H209.29C231.214 1 248.981 18.4722 248.981 40.0333V187.969C248.981 209.51 231.214 226.982 209.29 226.982Z"
stroke="url(#paint5_linear_0_1)"
stroke-opacity="0.5"
stroke-width="0.7"
stroke-miterlimit="10"
stroke-linejoin="bevel"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
animate={pathMotionConfig.animate}
transition={pathMotionConfig.transition as any}
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M216.724 223.903H33.7779C18.719 223.903 6.5 211.684 6.5 196.625V31.9576C6.5 16.8987 18.719 4.67969 33.7779 4.67969H216.724C231.782 4.67969 244.001 16.8987 244.001 31.9576V196.625C244.001 211.704 231.802 223.903 216.724 223.903Z"
stroke="url(#paint6_linear_0_1)"
stroke-opacity="0.5"
stroke-width="0.7"
stroke-miterlimit="10"
stroke-linejoin="bevel"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
@@ -127,10 +127,10 @@ const DocsCoverLogo = () => {
transition={pathMotionConfig.transition as any}
d="M60.7578 30.9199V197.027"
stroke="url(#paint7_linear_0_1)"
stroke-opacity="0.5"
stroke-width="0.7"
stroke-miterlimit="10"
stroke-linejoin="bevel"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
@@ -138,10 +138,10 @@ const DocsCoverLogo = () => {
transition={pathMotionConfig.transition as any}
d="M125.252 5.01953V223.562"
stroke="url(#paint8_radial_0_1)"
stroke-opacity="0.5"
stroke-width="0.7"
stroke-miterlimit="10"
stroke-linejoin="bevel"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
@@ -149,10 +149,10 @@ const DocsCoverLogo = () => {
transition={pathMotionConfig.transition as any}
d="M149.809 4.83984L13.5996 178.866"
stroke="url(#paint9_linear_0_1)"
stroke-opacity="0.5"
stroke-width="0.7"
stroke-miterlimit="10"
stroke-linejoin="bevel"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
@@ -160,10 +160,10 @@ const DocsCoverLogo = () => {
transition={pathMotionConfig.transition as any}
d="M239.679 46.457L99.5703 223.643"
stroke="url(#paint10_linear_0_1)"
stroke-opacity="0.5"
stroke-width="0.7"
stroke-miterlimit="10"
stroke-linejoin="bevel"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
@@ -171,10 +171,10 @@ const DocsCoverLogo = () => {
transition={pathMotionConfig.transition as any}
d="M189.766 30.9199V197.027"
stroke="url(#paint11_linear_0_1)"
stroke-opacity="0.5"
stroke-width="0.7"
stroke-miterlimit="10"
stroke-linejoin="bevel"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
@@ -182,9 +182,9 @@ const DocsCoverLogo = () => {
transition={pathMotionConfig.transition as any}
d="M218.024 58.7344H32.498"
stroke="url(#paint12_radial_0_1)"
stroke-width="0.7"
stroke-miterlimit="10"
stroke-linejoin="bevel"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
@@ -192,10 +192,10 @@ const DocsCoverLogo = () => {
transition={pathMotionConfig.transition as any}
d="M6.76172 95.4141H243.743"
stroke="url(#paint13_linear_0_1)"
stroke-opacity="0.5"
stroke-width="0.7"
stroke-miterlimit="10"
stroke-linejoin="bevel"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
@@ -203,10 +203,10 @@ const DocsCoverLogo = () => {
transition={pathMotionConfig.transition as any}
d="M6.76172 131.869H243.743"
stroke="url(#paint14_linear_0_1)"
stroke-opacity="0.5"
stroke-width="0.7"
stroke-miterlimit="10"
stroke-linejoin="bevel"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
@@ -214,10 +214,10 @@ const DocsCoverLogo = () => {
transition={pathMotionConfig.transition as any}
d="M218.024 169.846H32.498"
stroke="var(--colors-scale11)"
stroke-opacity="0.1"
stroke-width="0.7"
stroke-miterlimit="10"
stroke-linejoin="bevel"
strokeOpacity="0.1"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
@@ -225,10 +225,10 @@ const DocsCoverLogo = () => {
transition={pathMotionConfig.transition as any}
d="M126.559 4.6867L126.559 224.254"
stroke="url(#paint15_linear_0_1)"
stroke-opacity="0.5"
stroke-width="0.7"
stroke-miterlimit="10"
stroke-linejoin="bevel"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<g filter="url(#filter0_d_0_1)">
<m.path
@@ -237,8 +237,8 @@ const DocsCoverLogo = () => {
transition={logoMotionConfig.transition as any}
d="M125.751 174.003C125.833 179.464 134.091 181.807 137.49 177.53L188.786 112.985C194.84 105.367 189.41 94.1317 179.674 94.1317H126.964L127.046 52.5389C126.964 47.078 119.027 44.7344 115.628 49.0116L65.5801 113.556C59.5255 121.175 64.9558 132.41 74.6925 132.41H125.161L125.751 174.003Z"
fill="url(#paint16_linear_0_1)"
fill-opacity="0.79"
shape-rendering="crispEdges"
fillOpacity="0.79"
shapeRendering="crispEdges"
/>
<m.path
initial={pathMotionConfig.initial}
@@ -246,10 +246,10 @@ const DocsCoverLogo = () => {
transition={pathMotionConfig.transition as any}
d="M126.964 94.6797L126.416 94.1306L126.498 52.5425C126.459 50.117 124.68 48.3327 122.422 47.6478C120.162 46.9621 117.586 47.432 116.059 49.3499C116.059 49.3508 116.058 49.3516 116.057 49.3525L66.0131 113.892L66.0091 113.897C60.2405 121.156 65.4135 131.862 74.6925 131.862H125.161L125.709 132.402L126.299 173.995L126.299 173.995M126.964 94.6797L125.751 174.003L126.299 173.995M126.964 94.6797H179.674C188.953 94.6797 194.126 105.386 188.357 112.644L137.061 177.189C135.539 179.104 132.883 179.583 130.531 178.892C128.18 178.202 126.336 176.41 126.299 173.995M126.964 94.6797L126.299 173.995"
stroke="url(#paint17_linear_0_1)"
stroke-width="1.09591"
stroke-miterlimit="10"
stroke-linejoin="bevel"
shape-rendering="crispEdges"
strokeWidth="1.09591"
strokeMiterlimit="10"
strokeLinejoin="bevel"
shapeRendering="crispEdges"
/>
</g>
<defs>
@@ -260,9 +260,9 @@ const DocsCoverLogo = () => {
width="137.062"
height="141.703"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
colorInterpolationFilters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
@@ -289,8 +289,8 @@ const DocsCoverLogo = () => {
gradientUnits="userSpaceOnUse"
gradientTransform="translate(125.451 114.82) rotate(90) scale(109.781 0.5)"
>
<stop stop-color="var(--colors-scale11)" stop-opacity="0.5" />
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0" />
<stop stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
</radialGradient>
<linearGradient
id="paint1_linear_0_1"
@@ -300,8 +300,8 @@ const DocsCoverLogo = () => {
y2="11.9994"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="var(--colors-scale11)" stop-opacity="0.5" />
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0" />
<stop stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
</linearGradient>
<linearGradient
id="paint2_linear_0_1"
@@ -311,8 +311,8 @@ const DocsCoverLogo = () => {
y2="204.984"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="var(--colors-scale11)" stop-opacity="0.5" />
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0" />
<stop stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
</linearGradient>
<linearGradient
id="paint3_linear_0_1"
@@ -322,8 +322,8 @@ const DocsCoverLogo = () => {
y2="13.9993"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="var(--colors-scale11)" stop-opacity="0.5" />
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0" />
<stop stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
</linearGradient>
<linearGradient
id="paint4_linear_0_1"
@@ -333,8 +333,8 @@ const DocsCoverLogo = () => {
y2="204.984"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="var(--colors-scale11)" stop-opacity="0.5" />
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0" />
<stop stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
</linearGradient>
<linearGradient
id="paint5_linear_0_1"
@@ -344,9 +344,9 @@ const DocsCoverLogo = () => {
y2="-7.94661"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="var(--colors-scale10)" stop-opacity="0" />
<stop offset="0.489583" stop-color="var(--colors-scale11)" stop-opacity="0.5" />
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0" />
<stop stopColor="var(--colors-scale10)" stopOpacity="0" />
<stop offset="0.489583" stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
</linearGradient>
<linearGradient
id="paint6_linear_0_1"
@@ -356,9 +356,9 @@ const DocsCoverLogo = () => {
y2="-3.99932"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="var(--colors-scale10)" stop-opacity="0" />
<stop offset="0.489583" stop-color="var(--colors-scale11)" stop-opacity="0.5" />
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0" />
<stop stopColor="var(--colors-scale10)" stopOpacity="0" />
<stop offset="0.489583" stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
</linearGradient>
<linearGradient
id="paint7_linear_0_1"
@@ -368,9 +368,9 @@ const DocsCoverLogo = () => {
y2="199.061"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="var(--colors-scale10)" stop-opacity="0.1" />
<stop offset="0.505208" stop-color="var(--colors-scale11)" stop-opacity="0.5" />
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0.1" />
<stop stopColor="var(--colors-scale10)" stopOpacity="0.1" />
<stop offset="0.505208" stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0.1" />
</linearGradient>
<radialGradient
id="paint8_radial_0_1"
@@ -380,8 +380,8 @@ const DocsCoverLogo = () => {
gradientUnits="userSpaceOnUse"
gradientTransform="translate(125.752 114.291) rotate(90) scale(109.271 0.5)"
>
<stop stop-color="var(--colors-scale11)" stop-opacity="0.5" />
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0" />
<stop stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
</radialGradient>
<linearGradient
id="paint9_linear_0_1"
@@ -391,8 +391,8 @@ const DocsCoverLogo = () => {
y2="6.31757"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="var(--colors-scale11)" stop-opacity="0.3" />
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0" />
<stop stopColor="var(--colors-scale11)" stopOpacity="0.3" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
</linearGradient>
<linearGradient
id="paint10_linear_0_1"
@@ -402,8 +402,8 @@ const DocsCoverLogo = () => {
y2="200"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="var(--colors-scale11)" stop-opacity="0.3" />
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0" />
<stop stopColor="var(--colors-scale11)" stopOpacity="0.3" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
</linearGradient>
<linearGradient
id="paint11_linear_0_1"
@@ -413,9 +413,9 @@ const DocsCoverLogo = () => {
y2="199.061"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="var(--colors-scale10)" stop-opacity="0.1" />
<stop offset="0.505208" stop-color="var(--colors-scale11)" stop-opacity="0.5" />
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0.1" />
<stop stopColor="var(--colors-scale10)" stopOpacity="0.1" />
<stop offset="0.505208" stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0.1" />
</linearGradient>
<radialGradient
id="paint12_radial_0_1"
@@ -425,8 +425,8 @@ const DocsCoverLogo = () => {
gradientUnits="userSpaceOnUse"
gradientTransform="translate(125.261 59.2344) rotate(90) scale(0.5 263.749)"
>
<stop stop-color="var(--colors-scale11)" stop-opacity="0.5" />
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0" />
<stop stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
</radialGradient>
<linearGradient
id="paint13_linear_0_1"
@@ -436,9 +436,9 @@ const DocsCoverLogo = () => {
y2="94.9749"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="var(--colors-scale10)" stop-opacity="0.5" />
<stop offset="0.505208" stop-color="var(--colors-scale11)" stop-opacity="0.5" />
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0.5" />
<stop stopColor="var(--colors-scale10)" stopOpacity="0.5" />
<stop offset="0.505208" stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0.5" />
</linearGradient>
<linearGradient
id="paint14_linear_0_1"
@@ -448,9 +448,9 @@ const DocsCoverLogo = () => {
y2="131.43"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="var(--colors-scale10)" stop-opacity="0.5" />
<stop offset="0.505208" stop-color="var(--colors-scale11)" stop-opacity="0.5" />
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0.5" />
<stop stopColor="var(--colors-scale10)" stopOpacity="0.5" />
<stop offset="0.505208" stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0.5" />
</linearGradient>
<linearGradient
id="paint15_linear_0_1"
@@ -460,9 +460,9 @@ const DocsCoverLogo = () => {
y2="1.99785"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="var(--colors-scale10)" stop-opacity="0.1" />
<stop offset="0.505208" stop-color="var(--colors-scale11)" stop-opacity="0.5" />
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0.1" />
<stop stopColor="var(--colors-scale10)" stopOpacity="0.1" />
<stop offset="0.505208" stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0.1" />
</linearGradient>
<linearGradient
id="paint16_linear_0_1"
@@ -472,8 +472,8 @@ const DocsCoverLogo = () => {
y2="206.049"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#212121" />
<stop offset="1" stop-color="white" stop-opacity="0.12" />
<stop stopColor="#212121" />
<stop offset="1" stopColor="white" stopOpacity="0.12" />
</linearGradient>
<linearGradient
id="paint17_linear_0_1"
@@ -483,9 +483,9 @@ const DocsCoverLogo = () => {
y2="175.889"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#868585" />
<stop offset="0.208333" stop-color="#838383" />
<stop offset="1" stop-color="#5A5A5A" />
<stop stopColor="#868585" />
<stop offset="0.208333" stopColor="#838383" />
<stop offset="1" stopColor="#5A5A5A" />
</linearGradient>
</defs>
</m.svg>
+8 -8
View File
@@ -1,5 +1,5 @@
import Link from 'next/link'
import { Button } from '~/../../packages/ui'
import { Button } from 'ui'
import { primaryLinks, secondaryLinks } from '~/data/footer'
import { LayoutMainContent } from '../../layouts/DefaultLayout'
@@ -21,8 +21,8 @@ const Footer = () => (
d={icon}
stroke="var(--colors-scale9)"
strokeWidth={0.7}
stroke-linecap="round"
stroke-linejoin="round"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
@@ -71,8 +71,8 @@ const Footer = () => (
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M8.66832 1.55469C4.98649 1.55469 2.00195 4.54402 2.00195 8.23269C2.00195 11.1827 3.91187 13.686 6.56108 14.5687C6.8944 14.63 7.01573 14.424 7.01573 14.2467C7.01573 14.0887 7.0104 13.668 7.00706 13.1114C5.15248 13.5147 4.76116 12.216 4.76116 12.216C4.45851 11.444 4.0212 11.2387 4.0212 11.2387C3.41589 10.8254 4.06719 10.8334 4.06719 10.8334C4.73583 10.88 5.08782 11.5214 5.08782 11.5214C5.68246 12.5414 6.64841 12.2467 7.02773 12.076C7.08906 11.6447 7.26105 11.3507 7.45171 11.184C5.97178 11.0154 4.41518 10.442 4.41518 7.88335C4.41518 7.15469 4.67517 6.55802 5.10115 6.09135C5.03248 5.92269 4.80383 5.24335 5.16648 4.32469C5.16648 4.32469 5.72645 4.14469 6.99973 5.00869C7.54355 4.86036 8.10464 4.78482 8.66832 4.78402C9.23496 4.78669 9.80494 4.86069 10.3376 5.00869C11.6102 4.14469 12.1688 4.32402 12.1688 4.32402C12.5328 5.24335 12.3035 5.92269 12.2355 6.09135C12.6621 6.55802 12.9208 7.15469 12.9208 7.88335C12.9208 10.4487 11.3615 11.0134 9.87694 11.1787C10.1163 11.3847 10.3289 11.792 10.3289 12.4154C10.3289 13.3074 10.3209 14.028 10.3209 14.2467C10.3209 14.4254 10.4409 14.6334 10.7796 14.568C12.107 14.1228 13.261 13.2716 14.0784 12.1347C14.8958 10.9979 15.3353 9.6329 15.3347 8.23269C15.3347 4.54402 12.3495 1.55469 8.66832 1.55469Z"
fill="var(--colors-scale9)"
/>
@@ -135,10 +135,10 @@ const Footer = () => (
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_1182_99731)">
<g clipPath="url(#clip0_1182_99731)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M13.8781 3.13594C12.8583 2.66802 11.7648 2.32328 10.6215 2.12583C10.6006 2.12202 10.5798 2.13154 10.5691 2.15059C10.4285 2.40073 10.2727 2.72706 10.1636 2.98356C8.93387 2.79945 7.71044 2.79945 6.50592 2.98356C6.39681 2.72136 6.23538 2.40073 6.09411 2.15059C6.08339 2.13218 6.06259 2.12265 6.04176 2.12583C4.89905 2.32265 3.80554 2.66739 2.78517 3.13594C2.77633 3.13975 2.76876 3.14611 2.76374 3.15435C0.689563 6.25326 0.12136 9.276 0.400102 12.2613C0.401363 12.2759 0.409561 12.2898 0.420913 12.2987C1.78939 13.3037 3.115 13.9139 4.41599 14.3183C4.43681 14.3247 4.45887 14.317 4.47212 14.2999C4.77987 13.8796 5.0542 13.4364 5.28941 12.9704C5.3033 12.9431 5.29005 12.9107 5.26167 12.9C4.82654 12.7349 4.4122 12.5336 4.01364 12.3051C3.98212 12.2867 3.97959 12.2416 4.00859 12.22C4.09247 12.1571 4.17636 12.0917 4.25645 12.0257C4.27094 12.0136 4.29113 12.0111 4.30816 12.0187C6.92655 13.2142 9.76126 13.2142 12.3488 12.0187C12.3658 12.0105 12.386 12.013 12.4011 12.0251C12.4812 12.0911 12.5651 12.1571 12.6496 12.22C12.6786 12.2416 12.6767 12.2867 12.6452 12.3051C12.2466 12.5381 11.8323 12.7349 11.3965 12.8993C11.3681 12.9101 11.3555 12.9431 11.3694 12.9704C11.6097 13.4358 11.884 13.879 12.1861 14.2993C12.1987 14.317 12.2214 14.3247 12.2422 14.3183C13.5495 13.9139 14.8751 13.3037 16.2436 12.2987C16.2556 12.2898 16.2631 12.2765 16.2644 12.2619C16.598 8.8106 15.7056 5.81265 13.8989 3.15498C13.8944 3.14611 13.8869 3.13975 13.8781 3.13594ZM5.68043 10.4435C4.89211 10.4435 4.24257 9.71978 4.24257 8.83093C4.24257 7.94207 4.87952 7.21831 5.68043 7.21831C6.48763 7.21831 7.13089 7.94843 7.11827 8.83093C7.11827 9.71978 6.48132 10.4435 5.68043 10.4435ZM10.9967 10.4435C10.2084 10.4435 9.55884 9.71978 9.55884 8.83093C9.55884 7.94207 10.1958 7.21831 10.9967 7.21831C11.8039 7.21831 12.4471 7.94843 12.4345 8.83093C12.4345 9.71978 11.8039 10.4435 10.9967 10.4435Z"
fill="var(--colors-scale9)"
/>
@@ -932,9 +932,15 @@ export const platform = {
],
},
{
name: 'Go-live Checklist',
name: 'Production Readiness',
url: undefined,
items: [{ name: 'Production Readiness', url: '/guides/platform/going-into-prod' }],
items: [
{
name: 'Shared Responsibility Model',
url: '/guides/platform/shared-responsibility-model',
},
{ name: 'Going into Production', url: '/guides/platform/going-into-prod' },
],
},
{
name: 'Troubleshooting',
+4 -6
View File
@@ -137,14 +137,12 @@ const Layout: FC<Props> = (props) => {
) : (
<div className="mt-16 not-prose">
<div>
<Link
<a
href={`https://github.com/supabase/supabase/edit/master/apps/docs/pages${router.asPath}.mdx`}
passHref
className="text-sm transition flex items-center gap-1 text-scale-1000 hover:text-scale-1200"
>
<a className="text-sm transition flex items-center gap-1 text-scale-1000 hover:text-scale-1200">
Edit this page on GitHub <IconExternalLink size={14} strokeWidth={1.5} />
</a>
</Link>
Edit this page on GitHub <IconExternalLink size={14} strokeWidth={1.5} />
</a>
</div>
</div>
)}
+3 -1
View File
@@ -1,3 +1,4 @@
// @ts-check
import nextMdx from '@next/mdx'
import remarkGfm from 'remark-gfm'
import rehypeSlug from 'rehype-slug'
@@ -64,7 +65,8 @@ const nextConfig = {
],
},
experimental: {
mdxRs: true,
// TODO: @next/mdx ^13.0.2 only supports experimental mdxRs flag. next ^13.0.2 will stop warning about this being unsupported.
// mdxRs: true,
modularizeImports: {
lodash: {
transform: 'lodash/{{member}}',
+1 -3
View File
@@ -3,9 +3,7 @@ import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html lang="en">
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"></meta>
</Head>
<Head />
<body className="dark">
<Main />
<NextScript />
@@ -28,12 +28,10 @@ This creates a corresponding route `todos` which can accept `GET`, `POST`, `PATC
1. Click **New Column** and create a column with the name `task` and type `text`.
1. Click **Save**.
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-create-table-sm.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -60,12 +58,10 @@ Every Supabase project has a unique API URL. Your API is secured behind an API g
2. Click **API** in the sidebar.
3. Find your API `URL`, `anon`, and `service_role` keys on this page.
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-url-and-key.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -207,12 +203,10 @@ By default Realtime is disabled on your database. Let's turn on Realtime for the
3. Control which database events are sent by toggling **Insert**, **Update**, and **Delete**.
4. Control which tables broadcast changes by selecting **Source** and toggling each table.
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-realtime.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
+1 -3
View File
@@ -44,12 +44,10 @@ We'll create a database table called `todos` for storing tasks. This creates a c
</TabPanel>
<TabPanel id="dashboard" label="Dashboard">
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-create-table-sm.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -12,12 +12,10 @@ Supabase generates documentation in the [Dashboard](https://app.supabase.com) wh
2. Select any table under **Tables and Views** in the sidebar.
3. Switch between the JavaScript and the cURL docs using the tabs.
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-docs.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -37,7 +37,7 @@ npx supabase login
Generate types for your project to produce the `types/supabase.ts` file:
```bash
npx supabase gen types typescript --project-id "$PROJECT_ID" --schema public > types/supabase.ts
npx supabase gen types typescript --project-id "$PROJECT_REF" --schema public > types/supabase.ts
```
After you have generated your types, you can use them in `src/index.ts`
@@ -65,7 +65,7 @@ One way to keep your type definitions in sync with your database is to set up a
Add the script above to your `package.json` to run it using `npm run update-types`
```json
"update-types": "npx supabase gen types typescript --project-id \"$PROJECT_ID\" > types/supabase.ts"
"update-types": "npx supabase gen types typescript --project-id \"$PROJECT_REF\" > types/supabase.ts"
```
Create a file `.github/workflows/update-types.yml` with the following snippet to define the action along with the environment variables. This script will commit new type changes to your repo every night.
@@ -83,7 +83,7 @@ jobs:
runs-on: ubuntu-latest
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
PROJECT_ID: <your-project-id>
PROJECT_REF: <your-project-id>
steps:
- uses: actions/checkout@v2
with:
+3 -9
View File
@@ -123,12 +123,10 @@ Get started with our [Row Level Security Guides](/docs/guides/auth/row-level-sec
Authentication only gets you so far. When you need granular authorization rules, nothing beats PostgreSQL's [Row Level Security (RLS)](https://www.postgresql.org/docs/current/ddl-rowsecurity.html). Supabase makes it simple to turn RLS on and off.
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/rls-zoom2.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -136,12 +134,10 @@ Authentication only gets you so far. When you need granular authorization rules,
[Policies](https://www.postgresql.org/docs/current/sql-createpolicy.html) are PostgreSQL's rule engine. They are incredibly powerful and flexible, allowing you to write complex SQL rules which fit your unique business needs.
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/policies-zoom2.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -189,12 +185,10 @@ Supabase provides multiple endpoints to authenticate and manage your users:
When users sign up, Supabase assigns them a unique ID. You can reference this ID anywhere in your database. For example, you might create a `profiles` table referencing `id` in the `auth.users` table using a `user_id` field.
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/auth-zoom2.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -9,13 +9,8 @@ export const meta = {
Auth UI is a pre-built React component for authenticating users.
It supports custom themes and extensible styles to match your brand and aesthetic.
<video width="99%" muted playsInline controls="true">
<source
src="https://supabase.com/images/blog/lw5-one-more/auth-ui-demo.mp4"
type="video/mp4"
muted
playsInline
/>
<video width="99%" muted playsInline controls={true}>
<source src="https://supabase.com/images/blog/lw5-one-more/auth-ui-demo.mp4" type="video/mp4" />
</video>
## Set up Auth UI
@@ -438,11 +438,6 @@ export const handle: Handle = async ({ event, resolve }) => {
}
return resolve(event, {
/**
* There´s an issue with `filterSerializedResponseHeaders` not working when using `sequence`
*
* https://github.com/sveltejs/kit/issues/8061
*/
filterSerializedResponseHeaders(name) {
return name === 'content-range'
},
+1 -7
View File
@@ -33,7 +33,7 @@ You don't have to be a database expert to start using Supabase. Our table view m
Dig into the relationships within your data.
<video width="99%" loop="" muted="" playsInline="" controls="true">
<video width="99%" loop muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/relational-drilldown-zoom.mp4"
type="video/mp4"
@@ -48,8 +48,6 @@ You can duplicate your tables, just like you would inside a spreadsheet.
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/duplicate-tables.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -61,8 +59,6 @@ Supabase comes with a SQL Editor. You can also save your favorite queries to run
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/favorites.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -90,8 +86,6 @@ You can enable Postgres extensions with the click of a button within the Supabas
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/toggle-extensions.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -44,12 +44,10 @@ You cannot manage the database schema via the API (for security reasons). To do
You can find the API URL and Keys in the [Dashboard](https://app.supabase.com/project/_/settings/api).
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-url-and-key.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -63,12 +61,10 @@ Every Supabase project provides a full Postgres database. You can connect to the
2. Click `Database`.
3. Find your Connection Info and Connection String. Direct connections are on port `5432`.
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/postgres-connection.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -113,12 +109,10 @@ This is the most granular option. Connections are returned to the pool after eve
2. Click `Database`.
3. Find your Connection Info and Connection String. Connection pooling is on port `6543`.
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/connection-pool-config.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
+1 -7
View File
@@ -22,7 +22,7 @@ You don't have to be a database expert to start using Supabase. Our table view m
Dig into the relationships within your data.
<video width="99%" loop="" muted="" playsInline="" controls="true">
<video width="99%" loop muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/relational-drilldown-zoom.mp4"
type="video/mp4"
@@ -37,8 +37,6 @@ You can duplicate your tables, just like you would inside a spreadsheet.
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/duplicate-tables.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -50,8 +48,6 @@ Supabase comes with a SQL Editor. You can also save your favorite queries to run
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/favorites.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -79,8 +75,6 @@ You can enable Postgres extensions with the click of a button within the Supabas
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/toggle-extensions.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -20,12 +20,10 @@ Replication is done through _publications_, a method of choosing which changes t
3. Control which database events are sent by toggling **Insert**, **Update**, and **Delete**.
4. Control which tables broadcast changes by selecting **Source** and toggling each table.
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-realtime.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
+1 -3
View File
@@ -41,12 +41,10 @@ and run the SQL queries yourself.
>
<TabPanel id="dashboard" label="Dashboard">
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-create-table-sm.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -11,8 +11,11 @@ Supabase is open source. We choose open source tools which are scalable and make
Supabase is not a 1-to-1 mapping of Firebase. While we are building many of the features that Firebase offers, we are not going about it the same way:
our technological choices are quite different; everything we use is open source; and wherever possible, we use and support existing tools rather than developing from scratch.
Most notably, we use Postgres rather than a NoSQL store. This choice was deliberate. We believe that no other database offers the functionality required to compete with Firebase,
while maintaining the scalability required to go beyond it.
Most notably, we use Postgres rather than a NoSQL store. This choice was deliberate. We believe that no other database offers the functionality required to compete with Firebase, while maintaining the scalability required to go beyond it.
## Choose your comfort level
Our goal at Supabase is to make _all_ of Postgres easy to use. That doesn’t mean you have to use all of it. If you’re a Postgres veteran, you’ll probably love the tools that we offer. If you’ve never used Postgres before, then start smaller and grow into it. If you just want to treat Postgres like a simple table-store, that’s perfectly fine.
## Architecture
@@ -85,11 +85,6 @@ export const handle: Handle = async ({ event, resolve }) => {
}
return resolve(event, {
/**
* There´s an issue with `filterSerializedResponseHeaders` not working when using `sequence`
*
* https://github.com/sveltejs/kit/issues/8061
*/
filterSerializedResponseHeaders(name) {
return name === 'content-range'
},
@@ -46,8 +46,6 @@ You can then connect your Supabase project to your FlutterFlow project with the
<source
src="https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/videos/docs/guides/integrations/flutterflow/connect-flutterflow-to-supabase.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -69,8 +67,6 @@ Go to your project page on FlutterFlow and follow the steps below to define the
<source
src="https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/videos/docs/guides/integrations/flutterflow/insert.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -90,8 +86,6 @@ To query a Supabase table on a ListView:
<source
src="https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/videos/docs/guides/integrations/flutterflow/select.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -134,8 +128,6 @@ Go to your project page on FlutterFlow and follow the steps below to define the
<source
src="https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/videos/docs/guides/integrations/flutterflow/delete.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -104,12 +104,10 @@ To tell Plasmic to look for your code components on your dev server, you’ll ne
> Note: At this point, you’ll need to keep your dev server running at `http://localhost:3000`for the project to load.
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<video width="99%" autoPlay muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/guides/integrations/plasmic/configure-app-host-plasmic-05.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -121,12 +119,10 @@ Let’s make a visual gallery for our Pokemon by using the code components from
Create a new page called `Gallery`, and set a path for this page (`/gallery`).
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<video width="99%" autoPlay muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/guides/integrations/plasmic/create-gallery-page-plasmic-06.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -154,12 +150,10 @@ Now customize the repeated content by inserting instances of `SupabaseField`. Se
For your convenience, the following video shows you how to create the page end-to-end.
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<video width="99%" autoPlay muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/guides/integrations/plasmic/end-to-end-plasmic-10.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -2,7 +2,7 @@ import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'going-into-prod',
title: 'Production Readiness',
title: 'Going into Production',
description: 'Things to do before making your app publicly available',
}
@@ -85,6 +85,17 @@ After developing your project and deciding it's Production Ready, you should run
- When working with enterprise systems, email scanners may scan and make a `GET` request to the reset password link or sign up link in your email. Since links in Supabase Auth are single use, a user who opens an email post-scan to click on a link will receive an error. To get around this problem,
consider altering the email template to replace the original magic link with a link to a domain you control. The domain can present the user with a "Sign-in" button which redirect the user to the original magic link URL when clicked.
### Restricted Access Levels for Team Members
As your team grows, consider the different [access levels](/docs/guides/platform/access-control) that your team requires.
- Free and Pro: Supabase provides access controls within the Dashboard for Developers and Administrators.
- Teams and Enterprise: these tiers include advanced access controls, such as read-only Dashboard roles.
For a more secure setup, consider running your workload across several organizations. It's a common pattern to have a Production organization which is restricted to only those team members who are qualified to have direct access to production databases.
One other small tip: the [PGAudit](/docs/guides/database/extensions/pgaudit) extension tracks changes to your database. You can use it to track schema changes.
## Next steps
This checklist is always growing so be sure to check back frequently, and also feel free to suggest additions and amendments by making a PR on [GitHub](https://github.com/supabase/supabase).
@@ -0,0 +1,49 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'shared-responsibility-model',
title: 'Shared Responsibility Model',
description: 'Running databases is a shared responsibility between you and Supabase.',
}
Running databases is a shared responsibility between you and Supabase. There are some things that we can take care of for you, and some things that you are responsible for. This is by design: we to give you the freedom to use your database however you want. While we _could_ put many more restrictions in place to ensure that you can’t do anything wrong, you will eventually find those restrictions prohibitive.
Generally, we aim reduce your burden of managing infrastructure and knowing about Postgres internals, minimizing configuration as much as we can. Here are a few things that you should know:
## You share the security responsibility
We give you full access to the database. If you share that access with other people (either people on your team, or the public in general) then it is your responsibility to ensure that the access levels you provide are correctly managed.
If you have an inexperienced member on your team, then you probably shouldn’t give them access to Production. You should set internal workflows around what they should and should not be able to do, with restricted access to avoid anything that might be deemed dangerous.
You are also responsible for ensuring that tables with sensitive data have the right level of access. You are also responsible for managing your database secrets and API keys, storing them safely in an encrypted store.
## You can crash your database
”Crash” is used liberally here. It refers to any situation where your application is unable to work as desired because of DB issues. It could be because the DB is unable to support the number of connections being attempted, or because its performance is unacceptably low because of CPU or memory constraints.
You are responsible of provisioning enough compute to run the workload that your application requires. The Supabase Dashboard provides [observability tooling](https://app.supabase.com/project/_/reports/database) to help with this. You are also responsible for employing methods to manage the workload: adding indexes, adding filters on large queries, caching strategies, materialized views, and optimizing your database queries.
## You decide your own workflow
There are _many_ ways to work with Supabase.
You can use our Dashboard, our client libraries, external tools like Prisma and Drizzle, or migration tools like our CLI, Flyway, Sqitch, and anything else that is Postgres-compatible. You can develop directly on your database while you're getting started, run migrations from [local to production](/docs/guides/getting-started/local-development), or you can use [multiple environments](/docs/guides/cli/managing-environments).
None of these are right or wrong. It depends on the stage of your project. You _definitely_ shouldn’t be developing on your database directly when you’re in production - but that’s absolutely fine when you’re prototyping and don’t have users.
## You are responsible for your application architecture
Supabase isn't a silver-bullet for bad architectural decisions. A poorly designed database will run poorly, no matter where it’s hosted.
You can get away with a poorly-designed database for a while by simply adding compute. After a while, things will start to break. The database schema is the area you want to spend _the most_ time thinking about. That’s the benefit of Supabase - you can spend more time designing a scalable database system and less time thinking about the mundane tasks like implementing CRUD APIs.
If you don’t want to implement logic inside your database, that is 100% fine. You can use _any_ tools which work with Postgres.
## You choose your level of comfort with Postgres
Our goal at Supabase is to make _all_ of Postgres easy to use. That doesn’t mean you have to use all of it. If you’re a Postgres veteran, you’ll probably love the tools that we offer. If you’ve never used Postgres before, then start smaller and grow into it. If you just want to treat Postgres like a simple table-store, that’s perfectly fine.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -40,7 +40,7 @@ We've made some massive improvements to our Table Editor that we're excited to s
Last month we made it easy to drill into your table relationships. This month, we make it possible to drill multiple levels deep.
<video width="99%" autoPlay="" loop="" muted="" playsInline="" controls="true">
<video width="99%" autoPlay loop muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/relational-drilldown-zoom.mp4"
type="video/mp4"
@@ -51,7 +51,7 @@ Last month we made it easy to drill into your table relationships. This month, w
We're making it easier to manipulate your data. Next month, you'll be able to add and remove columns directly from the Table view.
<video width="99%" autoPlay="" loop="" muted="" playsInline="" controls="true">
<video width="99%" autoPlay loop muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/csv-download-zoom.mp4"
type="video/mp4"
+3 -12
View File
@@ -36,13 +36,10 @@ Supabase Auth provides all the backend services you need to authenticate and aut
Supabase makes it simple to onboard your users with our new `supabase.auth.signUp()` and `supabase.auth.signIn()` [functions](/docs/guides/auth).
<video width="99%" autoPlay="autoplay" loop muted playsInline controls="true">
<video width="99%" autoPlay loop muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/auth-zoom2.mp4"
type="video/mp4"
loop
muted
playsInline
/>
</video>
@@ -50,13 +47,10 @@ Supabase makes it simple to onboard your users with our new `supabase.auth.signU
Authentication only gets you so far. When you need granular authorization rules, nothing beats PostgreSQL's [Row Level Security](https://www.postgresql.org/docs/current/ddl-rowsecurity.html). Supabase makes it simple to turn RLS on and off.
<video width="99%" autoPlay="autoplay" loop muted playsInline controls="true">
<video width="99%" autoPlay loop muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/rls-zoom2.mp4"
type="video/mp4"
loop
muted
playsInline
/>
</video>
@@ -64,13 +58,10 @@ Authentication only gets you so far. When you need granular authorization rules,
[Policies](https://www.postgresql.org/docs/current/sql-createpolicy.html) are PostgreSQL's rule engine. They are incredibly powerful and flexible, allowing you to write complex SQL rules which fit your unique business needs.
<video width="99%" autoPlay="autoplay" loop muted playsInline controls="true">
<video width="99%" autoPlay loop muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/policies-zoom2.mp4"
type="video/mp4"
loop
muted
playsInline
/>
</video>
@@ -30,13 +30,10 @@ Watch a full demo:
Set up tables and columns directly from the table view.
<video width="99%" autoPlay="autoplay" loop muted playsInline controls="true">
<video width="99%" autoPlay loop muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/new-tables.mp4"
type="video/mp4"
loop
muted
playsInline
/>
</video>
@@ -44,13 +41,10 @@ Set up tables and columns directly from the table view.
You can now invite team members to your organisation.
<video width="99%" autoPlay="autoplay" loop muted playsInline controls="true">
<video width="99%" autoPlay loop muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/invite-team.mp4"
type="video/mp4"
loop
muted
playsInline
/>
</video>
@@ -58,13 +52,10 @@ You can now invite team members to your organisation.
You can now enable Email Confirmations for new users. This can be toggled on or off and the template for this email can be edited via the dashboard.
<video width="99%" autoPlay="autoplay" loop muted playsInline controls="true">
<video width="99%" autoPlay loop muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/confirm-email.mp4"
type="video/mp4"
loop
muted
playsInline
/>
</video>
@@ -37,12 +37,10 @@ We've released OAuth logins! You can now enable third-party logins on your app f
You can duplicate your tables, just like you would inside a spreadsheet.
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<video width="99%" autoPlay muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/duplicate-tables.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -50,12 +48,10 @@ You can duplicate your tables, just like you would inside a spreadsheet.
Extensions are easier to use. You can enable Postgres extensions with the click of a button.
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<video width="99%" autoPlay muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/toggle-extensions.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -63,12 +59,10 @@ Extensions are easier to use. You can enable Postgres extensions with the click
The SQL editor now stores your query history in your browser. You can also save your favorite queries to run later!
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<video width="99%" autoPlay muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/favorites.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -73,12 +73,10 @@ Thanks to some skilled haxors (well, weak passwords mainly) we had crypto miners
The spreadsheet import is triggered when the user is creating a new table. However the code was previously loaded every time the page was visited - even when a new table was not being created. This made it a good candidate for lazy loading. Using [Next.js dynamic imports](https://nextjs.org/docs/advanced-features/dynamic-import) we are able to load this component (313 kb brotlied) dynamically, whenever the user clicks the "Add content" button.
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/lazy_loading.mov"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -40,12 +40,10 @@ We spent months working on Performance, Security, and Reliability. Read more on
Add comments and descriptions to your Tables directly from our auto-generated docs. Descriptions are stored as PostgreSQL comments (https://postgresql.org/docs/current/sql-comment.html), and are exposed over your OpenAPI spec.
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<video width="99%" autoPlay muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/update-docs.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -53,12 +51,10 @@ Add comments and descriptions to your Tables directly from our auto-generated do
Any updates that happen to your database are reflected in the Table View immediately.
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<video width="99%" autoPlay muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/realtime-updates.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -66,12 +62,10 @@ Any updates that happen to your database are reflected in the Table View immedia
Our table view now has pagination - better for working with large data sets.
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<video width="99%" autoPlay muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/table-pagination.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -39,8 +39,8 @@ We've improved the UX of our Dashboard with sidebars in every section, including
Writing SQL just got 10x easier. We added autocomplete to the SQL editor, including table & column suggestions.
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<source src="/images/blog/feb/autocomplete.mp4" type="video/mp4" muted playsInline />
<video width="99%" autoPlay muted playsInline controls={true}>
<source src="/images/blog/feb/autocomplete.mp4" type="video/mp4" />
</video>
### Auth Redirects
@@ -41,8 +41,8 @@ or even just working in brightly-lit environments.
So today we're shipping Light Mode. Access it in the settings of your [Dashboard](https://app.supabase.com).
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<source src="/images/blog/april-2021/light-mode.mp4" type="video/mp4" muted playsInline />
<video width="99%" autoPlay muted playsInline controls={true}>
<source src="/images/blog/april-2021/light-mode.mp4" type="video/mp4" />
</video>
## Translations
@@ -89,8 +89,8 @@ which provide a more general solution (but are less "realtime"), and we're openi
One of the most powerful Postgres features is "recursive CTEs" which can be used for nested items (comments, pages, friend-graphs). [@lawrencecchen](https://twitter.com/lawrencecchen) has built a full [Threaded Comments demo](https://github.com/lawrencecchen/threaded-comments) which you can [Deploy with a single click](https://github.com/lawrencecchen/threaded-comments#instant-deploy). Want to add comments to your blog with [Full Text Search](/docs/reference/javascript/textsearch)? Just use Postgres.
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<source src="/images/blog/april-2021/threaded-small.mp4" type="video/mp4" muted playsInline />
<video width="99%" autoPlay muted playsInline controls={true}>
<source src="/images/blog/april-2021/threaded-small.mp4" type="video/mp4" />
</video>
## Community spotlight: SupaScript
@@ -80,8 +80,8 @@ Did you know that you can use Javascript inside your Postgres database? Here's h
Want to share all your favourite memes? Now it's even easier with Public Storage Buckets. Simply mark a bucket as
"Public" and the content will be accessible without a login.
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<source src="/images/blog/2021-june/public-buckets.mp4" type="video/mp4" muted playsInline />
<video width="99%" autoPlay muted playsInline controls={true}>
<source src="/images/blog/2021-june/public-buckets.mp4" type="video/mp4" />
</video>
## Storage upserts
+5 -11
View File
@@ -19,12 +19,10 @@ We've added support for streaming audio and video files.
Clients can use the `Content-Range` HTTP header to request specific parts of a file from a server. This allows them to intelligently download the parts of the video that a user is viewing, without buffering irrelevant sections of the media file. Supabase Storage now supports this header, making it easier to host and watch [nyan cat videos](https://www.youtube.com/watch?v=SkgTxQm9DWM) all day long.
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<video width="99%" autoPlay muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/blog/launch-week-storage-2/nyan-cat.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -40,20 +38,18 @@ Objects in public buckets can be accessed via a URL without any Authorization to
Public buckets still require [Auth Policies](/docs/guides/storage#policy-examples) to upload and delete objects, allowing you to securely manage their contents.
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<source src="/images/blog/2021-june/public-buckets.mp4" type="video/mp4" muted playsInline />
<video width="99%" autoPlay muted playsInline controls={true}>
<source src="/images/blog/2021-june/public-buckets.mp4" type="video/mp4" />
</video>
## Directory uploads
You can now upload an entire directory to Supabase Storage and the directory structure is fully preserved after the upload. We used the [File and Directory entries API](https://developer.mozilla.org/en-US/docs/Web/API/File_and_Directory_Entries_API) for implementing this. This is a non-standard API, but we didn't find any compatibility issues in the browsers we support. So, now you can upload the [Storage GitHub repo](https://github.com/supabase/storage-api/) to storage 🤓
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<video width="99%" autoPlay muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/blog/launch-week-storage-2/directory-uploads.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -119,12 +115,10 @@ We significantly improved rendering for a large number of items in the Dashboard
Here is a demo testing rendering the heaviest object known to mankind - the `node_modules` folder!
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<video width="99%" autoPlay muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/blog/launch-week-storage-2/large-buckets.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -230,12 +230,10 @@ As a result of these optimizations, the throughput of a “hello world” equiva
To enable GraphQL in your Supabase instance, enable `pg_graphql` from the [dashboard](https://app.supabase.com/).
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<video width="99%" autoPlay muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/blog/launch-week-4/api-enable-graphql.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -123,12 +123,10 @@ Some features are too good to limit to large customers, so today we're introduci
### Spend caps
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<video width="99%" autoPlay muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/blog/launch-week-4/spend-caps.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -140,12 +138,10 @@ We're also retaining our soft limits while we manage the transition to granular
### Database Add-ons
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<video width="99%" autoPlay muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/blog/launch-week-4/database-addons.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -158,12 +154,10 @@ Today, Database Add-ons are available for a small set of customers. We will prog
### New Log Explorer
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<video width="99%" autoPlay muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/blog/launch-week-4/logexplorer.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -79,23 +79,19 @@ Delivering a phenomenal Developer Experience is something we care about a lot at
We've [extended the Supabase CLI](https://supabase.com/docs/guides/local-development) to optimize the Edge Functions DX along these two dimensions. You get live-reloading, and a local Deno environment that matches the production environment, making for a great local iteration environment.
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<video width="99%" autoPlay muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/blog/launch-week-4/functions-hot-reloading.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
Once you've tested your Function locally (or gone straight to production cos you live _on the edge_ ... get it?) it takes just seconds to deploy a new function and see the logs streaming in your Dashboard—ensuring a tight iteration loop with your production environment.
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<video width="99%" autoPlay muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/blog/launch-week-4/logs-polish.mov"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -30,14 +30,8 @@ Vault is not available on the Supabase Platform yet. This release is intended to
## Auth UI
<video width="99%" muted playsInline controls="true" autoplay="true">
<source
src="/images/blog/lw5-one-more/auth-ui-demo.mp4"
type="video/mp4"
muted
playsInline
autoplay
/>
<video width="99%" muted playsInline controls={true} autoPlay>
<source src="/images/blog/lw5-one-more/auth-ui-demo.mp4" type="video/mp4" />
</video>
Auth UI was previously released as a single `<Auth/>` component in the [`@supabase/ui`](http://github.com/supabase/ui) library.
@@ -24,13 +24,8 @@ Practically speaking, the Vault is a table of Secrets and Encryption Keys that a
We've created a dashboard UI for the Vault that makes storing secrets easy. Click a button and type in your secret, optionally create a new key that is referenced by id (or use the existing default), and submit. Your secret is now stored on disk encrypted using the specified key id.
<video width="99%" muted playsInline controls="true">
<source
src="/images/blog/launch-week-6/vault/vault-hello-compressed.mp4"
type="video/mp4"
muted
playsInline
/>
<video width="99%" muted playsInline controls={true}>
<source src="/images/blog/launch-week-6/vault/vault-hello-compressed.mp4" type="video/mp4" />
</video>
There are two main parts to the Vault UI, Secrets and Encryption Keys:
@@ -360,7 +360,7 @@ serve(async (req) => {
OpenAI API responses take longer to depending on the length of the “answer”. ChatGPT has a nice UX for this by streaming the response to the user immediately. You can see a similar effect for the Supabase docs:
<video width="99%" autoPlay="" loop="" muted="" playsInline="" controls="true">
<video width="99%" autoPlay loop muted playsInline controls={true}>
<source src="/images/blog/embeddings/embeddings.mp4" type="video/mp4" />
</video>
@@ -40,7 +40,7 @@ Developers have recently gained the ability to trust a bot. Where Clippy failed,
This is convenient timing for us, since our documentation content is more than the average developer wants to consume in one go. Today we're providing a similar interface to ChatGPT which is trained on our own docs.
<video width="100%" autoPlay="" loop="" muted="" playsInline="" controls="true">
<video width="100%" autoPlay loop muted playsInline controls={true}>
<source src="/images/blog/embeddings/embeddings.mp4" type="video/mp4" />
</video>
@@ -28,7 +28,7 @@ In this article, we will use Flutter, Flame, and Supabase's real-time features t
## Overview of the final game
<video width="99%" loop="" muted="" playsInline="" controls="true">
<video width="99%" loop muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/blog/flutter-realtime-game/playing-realtime-game.mp4"
type="video/mp4"
@@ -17,7 +17,7 @@ The key feature: **Resumable Uploads**! With Resumable Uploads, you can continue
Resumable uploads divides the file into chunks before uploading them, emitting progress events during upload.
<video width="100%" autoPlay="" loop="" muted="" playsInline="" controls="true">
<video width="100%" autoPlay loop muted playsInline controls={true}>
<source
src="/images/blog/launch-week-7/day-3-storage-resumable-uploads/pause-upload.mov"
type="video/mp4"
@@ -165,7 +165,7 @@ uppy.on('complete', (result) => {
And there you have it, with a few lines of code, you can support parallel, resumable uploads of multiple files, with progress events!
<video width="100%" autoPlay="" loop="" muted="" playsInline="" controls="true">
<video width="100%" autoPlay loop muted playsInline controls={true}>
<source
src="/images/blog/launch-week-7/day-3-storage-resumable-uploads/parallel-uploads.mov"
type="video/mp4"
@@ -51,7 +51,7 @@ Here's the birds-eye-view:
## Supabase AI — assisted database development
<video width="99%" autoPlay="" loop="" muted="" playsInline="" controls={true}>
<video width="99%" autoPlay loop muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/cmdk.mp4"
type="video/mp4"
@@ -92,7 +92,7 @@ While GraphQL has been available in Supabase for just over a year, we haven't pr
## Cascade deletes
<video width="99%" autoPlay="" loop="" muted="" playsInline="" controls={true}>
<video width="99%" autoPlay loop muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/cascade-delete.mp4"
type="video/mp4"
@@ -109,7 +109,7 @@ We've just released a new Query Performance tool to help you identify slow queri
## Foreign key selector
<video width="99%" autoPlay="" loop="" muted="" playsInline="" controls={true}>
<video width="99%" autoPlay loop muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/fk-lookup.mp4"
type="video/mp4"
+15 -15
View File
@@ -1,4 +1,19 @@
[
{
"type": "Announcement",
"title": "Flutter Hackathon",
"description": "Build Flutter apps and win limited edition swag.",
"imgUrl": "/images/blog/flutter-hackathon/thumbnail.jpg",
"logoUrl": "/images/blog/flutter-hackathon/thumbnail.jpg",
"organization": "Supabase",
"url": "/blog/flutter-hackathon",
"postMeta": {
"name": "Tyler",
"publishDate": "May 12, 2023",
"readLength": 3
},
"ctaText": "See how to join"
},
{
"type": "Announcement",
"title": "Supabase Launch Week 7",
@@ -13,20 +28,5 @@
"readLength": 5
},
"ctaText": "Learn more"
},
{
"type": "Announcement",
"title": "The Supabase Content Storm",
"description": "We worked with dozens of content creators from around the world to create Supabase-related content and publish everything on the same day!",
"imgUrl": "/images/blog/2022-12-06-content-storm/content-storm.jpg",
"logoUrl": "/images/blog/2022-12-06-content-storm/content-storm.jpg",
"organization": "Supabase",
"url": "/blog/the-supabase-content-storm",
"postMeta": {
"name": "Ramiro",
"publishDate": "Dec 06, 2022",
"readLength": 4
},
"ctaText": "See the full list"
}
]
+3 -3
View File
@@ -30,7 +30,7 @@ const data: CommunityItem[] = [
},
{
title: 'GitHub',
stat: '48,000+',
stat: '50,000+',
statLabel: 'GitHub stars',
img: 'github.png',
invertImgDarkMode: true,
@@ -42,7 +42,7 @@ const data: CommunityItem[] = [
},
{
title: 'Twitter',
stat: '64,000+',
stat: '70,000+',
statLabel: 'Followers',
img: 'twitter.png',
detail: () => (
@@ -53,7 +53,7 @@ const data: CommunityItem[] = [
},
{
title: 'Discord',
stat: '12,000+',
stat: '14,000+',
statLabel: 'SupaTroopers',
img: 'discord.png',
detail: () => (
+16 -4
View File
@@ -184,25 +184,29 @@ const CareerPage: NextPage = ({ jobs, contributors }: any) => {
alt="team photo"
layout="fill"
objectFit="cover"
placeholder="blur"
blurDataURL="/images/blur.png"
className="rounded-md"
/>
</div>
</div>
<div className="grid justify-items-end">
<div className="w-5/6 rounded-md mt-6">
<div className="w-full md:w-5/6 rounded-md mt-6">
<div className="relative w-full aspect-[29/22]">
<Image
src="/images/career/2.jpg"
alt="team photo"
layout="fill"
objectFit="cover"
placeholder="blur"
blurDataURL="/images/blur.png"
className="rounded-md"
/>
</div>
</div>
</div>
</div>
<div className="mt-6 md:mt-0 md:w-1/2">
<div className="mt-6 md:mt-0 w-full md:w-1/2">
<div className="w-full rounded-md">
<div className="relative w-full aspect-[137/110]">
<Image
@@ -210,6 +214,8 @@ const CareerPage: NextPage = ({ jobs, contributors }: any) => {
alt="team photo"
layout="fill"
objectFit="cover"
placeholder="blur"
blurDataURL="/images/blur.png"
className="rounded-md"
/>
</div>
@@ -222,6 +228,8 @@ const CareerPage: NextPage = ({ jobs, contributors }: any) => {
alt="team photo"
layout="fill"
objectFit="cover"
placeholder="blur"
blurDataURL="/images/blur.png"
className="rounded-md"
/>
</div>
@@ -233,18 +241,22 @@ const CareerPage: NextPage = ({ jobs, contributors }: any) => {
alt="team photo"
layout="fill"
objectFit="cover"
placeholder="blur"
blurDataURL="/images/blur.png"
className="rounded-md"
/>
</div>
</div>
</div>
<div className="w-5/6 rounded-md mt-6">
<div className="relative w-full aspect-[41/43]">
<div className="w-full rounded-md mt-6">
<div className="relative w-full aspect-[16/9]">
<Image
src="/images/career/6.jpg"
alt="team photo"
layout="fill"
objectFit="cover"
placeholder="blur"
blurDataURL="/images/blur.png"
className="rounded-md"
/>
</div>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 447 KiB

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 618 KiB

After

Width:  |  Height:  |  Size: 176 KiB

+131
View File
@@ -962,6 +962,12 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/@adobe/css-tools": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz",
"integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==",
"dev": true
},
"node_modules/@algolia/autocomplete-core": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.8.3.tgz",
@@ -9744,6 +9750,54 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@testing-library/jest-dom": {
"version": "5.16.5",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz",
"integrity": "sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==",
"dev": true,
"dependencies": {
"@adobe/css-tools": "^4.0.1",
"@babel/runtime": "^7.9.2",
"@types/testing-library__jest-dom": "^5.9.1",
"aria-query": "^5.0.0",
"chalk": "^3.0.0",
"css.escape": "^1.5.1",
"dom-accessibility-api": "^0.5.6",
"lodash": "^4.17.15",
"redent": "^3.0.0"
},
"engines": {
"node": ">=8",
"npm": ">=6",
"yarn": ">=1"
}
},
"node_modules/@testing-library/jest-dom/node_modules/chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@testing-library/jest-dom/node_modules/redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
"dev": true,
"dependencies": {
"indent-string": "^4.0.0",
"strip-indent": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@testing-library/react": {
"version": "12.1.5",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.1.5.tgz",
@@ -10463,6 +10517,15 @@
"integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==",
"dev": true
},
"node_modules/@types/testing-library__jest-dom": {
"version": "5.14.5",
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz",
"integrity": "sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ==",
"dev": true,
"dependencies": {
"@types/jest": "*"
}
},
"node_modules/@types/uglify-js": {
"version": "3.17.1",
"resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.1.tgz",
@@ -15165,6 +15228,12 @@
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css.escape": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
"dev": true
},
"node_modules/cssdb": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/cssdb/-/cssdb-4.4.0.tgz",
@@ -39038,6 +39107,7 @@
},
"devDependencies": {
"@storybook/react": "^6.5.16",
"@testing-library/jest-dom": "^5.16.5",
"@types/common-tags": "^1.8.1",
"@types/react": "^17.0.37",
"@types/react-copy-to-clipboard": "^5.0.4",
@@ -40066,6 +40136,12 @@
}
},
"dependencies": {
"@adobe/css-tools": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz",
"integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==",
"dev": true
},
"@algolia/autocomplete-core": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.8.3.tgz",
@@ -46429,6 +46505,45 @@
}
}
},
"@testing-library/jest-dom": {
"version": "5.16.5",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz",
"integrity": "sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==",
"dev": true,
"requires": {
"@adobe/css-tools": "^4.0.1",
"@babel/runtime": "^7.9.2",
"@types/testing-library__jest-dom": "^5.9.1",
"aria-query": "^5.0.0",
"chalk": "^3.0.0",
"css.escape": "^1.5.1",
"dom-accessibility-api": "^0.5.6",
"lodash": "^4.17.15",
"redent": "^3.0.0"
},
"dependencies": {
"chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
"dev": true,
"requires": {
"indent-string": "^4.0.0",
"strip-indent": "^3.0.0"
}
}
}
},
"@testing-library/react": {
"version": "12.1.5",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.1.5.tgz",
@@ -47108,6 +47223,15 @@
"integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==",
"dev": true
},
"@types/testing-library__jest-dom": {
"version": "5.14.5",
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz",
"integrity": "sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ==",
"dev": true,
"requires": {
"@types/jest": "*"
}
},
"@types/uglify-js": {
"version": "3.17.1",
"resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.1.tgz",
@@ -50793,6 +50917,12 @@
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
"dev": true
},
"css.escape": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
"dev": true
},
"cssdb": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/cssdb/-/cssdb-4.4.0.tgz",
@@ -68107,6 +68237,7 @@
"@supabase/auth-helpers-react": "^0.3.1",
"@tailwindcss/forms": "^0.4.0",
"@tailwindcss/typography": "^0.5.0",
"@testing-library/jest-dom": "^5.16.5",
"@types/common-tags": "^1.8.1",
"@types/react": "^17.0.37",
"@types/react-copy-to-clipboard": "^5.0.4",
+1 -1
View File
@@ -26,7 +26,7 @@
"docker:up": "cd docker && docker compose up",
"docker:down": "cd docker && docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml down --remove-orphans",
"docker:remove": "cd docker && docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml rm -vfs",
"test": "jest",
"test:ui": "turbo run test --filter=ui",
"test:studio": "turbo run test --filter=studio",
"perf:kong": "ab -t 5 -c 20 -T application/json http://localhost:8000/",
"perf:meta": "ab -t 5 -c 20 -T application/json http://localhost:5555/tables",
+1 -3
View File
@@ -3,9 +3,7 @@
"version": "0.0.0",
"description": "Data shared between Supabase apps",
"main": "./index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"scripts": {},
"author": "",
"license": "MIT"
}
+12
View File
@@ -0,0 +1,12 @@
import type { Config } from '@jest/types'
const config: Config.InitialOptions = {
preset: 'ts-jest',
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
testEnvironment: 'jsdom',
// Restricting test file pattern to just tsx files since the js files need a babel preset to run
// and most likely won't pass now anyway - the tests were ported from the UI repo and are out of date
testRegex: '(.*\\.test.tsx$)',
}
export default config
+3 -1
View File
@@ -6,9 +6,10 @@
"license": "MIT",
"sideEffects": false,
"scripts": {
"build-storybook": "build-storybook",
"build:icons": "node --experimental-modules internals/populate-icons.js",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
"test": "jest"
},
"dependencies": {
"@headlessui/react": "^1.0.0",
@@ -48,6 +49,7 @@
},
"devDependencies": {
"@storybook/react": "^6.5.16",
"@testing-library/jest-dom": "^5.16.5",
"@types/common-tags": "^1.8.1",
"@types/react": "^17.0.37",
"@types/react-copy-to-clipboard": "^5.0.4",
@@ -1,32 +1,41 @@
import React from 'react'
import { render, screen, fireEvent } from '@testing-library/react'
import Button from './Button'
import { Button, ButtonSize, ButtonType } from './Button'
import defaultTheme from '../../lib/theme/defaultTheme'
import Link from 'next/link'
const SIZES = ['tiny', 'small', 'medium', 'large', 'xlarge']
const TYPES = [
const SIZES: ButtonSize[] = ['tiny', 'small', 'medium', 'large', 'xlarge']
const TYPES: ButtonType[] = [
'primary',
'default',
'secondary',
'alternative',
'outline',
'dashed',
'link',
'text',
'danger',
'warning',
]
describe('#Button', () => {
it('should render button correctly', () => {
const wrapper = render(<Button>Button</Button>)
expect(wrapper.getByText('Button')).toBeInTheDocument()
expect(() => wrapper.unmount()).not.toThrow()
})
it('should render different text', () => {
const wrapper = render(<Button>Button</Button>)
expect(screen.getByText('Button')).toBeInTheDocument()
wrapper.rerender(<Button>按钮</Button>)
expect(screen.getByText('按钮')).toBeInTheDocument()
})
it('should ignore events when disabled', () => {
const WrapperButton = () => {
const [state, setState] = React.useState('state1')
@@ -36,9 +45,12 @@ describe('#Button', () => {
</Button>
)
}
render(<WrapperButton />)
expect(screen.getByText('state1')).toBeInTheDocument()
fireEvent.click(screen.getByText('state1'))
expect(screen.getByText('state1')).toBeInTheDocument()
expect(screen.queryByText('state2')).not.toBeInTheDocument()
})
@@ -57,31 +69,44 @@ describe('#Button', () => {
expect(screen.queryByText('state2')).not.toBeInTheDocument()
})
it('should have "w-full" class', async () => {
it('should have "w-full" class when block is true', async () => {
render(<Button block>Button Block</Button>)
expect(screen.queryByRole('button')).toHaveClass(
'sbui-btn sbui-btn-primary sbui-btn--w-full sbui-btn-container--shadow sbui-btn--tiny'
)
expect(screen.queryByRole('button')).toHaveClass('w-full')
})
it.each(TYPES)('should have "btn--%s" class', (type) => {
it.each(TYPES)(`should have %p class from theme`, (type) => {
const expected = defaultTheme.button.type[type]
render(<Button type={type}>Button Variant</Button>)
if (type !== 'text' && type !== 'link') {
expect(screen.queryByRole('button')).toHaveClass(
`sbui-btn sbui-btn-${type} sbui-btn-container--shadow sbui-btn--tiny`
)
} else {
expect(screen.queryByRole('button')).toHaveClass(
`sbui-btn sbui-btn-${type} sbui-btn--tiny`
)
}
expect(screen.queryByRole('button')).toHaveClass(expected)
})
it.each(SIZES)('should have "btn--%s" class', (size) => {
it.each(SIZES)('should have %p class from theme', (size) => {
const expected = defaultTheme.button.size[size]
render(<Button size={size}>Button</Button>)
expect(screen.queryByRole('button')).toHaveClass(
`sbui-btn sbui-btn-primary sbui-btn-container--shadow sbui-btn--${size}`
)
expect(screen.queryByRole('button')).toHaveClass(expected)
})
it("shouldn't crash when wrapped with next/link", () => {
expect(() =>
render(
<Link href="https://supabase.com">
<Button>Button</Button>
</Link>
)
).not.toThrow()
})
it('should forward ref', () => {
const ref: React.MutableRefObject<HTMLButtonElement | null> = {
current: null,
}
render(<Button ref={ref}>Button</Button>)
expect(ref.current).toBeInstanceOf(HTMLButtonElement)
})
})
+18 -30
View File
@@ -1,4 +1,4 @@
import React, { forwardRef, useRef, useImperativeHandle } from 'react'
import React, { forwardRef } from 'react'
import { IconContext } from './../Icon/IconContext'
import { IconLoader } from './../Icon/icons/IconLoader'
@@ -15,19 +15,9 @@ export interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
loading?: boolean
loadingCentered?: boolean
shadow?: boolean
size?: 'tiny' | 'small' | 'medium' | 'large' | 'xlarge'
size?: ButtonSize
style?: React.CSSProperties
type?:
| 'primary'
| 'default'
| 'secondary'
| 'alternative'
| 'outline'
| 'dashed'
| 'link'
| 'text'
| 'danger'
| 'warning'
type?: ButtonType
danger?: boolean
htmlType?: 'button' | 'submit' | 'reset'
ref?: any
@@ -40,14 +30,22 @@ export interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
form?: string
}
export type ButtonSize = 'tiny' | 'small' | 'medium' | 'large' | 'xlarge'
export type ButtonType =
| 'primary'
| 'default'
| 'secondary'
| 'alternative'
| 'outline'
| 'dashed'
| 'link'
| 'text'
| 'danger'
| 'warning'
interface CustomButtonProps extends React.HTMLAttributes<HTMLButtonElement> {}
interface RefHandle {
// container: () => HTMLElement | null
button: () => HTMLButtonElement | null
}
export const Button = forwardRef<RefHandle, ButtonProps>(
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
block,
@@ -75,16 +73,6 @@ export const Button = forwardRef<RefHandle, ButtonProps>(
}: ButtonProps,
ref
) => {
// button ref
// const containerRef = useRef<HTMLElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
useImperativeHandle(ref, () => ({
button: () => {
return buttonRef.current
},
}))
let __styles = styleHandler('button')
// styles
@@ -150,7 +138,7 @@ export const Button = forwardRef<RefHandle, ButtonProps>(
// <span ref={containerRef} className={containerClasses.join(' ')}>
<button
{...props}
ref={buttonRef}
ref={ref}
className={classes.join(' ')}
disabled={loading || (disabled && true)}
onClick={onClick}
+11
View File
@@ -1129,6 +1129,17 @@ functions:
fileOptions: const FileOptions(cacheControl: '3600', upsert: false),
);
```
- id: upload-file-on-web
name: Upload file on web
code: |
```dart
final avatarFile = chosenFile.bytes;
final String path = await supabase.storage.from('avatars').uploadBinary(
'public/avatar1.png',
avatarFile,
fileOptions: const FileOptions(cacheControl: '3600', upsert: false),
);
```
- id: from-update
description: |
+103 -103
View File
@@ -19,9 +19,9 @@ functions:
title: 'Initializing'
description: |
Independently of which Supabase module you are using, you will need to initialize the main client first and install the module.
To create a new client, you can use the `createSupabaseClient` function.
When installing a module, you can pass a block to configure it.
examples:
@@ -55,43 +55,43 @@ functions:
```
description: |
**Common:**
`alwaysAutoRefresh` - whether the GoTrue plugin should always auto refresh expired sessions automatically. Default: `true`
`autoLoadFromStorage` - whether the GoTrue plugin should automatically load the session from the session manager. Default: `true`
`flowType` - Which authentication flow to use. Currently available: FlowType.PKCE and FlowType.IMPLICIT. Default: `FlowType.IMPLICIT`
`codeVerifierCache` - Interface for saving and loading codes for the PKCE authentication flow. Default: `SettingsCodeVerifierCache`
`customUrl` - Custom url for the GoTrue API. Can be safely ignored when using Supabase. Default: `null`
`jwtToken` - Plugin specific JWT Token. Can be ignored when using the GoTrue plugin. Default: `null`
`retryDelay` - Duration after which the GoTrue plugin should retry a failed session refresh. Default: `10.seconds`
`sessionManager` - Interface for saving and loading the user session. Default: `SettingsSessionManager`
**Android & IOS:**
`scheme` - The scheme for the redirect url, when using deep linking. Default: `supabase`
`host` - The host for the redirect url, when using deep linking. Default: `login`
**Android:**
`enableLifecycleCallbacks` - Whether to stop auto-refresh on focus loss, and resume it on focus again. Default: `true`
**Desktop:**
`httpPort`: The port the web server is running on, when logging in with OAuth. Default: `0` (random port).
`timeout`: The timeout for the web server, when logging in with OAuth. Default: `1.minutes`.
`htmlTitle`: The title of the redirect page, when logging in with OAuth. Default: `"Supabase Auth"`.
`htmlText`: The text of the redirect page, when logging in with OAuth. Default: `"Logged in. You may continue in your app."`.
`htmlIconUrl`: The icon of the redirect page, when logging in with OAuth. Default: `"https://supabase.com/brand-assets/supabase-logo-icon.png"`.
- id: configure-postgrest
name: Configure PostgREST module
@@ -109,11 +109,11 @@ functions:
```
description: |
`propertyConversionMethod` - The method to use to convert the property names to the column names when applying filters & using the update method. Default: `PropertyConversionMethod.CAMEL_CASE_TO_SNAKE_CASE`
`defaultSchema` - The default schema to use for database requests. Default: `public`
`customUrl` - Custom url for the PostgREST API. Can be safely ignored when using Supabase. Default: `null`
`jwtToken` - Plugin specific JWT Token. Can be ignored when using the GoTrue plugin. Default: `null`
- id: configure-storage
name: Configure Storage module
@@ -130,17 +130,17 @@ functions:
```
description: |
`transferTimeout` - the timeout for uploading and downloading files. Default: `120.seconds`
`resumable.cache` - Interface for storing resumable upload urls. Default: `ResumableCache.Disk`
`resumable.defaultChunkSize` - The default chunk size for resumable uploads. Supabase currently only supports a chunk size of 6MB, so be careful when changing this value. Default: `6MB`
`resumable.retryTimeout` - the timeout for retrying resumable uploads when uploading a chunk fails. Default: `5.seconds`
`resumable.onlyUpdateStateAfterChunk` - whether the upload state should only be updated after a chunk was uploaded successfully or also when the chunk is currently being uploaded. Default: `false`
`customUrl` - Custom url for the Storage API. Can be safely ignored when using Supabase. Default: `null`
`jwtToken` - Plugin specific JWT Token. Can be ignored when using the GoTrue plugin. Default: `null`
- id: configure-realtime
name: Configure Realtime module
@@ -157,17 +157,17 @@ functions:
```
description: |
`reconnectDelay` - The delay between reconnect attempts. Default: `7.seconds`
`heartbeatInterval` - The interval between heartbeat messages. Default: `15.seconds`
`disconnectOnSessionLoss` - Whether to disconnect from the websocket when the session is lost. Default: `true`
`secure` - Whether to use wss or ws. Defaults to [SupabaseClient.useHTTPS] when null
`websocketConfig` - Custom Ktor websocket config
`customUrl` - Custom url for the Realtime websocket. Can be safely ignored when using Supabase. Default: `null`
`jwtToken` - Plugin specific JWT Token. Can be ignored when using the GoTrue plugin. Default: `null`
- id: configure-functions
name: Configure Functions plugin
@@ -184,8 +184,8 @@ functions:
```
description: |
`customUrl` - Custom url for the Functions API. Can be safely ignored when using Supabase. Default: `null`
`jwtToken` - Plugin specific JWT Token. Can be ignored when using the GoTrue plugin. Default: `null`
`jwtToken` - Plugin specific JWT Token. Can be ignored when using the GoTrue plugin. Default: `null`
- id: configure-graphql
name: Configure GraphQL plugin
code: |
@@ -203,11 +203,11 @@ functions:
```
description: |
`apolloConfiguration` - Custom configuration for the ApolloClient
`customUrl` - Custom url for the GraphQL API. Can be safely ignored when using Supabase. Default: `null`
`jwtToken` - Plugin specific JWT Token. Can be ignored when using the GoTrue plugin. Default: `null`
**You can access the created ApolloClient via `client.graphql.apolloClient`, which automatically adds the required headers depending on your session.**
- id: select
@@ -347,7 +347,7 @@ functions:
title: 'Modify data: update()'
notes: |
- `update()` should always be combined with a [filter](/docs/reference/kotlin/using-filters) block to avoid updating all records.
- When calling `insert` or `update`, you have to provide a [serializable value](/docs/reference/kotlin/installing#add-kotlinx-serialization-required-for-postgrest-and-realtime-serialization) in the function parameter.
- When calling `insert` or `update`, you have to provide a [serializable value](/docs/reference/kotlin/installing#add-kotlinx-serialization-required-for-postgrest-and-realtime-serialization) in the function parameter.
examples:
- id: updating-your-data
name: Updating your data
@@ -435,7 +435,7 @@ functions:
- Primary keys should be included in the data payload in order for an update to work correctly.
- Primary keys must be natural, not surrogate. There are however, [workarounds](https://github.com/PostgREST/postgrest/issues/1118) for surrogate primary keys.
- If you need to insert new data and update existing data at the same time, use [Postgres triggers](https://github.com/supabase/postgrest-js/issues/173#issuecomment-825124550).
- When calling `insert` or `update`, you have to provide a [serializable value](/docs/reference/kotlin/installing#add-kotlinx-serialization-required-for-postgrest-and-realtime-serialization) in the function parameter.
- When calling `insert` or `update`, you have to provide a [serializable value](/docs/reference/kotlin/installing#add-kotlinx-serialization-required-for-postgrest-and-realtime-serialization) in the function parameter.
examples:
- id: upsert-your-data
name: Upsert your data
@@ -505,8 +505,8 @@ functions:
That's a fancy way of saying that you can put some logic into your database then call it from anywhere.
It's especially useful when the logic rarely changes - like password resets and updates.
- When calling `rpc` with parameters, you have to provide a [serializable value](/docs/reference/kotlin/installing#add-kotlinx-serialization-required-for-postgrest-and-realtime-serialization) in the function parameter.
- When calling `rpc` with parameters, you have to provide a [serializable value](/docs/reference/kotlin/installing#add-kotlinx-serialization-required-for-postgrest-and-realtime-serialization) in the function parameter.
examples:
- id: call-a-stored-procedure
name: Call a stored procedure
@@ -529,7 +529,7 @@ functions:
Filters allow you to only return rows that match certain conditions.
Filters can be used on `select()`, `update()`, and `delete()` queries.
You can use two different types for applying filters:
```kotlin
eq("country_id", 1)
@@ -538,10 +538,10 @@ functions:
```kotlin
City::countryId eq 1
```
As you can see on the property syntax:
the name of the `countryId` gets converted to `country_id`.
By default, this is done by converting camel case to snake case, but you can customize this by changing the `PropertyConversionMethod` in the Postgrest Config
If a database function returns a table response, you can also apply filters.
@@ -2074,7 +2074,7 @@ functions:
phoneNumber = "+4912345679"
password = "example-password"
}
```
```
- id: sign-up-with-additional-user-metadata
name: Sign up with additional user metadata
isSpotlight: false
@@ -2126,7 +2126,7 @@ functions:
phoneNumber = "+4912345679"
password = "example-password"
}
```
```
- id: sign-in-with-id-token
name: Sign in with id token
isSpotlight: false
@@ -2142,7 +2142,7 @@ functions:
//...
}
}
```
```
- id: sign-in-with-otp
title: 'sendOtpTo()'
$ref: '@supabase/gotrue-js.GoTrueClient.signInWithOtp'
@@ -2172,7 +2172,7 @@ functions:
client.gotrue.sendOtpTo(Email) {
email = "example@email.com"
}
```
```
- id: sign-in-with-sms-otp
name: Sign in with SMS OTP
isSpotlight: false
@@ -2278,7 +2278,7 @@ functions:
client.gotrue.loginWith(SSO.withProvider("21648a9d-8d5a-4555-a9d1-d6375dc14e92"))
//the url was opened automatically, if you don't want that, provide a custom redirect url
```
```
- id: sign-out
title: 'logout()'
$ref: '@supabase/gotrue-js.GoTrueClient.signOut'
@@ -2360,7 +2360,7 @@ functions:
code: |
```kotlin
val user = client.gotrue.retrieveUser("JWT")
```
```
- id: update-user
title: 'modifyUser()'
$ref: '@supabase/gotrue-js.GoTrueClient.updateUser'
@@ -2420,7 +2420,7 @@ functions:
$ref: '@supabase/gotrue-js.GoTrueClient.refreshSession'
notes: |
This method will refresh the session whether the current one is expired or not.
- This is done automatically, but can be disabled in the GoTrue config.
examples:
- id: refresh-current-session
@@ -2516,14 +2516,14 @@ functions:
code: |
```kotlin
val verifiedFactors = client.gotrue.mfa.verifiedFactors
```
```
- id: retrieve-verified-factors
name: Retrieve verified factors
isSpotlight: true
code: |
```kotlin
val verifiedFactors = client.gotrue.mfa.retrieveFactorsForCurrentUser()
```
```
- id: mfa-challenge
title: 'mfa.challenge()'
$ref: '@supabase/gotrue-js.GoTrueMFAApi.challenge'
@@ -2604,16 +2604,16 @@ functions:
code: |
```kotlin
val (current, next) = client.gotrue.mfa.getAuthenticatorAssuranceLevel()
```
```
- id: aal-enabled
name: Check whether the user has atleast one verified factor
name: Check whether the user has at least one verified factor
isSpotlight: true
code: |
```kotlin
val enabled = client.gotrue.mfa.isMfaEnabled
//flow variant, automatically emitting new values on session changes
val enabledFlow = client.gotrue.mfa.isMfaEnabledFlow
```
```
- id: aal-enabled-for-current-session
name: Check whether the user is logged in using AAL2
isSpotlight: true
@@ -2622,7 +2622,7 @@ functions:
val loggedInUsingMfa = client.gotrue.mfa.loggedInUsingMfa
//flow variant, automatically emitting new values on session changes
val loggedInUsingMfaFlow = client.gotrue.mfa.loggedInUsingMfaFlow
```
```
- id: admin-api
title: 'Overview'
notes: |
@@ -2721,7 +2721,7 @@ functions:
put("name", "John")
}
}
```
```
- id: auto-confirm-the-users-email
name: Auto-confirm the user's email
code: |
@@ -2928,12 +2928,12 @@ functions:
code: |
```kotlin
client.gotrue.admin.deleteFactor(uid = "id", factorId = "factor_id")
```
```
- id: invoke
title: 'invoke()'
description: |
Invokes a Supabase Function. See the [guide](/docs/guides/functions) for details on writing Functions.
- When invoking a function with parameters, you have to provide a [serializable value](/docs/reference/kotlin/installing#add-kotlinx-serialization-required-for-postgrest-and-realtime-serialization) in the function parameter.
- When invoking a function with parameters, you have to provide a [serializable value](/docs/reference/kotlin/installing#add-kotlinx-serialization-required-for-postgrest-and-realtime-serialization) in the function parameter.
notes: |
- Requires an Authorization header.
examples:
@@ -2958,7 +2958,7 @@ functions:
append(HttpHeaders.ContentType, "application/json")
}
)
```
```
- id: reuse-function
name: Reuse function by saving it to a variable
isSpotlight: true
@@ -2980,7 +2980,7 @@ functions:
function(body) {
set("Header", "Value")
}
```
```
- id: subscribe
description: |
Subscribe to realtime changes in your database.
@@ -2991,7 +2991,7 @@ functions:
- When using a method with a generic type like `track`, `broadcast` or `broadcastFlow`, you have to provide a [serializable class](/docs/reference/kotlin/installing#add-kotlinx-serialization-required-for-postgrest-and-realtime-serialization) as the type parameter.
examples:
- id: connect-to-realtime-client
name: Connect to Realtime
name: Connect to Realtime
isSpotlight: true
code: |
```kotlin
@@ -3003,45 +3003,45 @@ functions:
```kotlin
@Serializable
data class Message(val content: String, val sender: String)
val channel = client.realtime.createChannel("channelId") {
//optional config
}
val broadcastFlow = channel.broadcastFlow<Message>(event = "message")
//in a new coroutine (or use Flow.onEach().launchIn(scope)):
broadcastFlow.collect { //it: Message
println(it)
}
channel.join(blockUntilJoined = true)
channel.broadcast(event = "message", Message("I joined!", "John"))
```
```
- id: listen-to-presence-updates
name: Listen to presence updates
code: |
code: |
```kotlin
@Serializable
data class PresenceState(val username: String)
val connectedUsers = mutableSetOf<PresenceState>()
val channel = client.realtime.createChannel("channelId") {
//optional config
}
val presenceChangeFlow = channel.presenceChangeFlow()
//in a new coroutine (or use Flow.onEach().launchIn(scope)):
presenceChangeFlow.collect {
connectedUsers += it.decodeJoinsAs<PresenceState>()
connectedUsers -= it.decodeLeavesAs<PresenceState>()
}
channel.join(blockUntilJoined = true)
//send own state
channel.track(PresenceState(username = "John"))
channel.track(PresenceState(username = "John"))
- id: listen-to-all-database-changes
name: Listen to all database changes
code: |
@@ -3050,7 +3050,7 @@ functions:
//optional config
}
val changeFlow = channel.postgresChangeFlow<PostgresAction>(schema = "public")
//in a new coroutine (or use Flow.onEach().launchIn(scope)):
changeFlow.collect {
when(it) {
@@ -3060,7 +3060,7 @@ functions:
is PostgresAction.Update -> println("Updated: ${it.oldRecord} with ${it.record}")
}
}
channel.join()
```
- id: listen-to-a-specific-table
@@ -3073,7 +3073,7 @@ functions:
val changeFlow = channel.postgresChangeFlow<PostgresAction>(schema = "public") {
table = "users"
}
//in a new coroutine (or use Flow.onEach().launchIn(scope)):
changeFlow.collect {
when(it) {
@@ -3083,7 +3083,7 @@ functions:
is PostgresAction.Update -> println("Updated: ${it.oldRecord} with ${it.record}")
}
}
channel.join()
```
- id: listen-to-inserts
@@ -3096,12 +3096,12 @@ functions:
val changeFlow = channel.postgresChangeFlow<PostgresAction.Insert>(schema = "public") {
table = "users"
}
//in a new coroutine (or use Flow.onEach().launchIn(scope)):
changeFlow.collect {
println(it.record)
}
channel.join()
```
- id: listen-to-updates
@@ -3121,13 +3121,13 @@ functions:
val changeFlow = channel.postgresChangeFlow<PostgresAction.Update>(schema = "public") {
table = "users"
}
//in a new coroutine (or use Flow.onEach().launchIn(scope)):
changeFlow.collect {
println(it.record)
println(it.oldRecord)
}
channel.join()
```
- id: listen-to-deletes
@@ -3147,12 +3147,12 @@ functions:
val changeFlow = channel.postgresChangeFlow<PostgresAction.Delete>(schema = "public") {
table = "users"
}
//in a new coroutine (or use Flow.onEach().launchIn(scope)):
changeFlow.collect {
println(it.oldRecord)
}
channel.join()
```
- id: listening-to-row-level-changes
@@ -3167,12 +3167,12 @@ functions:
table = "users"
filter = "id.eq.1"
}
//in a new coroutine:
changeFlow.collect {
println(it.oldRecord)
}
channel.join()
```
@@ -3325,7 +3325,7 @@ functions:
is UploadStatus.Success -> println("Success")
}
}
```
```
- id: create-resumable-upload
name: Create resumable upload
isSpotlight: true
@@ -3342,10 +3342,10 @@ functions:
size = dataSize,
path = "icon.png"
)
```
```
- id: start-resumable-upload
name: Start and resumable upload
isSpotlight: true
isSpotlight: true
code: |
```kotlin
upload.startOrResumeUploading()
@@ -3361,7 +3361,7 @@ functions:
code: |
```kotlin
upload.cancel()
```
```
description: |
This will also remove the upload url from the cache
- id: listen-to-upload-state
@@ -3373,21 +3373,21 @@ functions:
println("Paused: ${it.paused}")
println("Is done: ${it.isDone}")
}
```
```
- id: continue-previous-upload
name: Continue previous uploads
isSpotlight: true
code: |
```kotlin
val bucket = client.storage["avatars"]
//only on JVM/Android:
bucket.resumable.continuePreviousFileUploads()
.map { it.await() } //await all uploads. This just makes sure the uploads have an update-to-date url. You can also do this in parallel
.forEach { upload ->
upload.startOrResumeUploading()
}
//on other platforms you may have to continue uploads from the source (Probably better to write an extension function):
bucket.resumable.continuePreviousUploads { source, offset ->
//create ByteReadChannel from source and seek to offset
@@ -3396,7 +3396,7 @@ functions:
.forEach { upload ->
upload.startOrResumeUploading()
}
```
```
- id: from-update
title: from.update()
@@ -3492,7 +3492,7 @@ functions:
val bytes = bucket.downloadPublic("test.png")
//or on JVM:
bucket.downloadPublicTo("test.png", File("test.png"))
```
```
- id: download-with-transformation
name: Download file with transformation
code: |
@@ -3509,9 +3509,9 @@ functions:
fill()
quality = 100
}
```
```
- id: download-with-progress
name: Download file with progress
name: Download file with progress
code: |
```kotlin
val bucket = client.storage["avatars"]
@@ -3530,7 +3530,7 @@ functions:
else -> {} //The ByteData status will never occur as we are writing directly to a file
}
}
```
```
- id: from-remove
title: from.remove()
$ref: '@supabase/storage-js.packages/StorageFileApi.default.remove'
+125 -134
View File
@@ -3,12 +3,13 @@ import Link from 'next/link'
import { FC, useState } from 'react'
import { useRouter } from 'next/router'
import { Button, Dropdown, IconArchive, IconChevronDown, IconDatabase, IconKey, IconZap } from 'ui'
import ChartHandler from 'components/to-be-cleaned/Charts/ChartHandler'
import Panel from 'components/ui/Panel'
import { DATE_FORMAT, METRICS } from 'lib/constants'
import { ChartIntervals } from 'types'
import { useParams } from 'common/hooks'
import { useProjectLogStatsQuery } from 'data/logs/project-log-stats-query'
import { UsageApiCounts, useProjectLogStatsQuery } from 'data/analytics/project-log-stats-query'
import BarChart from 'components/ui/Charts/BarChart'
import sumBy from 'lodash/sumBy'
import useFillTimeseriesSorted from 'hooks/analytics/useFillTimeseriesSorted'
const CHART_INTERVALS: ChartIntervals[] = [
{
@@ -29,29 +30,36 @@ const ProjectUsage: FC<Props> = ({}) => {
const [interval, setInterval] = useState<string>('hourly')
const { data, error } = useProjectLogStatsQuery({ projectRef, interval })
const { data, error, isLoading } = useProjectLogStatsQuery({ projectRef, interval })
const selectedInterval = CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1]
const startDate = dayjs()
.subtract(selectedInterval.startValue, selectedInterval.startUnit as dayjs.ManipulateType)
.format(DATE_FORMAT)
const endDate = dayjs().format(DATE_FORMAT)
const charts = data?.data
const startDateLocal = dayjs().subtract(
selectedInterval.startValue,
selectedInterval.startUnit as dayjs.ManipulateType
)
const endDateLocal = dayjs()
const charts = useFillTimeseriesSorted(
data?.result || [],
'timestamp',
[
'total_auth_requests',
'total_rest_requests',
'total_storage_requests',
'total_realtime_requests',
],
0,
startDateLocal.toISOString(),
endDateLocal.toISOString()
)
const datetimeFormat = selectedInterval.format || 'MMM D, ha'
const handleBarClick = (v: any, search: string) => {
if (!v || !v.activePayload?.[0]?.payload) return
// returns rechart internal tooltip data type
const payload = v.activePayload[0].payload
const timestamp = payload.timestamp
const timestampDigits = String(timestamp).length
if (timestampDigits < 16) {
// pad unix timestamp with additional 0 and then forward
const paddedTimestamp = String(timestamp) + '0'.repeat(16 - timestampDigits)
router.push(`/project/${projectRef}/logs/edge-logs?te=${paddedTimestamp}`)
} else {
router.push(`/project/${projectRef}/logs/edge-logs?te=${timestamp}`)
}
const handleBarClick = (
value: UsageApiCounts,
// TODO (ziinc): link to edge logs with correct filter applied
_type: 'rest' | 'realtime' | 'storage' | 'auth'
) => {
const selected = dayjs(value.timestamp).toISOString()
router.push(`/project/${projectRef}/logs/edge-logs?ite=${encodeURIComponent(selected)}`)
}
return (
@@ -78,118 +86,101 @@ const ProjectUsage: FC<Props> = ({}) => {
Statistics for past {selectedInterval.label}
</span>
</div>
<div className="">
{startDate && endDate && (
<>
<div className="grid grid-cols-1 md:grid-cols-2 md:gap-4 lg:grid-cols-4 lg:gap-8">
<Panel key="database-chart">
<Panel.Content className="space-y-4">
<PanelHeader
icon={
<div className="rounded bg-scale-600 p-1.5 text-scale-1000 shadow-sm">
<IconDatabase strokeWidth={2} size={16} />
</div>
}
title="Database"
href={`/project/${projectRef}/editor`}
/>
<ChartHandler
startDate={startDate}
endDate={endDate}
attribute={'total_rest_requests'}
label={METRICS.find((x: any) => x.key == 'total_rest_requests')?.label ?? ''}
provider="log-stats"
interval="1d"
hideChartType
customDateFormat={datetimeFormat}
data={charts}
isLoading={!charts && !error ? true : false}
onBarClick={(v) => handleBarClick(v, '/rest')}
/>
</Panel.Content>
</Panel>
<Panel key="auth-chart">
<Panel.Content className="space-y-4">
<PanelHeader
icon={
<div className="rounded bg-scale-600 p-1.5 text-scale-1000 shadow-sm">
<IconKey strokeWidth={2} size={16} />
</div>
}
title="Auth"
href={`/project/${projectRef}/auth/users`}
/>
<ChartHandler
startDate={startDate}
endDate={endDate}
attribute={'total_auth_requests'}
label={METRICS.find((x) => x.key == 'total_auth_requests')?.label ?? ''}
provider="log-stats"
interval="1d"
hideChartType
customDateFormat={datetimeFormat}
data={charts}
isLoading={!charts && !error ? true : false}
onBarClick={(v) => handleBarClick(v, '/auth')}
/>
</Panel.Content>
</Panel>
<Panel key="storage-chart">
<Panel.Content className="space-y-4">
<PanelHeader
icon={
<div className="rounded bg-scale-600 p-1.5 text-scale-1000 shadow-sm">
<IconArchive strokeWidth={2} size={16} />
</div>
}
title="Storage"
href={`/project/${projectRef}/storage/buckets`}
/>
<ChartHandler
startDate={startDate}
endDate={endDate}
attribute={'total_storage_requests'}
label={METRICS.find((x) => x.key == 'total_storage_requests')?.label ?? ''}
provider="log-stats"
interval="1d"
hideChartType
customDateFormat={datetimeFormat}
data={charts}
isLoading={!charts && !error ? true : false}
onBarClick={(v) => handleBarClick(v, '/storage')}
/>
</Panel.Content>
</Panel>
<Panel key="realtime-chart">
<Panel.Content className="space-y-4">
<PanelHeader
icon={
<div className="rounded bg-scale-600 p-1.5 text-scale-1000 shadow-sm">
<IconZap strokeWidth={2} size={16} />
</div>
}
title="Realtime"
/>
<ChartHandler
startDate={startDate}
endDate={endDate}
attribute={'total_realtime_requests'}
label={
METRICS.find((x: any) => x.key == 'total_realtime_requests')?.label ?? ''
}
provider="log-stats"
interval="1h"
hideChartType
customDateFormat={datetimeFormat}
data={charts}
isLoading={!charts && !error ? true : false}
onBarClick={(v) => handleBarClick(v, '/realtime')}
/>
</Panel.Content>
</Panel>
</div>
</>
)}
<div className="grid grid-cols-1 md:grid-cols-2 md:gap-4 lg:grid-cols-4 lg:gap-8">
<Panel>
<Panel.Content className="space-y-4">
<PanelHeader
icon={
<div className="rounded bg-scale-600 p-1.5 text-scale-1000 shadow-sm">
<IconDatabase strokeWidth={2} size={16} />
</div>
}
title="Database"
href={`/project/${projectRef}/editor`}
/>
<BarChart
title="REST Requests"
data={charts}
xAxisKey="timestamp"
yAxisKey="total_rest_requests"
isLoading={isLoading}
onBarClick={(v: unknown) => handleBarClick(v as UsageApiCounts, 'rest')}
customDateFormat={datetimeFormat}
highlightedValue={sumBy(charts, 'total_rest_requests')}
/>
</Panel.Content>
</Panel>
<Panel>
<Panel.Content className="space-y-4">
<PanelHeader
icon={
<div className="rounded bg-scale-600 p-1.5 text-scale-1000 shadow-sm">
<IconKey strokeWidth={2} size={16} />
</div>
}
title="Auth"
href={`/project/${projectRef}/auth/users`}
/>
<BarChart
title="Auth Requests"
data={charts}
xAxisKey="timestamp"
yAxisKey="total_auth_requests"
isLoading={!charts && !error ? true : false}
onBarClick={(v: unknown) => handleBarClick(v as UsageApiCounts, 'auth')}
customDateFormat={datetimeFormat}
highlightedValue={sumBy(charts || [], 'total_auth_requests')}
/>
</Panel.Content>
</Panel>
<Panel>
<Panel.Content className="space-y-4">
<PanelHeader
icon={
<div className="rounded bg-scale-600 p-1.5 text-scale-1000 shadow-sm">
<IconArchive strokeWidth={2} size={16} />
</div>
}
title="Storage"
href={`/project/${projectRef}/storage/buckets`}
/>
<BarChart
title="Storage Requests"
data={charts}
xAxisKey="timestamp"
yAxisKey="total_storage_requests"
isLoading={isLoading}
onBarClick={(v: unknown) => handleBarClick(v as UsageApiCounts, 'storage')}
customDateFormat={datetimeFormat}
highlightedValue={sumBy(charts, 'total_storage_requests')}
/>
</Panel.Content>
</Panel>
<Panel>
<Panel.Content className="space-y-4">
<PanelHeader
icon={
<div className="rounded bg-scale-600 p-1.5 text-scale-1000 shadow-sm">
<IconZap strokeWidth={2} size={16} />
</div>
}
title="Realtime"
/>
<BarChart
title="Realtime Requests"
data={charts}
xAxisKey="timestamp"
yAxisKey="total_realtime_requests"
isLoading={isLoading}
onBarClick={(v: unknown) => handleBarClick(v as UsageApiCounts, 'realtime')}
customDateFormat={datetimeFormat}
highlightedValue={sumBy(charts, 'total_realtime_requests')}
/>
</Panel.Content>
</Panel>
</div>
</div>
)
@@ -278,3 +278,4 @@ select
}
export const DATETIME_FORMAT = 'MMM D, ha'
export const DATETIME_SECOND_FORMAT = 'MMM D, ha'
@@ -7,20 +7,6 @@ export enum Presets {
QUERY_PERFORMANCE = 'query_performance',
}
export interface QueryDataBase {
isLoading: boolean
error: string
}
export interface DbQueryData<T = any> extends QueryDataBase {
data: T[]
params: BaseReportParams
logData?: never
}
export interface DbQueryHandler {
runQuery: () => void
setParams?: never
changeQuery?: never
}
export type MetaQueryResponse = any & { error: ResponseError }
export type BaseReportParams = typeof DEFAULT_QUERY_PARAMS & { sql?: string } & unknown
@@ -1,12 +1,6 @@
import useDbQuery from 'hooks/analytics/useDbQuery'
import useLogsQuery, { LogsQueryData, LogsQueryHandlers } from 'hooks/analytics/useLogsQuery'
import {
BaseQueries,
DbQueryData,
DbQueryHandler,
PresetConfig,
ReportQuery,
} from './Reports.types'
import useDbQuery, { DbQueryHook } from 'hooks/analytics/useDbQuery'
import useLogsQuery, { LogsQueryHook } from 'hooks/analytics/useLogsQuery'
import { BaseQueries, PresetConfig, ReportQuery } from './Reports.types'
/**
* Converts a query params string to an object
@@ -16,7 +10,7 @@ export const queryParamsToObject = (params: string) => {
}
// generate hooks based on preset config
type PresetHookResult = [LogsQueryData | DbQueryData, LogsQueryHandlers | DbQueryHandler]
type PresetHookResult = LogsQueryHook | DbQueryHook
type PresetHooks = Record<keyof PresetConfig['queries'], () => PresetHookResult>
export const queriesFactory = <T extends string>(
@@ -1,7 +1,6 @@
import { useParams } from 'common'
import { useContentUpdateMutation } from 'data/content/content-update-mutation'
import { useStore } from 'hooks'
import { sqlEditorState } from 'state/sql-editor'
import { useSqlEditorStateSnapshot } from 'state/sql-editor'
import { Button, Form, Input, Modal } from 'ui'
export interface RenameQueryModalProps {
@@ -14,13 +13,7 @@ export interface RenameQueryModalProps {
const RenameQueryModal = ({ snippet, visible, onCancel, onComplete }: RenameQueryModalProps) => {
const { ui } = useStore()
const { ref } = useParams()
const { mutateAsync: updateContent } = useContentUpdateMutation({
onSuccess(data) {
if (data.content.id) {
sqlEditorState.snippets[data.content.id].snippet.name = data.content.name
}
},
})
const snap = useSqlEditorStateSnapshot()
const { id, name, description } = snippet
@@ -36,7 +29,7 @@ const RenameQueryModal = ({ snippet, visible, onCancel, onComplete }: RenameQuer
setSubmitting(true)
try {
await updateContent({ projectRef: ref, id, content: values })
snap.renameSnippet(id, values.name, values.description)
if (onComplete) onComplete()
return Promise.resolve()
} catch (error: any) {
@@ -35,7 +35,7 @@ const LogSelection: FC<LogSelectionProps> = ({
queryType,
params = {},
}) => {
const [{ logData: fullLog, isLoading }] = useSingleLog(
const { logData: fullLog, isLoading } = useSingleLog(
projectRef,
queryType,
params,
@@ -1,4 +1,4 @@
import { useEffect, useState, useMemo } from 'react'
import { useEffect, useState, useMemo, useCallback } from 'react'
import { Alert, Button, IconEye, IconEyeOff } from 'ui'
import DataGrid, { Row, RowRendererProps } from '@supabase/react-data-grid'
@@ -15,6 +15,7 @@ import DefaultErrorRenderer from './LogsErrorRenderers/DefaultErrorRenderer'
import FunctionsLogsColumnRender from './LogColumnRenderers/FunctionsLogsColumnRender'
import FunctionsEdgeColumnRender from './LogColumnRenderers/FunctionsEdgeColumnRender'
import AuthColumnRenderer from './LogColumnRenderers/AuthColumnRenderer'
import { isEqual } from 'lodash'
interface Props {
data?: Array<LogData | Object>
@@ -59,7 +60,6 @@ const LogTable = ({
onHistogramToggle,
isHistogramShowing,
isLoading,
showDownload,
error,
projectRef,
params,
@@ -144,8 +144,10 @@ const LogTable = ({
}, [stringData])
useEffect(() => {
if (!hasId || data === null) return
if (focusedLog && !(focusedLog.id in logMap)) {
if (!data) return
const found = data.find((datum) => isEqual(datum, focusedLog))
if (!found) {
// close selection panel if not found in dataset
setFocusedLog(null)
}
}, [stringData])
@@ -161,14 +163,12 @@ const LogTable = ({
}
}, [stringData])
const RowRenderer = (props: RowRendererProps<any>) => {
return (
<Row
{...props}
className="font-mono tracking-tight cursor-pointer !bg-scale-200 hover:!bg-scale-300"
/>
)
}
const RowRenderer = useCallback(
(props: RowRendererProps<any>) => (
<Row {...props} isRowSelected={false} selectedCellIdx={undefined} />
),
[]
)
const LogsExplorerTableHeader = () => (
<div className="flex w-full items-center justify-between rounded-tl rounded-tr border-t border-l border-r bg-scale-100 px-5 py-2 dark:bg-scale-300">
@@ -268,10 +268,11 @@ const LogTable = ({
`}
rowHeight={40}
headerRowHeight={queryType ? 0 : 28}
onSelectedCellChange={({ idx, rowIdx }) => {
onSelectedCellChange={({ rowIdx }) => {
if (!hasId) return
setFocusedLog(data[rowIdx] as LogData)
}}
selectedRows={new Set([])}
noRowsFallback={
!isLoading ? (
<div className="mx-auto flex h-full w-full items-center justify-center space-y-12 py-4 transition-all delay-200 duration-500">
@@ -282,15 +283,20 @@ const LogTable = ({
}
columns={columns as any}
rowClass={(row: LogData) =>
row.id === focusedLog?.id ? '!bg-scale-400 rdg-row--focused' : 'cursor-pointer'
[
'font-mono tracking-tight',
isEqual(row, focusedLog)
? '!bg-scale-800 rdg-row--focused'
: ' !bg-scale-200 hover:!bg-scale-300 cursor-pointer',
].join(' ')
}
rows={logDataRows}
rowKeyGetter={(r) => {
if (!hasId) return Object.keys(r)[0]
if (!hasId) return JSON.stringify(r)
const row = r as LogData
return row.id
}}
onRowClick={(r) => setFocusedLog(r)}
onRowClick={setFocusedLog}
rowRenderer={RowRenderer}
/>
{logDataRows.length > 0 ? (
@@ -658,12 +658,12 @@ export const getDefaultHelper = (helpers: DatetimeHelper[]) =>
helpers.find((helper) => helper.default) || helpers[0]
export const TIER_QUERY_LIMITS: {
[x: string]: { text: string; value: 1 | 7 | 90; unit: 'day'; promptUpgrade: boolean }
[x: string]: { text: string; value: 1 | 7 | 28 | 90; unit: 'day'; promptUpgrade: boolean }
} = {
FREE: { text: '1 day', value: 1, unit: 'day', promptUpgrade: true },
PRO: { text: '7 days', value: 7, unit: 'day', promptUpgrade: true },
PAYG: { text: '90 days', value: 90, unit: 'day', promptUpgrade: false },
TEAM: { text: '90 days', value: 90, unit: 'day', promptUpgrade: false },
PAYG: { text: '7 days', value: 7, unit: 'day', promptUpgrade: true },
TEAM: { text: '28 days', value: 28, unit: 'day', promptUpgrade: true },
ENTERPRISE: { text: '90 days', value: 90, unit: 'day', promptUpgrade: false },
}
@@ -239,6 +239,7 @@ export const genChartQuery = (
return `
SELECT
-- event-chart
timestamp_trunc(t.timestamp, ${trunc}) as timestamp,
count(t.timestamp) as count
FROM
@@ -367,7 +368,7 @@ export const useEditorHints = () => {
export const fillTimeseries = (
timeseriesData: any[],
timestampKey: string,
valueKey: string,
valueKey: string | string[],
defaultValue: number,
min?: string,
max?: string
@@ -390,10 +391,21 @@ export const fillTimeseries = (
const diff = maxDate.diff(minDate, truncation as dayjs.UnitType)
for (let i = 0; i <= diff; i++) {
const dateToMaybeAdd = minDate.add(i, truncation as dayjs.ManipulateType)
const keys = typeof valueKey === 'string' ? [valueKey] : valueKey
const toMerge = keys.reduce(
(acc, key) => ({
...acc,
[key]: defaultValue,
}),
{}
)
if (!dates.find((d) => isEqual(d, dateToMaybeAdd))) {
newData.push({
[timestampKey]: dateToMaybeAdd.toISOString(),
[valueKey]: defaultValue,
...toMerge,
})
}
}
@@ -57,10 +57,20 @@ export const LogsPreviewer: React.FC<Props> = ({
const table = !tableName ? LOGS_TABLES[queryType] : tableName
const [
{ error, logData, params, newCount, filters, isLoading, eventChartData, isLoadingOlder },
{ loadOlder, setFilters, refresh, setParams },
] = useLogsPreview(projectRef as string, table, filterOverride)
const {
error,
logData,
params,
newCount,
filters,
isLoading,
eventChartData,
isLoadingOlder,
loadOlder,
setFilters,
refresh,
setParams,
} = useLogsPreview(projectRef as string, table, filterOverride)
const { showUpgradePrompt, setShowUpgradePrompt } = useUpgradePrompt(
params.iso_timestamp_start as string
@@ -231,7 +241,11 @@ export const LogsPreviewer: React.FC<Props> = ({
Load older
</Button>
<div className="flex flex-row justify-end mt-2">
<UpgradePrompt show={showUpgradePrompt} setShowUpgradePrompt={setShowUpgradePrompt} />
<UpgradePrompt
show={showUpgradePrompt}
setShowUpgradePrompt={setShowUpgradePrompt}
subscription={subscription}
/>
</div>
</div>
)}
@@ -4,17 +4,22 @@ import { TIER_QUERY_LIMITS } from '.'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useFlag } from 'hooks'
import { StripeSubscription } from 'components/interfaces/Billing'
import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
interface Props {
show: boolean
setShowUpgradePrompt: (value: boolean) => void
subscription: StripeSubscription | undefined
}
const UpgradePrompt: React.FC<Props> = ({ show, setShowUpgradePrompt }) => {
const UpgradePrompt: React.FC<Props> = ({ show, setShowUpgradePrompt, subscription }) => {
const router = useRouter()
const { ref } = router.query
const teamTierEnabled = useFlag('teamTier')
// Team tier is enabled when the flag is turned on OR the user is already on the team tier (manually assigned by us)
const userIsOnTeamTier = subscription?.tier?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM
const teamTierEnabled = useFlag('teamTier') || userIsOnTeamTier
return (
<Modal
@@ -181,12 +181,10 @@ const SideBarContent = observer(() => {
<div className="editor-product-menu">
<Menu.Group title="Favorites" />
<div className="space-y-1">
{favouriteTabs
.sort((a, b) => a.name?.localeCompare(b.name)) // first alphabetical
.map((tabInfo) => {
const { id } = tabInfo || {}
return <QueryItem key={id} tabInfo={tabInfo} />
})}
{favouriteTabs.map((tabInfo) => {
const { id } = tabInfo || {}
return <QueryItem key={id} tabInfo={tabInfo} />
})}
</div>
</div>
)}
@@ -194,12 +192,10 @@ const SideBarContent = observer(() => {
<div className="editor-product-menu">
<Menu.Group title="SQL snippets" />
<div className="space-y-1 pb-8">
{queryTabs
.sort((a, b) => a.name?.localeCompare(b.name)) // first alphabetical
.map((tabInfo) => {
const { id } = tabInfo || {}
return <QueryItem key={id} tabInfo={tabInfo} />
})}
{queryTabs.map((tabInfo) => {
const { id } = tabInfo || {}
return <QueryItem key={id} tabInfo={tabInfo} />
})}
</div>
</div>
)}
+1 -1
View File
@@ -41,7 +41,7 @@ const AreaChart: React.FC<AreaChartProps> = ({
const { Container } = useChartSize(size)
const [focusDataIndex, setFocusDataIndex] = useState<number | null>(null)
if (data.length === 0) return <ChartNoData className={className} />
if (data.length === 0) return <ChartNoData size={size} className={className} />
const day = (value: number | string) => (displayDateInUtc ? dayjs(value).utc() : dayjs(value))
const resolvedHighlightedLabel =
+2 -2
View File
@@ -37,7 +37,7 @@ const BarChart: React.FC<BarChartProps> = ({
const { Container } = useChartSize(size)
const [focusDataIndex, setFocusDataIndex] = useState<number | null>(null)
if (data.length === 0) return <ChartNoData className={className} />
if (data.length === 0) return <ChartNoData size={size} className={className} />
const day = (value: number | string) => (displayDateInUtc ? dayjs(value).utc() : dayjs(value))
const resolvedHighlightedLabel =
@@ -48,7 +48,7 @@ const BarChart: React.FC<BarChartProps> = ({
highlightedLabel
const resolvedHighlightedValue =
(focusDataIndex !== null ? data[focusDataIndex]?.[yAxisKey] : highlightedValue)
focusDataIndex !== null ? data[focusDataIndex]?.[yAxisKey] : highlightedValue
return (
<div className={['flex flex-col gap-3', className].join(' ')}>
@@ -1,27 +1,34 @@
import { IconBarChart2 } from 'ui'
import { useChartSize } from './Charts.utils'
interface Props {
title?: string
message?: string
className?: string
size: Parameters<typeof useChartSize>[0]
}
const NoDataPlaceholder: React.FC<Props> = ({
title = 'No data to show',
message,
className = '',
}) => (
<div
className={
'border-scale-600 flex w-full flex-col items-center justify-center space-y-2 border border-dashed text-center ' +
className
}
style={{minHeight: "100px"}}
>
<IconBarChart2 className="text-scale-800" />
<div>
<p className="text-scale-1100 text-xs">{title}</p>
{message && <p className="text-scale-900 text-xs">{message}</p>}
size,
}) => {
const { minHeight } = useChartSize(size)
return (
<div
className={
'border-scale-600 flex flex-grow w-full flex-col items-center justify-center space-y-2 border border-dashed text-center ' +
className
}
// extra 20 px for the x ticks
style={{ minHeight: minHeight + 20 }}
>
<IconBarChart2 className="text-scale-800" />
<div>
<p className="text-scale-1100 text-xs">{title}</p>
{message && <p className="text-scale-900 text-xs">{message}</p>}
</div>
</div>
</div>
)
)
}
export default NoDataPlaceholder
@@ -43,7 +43,7 @@ const StackedBarChart: React.FC<Props> = ({
variant,
})
const [focusDataIndex, setFocusDataIndex] = useState<number | null>(null)
if (!data || data.length === 0) return <NoDataPlaceholder />
if (!data || data.length === 0) return <NoDataPlaceholder size={size} />
return (
<div className="w-full">
{!hideHeader && (
+3
View File
@@ -30,4 +30,7 @@ export const analyticsKeys = {
'infra-monitoring',
{ attribute, startDate, endDate, interval },
] as const,
usageApiCounts: (projectRef: string | undefined, interval: string | undefined) =>
['projects', projectRef, 'usage.api-counts', interval] as const,
}
@@ -2,14 +2,23 @@ import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { useCallback } from 'react'
import { logKeys } from './keys'
import { analyticsKeys } from './keys'
export type ProjectLogStatsVariables = {
projectRef?: string
interval?: string
}
export type ProjectLogStatsResponse = any
export type ProjectLogStatsResponse = {
result: UsageApiCounts[]
}
export interface UsageApiCounts {
total_auth_requests: number
total_storage_requests: number
total_rest_requests: number
total_realtime_requests: number
timestamp: string
}
export async function getProjectLogStats(
{ projectRef, interval }: ProjectLogStatsVariables,
@@ -22,9 +31,12 @@ export async function getProjectLogStats(
throw new Error('interval is required')
}
const response = await get(`${API_URL}/projects/${projectRef}/log-stats?interval=${interval}`, {
signal,
})
const response = await get(
`${API_URL}/projects/${projectRef}/analytics/endpoints/usage.api-counts?interval=${interval}`,
{
signal,
}
)
if (response.error) {
throw response.error
}
@@ -43,7 +55,7 @@ export const useProjectLogStatsQuery = <TData = ProjectLogStatsData>(
}: UseQueryOptions<ProjectLogStatsData, ProjectLogStatsError, TData> = {}
) =>
useQuery<ProjectLogStatsData, ProjectLogStatsError, TData>(
logKeys.logStats(projectRef, interval),
analyticsKeys.usageApiCounts(projectRef, interval),
({ signal }) => getProjectLogStats({ projectRef, interval }, signal),
{
enabled: enabled && typeof projectRef !== 'undefined' && typeof interval !== 'undefined',
@@ -56,7 +68,7 @@ export const useProjectLogStatsPrefetch = ({ projectRef, interval }: ProjectLogS
return useCallback(() => {
if (projectRef && interval) {
client.prefetchQuery(logKeys.logStats(projectRef, interval), ({ signal }) =>
client.prefetchQuery(analyticsKeys.usageApiCounts(projectRef, interval), ({ signal }) =>
getProjectLogStats({ projectRef, interval }, signal)
)
}
@@ -11,6 +11,7 @@ export type ApiAuthorizationVariables = {
export type ApiAuthorizationResponse = {
name: string
website: string
icon: string | null
domain: string
expires_at: string
approved_at: string | null
-4
View File
@@ -1,4 +0,0 @@
export const logKeys = {
logStats: (projectRef: string | undefined, interval: string | undefined) =>
['projects', projectRef, 'log-stats', interval] as const,
}
+2 -1
View File
@@ -10,7 +10,8 @@ export function formatFilterValue(table: SupaTable, filter: Filter) {
const column = table.columns.find((x) => x.name == filter.column)
if (column && isNumericalColumn(column.format)) {
const numberValue = Number(filter.value)
if (Number.isNaN(numberValue)) return filter.value
// Supports BigInt filter values
if (Number.isNaN(numberValue) || numberValue > Number.MAX_SAFE_INTEGER) return filter.value
else return Number(filter.value)
}
return filter.value
+15 -10
View File
@@ -2,19 +2,27 @@ import { useQuery } from '@tanstack/react-query'
import { DEFAULT_QUERY_PARAMS } from 'components/interfaces/Reports/Reports.constants'
import {
BaseReportParams,
DbQueryData,
DbQueryHandler,
MetaQueryResponse,
ReportQuery,
} from 'components/interfaces/Reports/Reports.types'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { executeSql } from 'data/sql/execute-sql-query'
type UseDbQuery = (
export interface DbQueryHook<T = any> {
isLoading: boolean
error: string
data: T[]
params: BaseReportParams
logData?: never
runQuery: () => void
setParams?: never
changeQuery?: never
}
const useDbQuery = (
sql: ReportQuery['sql'],
params?: BaseReportParams
) => [DbQueryData, DbQueryHandler]
const useDbQuery: UseDbQuery = (sql, params = DEFAULT_QUERY_PARAMS) => {
params: BaseReportParams = DEFAULT_QUERY_PARAMS
): DbQueryHook => {
const { project } = useProjectContext()
const resolvedSql = typeof sql === 'function' ? sql([]) : sql
@@ -45,10 +53,7 @@ const useDbQuery: UseDbQuery = (sql, params = DEFAULT_QUERY_PARAMS) => {
)
const error = rqError || (typeof data === 'object' ? data?.error : '')
return [
{ error, data, isLoading: isLoading || isRefetching, params },
{ runQuery: () => refetch() },
]
return { error, data, isLoading: isLoading || isRefetching, params, runQuery: refetch }
}
export default useDbQuery
+20 -26
View File
@@ -15,14 +15,14 @@ import {
LogsTableName,
PREVIEWER_DATEPICKER_HELPERS,
} from 'components/interfaces/Settings/Logs'
import { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from 'react'
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'
import { API_URL } from 'lib/constants'
import { get } from 'lib/common/fetch'
import dayjs from 'dayjs'
import useFillTimeseriesSorted from './useFillTimeseriesSorted'
import useTimeseriesUnixToIso from './useTimeseriesUnixToIso'
interface Data {
interface LogsPreviewHook {
logData: LogData[]
error: string | Object | null
newCount: number
@@ -32,8 +32,6 @@ interface Data {
params: LogsEndpointParams
oldestTimestamp?: string
eventChartData: EventChartData[]
}
interface Handlers {
loadOlder: () => void
refresh: () => void
setFilters: (filters: Filters | ((previous: Filters) => Filters)) => void
@@ -43,7 +41,7 @@ function useLogsPreview(
projectRef: string,
table: LogsTableName,
filterOverride?: Filters
): [Data, Handlers] {
): LogsPreviewHook {
const defaultHelper = getDefaultHelper(PREVIEWER_DATEPICKER_HELPERS)
const [latestRefresh, setLatestRefresh] = useState<string>(new Date().toISOString())
@@ -168,7 +166,7 @@ function useLogsPreview(
const oldestTimestamp = logData[logData.length - 1]?.timestamp
const handleSetFilters: Handlers['setFilters'] = (newFilters) => {
const handleSetFilters: LogsPreviewHook['setFilters'] = (newFilters) => {
if (typeof newFilters === 'function') {
setFilters((prev) => {
const resolved = newFilters(prev)
@@ -189,29 +187,25 @@ function useLogsPreview(
'timestamp',
'count',
0,
params.iso_timestamp_start ,
params.iso_timestamp_start,
// default to current time if not set
params.iso_timestamp_end || new Date().toISOString()
)
return [
{
newCount,
logData,
isLoading: isLoading || isRefetching,
isLoadingOlder: isFetchingNextPage,
error,
filters,
params,
oldestTimestamp: oldestTimestamp ? String(oldestTimestamp) : undefined,
eventChartData,
},
{
setFilters: handleSetFilters,
refresh,
loadOlder: () => fetchNextPage(),
setParams,
},
]
return {
newCount,
logData,
isLoading: isLoading || isRefetching,
isLoadingOlder: isFetchingNextPage,
error,
filters,
params,
oldestTimestamp: oldestTimestamp ? String(oldestTimestamp) : undefined,
eventChartData,
setFilters: handleSetFilters,
refresh,
loadOlder: () => fetchNextPage(),
setParams,
}
}
export default useLogsPreview
+11 -13
View File
@@ -8,14 +8,12 @@ import { Dispatch, SetStateAction, useState } from 'react'
import { LogsEndpointParams, Logs, LogData } from 'components/interfaces/Settings/Logs/Logs.types'
import { API_URL } from 'lib/constants'
import { get } from 'lib/common/fetch'
export interface LogsQueryData {
export interface LogsQueryHook {
params: LogsEndpointParams
isLoading: boolean
logData: LogData[]
data?: never
error: string | Object | null
}
export interface LogsQueryHandlers {
changeQuery: (newQuery?: string) => void
runQuery: () => void
setParams: Dispatch<SetStateAction<LogsEndpointParams>>
@@ -24,7 +22,7 @@ export interface LogsQueryHandlers {
const useLogsQuery = (
projectRef: string,
initialParams: Partial<LogsEndpointParams> = {}
): [LogsQueryData, LogsQueryHandlers] => {
): LogsQueryHook => {
const defaultHelper = getDefaultHelper(EXPLORER_DATEPICKER_HELPERS)
const [params, setParams] = useState<LogsEndpointParams>({
sql: initialParams?.sql || '',
@@ -67,14 +65,14 @@ const useLogsQuery = (
setParams((prev) => ({ ...prev, sql: newQuery }))
}
return [
{
params,
isLoading: (enabled && isLoading) || isRefetching,
logData: data?.result ? data?.result : [],
error,
},
{ changeQuery, runQuery: () => refetch(), setParams },
]
return {
params,
isLoading: (enabled && isLoading) || isRefetching,
logData: data?.result ? data?.result : [],
error,
changeQuery,
runQuery: () => refetch(),
setParams,
}
}
export default useLogsQuery
+8 -14
View File
@@ -11,12 +11,10 @@ import { API_URL } from 'lib/constants'
import { get } from 'lib/common/fetch'
import { useQuery } from '@tanstack/react-query'
interface Data {
interface SingleLogHook {
logData: LogData | undefined
error: string | Object | null
isLoading: boolean
}
interface Handlers {
refresh: () => void
}
function useSingleLog(
@@ -24,7 +22,7 @@ function useSingleLog(
queryType?: QueryType,
paramsToMerge?: Partial<LogsEndpointParams>,
id?: string | null
): [Data, Handlers] {
): SingleLogHook {
const table = queryType ? LOGS_TABLES[queryType] : undefined
const sql = id && table ? genSingleLogQuery(table, id) : ''
const params: LogsEndpointParams = { ...paramsToMerge, project: projectRef, sql }
@@ -52,15 +50,11 @@ function useSingleLog(
)
let error: null | string | object = rcError ? (rcError as any).message : null
return [
{
logData: data?.result ? data.result[0] : undefined,
isLoading: (enabled && isLoading) || isRefetching,
error,
},
{
refresh: () => refetch(),
},
]
return {
logData: data?.result ? data.result[0] : undefined,
isLoading: (enabled && isLoading) || isRefetching,
error,
refresh: () => refetch(),
}
}
export default useSingleLog
+3 -1
View File
@@ -3,7 +3,7 @@ module.exports = {
moduleDirectories: ['<rootDir>', 'node_modules'],
setupFiles: ['jest-canvas-mock', './tests/setup/radix'],
testEnvironment: 'jsdom',
testTimeout: 20000,
testTimeout: 10000,
testRegex: '(.*\\.test.(js|jsx|ts|tsx)$)',
transform: { '^.+\\.(t|j)sx?$': 'ts-jest' },
globals: {
@@ -11,4 +11,6 @@ module.exports = {
tsconfig: 'tsconfig.test.json',
},
},
maxConcurrency: 3,
maxWorkers: '50%',
}
+6 -3
View File
@@ -176,10 +176,13 @@ const APIAuthorizationPage: NextPageWithLayout = () => {
<div className="rounded-md border border-scale-600 p-2.5 flex items-center">
<div
className="w-8 h-8 md:w-10 md:h-10 bg-center bg-no-repeat bg-cover flex items-center justify-center"
// [Joshen] For when we support icons
// style={{ backgroundImage: `url('${requester?.icon}')` }}
style={{
backgroundImage: requester.icon !== null ? `url('${requester?.icon}')` : 'none',
}}
>
<p className="text-scale-1000 text-lg">{requester?.name[0]}</p>
{requester?.icon === null && (
<p className="text-scale-1000 text-lg">{requester?.name[0]}</p>
)}
</div>
</div>
</div>
@@ -17,15 +17,17 @@ import AreaChart from 'components/ui/Charts/AreaChart'
import { isUnixMicro, unixMicroToIsoTimestamp } from 'components/interfaces/Settings/Logs'
import meanBy from 'lodash/meanBy'
import sumBy from 'lodash/sumBy'
import useFillTimeseriesSorted from 'hooks/analytics/useFillTimeseriesSorted'
const CHART_INTERVALS: ChartIntervals[] = [
{
key: '5min',
label: '5 min',
startValue: 1,
startUnit: 'hour',
startValue: 5,
startUnit: 'minute',
format: 'MMM D, h:mm:ssa',
},
{ key: '15min', label: '15 min', startValue: 15, startUnit: 'min' },
{ key: '15min', label: '15 min', startValue: 15, startUnit: 'minute' },
{ key: '1hr', label: '1 hour', startValue: 1, startUnit: 'hour' },
{ key: '1day', label: '1 day', startValue: 1, startUnit: 'day' },
{ key: '7day', label: '7 days', startValue: 7, startUnit: 'day' },
@@ -45,7 +47,7 @@ const PageLayout: NextPageWithLayout = () => {
interval: selectedInterval.key,
})
const isChartLoading = !data?.result && !error ? true : false
const chartData = useMemo(() => {
const normalizedData = useMemo(() => {
return (data?.result || []).map((d: any) => ({
...d,
timestamp: isUnixMicro(d.timestamp) ? unixMicroToIsoTimestamp(d.timestamp) : d.timestamp,
@@ -56,6 +58,16 @@ const PageLayout: NextPageWithLayout = () => {
selectedInterval.startValue,
selectedInterval.startUnit as dayjs.ManipulateType
)
const chartData = useFillTimeseriesSorted(
normalizedData,
'timestamp',
['avg_execution_time', 'count'],
0,
startDate.toISOString(),
dayjs().toISOString()
)
const canReadFunction = checkPermissions(PermissionAction.FUNCTIONS_READ, functionSlug as string)
if (!canReadFunction) {
return <NoPermission isFullPage resourceText="access this edge function" />
@@ -103,6 +115,7 @@ const PageLayout: NextPageWithLayout = () => {
<AreaChart
className="w-full"
xAxisKey="timestamp"
customDateFormat={selectedInterval.format}
yAxisKey="avg_execution_time"
data={props.data}
format="ms"
@@ -121,6 +134,7 @@ const PageLayout: NextPageWithLayout = () => {
yAxisKey="count"
data={props.data}
highlightedValue={sumBy(props.data, 'count')}
customDateFormat={selectedInterval.format}
onBarClick={(v) => {
router.push(
`/project/${projectRef}/functions/${functionSlug}/invocations?its=${startDate.toISOString()}&ite=${
@@ -43,7 +43,7 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
const [warnings, setWarnings] = useState<LogsWarning[]>([])
const { data: subscription } = useProjectSubscriptionQuery({ projectRef })
const tier = subscription?.tier
const [{ params, logData, error, isLoading }, { changeQuery, runQuery, setParams }] =
const { params, logData, error, isLoading, changeQuery, runQuery, setParams } =
useLogsQuery(projectRef as string, {
iso_timestamp_start: its ? (its as string) : undefined,
iso_timestamp_end: ite ? (ite as string) : undefined,
@@ -193,7 +193,11 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
</div>
</LoadingOpacity>
<div className="flex flex-row justify-end mt-2">
<UpgradePrompt show={showUpgradePrompt} setShowUpgradePrompt={setShowUpgradePrompt} />
<UpgradePrompt
show={showUpgradePrompt}
setShowUpgradePrompt={setShowUpgradePrompt}
subscription={subscription}
/>
</div>
</div>
</div>
@@ -141,55 +141,53 @@ const useApiReport = () => {
useEffect(() => {
// update sql for each query
if (totalRequests[1].changeQuery) {
totalRequests[1].changeQuery(PRESET_CONFIG.api.queries.totalRequests.sql(filters))
if (totalRequests.changeQuery) {
totalRequests.changeQuery(PRESET_CONFIG.api.queries.totalRequests.sql(filters))
}
if (topRoutes[1].changeQuery) {
topRoutes[1].changeQuery(PRESET_CONFIG.api.queries.topRoutes.sql(filters))
if (topRoutes.changeQuery) {
topRoutes.changeQuery(PRESET_CONFIG.api.queries.topRoutes.sql(filters))
}
if (errorCounts[1].changeQuery) {
errorCounts[1].changeQuery(PRESET_CONFIG.api.queries.errorCounts.sql(filters))
if (errorCounts.changeQuery) {
errorCounts.changeQuery(PRESET_CONFIG.api.queries.errorCounts.sql(filters))
}
if (topErrorRoutes[1].changeQuery) {
topErrorRoutes[1].changeQuery(PRESET_CONFIG.api.queries.topErrorRoutes.sql(filters))
if (topErrorRoutes.changeQuery) {
topErrorRoutes.changeQuery(PRESET_CONFIG.api.queries.topErrorRoutes.sql(filters))
}
if (responseSpeed[1].changeQuery) {
responseSpeed[1].changeQuery(PRESET_CONFIG.api.queries.responseSpeed.sql(filters))
if (responseSpeed.changeQuery) {
responseSpeed.changeQuery(PRESET_CONFIG.api.queries.responseSpeed.sql(filters))
}
if (topSlowRoutes[1].changeQuery) {
topSlowRoutes[1].changeQuery(PRESET_CONFIG.api.queries.topSlowRoutes.sql(filters))
if (topSlowRoutes.changeQuery) {
topSlowRoutes.changeQuery(PRESET_CONFIG.api.queries.topSlowRoutes.sql(filters))
}
}, [JSON.stringify(filters)])
const handleRefresh = async () => {
activeHooks.forEach(([_hookData, hookHandler]) => {
hookHandler.runQuery()
})
activeHooks.forEach(hook => hook.runQuery())
}
const handleSetParams = (params: Partial<LogsEndpointParams>) => {
activeHooks.forEach(([_hookData, hookHandler]) => {
hookHandler.setParams?.((prev: LogsEndpointParams) => ({ ...prev, ...params }))
activeHooks.forEach(hook => {
hook.setParams?.((prev: LogsEndpointParams) => ({ ...prev, ...params }))
})
}
const isLoading = activeHooks.some(([hookData]) => hookData.isLoading)
const isLoading = activeHooks.some(hook => hook.isLoading)
return {
data: {
totalRequests: totalRequests[0].logData,
errorCounts: errorCounts[0].logData,
responseSpeed: responseSpeed[0].logData,
topRoutes: topRoutes[0].logData,
topErrorRoutes: topErrorRoutes[0].logData,
topSlowRoutes: topSlowRoutes[0].logData,
totalRequests: totalRequests.logData,
errorCounts: errorCounts.logData,
responseSpeed: responseSpeed.logData,
topRoutes: topRoutes.logData,
topErrorRoutes: topErrorRoutes.logData,
topSlowRoutes: topSlowRoutes.logData,
},
params: {
totalRequests: totalRequests[0].params,
errorCounts: errorCounts[0].params,
responseSpeed: responseSpeed[0].params,
topRoutes: topRoutes[0].params,
topErrorRoutes: topErrorRoutes[0].params,
topSlowRoutes: topSlowRoutes[0].params,
totalRequests: totalRequests.params,
errorCounts: errorCounts.params,
responseSpeed: responseSpeed.params,
topRoutes: topRoutes.params,
topErrorRoutes: topErrorRoutes.params,
topSlowRoutes: topSlowRoutes.params,
},
mergeParams: handleSetParams,
filters,
@@ -31,17 +31,17 @@ const QueryPerformanceReport: NextPageWithLayout = () => {
const queryHitRate = hooks.queryHitRate()
const isLoading = [
mostFrequentlyInvoked[0].isLoading,
mostTimeConsuming[0].isLoading,
slowestExecutionTime[0].isLoading,
queryHitRate[0].isLoading,
mostFrequentlyInvoked.isLoading,
mostTimeConsuming.isLoading,
slowestExecutionTime.isLoading,
queryHitRate.isLoading,
].every((value) => value)
const handleRefresh = async () => {
mostFrequentlyInvoked[1].runQuery()
mostTimeConsuming[1].runQuery()
slowestExecutionTime[1].runQuery()
queryHitRate[1].runQuery()
mostFrequentlyInvoked.runQuery()
mostTimeConsuming.runQuery()
slowestExecutionTime.runQuery()
queryHitRate.runQuery()
}
const checkAlert = (
@@ -60,8 +60,8 @@ const QueryPerformanceReport: NextPageWithLayout = () => {
</div>
)
const indexHitRate = queryHitRate[0]?.data?.[0]?.ratio
const tableHitRate = queryHitRate[0]?.data?.[1]?.ratio
const indexHitRate = queryHitRate.data?.[0]?.ratio
const tableHitRate = queryHitRate.data?.[1]?.ratio
const showIndexWarning =
indexHitRate && tableHitRate && (indexHitRate <= 0.99 || tableHitRate <= 0.99)
@@ -130,7 +130,7 @@ const QueryPerformanceReport: NextPageWithLayout = () => {
: dangerAlert}
<div className="flex items-baseline">
<span className="text-3xl">
{(queryHitRate[0]?.data![0]?.ratio * 100).toFixed(2)}
{(queryHitRate?.data![0]?.ratio * 100).toFixed(2)}
</span>
<span className="text-xl">%</span>
</div>
@@ -138,7 +138,7 @@ const QueryPerformanceReport: NextPageWithLayout = () => {
</div>
<div className="w-1/2 bg-slate-200 rounded-md p-4">
{queryHitRate[0]?.data![1]?.name == 'table hit rate' && 'Table Hit Rate'}
{queryHitRate?.data![1]?.name == 'table hit rate' && 'Table Hit Rate'}
<div className="flex items-center gap-2">
{tableHitRate >= 0.99
? checkAlert
@@ -147,7 +147,7 @@ const QueryPerformanceReport: NextPageWithLayout = () => {
: dangerAlert}
<div className="flex items-baseline">
<span className="text-3xl">
{(queryHitRate[0]?.data![1]?.ratio * 100).toFixed(2)}
{(queryHitRate?.data![1]?.ratio * 100).toFixed(2)}
</span>
<span className="text-xl">%</span>
</div>
@@ -218,8 +218,8 @@ const QueryPerformanceReport: NextPageWithLayout = () => {
</>
}
body={
!isLoading && mostTimeConsuming && mostTimeConsuming[0]?.data ? (
mostTimeConsuming[0].data!.map((item, i) => {
!isLoading && mostTimeConsuming && mostTimeConsuming?.data ? (
mostTimeConsuming?.data!.map((item, i) => {
return (
<Table.tr key={i} hoverable className="relative">
<Table.td className="table-cell whitespace-nowrap w-36">
@@ -273,8 +273,8 @@ const QueryPerformanceReport: NextPageWithLayout = () => {
</>
}
body={
!isLoading && mostFrequentlyInvoked && mostFrequentlyInvoked[0]?.data ? (
mostFrequentlyInvoked[0].data!.map((item, i) => {
!isLoading && mostFrequentlyInvoked && mostFrequentlyInvoked?.data ? (
mostFrequentlyInvoked.data!.map((item, i) => {
return (
<Table.tr key={i} hoverable className="relative">
<Table.td className="table-cell whitespace-nowrap w-28">
@@ -336,8 +336,8 @@ const QueryPerformanceReport: NextPageWithLayout = () => {
</>
}
body={
!isLoading && slowestExecutionTime && slowestExecutionTime[0]?.data ? (
slowestExecutionTime[0].data!.map((item, i) => {
!isLoading && slowestExecutionTime && slowestExecutionTime?.data ? (
slowestExecutionTime.data!.map((item, i) => {
return (
<Table.tr key={i} hoverable className="relative">
<Table.td className="table-cell whitespace-nowrap w-24">
+29 -13
View File
@@ -37,21 +37,25 @@ export const sqlEditorState = proxy({
[key: string]: 'IDLE' | 'UPDATING' | 'UPDATING_FAILED'
},
orderSnippets: (snippets: SqlSnippet[]) => {
return (
snippets
.filter((s) => Boolean(s.id))
// first alphabetical
.sort((a, b) => a.name?.localeCompare(b.name))
)
},
reorderSnippets: (projectRef: string) => {
sqlEditorState.orders[projectRef] = sqlEditorState
.orderSnippets(
sqlEditorState.orders[projectRef].map((id) => sqlEditorState.snippets[id].snippet)
)
.map((s) => s.id!)
},
setRemoteSnippets: (snippets: SqlSnippet[], projectRef: string) => {
if (!sqlEditorState.orders[projectRef]) {
const orderedSnippets = snippets
.filter((s) => Boolean(s.id))
.sort((a, b) => {
// sort by snippet.inserted_at, falling back to snippet.id
if (a.inserted_at && b.inserted_at) {
const aDate = new Date(a.inserted_at)
const bDate = new Date(b.inserted_at)
// sort by date
return aDate > bDate ? -1 : 1
}
return a.id! > b.id! ? -1 : 1
})
const orderedSnippets = sqlEditorState.orderSnippets(snippets)
sqlEditorState.orders[projectRef] = orderedSnippets.map((s) => s.id!)
}
@@ -74,6 +78,7 @@ export const sqlEditorState = proxy({
!sqlEditorState.orders[projectRef].includes(snippet.id)
) {
sqlEditorState.orders[projectRef].unshift(snippet.id)
sqlEditorState.reorderSnippets(projectRef)
}
if (isNew) {
sqlEditorState.needsCreating.add(snippet.id)
@@ -116,6 +121,17 @@ export const sqlEditorState = proxy({
sqlEditorState.needsSaving.add(id)
}
},
renameSnippet: (id: string, name: string, description?: string) => {
if (sqlEditorState.snippets[id]) {
const { snippet, projectRef } = sqlEditorState.snippets[id]
snippet.name = name
snippet.description = description
sqlEditorState.reorderSnippets(projectRef)
sqlEditorState.needsSaving.add(id)
}
},
addNeedsSaving: (id: string) => {
sqlEditorState.needsSaving.add(id)
},
@@ -1,17 +1,8 @@
import { waitFor, screen } from '@testing-library/react'
import { render } from '../../helpers'
import { clickDropdown, render } from '../../helpers'
import userEvent from '@testing-library/user-event'
jest.mock('lib/common/fetch')
import { get } from 'lib/common/fetch'
// mock the router
jest.mock('next/router')
import { useRouter } from 'next/router'
import { clickDropdown } from 'tests/helpers'
const mockPush = jest.fn()
useRouter.mockReturnValue({ query: { ref: '123' }, push: mockPush })
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
// TODO: abstract out to global setup
const ProjectUsage = jest.fn()
@@ -40,49 +31,35 @@ ProjectUsage.mockImplementation((props) => {
)
})
jest.mock('components/ui/Flag/Flag')
import Flag from 'components/ui/Flag/Flag'
Flag.mockImplementation(({ children }) => <>{children}</>)
jest.mock('hooks')
import { useFlag } from 'hooks'
jest.mock('data/subscriptions/project-subscription-query')
import { useProjectSubscriptionQuery } from 'data/subscriptions/project-subscription-query'
useFlag.mockReturnValue(true)
useProjectSubscriptionQuery.mockReturnValue({
data: undefined,
})
beforeEach(() => {
get.mockReset()
mockPush.mockReset()
})
const MOCK_CHART_DATA = {
data: [
result: [
{
total_auth_requests: 123,
total_realtime_requests: 223,
total_storage_requests: 323,
total_rest_requests: 333,
timestamp: new Date().getTime() / 1000,
timestamp: new Date().toISOString(),
},
],
total: 123 + 223 + 323,
totalAverage: 123 + 223 + 323,
totalGrouped: {
total_auth_requests: 123,
total_realtime_requests: 223,
total_storage_requests: 323,
total_rest_requests: 333,
},
]
}
test('mounts correctly', async () => {
get.mockImplementation((url) => {
if (url.includes('usage')) return {}
if (url.includes('subscription')) return {}
return { data: MOCK_CHART_DATA }
if (decodeURIComponent(url).includes('usage.api-counts')) {
return MOCK_CHART_DATA
}
return {}
})
render(<ProjectUsage project="12345" />)
await waitFor(() => screen.getByText(/Statistics for past 24 hours/))
@@ -94,8 +71,10 @@ test('mounts correctly', async () => {
test('dropdown options changes chart query', async () => {
get.mockImplementation((url) => {
if (url.includes('usage')) return {}
return { data: MOCK_CHART_DATA }
if (decodeURIComponent(url).includes('usage.api-counts')) {
return MOCK_CHART_DATA
}
return {}
})
render(<ProjectUsage project="12345" />)
await waitFor(() => screen.getByText(/Statistics for past 24 hours/))
@@ -68,6 +68,18 @@ test('can display standard preview table columns', async () => {
await expect(screen.findByText(fakeMicroTimestamp)).rejects.toThrow()
})
test("closes the selection if the selected row's data changes", async () => {
const { rerender } = render(
<LogTable queryType="auth" data={[{ id: '12345', event_message: 'some event message' }]} />
)
const text = await screen.findByText(/some event message/)
userEvent.click(text)
await screen.findByText('Copy')
rerender(<LogTable data={[{ id: '12346', event_message: 'some other message' }]} />)
await expect(screen.findByText(/some event message/)).rejects.toThrow()
await screen.findByText(/some other message/)
})
test.each([
{
queryType: 'functions',
@@ -144,6 +144,19 @@ test.each([
{ timestamp: '2023-04-26T20:00:00.000Z', count: 0 },
],
},
// fill multiple keys in one go
{
case: 'fill beyond min/max',
data: [],
len: 2,
min: '2023-04-26T18:00:00.000Z',
max: '2023-04-26T19:00:00.000Z',
valKey: ['count', 'other'],
includes: [
{ timestamp: '2023-04-26T18:00:00.000Z', count: 0, other: 0 },
{ timestamp: '2023-04-26T19:00:00.000Z', count: 0, other: 0 },
],
},
])(
'fillTimeseries : $case',
({ data, len, includes, min, max, tsKey = 'timestamp', valKey = 'count', defaultVal = 0 }) => {
@@ -131,6 +131,7 @@ test('custom sql querying', async () => {
expect(get).toHaveBeenCalledWith(expect.stringContaining('sql='), expect.anything())
expect(get).toHaveBeenCalledWith(expect.stringContaining('select'), expect.anything())
expect(get).toHaveBeenCalledWith(expect.stringContaining('edge_logs'), expect.anything())
expect(get).toHaveBeenCalledWith(expect.stringContaining(encodeURIComponent("my_count")), expect.anything())
expect(get).toHaveBeenCalledWith(
expect.stringContaining('iso_timestamp_start'),
expect.anything()
@@ -159,6 +160,40 @@ test('custom sql querying', async () => {
await expect(screen.findByText(/Load older/)).rejects.toThrow()
})
test("bug: can edit query after selecting a log", async ()=>{
get.mockImplementation((url) => {
if (url.includes('sql=') && url.includes('select') && !url.includes("limit 222")) {
return {
result: [ { my_count: 12345 }],
}
}
return { result: [] }
})
const { container } = render(<LogsExplorerPage />)
// run default query
userEvent.click(await screen.findByText('Run'))
const rowValue = await screen.findByText(/12345/) // row value
// open up an show selection panel
await userEvent.click(rowValue)
await screen.findByText(/Copy/)
// change the query
let editor = container.querySelector('.monaco-editor')
// type new query
userEvent.click(editor)
userEvent.type(editor, ' something')
userEvent.click(await screen.findByText('Run'))
await waitFor(
() => {
expect(get).toHaveBeenCalledWith(expect.stringContaining(encodeURIComponent("something")), expect.anything())
},
{ timeout: 1000 }
)
// closes the selection panel
await expect(screen.findByText(/Copy/)).rejects.toThrow()
})
test('query warnings', async () => {
const router = defaultRouterMock()
router.query = {
+2 -2
View File
@@ -19,8 +19,8 @@ export interface ChartIntervals {
key: 'minutely' | 'hourly' | 'daily' | '5min' | '15min' | '1hr' | '1day' | '7day'
label: string
startValue: number
startUnit: 'min' | 'hour' | 'day'
format?: 'MMM D, h:mma' | 'MMM D, ha' | 'MMM D'
startUnit: 'minute' | 'hour' | 'day'
format?: 'MMM D, h:mm:ssa' | 'MMM D, h:mma' | 'MMM D, ha' | 'MMM D'
}
export interface VaultSecret {