No files matched your search
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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}}',
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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: () => (
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
Before Width: | Height: | Size: 447 KiB After Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 244 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 618 KiB After Width: | Height: | Size: 176 KiB |
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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}
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export const logKeys = {
|
||||
logStats: (projectRef: string | undefined, interval: string | undefined) =>
|
||||
['projects', projectRef, 'log-stats', interval] as const,
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,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%',
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 {
|
||||
|
||||