From f52eb3512b086091e013a72b87a665660f669c14 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Wed, 2 Nov 2022 14:25:09 +1100 Subject: [PATCH 01/75] feat: adds graphiql page --- package-lock.json | 1326 +++++++++++++----- studio/package.json | 2 + studio/pages/project/[ref]/graphql/index.tsx | 34 + 3 files changed, 1017 insertions(+), 345 deletions(-) create mode 100644 studio/pages/project/[ref]/graphql/index.tsx diff --git a/package-lock.json b/package-lock.json index faa6b32827d..c676116c2ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,7 +55,7 @@ "jsrsasign": "^10.5.26", "lodash": "^4.17.21", "markdown-toc": "^1.2.0", - "next": "12.0.9", + "next": "12.1.0", "next-compose-plugins": "^2.2.1", "next-mdx-remote": "^3.0.8", "next-themes": "0.0.15", @@ -177,176 +177,6 @@ "react": "^16.13.1 || ^17.0.0" } }, - "apps/temp-docs/node_modules/@next/env": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/@next/env/-/env-12.0.9.tgz", - "integrity": "sha512-oBlkyDop0Stf7MPIzETGv5r0YT/G/weBrknoPOUTaa5qwOeGjuy6gsOVc/SBtrBkOoBmRpD+fFhQJPvmo1mS+g==" - }, - "apps/temp-docs/node_modules/@next/swc-android-arm64": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.0.9.tgz", - "integrity": "sha512-aVqgsEn5plmUH2X58sjzhHsH/6majucWTMaaBEs7hHO2+GCwCZc7zaLH4XCBMKPES9Yaja8/pYUbvZQE9DqgFw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "apps/temp-docs/node_modules/@next/swc-darwin-arm64": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.0.9.tgz", - "integrity": "sha512-uAgRKm4a2nVdyBiPPJokvmDD1saugOvxljz9ld2ih0CCg5S9vBhqaj3kPGCQBj9hSu3q+Lng2CHnQqG3ga1jzA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "apps/temp-docs/node_modules/@next/swc-darwin-x64": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.0.9.tgz", - "integrity": "sha512-fDOs2lZIyrAdU18IxMA5orBPn9qLbOdu55gXSTNZOhyRJ8ugtbUAejsK7OL0boJy0CCHPAdVRXm01Mwk8tZ9RQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "apps/temp-docs/node_modules/@next/swc-linux-arm-gnueabihf": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.0.9.tgz", - "integrity": "sha512-/ni0p9DBvATUML9RQ1ycQuf05uOYKdzA6iI8+eRsARjpGbFVUFbge7XPzlj9g2Q9YWgoN8CSjFGnKRlyky5uHA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "apps/temp-docs/node_modules/@next/swc-linux-arm64-gnu": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.0.9.tgz", - "integrity": "sha512-AphxilJDf95rUxJDHgM9Ww1DaYXZWqTvoKwXeej/0SgSvICcRZrLaFDrkojdXz0Rxr4igX2OdYR1S4/Hj1jWOQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "apps/temp-docs/node_modules/@next/swc-linux-arm64-musl": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.0.9.tgz", - "integrity": "sha512-K5jbvNNzF3mRjWmPdxP5Bg87i7FHivfBj/L0KJlxpkLSC8sffBJDmB6jtMnI7wiPj9J6vmLkbGtSosln78xAlQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "apps/temp-docs/node_modules/@next/swc-linux-x64-gnu": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.0.9.tgz", - "integrity": "sha512-bJZ9bkMkQzsY+UyWezEZ77GWQ4TzwKeXdayX3U3+aEkL8k5C6eKBXlidWdrhu0teLmaUXIyWerWrLnJzwGXdfw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "apps/temp-docs/node_modules/@next/swc-linux-x64-musl": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.0.9.tgz", - "integrity": "sha512-SR9p0R+v1T32DTXPVAXZw31pmJAkSDotC6Afy+mfC0xrEL3pp95R8sGXYAAUCEPkQp0MEeUOVy2LrToe92X7hQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "apps/temp-docs/node_modules/@next/swc-win32-arm64-msvc": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.0.9.tgz", - "integrity": "sha512-mzQ1A8vfHhJrvEy5KJZGZWEByXthyKfWofvFaf+oo/5nJl/0Bz1ODP2ajSmbLG++77Eo2AROgbm9pkW1ucvG2A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "apps/temp-docs/node_modules/@next/swc-win32-ia32-msvc": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.0.9.tgz", - "integrity": "sha512-MpD2vj1zjo1u3J3wiz3pEKse19Etz+P0GL6XfQkB/9a84vJQ1JWMaWBjmIdivzZv718Il2pRSSx8hymwPfguYQ==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "apps/temp-docs/node_modules/@next/swc-win32-x64-msvc": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.0.9.tgz", - "integrity": "sha512-1c/sxp/4Qz4F6rCxiYqAnrmghCOFt5hHZ9Kd+rXFW5Mqev4C4XDOUMHdBH55HgnJZqngYhOE0r/XNkCtsIojig==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "apps/temp-docs/node_modules/@radix-ui/primitive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", @@ -812,55 +642,6 @@ "dev": true, "license": "MIT" }, - "apps/temp-docs/node_modules/next": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/next/-/next-12.0.9.tgz", - "integrity": "sha512-omfYqoR/DvbdOIJ6SS1unKJ4mGIxUPs0RPa7wr/Mft22OCKgJhuG+aI9KFYi5ZJBwoFQk1vqaMKpWz5qr+dN0Q==", - "dependencies": { - "@next/env": "12.0.9", - "caniuse-lite": "^1.0.30001283", - "postcss": "8.4.5", - "styled-jsx": "5.0.0", - "use-subscription": "1.5.1" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": ">=12.22.0" - }, - "optionalDependencies": { - "@next/swc-android-arm64": "12.0.9", - "@next/swc-darwin-arm64": "12.0.9", - "@next/swc-darwin-x64": "12.0.9", - "@next/swc-linux-arm-gnueabihf": "12.0.9", - "@next/swc-linux-arm64-gnu": "12.0.9", - "@next/swc-linux-arm64-musl": "12.0.9", - "@next/swc-linux-x64-gnu": "12.0.9", - "@next/swc-linux-x64-musl": "12.0.9", - "@next/swc-win32-arm64-msvc": "12.0.9", - "@next/swc-win32-ia32-msvc": "12.0.9", - "@next/swc-win32-x64-msvc": "12.0.9" - }, - "peerDependencies": { - "fibers": ">= 3.1.0", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0", - "react": "^17.0.2 || ^18.0.0-0", - "react-dom": "^17.0.2 || ^18.0.0-0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "fibers": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, "apps/temp-docs/node_modules/next-mdx-remote": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/next-mdx-remote/-/next-mdx-remote-3.0.8.tgz", @@ -897,23 +678,6 @@ "node": ">= 0.8.0" } }, - "apps/temp-docs/node_modules/postcss": { - "version": "8.4.5", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz", - "integrity": "sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==", - "dependencies": { - "nanoid": "^3.1.30", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, "apps/temp-docs/node_modules/supports-color": { "version": "7.2.0", "dev": true, @@ -3574,6 +3338,37 @@ "node": ">=0.1.95" } }, + "node_modules/@codemirror/language": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.0.0.tgz", + "integrity": "sha512-rtjk5ifyMzOna1c7PBu7J1VCt0PvA5wy3o8eMVnxMKb7z8KA7JFecvD04dSn14vj/bBaAbqRsGed5OjtofEnLA==", + "peer": true, + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/state": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.1.2.tgz", + "integrity": "sha512-Mxff85Hp5va+zuj+H748KbubXjrinX/k28lj43H14T2D0+4kuvEFIEIO7hCEcvBT8ubZyIelt9yGOjj2MWOEQA==", + "peer": true + }, + "node_modules/@codemirror/view": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.4.0.tgz", + "integrity": "sha512-Kv32b6Tn7QVwFbj/EDswTLSocjk5kgggF6zzBFAL4o4hZ/vmtFD155+EjH1pVlbfoDyVC2M6SedPsMrwYscgNg==", + "peer": true, + "dependencies": { + "@codemirror/state": "^6.0.0", + "style-mod": "^4.0.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -3841,6 +3636,66 @@ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "dev": true }, + "node_modules/@graphiql/react": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/@graphiql/react/-/react-0.13.7.tgz", + "integrity": "sha512-mI+6mXd9AJFDJy39VuZGxCiVLvmBe2XoNidbN3lXgTLe689RZKifPaUw41USitQOu20sASoYDfzJ8i98G4urow==", + "dependencies": { + "@graphiql/toolkit": "^0.8.0", + "@reach/combobox": "^0.17.0", + "@reach/dialog": "^0.17.0", + "@reach/listbox": "^0.17.0", + "@reach/menu-button": "^0.17.0", + "@reach/tooltip": "^0.17.0", + "@reach/visually-hidden": "^0.17.0", + "codemirror": "^5.65.3", + "codemirror-graphql": "^2.0.2", + "copy-to-clipboard": "^3.2.0", + "graphql-language-service": "^5.1.0", + "markdown-it": "^12.2.0", + "set-value": "^4.1.0" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@graphiql/react/node_modules/set-value": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-4.1.0.tgz", + "integrity": "sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==", + "funding": [ + "https://github.com/sponsors/jonschlinkert", + "https://paypal.me/jonathanschlinkert", + "https://jonschlinkert.dev/sponsor" + ], + "dependencies": { + "is-plain-object": "^2.0.4", + "is-primitive": "^3.0.1" + }, + "engines": { + "node": ">=11.0" + } + }, + "node_modules/@graphiql/toolkit": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@graphiql/toolkit/-/toolkit-0.8.0.tgz", + "integrity": "sha512-DbMFhEKejpPzB6k8W3Mj+Rl8geXiw49USDF9Wdi06EEk1XLVh1iebDqveYY+4lViITsV4+BeGikxlqi8umfP4g==", + "dependencies": { + "@n1ru4l/push-pull-async-iterable-iterator": "^3.1.0", + "meros": "^1.1.4" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0", + "graphql-ws": ">= 4.5.0" + }, + "peerDependenciesMeta": { + "graphql-ws": { + "optional": true + } + } + }, "node_modules/@graphql-typed-document-node/core": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.1.tgz", @@ -5474,6 +5329,30 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "node_modules/@lezer/common": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.1.tgz", + "integrity": "sha512-8TR5++Q/F//tpDsLd5zkrvEX5xxeemafEaek7mUp7Y+bI8cKQXdSqhzTOBaOogETcMOVr0pT3BBPXp13477ciw==", + "peer": true + }, + "node_modules/@lezer/highlight": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.2.tgz", + "integrity": "sha512-CAun1WR1glxG9ZdOokTZwXbcwB7PXkIEyZRUMFBVwSrhTcogWq634/ByNImrkUnQhjju6xsIaOBIxvcRJtplXQ==", + "peer": true, + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.3.tgz", + "integrity": "sha512-qpB7rBzH8f6Mzjv2AVZRahcm+2Cf7nbIH++uXbvVOL1yIRvVWQ3HAM/saeBLCyz/togB7LGo76qdJYL1uKQlqA==", + "peer": true, + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, "node_modules/@mdx-js/loader": { "version": "1.6.22", "resolved": "https://registry.npmjs.org/@mdx-js/loader/-/loader-1.6.22.tgz", @@ -5877,6 +5756,14 @@ "integrity": "sha512-Iozmtbqv0noj0uDDqoL0zNq0VBEfK2YFoMAZoxJe4cwphvLR+JskfF30QhXHOR4m3KrE6NLRYw+U9MRXvifyig==", "dev": true }, + "node_modules/@n1ru4l/push-pull-async-iterable-iterator": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@n1ru4l/push-pull-async-iterable-iterator/-/push-pull-async-iterable-iterator-3.2.0.tgz", + "integrity": "sha512-3fkKj25kEjsfObL6IlKPAlHYPq/oYwUkkQ03zsTTiDjD7vg/RxjdiLeCydqtxHZP0JgsXL3D/X5oAkMGzuUp/Q==", + "engines": { + "node": ">=12" + } + }, "node_modules/@next/bundle-analyzer": { "version": "12.3.1", "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-12.3.1.tgz", @@ -6781,6 +6668,229 @@ "@babel/runtime": "^7.13.10" } }, + "node_modules/@reach/auto-id": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/auto-id/-/auto-id-0.17.0.tgz", + "integrity": "sha512-ud8iPwF52RVzEmkHq1twuqGuPA+moreumUHdtgvU3sr3/15BNhwp3KyDLrKKSz0LP1r3V4pSdyF9MbYM8BoSjA==", + "dependencies": { + "@reach/utils": "0.17.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "react": "^16.8.0 || 17.x", + "react-dom": "^16.8.0 || 17.x" + } + }, + "node_modules/@reach/combobox": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/combobox/-/combobox-0.17.0.tgz", + "integrity": "sha512-2mYvU5agOBCQBMdlM4cri+P1BbNwp05P1OuDyc33xJSNiBG7BMy4+ZSHJ0X4fyle6rHwSgCAOCLOeWV1XUYjoQ==", + "dependencies": { + "@reach/auto-id": "0.17.0", + "@reach/descendants": "0.17.0", + "@reach/popover": "0.17.0", + "@reach/portal": "0.17.0", + "@reach/utils": "0.17.0", + "prop-types": "^15.7.2", + "tiny-warning": "^1.0.3", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "react": "^16.8.0 || 17.x", + "react-dom": "^16.8.0 || 17.x" + } + }, + "node_modules/@reach/descendants": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/descendants/-/descendants-0.17.0.tgz", + "integrity": "sha512-c7lUaBfjgcmKFZiAWqhG+VnXDMEhPkI4kAav/82XKZD6NVvFjsQOTH+v3tUkskrAPV44Yuch0mFW/u5Ntifr7Q==", + "dependencies": { + "@reach/utils": "0.17.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "react": "^16.8.0 || 17.x", + "react-dom": "^16.8.0 || 17.x" + } + }, + "node_modules/@reach/dialog": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/dialog/-/dialog-0.17.0.tgz", + "integrity": "sha512-AnfKXugqDTGbeG3c8xDcrQDE4h9b/vnc27Sa118oQSquz52fneUeX9MeFb5ZEiBJK8T5NJpv7QUTBIKnFCAH5A==", + "dependencies": { + "@reach/portal": "0.17.0", + "@reach/utils": "0.17.0", + "prop-types": "^15.7.2", + "react-focus-lock": "^2.5.2", + "react-remove-scroll": "^2.4.3", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "react": "^16.8.0 || 17.x", + "react-dom": "^16.8.0 || 17.x" + } + }, + "node_modules/@reach/dropdown": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/dropdown/-/dropdown-0.17.0.tgz", + "integrity": "sha512-qBTIGInhxtPHtdj4Pl2XZgZMz3e37liydh0xR3qc48syu7g71sL4nqyKjOzThykyfhA3Pb3/wFgsFJKGTSdaig==", + "dependencies": { + "@reach/auto-id": "0.17.0", + "@reach/descendants": "0.17.0", + "@reach/popover": "0.17.0", + "@reach/utils": "0.17.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "react": "^16.8.0 || 17.x", + "react-dom": "^16.8.0 || 17.x" + } + }, + "node_modules/@reach/listbox": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/listbox/-/listbox-0.17.0.tgz", + "integrity": "sha512-AMnH1P6/3VKy2V/nPb4Es441arYR+t4YRdh9jdcFVrCOD6y7CQrlmxsYjeg9Ocdz08XpdoEBHM3PKLJqNAUr7A==", + "dependencies": { + "@reach/auto-id": "0.17.0", + "@reach/descendants": "0.17.0", + "@reach/machine": "0.17.0", + "@reach/popover": "0.17.0", + "@reach/utils": "0.17.0", + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": "^16.8.0 || 17.x", + "react-dom": "^16.8.0 || 17.x" + } + }, + "node_modules/@reach/machine": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/machine/-/machine-0.17.0.tgz", + "integrity": "sha512-9EHnuPgXzkbRENvRUzJvVvYt+C2jp7PGN0xon7ffmKoK8rTO6eA/bb7P0xgloyDDQtu88TBUXKzW0uASqhTXGA==", + "dependencies": { + "@reach/utils": "0.17.0", + "@xstate/fsm": "1.4.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "react": "^16.8.0 || 17.x", + "react-dom": "^16.8.0 || 17.x" + } + }, + "node_modules/@reach/menu-button": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/menu-button/-/menu-button-0.17.0.tgz", + "integrity": "sha512-YyuYVyMZKamPtivoEI6D0UEILYH3qZtg4kJzEAuzPmoR/aHN66NZO75Fx0gtjG1S6fZfbiARaCOZJC0VEiDOtQ==", + "dependencies": { + "@reach/dropdown": "0.17.0", + "@reach/popover": "0.17.0", + "@reach/utils": "0.17.0", + "prop-types": "^15.7.2", + "tiny-warning": "^1.0.3", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "react": "^16.8.0 || 17.x", + "react-dom": "^16.8.0 || 17.x", + "react-is": "^16.8.0 || 17.x" + } + }, + "node_modules/@reach/observe-rect": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@reach/observe-rect/-/observe-rect-1.2.0.tgz", + "integrity": "sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==" + }, + "node_modules/@reach/popover": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/popover/-/popover-0.17.0.tgz", + "integrity": "sha512-yYbBF4fMz4Ml4LB3agobZjcZ/oPtPsNv70ZAd7lEC2h7cvhF453pA+zOBGYTPGupKaeBvgAnrMjj7RnxDU5hoQ==", + "dependencies": { + "@reach/portal": "0.17.0", + "@reach/rect": "0.17.0", + "@reach/utils": "0.17.0", + "tabbable": "^4.0.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "react": "^16.8.0 || 17.x", + "react-dom": "^16.8.0 || 17.x" + } + }, + "node_modules/@reach/portal": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/portal/-/portal-0.17.0.tgz", + "integrity": "sha512-+IxsgVycOj+WOeNPL2NdgooUdHPSY285wCtj/iWID6akyr4FgGUK7sMhRM9aGFyrGpx2vzr+eggbUmAVZwOz+A==", + "dependencies": { + "@reach/utils": "0.17.0", + "tiny-warning": "^1.0.3", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "react": "^16.8.0 || 17.x", + "react-dom": "^16.8.0 || 17.x" + } + }, + "node_modules/@reach/rect": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/rect/-/rect-0.17.0.tgz", + "integrity": "sha512-3YB7KA5cLjbLc20bmPkJ06DIfXSK06Cb5BbD2dHgKXjUkT9WjZaLYIbYCO8dVjwcyO3GCNfOmPxy62VsPmZwYA==", + "dependencies": { + "@reach/observe-rect": "1.2.0", + "@reach/utils": "0.17.0", + "prop-types": "^15.7.2", + "tiny-warning": "^1.0.3", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "react": "^16.8.0 || 17.x", + "react-dom": "^16.8.0 || 17.x" + } + }, + "node_modules/@reach/tooltip": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/tooltip/-/tooltip-0.17.0.tgz", + "integrity": "sha512-HP8Blordzqb/Cxg+jnhGmWQfKgypamcYLBPlcx6jconyV5iLJ5m93qipr1giK7MqKT2wlsKWy44ZcOrJ+Wrf8w==", + "dependencies": { + "@reach/auto-id": "0.17.0", + "@reach/portal": "0.17.0", + "@reach/rect": "0.17.0", + "@reach/utils": "0.17.0", + "@reach/visually-hidden": "0.17.0", + "prop-types": "^15.7.2", + "tiny-warning": "^1.0.3", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "react": "^16.8.0 || 17.x", + "react-dom": "^16.8.0 || 17.x" + } + }, + "node_modules/@reach/utils": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.17.0.tgz", + "integrity": "sha512-M5y8fCBbrWeIsxedgcSw6oDlAMQDkl5uv3VnMVJ7guwpf4E48Xlh1v66z/1BgN/WYe2y8mB/ilFD2nysEfdGeA==", + "dependencies": { + "tiny-warning": "^1.0.3", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "react": "^16.8.0 || 17.x", + "react-dom": "^16.8.0 || 17.x" + } + }, + "node_modules/@reach/visually-hidden": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/visually-hidden/-/visually-hidden-0.17.0.tgz", + "integrity": "sha512-T6xF3Nv8vVnjVkGU6cm0+kWtvliLqPAo8PcZ+WxkKacZsaHTjaZb4v1PaCcyQHmuTNT/vtTVNOJLG0SjQOIb7g==", + "dependencies": { + "prop-types": "^15.7.2", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "react": "^16.8.0 || 17.x", + "react-dom": "^16.8.0 || 17.x" + } + }, "node_modules/@react-dnd/asap": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.1.tgz", @@ -10925,6 +11035,11 @@ "node": ">=8" } }, + "node_modules/@xstate/fsm": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@xstate/fsm/-/fsm-1.4.0.tgz", + "integrity": "sha512-uTHDeu2xI5E1IFwf37JFQM31RrH7mY7877RqPBS4ZqSNUwoLDuct8AhBWaXGnVizBAYyimVwgCyGa9z/NiRhXA==" + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -13461,6 +13576,39 @@ "node": ">=0.10.0" } }, + "node_modules/codemirror": { + "version": "5.65.9", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.9.tgz", + "integrity": "sha512-19Jox5sAKpusTDgqgKB5dawPpQcY+ipQK7xoEI+MVucEF9qqFaXpeqY1KaoyGBso/wHQoDa4HMMxMjdsS3Zzzw==" + }, + "node_modules/codemirror-graphql": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-2.0.2.tgz", + "integrity": "sha512-9c1cItR+8lG7thmTnDDQ3zI8YesNKiFCp2BnLFkYWCtdhSSuCUHebU/Vurew6ayyUl8MBCldNx3Ev66QAWM5Kw==", + "dependencies": { + "graphql-language-service": "5.0.6" + }, + "peerDependencies": { + "@codemirror/language": "6.0.0", + "codemirror": "^5.65.3", + "graphql": "^15.5.0 || ^16.0.0" + } + }, + "node_modules/codemirror-graphql/node_modules/graphql-language-service": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/graphql-language-service/-/graphql-language-service-5.0.6.tgz", + "integrity": "sha512-FjE23aTy45Lr5metxCv3ZgSKEZOzN7ERR+OFC1isV5mHxI0Ob8XxayLTYjQKrs8b3kOpvgTYmSmu6AyXOzYslg==", + "dependencies": { + "nullthrows": "^1.0.0", + "vscode-languageserver-types": "^3.15.1" + }, + "bin": { + "graphql": "dist/temp-bin.js" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0" + } + }, "node_modules/coffee-script": { "version": "1.12.7", "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", @@ -15426,7 +15574,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } @@ -18006,6 +18153,23 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "node_modules/graphiql": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/graphiql/-/graphiql-2.0.13.tgz", + "integrity": "sha512-hfqcd40aO2V44Z/rey6lGXYnEsbwseeokeeZGQbFfSImciGtyP0SS8MyE5s5yno6z/M0ymJccpaNaUlC6imSwA==", + "dependencies": { + "@graphiql/react": "^0.13.7", + "@graphiql/toolkit": "^0.8.0", + "entities": "^2.0.0", + "graphql-language-service": "^5.1.0", + "markdown-it": "^12.2.0" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/graphql": { "version": "16.6.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz", @@ -18014,6 +18178,21 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/graphql-language-service": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/graphql-language-service/-/graphql-language-service-5.1.0.tgz", + "integrity": "sha512-APffigZ/l2me6soek+Yq5Us3HBwmfw4vns4QoqsTePXkK3knVO8rn0uAC6PmTyglb1pmFFPbYaRIzW4wmcnnGQ==", + "dependencies": { + "nullthrows": "^1.0.0", + "vscode-languageserver-types": "^3.17.1" + }, + "bin": { + "graphql": "dist/temp-bin.js" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0" + } + }, "node_modules/graphql-tag": { "version": "2.12.6", "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", @@ -19668,6 +19847,14 @@ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" }, + "node_modules/is-primitive": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz", + "integrity": "sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-reference": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.0.tgz", @@ -23473,6 +23660,14 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, "node_modules/list-item": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/list-item/-/list-item-1.1.1.tgz", @@ -23845,6 +24040,34 @@ "node": ">=0.10.0" } }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/markdown-link": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/markdown-link/-/markdown-link-0.1.1.tgz", @@ -24598,6 +24821,22 @@ "node": ">= 8" } }, + "node_modules/meros": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/meros/-/meros-1.2.1.tgz", + "integrity": "sha512-R2f/jxYqCAGI19KhAvaxSOxALBMkaXWH2a7rOyqQw+ZmizX5bKkEYWLzdhC+U82ZVVPVp6MCXe3EkVligh+12g==", + "engines": { + "node": ">=13" + }, + "peerDependencies": { + "@types/node": ">=13" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -26155,6 +26394,11 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/nullthrows": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==" + }, "node_modules/num2fraction": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", @@ -29240,6 +29484,17 @@ "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-clientside-effect": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz", + "integrity": "sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==", + "dependencies": { + "@babel/runtime": "^7.12.13" + }, + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-contexify": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/react-contexify/-/react-contexify-5.0.0.tgz", @@ -29465,6 +29720,39 @@ "react": ">=16.8.6" } }, + "node_modules/react-focus-lock": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.9.1.tgz", + "integrity": "sha512-pSWOQrUmiKLkffPO6BpMXN7SNKXMsuOakl652IBuALAu1esk+IcpJyM+ALcYzPTTFz1rD0R54aB9A4HuP5t1Wg==", + "dependencies": { + "@babel/runtime": "^7.0.0", + "focus-lock": "^0.11.2", + "prop-types": "^15.6.2", + "react-clientside-effect": "^1.2.6", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-focus-lock/node_modules/focus-lock": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-0.11.3.tgz", + "integrity": "sha512-4n0pYcPTa/uI7Q66BZna61nRT7lDhnuJ9PJr6wiDjx4uStg491ks41y7uOG+s0umaaa+hulNKSldU9aTg9/yVg==", + "dependencies": { + "tslib": "^2.0.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/react-from-dom": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/react-from-dom/-/react-from-dom-0.6.2.tgz", @@ -32919,6 +33207,12 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/style-mod": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz", + "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==", + "peer": true + }, "node_modules/style-to-object": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", @@ -33077,6 +33371,11 @@ "integrity": "sha512-qImOD23aDfnIDNqlG1NOehdB9IYsn1V9oByPjKY1nakv2MQYCEMyX033/q+aEtYCpmYK1cv2+NTmlH+ra6GA5A==", "dev": true }, + "node_modules/tabbable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-4.0.0.tgz", + "integrity": "sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ==" + }, "node_modules/table": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz", @@ -34342,6 +34641,11 @@ "node": ">=4.2.0" } }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, "node_modules/uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", @@ -35255,6 +35559,11 @@ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", "devOptional": true }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.2", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz", + "integrity": "sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA==" + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -35264,6 +35573,12 @@ "browser-process-hrtime": "^1.0.0" } }, + "node_modules/w3c-keyname": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz", + "integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==", + "peer": true + }, "node_modules/w3c-xmlserializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", @@ -36830,6 +37145,8 @@ "dayjs": "^1.11.0", "file-saver": "^2.0.5", "generate-password": "^1.7.0", + "graphiql": "^2.0.13", + "graphql": "^16.6.0", "html-to-image": "^1.10.8", "immutability-helper": "^3.1.1", "json-logic-js": "^2.0.2", @@ -39084,6 +39401,37 @@ "minimist": "^1.2.0" } }, + "@codemirror/language": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.0.0.tgz", + "integrity": "sha512-rtjk5ifyMzOna1c7PBu7J1VCt0PvA5wy3o8eMVnxMKb7z8KA7JFecvD04dSn14vj/bBaAbqRsGed5OjtofEnLA==", + "peer": true, + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "@codemirror/state": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.1.2.tgz", + "integrity": "sha512-Mxff85Hp5va+zuj+H748KbubXjrinX/k28lj43H14T2D0+4kuvEFIEIO7hCEcvBT8ubZyIelt9yGOjj2MWOEQA==", + "peer": true + }, + "@codemirror/view": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.4.0.tgz", + "integrity": "sha512-Kv32b6Tn7QVwFbj/EDswTLSocjk5kgggF6zzBFAL4o4hZ/vmtFD155+EjH1pVlbfoDyVC2M6SedPsMrwYscgNg==", + "peer": true, + "requires": { + "@codemirror/state": "^6.0.0", + "style-mod": "^4.0.0", + "w3c-keyname": "^2.2.4" + } + }, "@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -39315,6 +39663,46 @@ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "dev": true }, + "@graphiql/react": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/@graphiql/react/-/react-0.13.7.tgz", + "integrity": "sha512-mI+6mXd9AJFDJy39VuZGxCiVLvmBe2XoNidbN3lXgTLe689RZKifPaUw41USitQOu20sASoYDfzJ8i98G4urow==", + "requires": { + "@graphiql/toolkit": "^0.8.0", + "@reach/combobox": "^0.17.0", + "@reach/dialog": "^0.17.0", + "@reach/listbox": "^0.17.0", + "@reach/menu-button": "^0.17.0", + "@reach/tooltip": "^0.17.0", + "@reach/visually-hidden": "^0.17.0", + "codemirror": "^5.65.3", + "codemirror-graphql": "^2.0.2", + "copy-to-clipboard": "^3.2.0", + "graphql-language-service": "^5.1.0", + "markdown-it": "^12.2.0", + "set-value": "^4.1.0" + }, + "dependencies": { + "set-value": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-4.1.0.tgz", + "integrity": "sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==", + "requires": { + "is-plain-object": "^2.0.4", + "is-primitive": "^3.0.1" + } + } + } + }, + "@graphiql/toolkit": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@graphiql/toolkit/-/toolkit-0.8.0.tgz", + "integrity": "sha512-DbMFhEKejpPzB6k8W3Mj+Rl8geXiw49USDF9Wdi06EEk1XLVh1iebDqveYY+4lViITsV4+BeGikxlqi8umfP4g==", + "requires": { + "@n1ru4l/push-pull-async-iterable-iterator": "^3.1.0", + "meros": "^1.1.4" + } + }, "@graphql-typed-document-node/core": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.1.tgz", @@ -40578,6 +40966,30 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "@lezer/common": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.1.tgz", + "integrity": "sha512-8TR5++Q/F//tpDsLd5zkrvEX5xxeemafEaek7mUp7Y+bI8cKQXdSqhzTOBaOogETcMOVr0pT3BBPXp13477ciw==", + "peer": true + }, + "@lezer/highlight": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.2.tgz", + "integrity": "sha512-CAun1WR1glxG9ZdOokTZwXbcwB7PXkIEyZRUMFBVwSrhTcogWq634/ByNImrkUnQhjju6xsIaOBIxvcRJtplXQ==", + "peer": true, + "requires": { + "@lezer/common": "^1.0.0" + } + }, + "@lezer/lr": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.3.tgz", + "integrity": "sha512-qpB7rBzH8f6Mzjv2AVZRahcm+2Cf7nbIH++uXbvVOL1yIRvVWQ3HAM/saeBLCyz/togB7LGo76qdJYL1uKQlqA==", + "peer": true, + "requires": { + "@lezer/common": "^1.0.0" + } + }, "@mdx-js/loader": { "version": "1.6.22", "resolved": "https://registry.npmjs.org/@mdx-js/loader/-/loader-1.6.22.tgz", @@ -40860,6 +41272,11 @@ } } }, + "@n1ru4l/push-pull-async-iterable-iterator": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@n1ru4l/push-pull-async-iterable-iterator/-/push-pull-async-iterable-iterator-3.2.0.tgz", + "integrity": "sha512-3fkKj25kEjsfObL6IlKPAlHYPq/oYwUkkQ03zsTTiDjD7vg/RxjdiLeCydqtxHZP0JgsXL3D/X5oAkMGzuUp/Q==" + }, "@next/bundle-analyzer": { "version": "12.3.1", "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-12.3.1.tgz", @@ -41494,6 +41911,172 @@ "@babel/runtime": "^7.13.10" } }, + "@reach/auto-id": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/auto-id/-/auto-id-0.17.0.tgz", + "integrity": "sha512-ud8iPwF52RVzEmkHq1twuqGuPA+moreumUHdtgvU3sr3/15BNhwp3KyDLrKKSz0LP1r3V4pSdyF9MbYM8BoSjA==", + "requires": { + "@reach/utils": "0.17.0", + "tslib": "^2.3.0" + } + }, + "@reach/combobox": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/combobox/-/combobox-0.17.0.tgz", + "integrity": "sha512-2mYvU5agOBCQBMdlM4cri+P1BbNwp05P1OuDyc33xJSNiBG7BMy4+ZSHJ0X4fyle6rHwSgCAOCLOeWV1XUYjoQ==", + "requires": { + "@reach/auto-id": "0.17.0", + "@reach/descendants": "0.17.0", + "@reach/popover": "0.17.0", + "@reach/portal": "0.17.0", + "@reach/utils": "0.17.0", + "prop-types": "^15.7.2", + "tiny-warning": "^1.0.3", + "tslib": "^2.3.0" + } + }, + "@reach/descendants": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/descendants/-/descendants-0.17.0.tgz", + "integrity": "sha512-c7lUaBfjgcmKFZiAWqhG+VnXDMEhPkI4kAav/82XKZD6NVvFjsQOTH+v3tUkskrAPV44Yuch0mFW/u5Ntifr7Q==", + "requires": { + "@reach/utils": "0.17.0", + "tslib": "^2.3.0" + } + }, + "@reach/dialog": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/dialog/-/dialog-0.17.0.tgz", + "integrity": "sha512-AnfKXugqDTGbeG3c8xDcrQDE4h9b/vnc27Sa118oQSquz52fneUeX9MeFb5ZEiBJK8T5NJpv7QUTBIKnFCAH5A==", + "requires": { + "@reach/portal": "0.17.0", + "@reach/utils": "0.17.0", + "prop-types": "^15.7.2", + "react-focus-lock": "^2.5.2", + "react-remove-scroll": "^2.4.3", + "tslib": "^2.3.0" + } + }, + "@reach/dropdown": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/dropdown/-/dropdown-0.17.0.tgz", + "integrity": "sha512-qBTIGInhxtPHtdj4Pl2XZgZMz3e37liydh0xR3qc48syu7g71sL4nqyKjOzThykyfhA3Pb3/wFgsFJKGTSdaig==", + "requires": { + "@reach/auto-id": "0.17.0", + "@reach/descendants": "0.17.0", + "@reach/popover": "0.17.0", + "@reach/utils": "0.17.0", + "tslib": "^2.3.0" + } + }, + "@reach/listbox": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/listbox/-/listbox-0.17.0.tgz", + "integrity": "sha512-AMnH1P6/3VKy2V/nPb4Es441arYR+t4YRdh9jdcFVrCOD6y7CQrlmxsYjeg9Ocdz08XpdoEBHM3PKLJqNAUr7A==", + "requires": { + "@reach/auto-id": "0.17.0", + "@reach/descendants": "0.17.0", + "@reach/machine": "0.17.0", + "@reach/popover": "0.17.0", + "@reach/utils": "0.17.0", + "prop-types": "^15.7.2" + } + }, + "@reach/machine": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/machine/-/machine-0.17.0.tgz", + "integrity": "sha512-9EHnuPgXzkbRENvRUzJvVvYt+C2jp7PGN0xon7ffmKoK8rTO6eA/bb7P0xgloyDDQtu88TBUXKzW0uASqhTXGA==", + "requires": { + "@reach/utils": "0.17.0", + "@xstate/fsm": "1.4.0", + "tslib": "^2.3.0" + } + }, + "@reach/menu-button": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/menu-button/-/menu-button-0.17.0.tgz", + "integrity": "sha512-YyuYVyMZKamPtivoEI6D0UEILYH3qZtg4kJzEAuzPmoR/aHN66NZO75Fx0gtjG1S6fZfbiARaCOZJC0VEiDOtQ==", + "requires": { + "@reach/dropdown": "0.17.0", + "@reach/popover": "0.17.0", + "@reach/utils": "0.17.0", + "prop-types": "^15.7.2", + "tiny-warning": "^1.0.3", + "tslib": "^2.3.0" + } + }, + "@reach/observe-rect": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@reach/observe-rect/-/observe-rect-1.2.0.tgz", + "integrity": "sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==" + }, + "@reach/popover": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/popover/-/popover-0.17.0.tgz", + "integrity": "sha512-yYbBF4fMz4Ml4LB3agobZjcZ/oPtPsNv70ZAd7lEC2h7cvhF453pA+zOBGYTPGupKaeBvgAnrMjj7RnxDU5hoQ==", + "requires": { + "@reach/portal": "0.17.0", + "@reach/rect": "0.17.0", + "@reach/utils": "0.17.0", + "tabbable": "^4.0.0", + "tslib": "^2.3.0" + } + }, + "@reach/portal": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/portal/-/portal-0.17.0.tgz", + "integrity": "sha512-+IxsgVycOj+WOeNPL2NdgooUdHPSY285wCtj/iWID6akyr4FgGUK7sMhRM9aGFyrGpx2vzr+eggbUmAVZwOz+A==", + "requires": { + "@reach/utils": "0.17.0", + "tiny-warning": "^1.0.3", + "tslib": "^2.3.0" + } + }, + "@reach/rect": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/rect/-/rect-0.17.0.tgz", + "integrity": "sha512-3YB7KA5cLjbLc20bmPkJ06DIfXSK06Cb5BbD2dHgKXjUkT9WjZaLYIbYCO8dVjwcyO3GCNfOmPxy62VsPmZwYA==", + "requires": { + "@reach/observe-rect": "1.2.0", + "@reach/utils": "0.17.0", + "prop-types": "^15.7.2", + "tiny-warning": "^1.0.3", + "tslib": "^2.3.0" + } + }, + "@reach/tooltip": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/tooltip/-/tooltip-0.17.0.tgz", + "integrity": "sha512-HP8Blordzqb/Cxg+jnhGmWQfKgypamcYLBPlcx6jconyV5iLJ5m93qipr1giK7MqKT2wlsKWy44ZcOrJ+Wrf8w==", + "requires": { + "@reach/auto-id": "0.17.0", + "@reach/portal": "0.17.0", + "@reach/rect": "0.17.0", + "@reach/utils": "0.17.0", + "@reach/visually-hidden": "0.17.0", + "prop-types": "^15.7.2", + "tiny-warning": "^1.0.3", + "tslib": "^2.3.0" + } + }, + "@reach/utils": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.17.0.tgz", + "integrity": "sha512-M5y8fCBbrWeIsxedgcSw6oDlAMQDkl5uv3VnMVJ7guwpf4E48Xlh1v66z/1BgN/WYe2y8mB/ilFD2nysEfdGeA==", + "requires": { + "tiny-warning": "^1.0.3", + "tslib": "^2.3.0" + } + }, + "@reach/visually-hidden": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@reach/visually-hidden/-/visually-hidden-0.17.0.tgz", + "integrity": "sha512-T6xF3Nv8vVnjVkGU6cm0+kWtvliLqPAo8PcZ+WxkKacZsaHTjaZb4v1PaCcyQHmuTNT/vtTVNOJLG0SjQOIb7g==", + "requires": { + "prop-types": "^15.7.2", + "tslib": "^2.3.0" + } + }, "@react-dnd/asap": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.1.tgz", @@ -44682,6 +45265,11 @@ "tslib": "^2.3.0" } }, + "@xstate/fsm": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@xstate/fsm/-/fsm-1.4.0.tgz", + "integrity": "sha512-uTHDeu2xI5E1IFwf37JFQM31RrH7mY7877RqPBS4ZqSNUwoLDuct8AhBWaXGnVizBAYyimVwgCyGa9z/NiRhXA==" + }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -46609,6 +47197,30 @@ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==" }, + "codemirror": { + "version": "5.65.9", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.9.tgz", + "integrity": "sha512-19Jox5sAKpusTDgqgKB5dawPpQcY+ipQK7xoEI+MVucEF9qqFaXpeqY1KaoyGBso/wHQoDa4HMMxMjdsS3Zzzw==" + }, + "codemirror-graphql": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-2.0.2.tgz", + "integrity": "sha512-9c1cItR+8lG7thmTnDDQ3zI8YesNKiFCp2BnLFkYWCtdhSSuCUHebU/Vurew6ayyUl8MBCldNx3Ev66QAWM5Kw==", + "requires": { + "graphql-language-service": "5.0.6" + }, + "dependencies": { + "graphql-language-service": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/graphql-language-service/-/graphql-language-service-5.0.6.tgz", + "integrity": "sha512-FjE23aTy45Lr5metxCv3ZgSKEZOzN7ERR+OFC1isV5mHxI0Ob8XxayLTYjQKrs8b3kOpvgTYmSmu6AyXOzYslg==", + "requires": { + "nullthrows": "^1.0.0", + "vscode-languageserver-types": "^3.15.1" + } + } + } + }, "coffee-script": { "version": "1.12.7", "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", @@ -48245,8 +48857,7 @@ "entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" }, "errno": { "version": "0.1.8", @@ -50235,11 +50846,32 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "graphiql": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/graphiql/-/graphiql-2.0.13.tgz", + "integrity": "sha512-hfqcd40aO2V44Z/rey6lGXYnEsbwseeokeeZGQbFfSImciGtyP0SS8MyE5s5yno6z/M0ymJccpaNaUlC6imSwA==", + "requires": { + "@graphiql/react": "^0.13.7", + "@graphiql/toolkit": "^0.8.0", + "entities": "^2.0.0", + "graphql-language-service": "^5.1.0", + "markdown-it": "^12.2.0" + } + }, "graphql": { "version": "16.6.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz", "integrity": "sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==" }, + "graphql-language-service": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/graphql-language-service/-/graphql-language-service-5.1.0.tgz", + "integrity": "sha512-APffigZ/l2me6soek+Yq5Us3HBwmfw4vns4QoqsTePXkK3knVO8rn0uAC6PmTyglb1pmFFPbYaRIzW4wmcnnGQ==", + "requires": { + "nullthrows": "^1.0.0", + "vscode-languageserver-types": "^3.17.1" + } + }, "graphql-tag": { "version": "2.12.6", "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", @@ -51418,6 +52050,11 @@ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" }, + "is-primitive": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz", + "integrity": "sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==" + }, "is-reference": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.0.tgz", @@ -54336,6 +54973,14 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "requires": { + "uc.micro": "^1.0.1" + } + }, "list-item": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/list-item/-/list-item-1.1.1.tgz", @@ -54639,6 +55284,30 @@ "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-1.1.1.tgz", "integrity": "sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==" }, + "markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "requires": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==" + } + } + }, "markdown-link": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/markdown-link/-/markdown-link-0.1.1.tgz", @@ -55214,6 +55883,12 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" }, + "meros": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/meros/-/meros-1.2.1.tgz", + "integrity": "sha512-R2f/jxYqCAGI19KhAvaxSOxALBMkaXWH2a7rOyqQw+ZmizX5bKkEYWLzdhC+U82ZVVPVp6MCXe3EkVligh+12g==", + "requires": {} + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -56288,6 +56963,11 @@ "boolbase": "^1.0.0" } }, + "nullthrows": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==" + }, "num2fraction": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", @@ -58618,6 +59298,14 @@ "use-memo-one": "^1.1.1" } }, + "react-clientside-effect": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz", + "integrity": "sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==", + "requires": { + "@babel/runtime": "^7.12.13" + } + }, "react-contexify": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/react-contexify/-/react-contexify-5.0.0.tgz", @@ -58784,6 +59472,29 @@ "prop-types": "^15.7.2" } }, + "react-focus-lock": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.9.1.tgz", + "integrity": "sha512-pSWOQrUmiKLkffPO6BpMXN7SNKXMsuOakl652IBuALAu1esk+IcpJyM+ALcYzPTTFz1rD0R54aB9A4HuP5t1Wg==", + "requires": { + "@babel/runtime": "^7.0.0", + "focus-lock": "^0.11.2", + "prop-types": "^15.6.2", + "react-clientside-effect": "^1.2.6", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "dependencies": { + "focus-lock": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-0.11.3.tgz", + "integrity": "sha512-4n0pYcPTa/uI7Q66BZna61nRT7lDhnuJ9PJr6wiDjx4uStg491ks41y7uOG+s0umaaa+hulNKSldU9aTg9/yVg==", + "requires": { + "tslib": "^2.0.3" + } + } + } + }, "react-from-dom": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/react-from-dom/-/react-from-dom-0.6.2.tgz", @@ -61415,6 +62126,8 @@ "dayjs": "^1.11.0", "file-saver": "^2.0.5", "generate-password": "^1.7.0", + "graphiql": "*", + "graphql": "*", "html-to-image": "^1.10.8", "immutability-helper": "^3.1.1", "jest": "^27.4.0", @@ -61658,6 +62371,12 @@ } } }, + "style-mod": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz", + "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==", + "peer": true + }, "style-to-object": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", @@ -62018,6 +62737,11 @@ "integrity": "sha512-qImOD23aDfnIDNqlG1NOehdB9IYsn1V9oByPjKY1nakv2MQYCEMyX033/q+aEtYCpmYK1cv2+NTmlH+ra6GA5A==", "dev": true }, + "tabbable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-4.0.0.tgz", + "integrity": "sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ==" + }, "table": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz", @@ -62159,7 +62883,7 @@ "lodash": "^4.17.21", "markdown-toc": "^1.2.0", "minimist": "^1.2.6", - "next": "12.0.9", + "next": "12.1.0", "next-compose-plugins": "^2.2.1", "next-mdx-remote": "^3.0.8", "next-themes": "0.0.15", @@ -62235,77 +62959,6 @@ "version": "1.6.22", "requires": {} }, - "@next/env": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/@next/env/-/env-12.0.9.tgz", - "integrity": "sha512-oBlkyDop0Stf7MPIzETGv5r0YT/G/weBrknoPOUTaa5qwOeGjuy6gsOVc/SBtrBkOoBmRpD+fFhQJPvmo1mS+g==" - }, - "@next/swc-android-arm64": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.0.9.tgz", - "integrity": "sha512-aVqgsEn5plmUH2X58sjzhHsH/6majucWTMaaBEs7hHO2+GCwCZc7zaLH4XCBMKPES9Yaja8/pYUbvZQE9DqgFw==", - "optional": true - }, - "@next/swc-darwin-arm64": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.0.9.tgz", - "integrity": "sha512-uAgRKm4a2nVdyBiPPJokvmDD1saugOvxljz9ld2ih0CCg5S9vBhqaj3kPGCQBj9hSu3q+Lng2CHnQqG3ga1jzA==", - "optional": true - }, - "@next/swc-darwin-x64": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.0.9.tgz", - "integrity": "sha512-fDOs2lZIyrAdU18IxMA5orBPn9qLbOdu55gXSTNZOhyRJ8ugtbUAejsK7OL0boJy0CCHPAdVRXm01Mwk8tZ9RQ==", - "optional": true - }, - "@next/swc-linux-arm-gnueabihf": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.0.9.tgz", - "integrity": "sha512-/ni0p9DBvATUML9RQ1ycQuf05uOYKdzA6iI8+eRsARjpGbFVUFbge7XPzlj9g2Q9YWgoN8CSjFGnKRlyky5uHA==", - "optional": true - }, - "@next/swc-linux-arm64-gnu": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.0.9.tgz", - "integrity": "sha512-AphxilJDf95rUxJDHgM9Ww1DaYXZWqTvoKwXeej/0SgSvICcRZrLaFDrkojdXz0Rxr4igX2OdYR1S4/Hj1jWOQ==", - "optional": true - }, - "@next/swc-linux-arm64-musl": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.0.9.tgz", - "integrity": "sha512-K5jbvNNzF3mRjWmPdxP5Bg87i7FHivfBj/L0KJlxpkLSC8sffBJDmB6jtMnI7wiPj9J6vmLkbGtSosln78xAlQ==", - "optional": true - }, - "@next/swc-linux-x64-gnu": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.0.9.tgz", - "integrity": "sha512-bJZ9bkMkQzsY+UyWezEZ77GWQ4TzwKeXdayX3U3+aEkL8k5C6eKBXlidWdrhu0teLmaUXIyWerWrLnJzwGXdfw==", - "optional": true - }, - "@next/swc-linux-x64-musl": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.0.9.tgz", - "integrity": "sha512-SR9p0R+v1T32DTXPVAXZw31pmJAkSDotC6Afy+mfC0xrEL3pp95R8sGXYAAUCEPkQp0MEeUOVy2LrToe92X7hQ==", - "optional": true - }, - "@next/swc-win32-arm64-msvc": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.0.9.tgz", - "integrity": "sha512-mzQ1A8vfHhJrvEy5KJZGZWEByXthyKfWofvFaf+oo/5nJl/0Bz1ODP2ajSmbLG++77Eo2AROgbm9pkW1ucvG2A==", - "optional": true - }, - "@next/swc-win32-ia32-msvc": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.0.9.tgz", - "integrity": "sha512-MpD2vj1zjo1u3J3wiz3pEKse19Etz+P0GL6XfQkB/9a84vJQ1JWMaWBjmIdivzZv718Il2pRSSx8hymwPfguYQ==", - "optional": true - }, - "@next/swc-win32-x64-msvc": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.0.9.tgz", - "integrity": "sha512-1c/sxp/4Qz4F6rCxiYqAnrmghCOFt5hHZ9Kd+rXFW5Mqev4C4XDOUMHdBH55HgnJZqngYhOE0r/XNkCtsIojig==", - "optional": true - }, "@radix-ui/primitive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", @@ -62642,29 +63295,6 @@ "version": "0.4.1", "dev": true }, - "next": { - "version": "12.0.9", - "resolved": "https://registry.npmjs.org/next/-/next-12.0.9.tgz", - "integrity": "sha512-omfYqoR/DvbdOIJ6SS1unKJ4mGIxUPs0RPa7wr/Mft22OCKgJhuG+aI9KFYi5ZJBwoFQk1vqaMKpWz5qr+dN0Q==", - "requires": { - "@next/env": "12.0.9", - "@next/swc-android-arm64": "12.0.9", - "@next/swc-darwin-arm64": "12.0.9", - "@next/swc-darwin-x64": "12.0.9", - "@next/swc-linux-arm-gnueabihf": "12.0.9", - "@next/swc-linux-arm64-gnu": "12.0.9", - "@next/swc-linux-arm64-musl": "12.0.9", - "@next/swc-linux-x64-gnu": "12.0.9", - "@next/swc-linux-x64-musl": "12.0.9", - "@next/swc-win32-arm64-msvc": "12.0.9", - "@next/swc-win32-ia32-msvc": "12.0.9", - "@next/swc-win32-x64-msvc": "12.0.9", - "caniuse-lite": "^1.0.30001283", - "postcss": "8.4.5", - "styled-jsx": "5.0.0", - "use-subscription": "1.5.1" - } - }, "next-mdx-remote": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/next-mdx-remote/-/next-mdx-remote-3.0.8.tgz", @@ -62692,16 +63322,6 @@ "word-wrap": "^1.2.3" } }, - "postcss": { - "version": "8.4.5", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz", - "integrity": "sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==", - "requires": { - "nanoid": "^3.1.30", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.1" - } - }, "supports-color": { "version": "7.2.0", "dev": true, @@ -63494,6 +64114,11 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==" }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, "uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", @@ -64175,6 +64800,11 @@ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", "devOptional": true }, + "vscode-languageserver-types": { + "version": "3.17.2", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz", + "integrity": "sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA==" + }, "w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -64183,6 +64813,12 @@ "browser-process-hrtime": "^1.0.0" } }, + "w3c-keyname": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz", + "integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==", + "peer": true + }, "w3c-xmlserializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", diff --git a/studio/package.json b/studio/package.json index 1b9790d0711..03b6da52c66 100644 --- a/studio/package.json +++ b/studio/package.json @@ -39,6 +39,8 @@ "dayjs": "^1.11.0", "file-saver": "^2.0.5", "generate-password": "^1.7.0", + "graphiql": "^2.0.13", + "graphql": "^16.6.0", "html-to-image": "^1.10.8", "immutability-helper": "^3.1.1", "json-logic-js": "^2.0.2", diff --git a/studio/pages/project/[ref]/graphql/index.tsx b/studio/pages/project/[ref]/graphql/index.tsx new file mode 100644 index 00000000000..01b89f9585e --- /dev/null +++ b/studio/pages/project/[ref]/graphql/index.tsx @@ -0,0 +1,34 @@ +import { createGraphiQLFetcher } from '@graphiql/toolkit' +import { GraphiQL } from 'graphiql' +import { observer } from 'mobx-react-lite' +import 'graphiql/graphiql.css' + +import { ProjectLayoutWithAuth } from 'components/layouts' +import { useStore } from 'hooks' +import { NextPageWithLayout } from 'types' + +const fetcher = createGraphiQLFetcher({ + url: 'https://yrjyofbjvjypdsexuvvu.supabase.red/graphql/v1', + fetch, + headers: { + apikey: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlyanlvZmJqdmp5cGRzZXh1dnZ1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2NjczNTg0OTIsImV4cCI6MTk4MjkzNDQ5Mn0.CMVzrM7R0ym8ukt5miDoNL0ELhsM9fxOx2Qpm0hsLFE', + }, +}) + +const GraphiQLPage: NextPageWithLayout = () => { + const { ui } = useStore() + + const project = ui.selectedProject + console.log('project:', project) + + return +} + +GraphiQLPage.getLayout = (page) => ( + +
{page}
+
+) + +export default observer(GraphiQLPage) From 6cfb9283d662b5b9f48171605c3bde255485880c Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Wed, 2 Nov 2022 16:31:57 +1100 Subject: [PATCH 02/75] adds graphiql menu item --- package-lock.json | 6 +- .../NavigationBar/NavigationBar.utils.tsx | 14 +++++ studio/package.json | 1 + studio/pages/project/[ref]/graphiql/index.tsx | 59 +++++++++++++++++++ studio/pages/project/[ref]/graphql/index.tsx | 34 ----------- studio/public/img/graphql.svg | 10 ++++ 6 files changed, 88 insertions(+), 36 deletions(-) create mode 100644 studio/pages/project/[ref]/graphiql/index.tsx delete mode 100644 studio/pages/project/[ref]/graphql/index.tsx create mode 100644 studio/public/img/graphql.svg diff --git a/package-lock.json b/package-lock.json index c676116c2ca..2d9b2c00154 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37120,6 +37120,7 @@ "studio": { "version": "0.0.9", "dependencies": { + "@graphiql/toolkit": "^0.8.0", "@headlessui/react": "^1.4.1", "@monaco-editor/react": "^4.3.1", "@next/bundle-analyzer": "^12.0.4", @@ -62070,6 +62071,7 @@ "version": "file:studio", "requires": { "@babel/core": "^7.17.5", + "@graphiql/toolkit": "*", "@headlessui/react": "^1.4.1", "@monaco-editor/react": "^4.3.1", "@next/bundle-analyzer": "^12.0.4", @@ -62126,8 +62128,8 @@ "dayjs": "^1.11.0", "file-saver": "^2.0.5", "generate-password": "^1.7.0", - "graphiql": "*", - "graphql": "*", + "graphiql": "^2.0.13", + "graphql": "^16.6.0", "html-to-image": "^1.10.8", "immutability-helper": "^3.1.1", "jest": "^27.4.0", diff --git a/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx b/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx index c5b6ddb9036..6ef0458c2be 100644 --- a/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx +++ b/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx @@ -50,6 +50,20 @@ export const generateToolRoutes = (ref?: string, project?: ProjectBase): Route[] ref && (isProjectPaused ? homeUrl : isProjectBuilding ? buildingUrl : `/project/${ref}/sql`), }, + { + key: 'graphiql', + label: 'GraphiQL', + icon: ( + code.replace(/svg/, 'svg class="m-auto text-color-inherit"')} + /> + ), + link: + ref && + (isProjectPaused ? homeUrl : isProjectBuilding ? buildingUrl : `/project/${ref}/graphiql`), + }, ] } export const generateProductRoutes = (ref?: string, project?: ProjectBase): Route[] => { diff --git a/studio/package.json b/studio/package.json index 03b6da52c66..62bd3fbd52a 100644 --- a/studio/package.json +++ b/studio/package.json @@ -14,6 +14,7 @@ "prettier:write": "prettier --write ." }, "dependencies": { + "@graphiql/toolkit": "^0.8.0", "@headlessui/react": "^1.4.1", "@monaco-editor/react": "^4.3.1", "@next/bundle-analyzer": "^12.0.4", diff --git a/studio/pages/project/[ref]/graphiql/index.tsx b/studio/pages/project/[ref]/graphiql/index.tsx new file mode 100644 index 00000000000..bbf6f911558 --- /dev/null +++ b/studio/pages/project/[ref]/graphiql/index.tsx @@ -0,0 +1,59 @@ +import { createGraphiQLFetcher } from '@graphiql/toolkit' +import { GraphiQL } from 'graphiql' +import 'graphiql/graphiql.css' + +import { ProjectLayoutWithAuth } from 'components/layouts' +import Connecting from 'components/ui/Loading/Loading' +import { useParams, useProjectSettings } from 'hooks' +import { DEFAULT_PROJECT_API_SERVICE_ID } from 'lib/constants' +import { useMemo } from 'react' +import { NextPageWithLayout } from 'types' +import { IconAlertCircle } from 'ui' + +const GraphiQLPage: NextPageWithLayout = () => { + const { ref } = useParams() + + const { + services, + isError: isProjectSettingsError, + isLoading: isProjectSettingsLoading, + } = useProjectSettings(ref) + + const apiService = (services ?? []).find((x: any) => x.app.id == DEFAULT_PROJECT_API_SERVICE_ID) + const apiConfig = apiService?.app_config ?? {} + const apiKeys = apiService?.service_api_keys ?? [] + + const graphqlUrl = `https://${apiConfig.endpoint}/graphql/v1` + const anonKey = apiKeys.find((key: any) => key.tags === 'anon')?.api_key + + const fetcher = useMemo(() => { + return createGraphiQLFetcher({ + url: graphqlUrl, + fetch, + headers: { + apikey: anonKey, + }, + }) + }, [graphqlUrl, anonKey]) + + if (isProjectSettingsLoading) { + return + } + + if (isProjectSettingsError) { + return ( +
+ +

Failed to retrieve API keys

+
+ ) + } + + return +} + +GraphiQLPage.getLayout = (page) => ( + {page} +) + +export default GraphiQLPage diff --git a/studio/pages/project/[ref]/graphql/index.tsx b/studio/pages/project/[ref]/graphql/index.tsx deleted file mode 100644 index 01b89f9585e..00000000000 --- a/studio/pages/project/[ref]/graphql/index.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { createGraphiQLFetcher } from '@graphiql/toolkit' -import { GraphiQL } from 'graphiql' -import { observer } from 'mobx-react-lite' -import 'graphiql/graphiql.css' - -import { ProjectLayoutWithAuth } from 'components/layouts' -import { useStore } from 'hooks' -import { NextPageWithLayout } from 'types' - -const fetcher = createGraphiQLFetcher({ - url: 'https://yrjyofbjvjypdsexuvvu.supabase.red/graphql/v1', - fetch, - headers: { - apikey: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlyanlvZmJqdmp5cGRzZXh1dnZ1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2NjczNTg0OTIsImV4cCI6MTk4MjkzNDQ5Mn0.CMVzrM7R0ym8ukt5miDoNL0ELhsM9fxOx2Qpm0hsLFE', - }, -}) - -const GraphiQLPage: NextPageWithLayout = () => { - const { ui } = useStore() - - const project = ui.selectedProject - console.log('project:', project) - - return -} - -GraphiQLPage.getLayout = (page) => ( - -
{page}
-
-) - -export default observer(GraphiQLPage) diff --git a/studio/public/img/graphql.svg b/studio/public/img/graphql.svg new file mode 100644 index 00000000000..8060db470ea --- /dev/null +++ b/studio/public/img/graphql.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file From 3274074846295f74397370ecc29a281aec6067e3 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Wed, 2 Nov 2022 17:10:02 +1100 Subject: [PATCH 03/75] only show graphiql interface if graphql extension is enabled --- studio/pages/project/[ref]/graphiql/index.tsx | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/studio/pages/project/[ref]/graphiql/index.tsx b/studio/pages/project/[ref]/graphiql/index.tsx index bbf6f911558..b7382665d15 100644 --- a/studio/pages/project/[ref]/graphiql/index.tsx +++ b/studio/pages/project/[ref]/graphiql/index.tsx @@ -1,17 +1,31 @@ +import { observer } from 'mobx-react-lite' import { createGraphiQLFetcher } from '@graphiql/toolkit' import { GraphiQL } from 'graphiql' import 'graphiql/graphiql.css' +import ExtensionCard from 'components/interfaces/Database/Extensions/ExtensionCard' import { ProjectLayoutWithAuth } from 'components/layouts' import Connecting from 'components/ui/Loading/Loading' -import { useParams, useProjectSettings } from 'hooks' +import { useParams, useProjectSettings, useStore } from 'hooks' import { DEFAULT_PROJECT_API_SERVICE_ID } from 'lib/constants' -import { useMemo } from 'react' +import { useEffect, useMemo } from 'react' import { NextPageWithLayout } from 'types' import { IconAlertCircle } from 'ui' const GraphiQLPage: NextPageWithLayout = () => { const { ref } = useParams() + const { ui, meta } = useStore() + + const isExtensionsLoading = meta.extensions.isLoading + const pgGraphqlExtension = meta.extensions.byId('pg_graphql') + + useEffect(() => { + if (ui.selectedProject?.ref) { + // Schemas may be needed when enabling the GraphQL extension + meta.schemas.load() + meta.extensions.load() + } + }, [ui.selectedProject?.ref]) const { services, @@ -36,7 +50,7 @@ const GraphiQLPage: NextPageWithLayout = () => { }) }, [graphqlUrl, anonKey]) - if (isProjectSettingsLoading) { + if (isProjectSettingsLoading || (isExtensionsLoading && !pgGraphqlExtension)) { return } @@ -49,6 +63,24 @@ const GraphiQLPage: NextPageWithLayout = () => { ) } + if (pgGraphqlExtension?.installed_version === null) { + return ( +
+
+
+

Enable the GraphQL Extension

+

+ Toggle the switch below to enable the GraphQL extension. Then you can use the GraphQL + API with your Supabase Database. +

+
+ + +
+
+ ) + } + return } @@ -56,4 +88,4 @@ GraphiQLPage.getLayout = (page) => ( {page} ) -export default GraphiQLPage +export default observer(GraphiQLPage) From bd09b45a73028a2254a42da69d45524ff4a35e8a Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 9 Feb 2023 16:57:11 +0800 Subject: [PATCH 04/75] Ensure read only privileges in project settings pages --- .../Database/Backups/BackupsList.tsx | 2 +- .../Settings/Database/ConnectionPooling.tsx | 10 +- .../NetworkRestrictions.tsx | 132 +++++++++++++++--- .../Settings/Database/SSLConfiguration.tsx | 35 ++++- .../ProjectUsageBars/ProjectUsageBars.tsx | 30 ++-- .../StorageSettings/StorageSettings.tsx | 21 ++- .../to-be-cleaned/forms/SchemaForm.js | 11 +- .../to-be-cleaned/forms/SchemaFormPanel.js | 44 +++--- studio/components/ui/UpgradeToPro.tsx | 64 ++++++++- .../project/[ref]/database/backups/pitr.tsx | 2 +- .../[ref]/settings/billing/subscription.tsx | 1 - studio/sentry.client.config.js | 3 + 12 files changed, 283 insertions(+), 72 deletions(-) diff --git a/studio/components/interfaces/Database/Backups/BackupsList.tsx b/studio/components/interfaces/Database/Backups/BackupsList.tsx index 7f4f41434c5..b444a8e913d 100644 --- a/studio/components/interfaces/Database/Backups/BackupsList.tsx +++ b/studio/components/interfaces/Database/Backups/BackupsList.tsx @@ -35,7 +35,7 @@ const BackupsList: FC = ({}) => { icon={} primaryText="Free Plan does not include project backups." projectRef={projectRef} - secondaryText="Please upgrade to the Pro plan for up to 7 days of scheduled backups." + secondaryText="Upgrade to the Pro plan for up to 7 days of scheduled backups." /> ) } diff --git a/studio/components/interfaces/Settings/Database/ConnectionPooling.tsx b/studio/components/interfaces/Settings/Database/ConnectionPooling.tsx index 111a003258a..43ce15af121 100644 --- a/studio/components/interfaces/Settings/Database/ConnectionPooling.tsx +++ b/studio/components/interfaces/Settings/Database/ConnectionPooling.tsx @@ -1,6 +1,6 @@ import { FC, useState, useEffect, Fragment } from 'react' import { observer } from 'mobx-react-lite' -import { useStore } from 'hooks' +import { checkPermissions, useStore } from 'hooks' import { pluckObjectFields } from 'lib/helpers' import Loading from 'components/ui/Loading' import Panel from 'components/ui/Panel' @@ -12,6 +12,7 @@ import ToggleField from 'components/to-be-cleaned/forms/ToggleField' import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel' import { Input } from 'ui' import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import { PermissionAction } from '@supabase/shared-types/out/constants' interface Props {} @@ -127,6 +128,11 @@ export const PgbouncerConfig: FC = observer( ({ projectRef, bouncerInfo, connectionInfo }) => { const { ui } = useStore() + const canUpdateConnectionPoolingConfiguration = checkPermissions( + PermissionAction.UPDATE, + 'projects' + ) + const [updates, setUpdates] = useState({ pgbouncer_enabled: bouncerInfo.pgbouncer_enabled, pool_mode: bouncerInfo.pool_mode || 'transaction', @@ -198,6 +204,8 @@ export const PgbouncerConfig: FC = observer( onChangeModel={(model: any) => setUpdates(model)} onSubmit={(model: any) => updateConfig(model)} onReset={() => setUpdates(bouncerInfo)} + disabled={!canUpdateConnectionPoolingConfiguration} + disabledMessage="You need additional permissions to update connection pooling settings" >
diff --git a/studio/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.tsx b/studio/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.tsx index 7fa753067e7..b3c47c59119 100644 --- a/studio/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.tsx +++ b/studio/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.tsx @@ -1,8 +1,10 @@ import Link from 'next/link' -import { useState } from 'react' +import { FC, useState } from 'react' +import * as Tooltip from '@radix-ui/react-tooltip' import { Button, IconAlertCircle, IconExternalLink, IconGlobe, IconLock } from 'ui' +import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useParams } from 'hooks' +import { checkPermissions, useParams } from 'hooks' import Panel from 'components/ui/Panel' import { FormPanel, FormHeader } from 'components/ui/Forms' import ShimmeringLoader from 'components/ui/ShimmeringLoader' @@ -12,6 +14,62 @@ import DisallowAllModal from './DisallowAllModal' import AllowAllModal from './AllowAllModal' import { useNetworkRestrictionsQuery } from 'data/network-restrictions/network-restrictions-query' +const AllowAllAccessButton: FC<{ disabled: boolean; onClick: (value: boolean) => void }> = ({ + disabled, + onClick, +}) => ( + + + + + {disabled && ( + + +
+ + You need additional permissions to update network restrictions + +
+
+ )} +
+) + +const DisallowAllAccessButton: FC<{ disabled: boolean; onClick: (value: boolean) => void }> = ({ + disabled, + onClick, +}) => ( + + + + + {disabled && ( + + +
+ + You need additional permissions to update network restrictions + +
+
+ )} +
+) + const NetworkRestrictions = ({}) => { const { ref } = useParams() @@ -21,6 +79,8 @@ const NetworkRestrictions = ({}) => { const [selectedRestrictionToRemove, setSelectedRestrictionToRemove] = useState() const { data, isLoading } = useNetworkRestrictionsQuery({ projectRef: ref }) + const canUpdateNetworkRestrictions = checkPermissions(PermissionAction.UPDATE, 'projects') + const hasAccessToRestrictions = data?.entitlement === 'allowed' const restrictedIps = data?.config.dbAllowedCidrs ?? [] const restrictionStatus = data?.status ?? '' @@ -48,7 +108,31 @@ const NetworkRestrictions = ({}) => { - + + + + + {!canUpdateNetworkRestrictions && ( + + +
+ + You need additional permissions to update network restrictions + +
+
+ )} +
{isLoading ? ( @@ -75,12 +159,14 @@ const NetworkRestrictions = ({}) => {

- - + +
@@ -101,9 +187,10 @@ const NetworkRestrictions = ({}) => {
- +
) : isDisallowedAll ? ( @@ -125,9 +212,10 @@ const NetworkRestrictions = ({}) => {
- +
) : ( @@ -146,12 +234,14 @@ const NetworkRestrictions = ({}) => {

- - + +
{restrictedIps.map((ip) => { diff --git a/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx b/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx index d1709896cf1..989df6e60fe 100644 --- a/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx +++ b/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx @@ -1,8 +1,10 @@ import Link from 'next/link' import { useState, useEffect } from 'react' -import { useParams, useStore } from 'hooks' +import { checkPermissions, useParams, useStore } from 'hooks' import * as Tooltip from '@radix-ui/react-tooltip' import { Button, IconDownload, Toggle, IconExternalLink, IconLoader, Alert } from 'ui' +import { PermissionAction } from '@supabase/shared-types/out/constants' + import { FormHeader, FormPanel, @@ -25,6 +27,8 @@ const SSLConfiguration = () => { }) const { mutateAsync: updateSSLEnforcement } = useSSLEnforcementUpdateMutation() + const canUpdateSSLEnforcement = checkPermissions(PermissionAction.UPDATE, 'projects') + const hasAccessToSSLEnforcement = !sslEnforcementConfiguration?.isNotAllowed const env = process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod' ? 'prod' : 'staging' const hasSSLCertificate = @@ -115,11 +119,30 @@ const SSLConfiguration = () => { {(isLoading || isSubmitting) && ( )} - + + + + + {!canUpdateSSLEnforcement && ( + + +
+ + You need additional permissions to update SSL configurations + +
+
+ )} +
diff --git a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx b/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx index ede0524c0a3..cfe5c5ac2e2 100644 --- a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx +++ b/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx @@ -1,7 +1,10 @@ import { FC, useEffect } from 'react' +import { useRouter } from 'next/router' import { Badge, Button, IconAlertCircle, IconInfo, Loading } from 'ui' +import * as Tooltip from '@radix-ui/react-tooltip' +import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useStore } from 'hooks' +import { checkPermissions, useStore } from 'hooks' import { formatBytes } from 'lib/helpers' import { PRICING_TIER_PRODUCT_IDS, USAGE_APPROACHING_THRESHOLD } from 'lib/constants' import SparkBar from 'components/ui/SparkBar' @@ -9,8 +12,6 @@ import ShimmeringLoader from 'components/ui/ShimmeringLoader' import InformationBox from 'components/ui/InformationBox' import { USAGE_BASED_PRODUCTS } from 'components/interfaces/Billing/Billing.constants' import { ProjectUsageResponse, useProjectUsageQuery } from 'data/usage/project-usage-query' -import { useRouter } from 'next/router' -import * as Tooltip from '@radix-ui/react-tooltip' interface Props { projectRef?: string @@ -21,6 +22,11 @@ const ProjectUsage: FC = ({ projectRef }) => { const { data: usage, error, isLoading } = useProjectUsageQuery({ projectRef }) const router = useRouter() + const canUpdateSubscription = checkPermissions( + PermissionAction.BILLING_WRITE, + 'stripe.subscriptions' + ) + const subscriptionTier = ui.selectedProject?.subscription_tier const projectHasNoLimits = @@ -128,14 +134,16 @@ const ProjectUsage: FC = ({ projectRef }) => { usageElement = (
Not included in {planName} tier - + {canUpdateSubscription && ( + + )}
) } else if (showUsageExceedMessage) { diff --git a/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx b/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx index 4e05953129a..65662c22665 100644 --- a/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx +++ b/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx @@ -1,12 +1,14 @@ import { useState } from 'react' +import * as Tooltip from '@radix-ui/react-tooltip' import { Button, Form, IconClock, Input, Listbox } from 'ui' -import { useStore } from 'hooks' +import { checkPermissions, useStore } from 'hooks' import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query' import { useProjectStorageConfigUpdateUpdateMutation } from 'data/config/project-storage-config-update-mutation' import UpgradeToPro from 'components/ui/UpgradeToPro' import { convertFromBytes, convertToBytes } from './StorageSettings.utils' import { StorageSizeUnits, STORAGE_FILE_SIZE_LIMIT_MAX_BYTES } from './StorageSettings.constants' +import { PermissionAction } from '@supabase/shared-types/out/constants' export type StorageSettingsProps = { projectRef: string | undefined @@ -42,6 +44,8 @@ const StorageConfig = ({ config, projectRef }: any) => { const [selectedUnit, setSelectedUnit] = useState(unit) let initialValues = { fileSizeLimit: value, unformattedFileSizeLimit: fileSizeLimit } + const canUpdateStorageSettings = checkPermissions(PermissionAction.UPDATE, 'projects') + const formattedMaxSizeBytes = `${new Intl.NumberFormat('en-US').format( STORAGE_FILE_SIZE_LIMIT_MAX_BYTES )} bytes` @@ -184,13 +188,20 @@ const StorageConfig = ({ config, projectRef }: any) => { icon={} primaryText="Free Plan has a fixed upload file size limit of 50 MB." projectRef={projectRef} - secondaryText="Please upgrade to Pro plan for a configurable upload file size limit of up to 5 GB." + secondaryText="Upgrade to the Pro plan for a configurable upload file size limit of up to 5 GB." /> )}
-
+
+ {!canUpdateStorageSettings ? ( +

+ You need additional permissions to update storage settings +

+ ) : ( +
+ )}
diff --git a/studio/components/to-be-cleaned/forms/SchemaForm.js b/studio/components/to-be-cleaned/forms/SchemaForm.js index bc5e6ac20a0..fa65c7b1560 100644 --- a/studio/components/to-be-cleaned/forms/SchemaForm.js +++ b/studio/components/to-be-cleaned/forms/SchemaForm.js @@ -2,10 +2,19 @@ import { AutoForm } from 'uniforms-bootstrap4' import Ajv from 'ajv' import { JSONSchemaBridge } from 'uniforms-bridge-json-schema' -const SchemaForm = ({ schema, model, children, formRef, onSubmit, onChangeModel = () => {} }) => { +const SchemaForm = ({ + schema, + model, + disabled = false, + children, + formRef, + onSubmit, + onChangeModel = () => {}, +}) => { const validatedSchema = new JSONSchemaBridge(schema, createValidator(schema)) return (
{title || ''}
-
- - -
+ + +
+ )} {message}
{ setHasChanged(true) if (onChangeModel) onChangeModel(model) diff --git a/studio/components/ui/UpgradeToPro.tsx b/studio/components/ui/UpgradeToPro.tsx index ca7b1b549b6..ccee10b1295 100644 --- a/studio/components/ui/UpgradeToPro.tsx +++ b/studio/components/ui/UpgradeToPro.tsx @@ -2,8 +2,11 @@ import { Button } from 'ui' import { observer } from 'mobx-react-lite' import Link from 'next/link' import { FC, ReactNode } from 'react' -import { useStore } from 'hooks' +import * as Tooltip from '@radix-ui/react-tooltip' + +import { checkPermissions, useStore, useFlag, useParams } from 'hooks' import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants' +import { PermissionAction } from '@supabase/shared-types/out/constants' interface Props { icon?: ReactNode @@ -14,8 +17,16 @@ interface Props { const UpgradeToPro: FC = ({ icon, primaryText, projectRef, secondaryText }) => { const { ui } = useStore() + const { ref } = useParams() const tier = ui.selectedProject?.subscription_tier + const canUpdateSubscription = checkPermissions( + PermissionAction.BILLING_WRITE, + 'stripe.subscriptions' + ) + const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate') + const isEnterprise = tier === PRICING_TIER_PRODUCT_IDS.ENTERPRISE + return (
= ({ icon, primaryText, projectRef, secondaryText

{secondaryText}

- - - - - + + {!canUpdateSubscription || projectUpdateDisabled ? ( + + +
+ + {projectUpdateDisabled ? ( + <> + Subscription changes are currently disabled. +
+ Our engineers are working on a fix. + + ) : !canUpdateSubscription ? ( + 'You need additional permissions to amend subscriptions' + ) : ( + '' + )} +
+
+
+ ) : ( + <> + )} +
diff --git a/studio/pages/project/[ref]/database/backups/pitr.tsx b/studio/pages/project/[ref]/database/backups/pitr.tsx index 5f3eb4aadad..694e62f97c7 100644 --- a/studio/pages/project/[ref]/database/backups/pitr.tsx +++ b/studio/pages/project/[ref]/database/backups/pitr.tsx @@ -65,7 +65,7 @@ const PITR = () => { primaryText="Point in time recovery is a Pro plan add-on." secondaryText={ tier === PRICING_TIER_PRODUCT_IDS.FREE - ? 'Please upgrade to the Pro plan with the PITR add-on selected to enable point in time recovery for your project.' + ? 'Upgrade to the Pro plan with the PITR add-on selected to enable point in time recovery for your project.' : 'Please enable the add-on to enable point in time recovery for your project.' } /> diff --git a/studio/pages/project/[ref]/settings/billing/subscription.tsx b/studio/pages/project/[ref]/settings/billing/subscription.tsx index 750af452736..a47a62ad90d 100644 --- a/studio/pages/project/[ref]/settings/billing/subscription.tsx +++ b/studio/pages/project/[ref]/settings/billing/subscription.tsx @@ -36,7 +36,6 @@ interface SettingsProps { const Settings: FC = ({ project }) => { const { ui } = useStore() - const projectTier = ui.selectedProject?.subscription_tier const { data: subscription, diff --git a/studio/sentry.client.config.js b/studio/sentry.client.config.js index 456f62c88a0..23e597a09cb 100644 --- a/studio/sentry.client.config.js +++ b/studio/sentry.client.config.js @@ -39,6 +39,9 @@ Sentry.init({ // Safe to ignore since it's not a user-facing issue + we've not received any user feedback/report about it // Ref: https://github.com/supabase/supabase/pull/9729 'The provided `href` (/org/[slug]/general) value is missing query values (slug)', + 'The provided `href` (/org/[slug]/team) value is missing query values (slug)', + 'The provided `href` (/org/[slug]/billing) value is missing query values (slug)', + 'The provided `href` (/org/[slug]/invoices) value is missing query values (slug)', ], beforeSend: (event) => filterConsoleErrors(event), }) From d150f7def7071717d2f611bf754e5a2c4e598e82 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 10 Feb 2023 02:01:32 +0800 Subject: [PATCH 05/75] mid --- .../grid/components/header/Header.tsx | 147 +++++++++--------- .../grid/components/menu/ColumnMenu.tsx | 1 - .../Auth/EmailTemplates/EmailTemplates.tsx | 17 +- .../Functions/FunctionsList/FunctionsList.tsx | 2 + .../interfaces/Database/Tables/ColumnList.tsx | 103 +++++++++--- .../interfaces/Database/Tables/TableList.tsx | 95 ++++++++++- .../TableGridEditor/GridHeaderActions.tsx | 30 +++- .../TableGridEditor/TableGridEditor.tsx | 6 +- .../layouts/StorageLayout/StorageLayout.tsx | 19 +-- .../layouts/StorageLayout/StorageMenu.tsx | 56 +++++-- .../project/[ref]/storage/buckets/index.tsx | 15 +- 11 files changed, 350 insertions(+), 141 deletions(-) diff --git a/studio/components/grid/components/header/Header.tsx b/studio/components/grid/components/header/Header.tsx index d839ced0cfd..1b7c0d8fbce 100644 --- a/studio/components/grid/components/header/Header.tsx +++ b/studio/components/grid/components/header/Header.tsx @@ -1,17 +1,9 @@ -import { FC, useState, ReactNode } from 'react' -import { - Button, - IconDownload, - IconPlus, - IconX, - IconTrash, - Dropdown, - IconColumns, - IconChevronDown, -} from 'ui' import { saveAs } from 'file-saver' +import { FC, useState, ReactNode } from 'react' +import { Button, IconDownload, IconX, IconTrash, Dropdown, IconChevronDown } from 'ui' +import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useStore } from 'hooks' +import { checkPermissions, useStore } from 'hooks' import FilterDropdown from './filter' import SortPopover from './sort' import RefreshButton from './RefreshButton' @@ -80,6 +72,9 @@ const DefaultHeader: FC = ({ }) => { const canAddNew = onAddRow !== undefined || onAddColumn !== undefined + // [Joshen] Using this logic to block both column and row creation/update/delete + const canCreateColumns = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns') + return (
@@ -91,71 +86,73 @@ const DefaultHeader: FC = ({ <>
- -
-
-
+ {canCreateColumns && ( + +
+
+
+
+ } + > +
+

Insert row

+

Insert a new row into {table.name}

- } - > -
-

Insert row

-

Insert a new row into {table.name}

-
- , - ] - : []), - ...(onAddColumn !== undefined - ? [ - -
-
-
+ , + ] + : []), + ...(onAddColumn !== undefined + ? [ + +
+
+
+
+ } + > +
+

Insert column

+

Insert a new column into {table.name}

- } - > -
-

Insert column

-

Insert a new column into {table.name}

-
- , - ] - : []), - ]} - > - - + , + ] + : []), + ]} + > + + + )}
)} diff --git a/studio/components/grid/components/menu/ColumnMenu.tsx b/studio/components/grid/components/menu/ColumnMenu.tsx index d252aa58289..27d5f59b57a 100644 --- a/studio/components/grid/components/menu/ColumnMenu.tsx +++ b/studio/components/grid/components/menu/ColumnMenu.tsx @@ -10,7 +10,6 @@ import { IconLock, IconUnlock, } from 'ui' -import * as React from 'react' import { useDispatch, useTrackedState } from '../../store' type ColumnMenuProps = { diff --git a/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx b/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx index a982880a845..85462a7e4be 100644 --- a/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx +++ b/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx @@ -8,19 +8,18 @@ import TemplateEditor from './TemplateEditor' const EmailTemplates = observer(() => { return (
-
+ = ({ title="Functions" ctaButtonLabel="Create a new function" onClickCta={() => createFunction()} + disabled={!canCreateFunctions} + disabledMessage="You need additional permissions to create functions" >

diff --git a/studio/components/interfaces/Database/Tables/ColumnList.tsx b/studio/components/interfaces/Database/Tables/ColumnList.tsx index c5b668777fc..e53ed442863 100644 --- a/studio/components/interfaces/Database/Tables/ColumnList.tsx +++ b/studio/components/interfaces/Database/Tables/ColumnList.tsx @@ -1,11 +1,13 @@ import { FC, useState } from 'react' import { observer } from 'mobx-react-lite' +import * as Tooltip from '@radix-ui/react-tooltip' import { Input, Button, IconSearch, IconPlus, IconChevronLeft, IconEdit3, IconTrash } from 'ui' +import type { PostgresTable } from '@supabase/postgres-meta' +import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useStore } from 'hooks' +import { useStore, checkPermissions } from 'hooks' import Table from 'components/to-be-cleaned/Table' import NoSearchResults from 'components/to-be-cleaned/NoSearchResults' -import type { PostgresTable } from '@supabase/postgres-meta' interface Props { selectedTable: PostgresTable @@ -30,6 +32,7 @@ const ColumnList: FC = ({ : selectedTable.columns?.filter((column: any) => column.name.includes(filterString))) ?? [] const isLocked = meta.excludedSchemas.includes(selectedTable.schema ?? '') + const canUpdateColumns = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns') return ( <> @@ -58,9 +61,32 @@ const ColumnList: FC = ({

{!isLocked && (
- + + + + + {!canUpdateColumns && ( + + +
+ + You need additional permissions to create columns + +
+
+ )} +
)}
@@ -103,20 +129,59 @@ const ColumnList: FC = ({
-
diff --git a/studio/components/interfaces/Database/Tables/TableList.tsx b/studio/components/interfaces/Database/Tables/TableList.tsx index de856582937..b058f9ce6f4 100644 --- a/studio/components/interfaces/Database/Tables/TableList.tsx +++ b/studio/components/interfaces/Database/Tables/TableList.tsx @@ -12,10 +12,11 @@ import { IconLock, IconCheck, } from 'ui' - import { partition } from 'lodash' +import * as Tooltip from '@radix-ui/react-tooltip' +import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useStore } from 'hooks' +import { useStore, checkPermissions } from 'hooks' import Table from 'components/to-be-cleaned/Table' import NoSearchResults from 'components/to-be-cleaned/NoSearchResults' import type { PostgresTable, PostgresSchema } from '@supabase/postgres-meta' @@ -38,8 +39,8 @@ const TableList: FC = ({ onOpenTable = () => {}, }) => { const { meta } = useStore() - const [filterString, setFilterString] = useState('') + const canUpdateTables = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') const schemas: PostgresSchema[] = meta.schemas.list() const [protectedSchemas, openSchemas] = partition(schemas, (schema) => @@ -120,9 +121,32 @@ const TableList: FC = ({
{!isLocked && (
- + + + + + {!canUpdateTables && ( + + +
+ + You need additional permissions to create tables + +
+
+ )} +
)}
@@ -183,7 +207,62 @@ const TableList: FC = ({ > {x.columns.length} columns -
diff --git a/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx b/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx index 198e9a2a964..2a8a757aaa5 100644 --- a/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx +++ b/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx @@ -1,9 +1,11 @@ import { FC } from 'react' import Link from 'next/link' +import * as Tooltip from '@radix-ui/react-tooltip' import { Button, IconAlertCircle, IconLock } from 'ui' import type { PostgresPolicy, PostgresTable } from '@supabase/postgres-meta' -import { useStore } from 'hooks' +import { useStore, checkPermissions } from 'hooks' +import { PermissionAction } from '@supabase/shared-types/out/constants' interface Props { table: PostgresTable @@ -14,8 +16,34 @@ const GridHeaderActions: FC = ({ table }) => { const projectRef = ui.selectedProject?.ref const policies = meta.policies.list((policy: PostgresPolicy) => policy.table_id === table.id) + const isReadOnly = + !checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') && + !checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns') + return (
+ {isReadOnly && ( + + +
+ Viewing as read-only +
+
+ + +
+ + You need additional permissions to manage your project's data + +
+
+
+ )}
- } - style={{ justifyContent: 'start' }} - onClick={openCreateBucketModal} - > - New bucket - + + +
+ } + disabled={!canAccessStorage} + style={{ justifyContent: 'start' }} + onClick={openCreateBucketModal} + > + New bucket + + + {!canAccessStorage && ( + + +
+ + You need additional permissions to create buckets + +
+
+ )} +
diff --git a/studio/pages/project/[ref]/storage/buckets/index.tsx b/studio/pages/project/[ref]/storage/buckets/index.tsx index 80ca79f9f03..396a914ba2a 100644 --- a/studio/pages/project/[ref]/storage/buckets/index.tsx +++ b/studio/pages/project/[ref]/storage/buckets/index.tsx @@ -3,21 +3,21 @@ import { useRouter } from 'next/router' import { observer } from 'mobx-react-lite' import { API_URL } from 'lib/constants' -import { useFlag, useStore } from 'hooks' +import { useFlag, useParams, useStore } from 'hooks' import { post } from 'lib/common/fetch' import { PROJECT_STATUS } from 'lib/constants' import { StorageLayout } from 'components/layouts' import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore' import { NextPageWithLayout } from 'types' +import { useProjectApiQuery } from 'data/config/project-api-query' +import { find } from 'lodash' /** * PageLayout is used to setup layout - as usual it will requires inject global store */ const PageLayout: NextPageWithLayout = ({}) => { - const router = useRouter() - const { ref } = router.query - + const { ref } = useParams() const { ui } = useStore() const project = ui.selectedProject @@ -26,6 +26,11 @@ const PageLayout: NextPageWithLayout = ({}) => { const kpsEnabled = useFlag('initWithKps') + const { data: settings, isLoading } = useProjectApiQuery({ projectRef: ref }) + const apiService = settings?.autoApiService + const serviceKey = find(apiService?.service_api_keys ?? [], (key) => key.tags === 'service_role') + const canAccessStorage = !isLoading && apiService && serviceKey !== undefined + useEffect(() => { if (project && project.status === PROJECT_STATUS.INACTIVE) { post(`${API_URL}/projects/${ref}/restore`, { kps_enabled: kpsEnabled }) @@ -42,6 +47,8 @@ const PageLayout: NextPageWithLayout = ({}) => { infoButtonLabel="About storage" infoButtonUrl="https://supabase.com/docs/guides/storage" onClickCta={openCreateBucketModal} + disabled={!canAccessStorage} + disabledMessage="You need additional permissions to create buckets" >

Create buckets to store and serve any type of digital content. From e9dd00e4cee8dfeb6e66a485076dbe172c98c442 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 10 Feb 2023 15:11:40 +0800 Subject: [PATCH 06/75] Add read only locks for storage and vault --- .../Vault/Keys/EncryptionKeysManagement.tsx | 68 +++++++++++++++--- .../Settings/Vault/Secrets/SecretRow.tsx | 69 ++++++++++++++++--- .../Vault/Secrets/SecretsManagement.tsx | 35 ++++++++-- .../layouts/StorageLayout/StorageLayout.tsx | 20 +++--- .../storageExplorer/StorageExplorerStore.js | 1 + 5 files changed, 157 insertions(+), 36 deletions(-) diff --git a/studio/components/interfaces/Settings/Vault/Keys/EncryptionKeysManagement.tsx b/studio/components/interfaces/Settings/Vault/Keys/EncryptionKeysManagement.tsx index 876805da8a1..63cac0d9820 100644 --- a/studio/components/interfaces/Settings/Vault/Keys/EncryptionKeysManagement.tsx +++ b/studio/components/interfaces/Settings/Vault/Keys/EncryptionKeysManagement.tsx @@ -1,6 +1,7 @@ import dayjs from 'dayjs' import Link from 'next/link' import { observer } from 'mobx-react-lite' +import * as Tooltip from '@radix-ui/react-tooltip' import { FC, Fragment, useEffect, useState } from 'react' import { Alert, @@ -11,13 +12,13 @@ import { Modal, Form, IconTrash, - Badge, IconKey, IconLoader, IconX, IconExternalLink, } from 'ui' -import { useParams, useStore } from 'hooks' +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { useParams, useStore, checkPermissions } from 'hooks' import Divider from 'components/ui/Divider' const DEFAULT_KEY_NAME = 'No description provided' @@ -34,6 +35,8 @@ const EncryptionKeysManagement: FC = ({}) => { const [selectedKeyToRemove, setSelectedKeyToRemove] = useState() const [isDeletingKey, setIsDeletingKey] = useState(false) + const canManageKeys = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') + useEffect(() => { if (id !== undefined) setSearchValue(id) }, [id]) @@ -143,9 +146,32 @@ const EncryptionKeysManagement: FC = ({}) => { - + + + + + {!canManageKeys && ( + + +

+ + You need additional permissions to add keys + +
+ + )} +
@@ -178,12 +204,32 @@ const EncryptionKeysManagement: FC = ({}) => {

Added on {dayjs(key.created).format('MMM D, YYYY')}

-
{idx !== keys.length - 1 && } diff --git a/studio/components/interfaces/Settings/Vault/Secrets/SecretRow.tsx b/studio/components/interfaces/Settings/Vault/Secrets/SecretRow.tsx index 412435028c3..c3c08a02e92 100644 --- a/studio/components/interfaces/Settings/Vault/Secrets/SecretRow.tsx +++ b/studio/components/interfaces/Settings/Vault/Secrets/SecretRow.tsx @@ -1,3 +1,4 @@ +import Link from 'next/link' import dayjs from 'dayjs' import { FC, useState } from 'react' import { @@ -12,9 +13,10 @@ import { IconMoreVertical, IconLoader, } from 'ui' +import * as Tooltip from '@radix-ui/react-tooltip' +import { PermissionAction } from '@supabase/shared-types/out/constants' import { VaultSecret } from 'types' -import { useParams, useStore } from 'hooks' -import Link from 'next/link' +import { useParams, useStore, checkPermissions } from 'hooks' interface Props { secret: VaultSecret @@ -30,6 +32,8 @@ const SecretRow: FC = ({ secret, onSelectEdit, onSelectRemove }) => { const [revealedValue, setRevealedValue] = useState() const name = secret?.name ?? 'No name provided' + const canManageSecrets = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') + const revealSecret = async () => { setIsLoading(true) if (revealedValue === undefined) { @@ -106,15 +110,58 @@ const SecretRow: FC = ({ secret, onSelectEdit, onSelectRemove }) => { className="w-[120px]" overlay={ <> - } onClick={() => onSelectEdit(secret)}> - Edit - - } - onClick={() => onSelectRemove(secret)} - > - Delete - + + + } + disabled={!canManageSecrets} + onClick={() => onSelectEdit(secret)} + > + Edit + + + {!canManageSecrets && ( + + +
+ + You need additional permissions to edit secrets + +
+
+ )} +
+ + + } + onClick={() => onSelectRemove(secret)} + > + Delete + + + {!canManageSecrets && ( + + +
+ + You need additional permissions to delete secrets + +
+
+ )} +
} > diff --git a/studio/components/interfaces/Settings/Vault/Secrets/SecretsManagement.tsx b/studio/components/interfaces/Settings/Vault/Secrets/SecretsManagement.tsx index 468773ef358..05ca9c5d11c 100644 --- a/studio/components/interfaces/Settings/Vault/Secrets/SecretsManagement.tsx +++ b/studio/components/interfaces/Settings/Vault/Secrets/SecretsManagement.tsx @@ -1,9 +1,11 @@ import Link from 'next/link' import { observer } from 'mobx-react-lite' +import * as Tooltip from '@radix-ui/react-tooltip' import { FC, Fragment, useState, useEffect } from 'react' import { IconSearch, Input, Button, Listbox, IconLoader, IconExternalLink, IconX } from 'ui' +import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useStore, useParams } from 'hooks' +import { useStore, useParams, checkPermissions } from 'hooks' import SecretRow from './SecretRow' import EditSecretModal from './EditSecretModal' import DeleteSecretModal from './DeleteSecretModal' @@ -22,6 +24,8 @@ const SecretsManagement: FC = ({}) => { const [selectedSecretToEdit, setSelectedSecretToEdit] = useState() const [selectedSecretToRemove, setSelectedSecretToRemove] = useState() + const canManageSecrets = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') + useEffect(() => { if (search !== undefined) setSearchValue(search) }, [search]) @@ -105,9 +109,32 @@ const SecretsManagement: FC = ({}) => { - + + + + + {!canManageSecrets && ( + + +
+ + You need additional permissions to add secrets + +
+
+ )} +
diff --git a/studio/components/layouts/StorageLayout/StorageLayout.tsx b/studio/components/layouts/StorageLayout/StorageLayout.tsx index 1a12f2e8c24..53432b6461c 100644 --- a/studio/components/layouts/StorageLayout/StorageLayout.tsx +++ b/studio/components/layouts/StorageLayout/StorageLayout.tsx @@ -40,6 +40,7 @@ const StorageLayout: FC = ({ title, children }) => { buckets, } = storageExplorerStore || {} + // [Joshen] At the moment, without the service key, we won't be able to pull any data from storage const { data: settings, isLoading } = useProjectApiQuery({ projectRef }) const apiService = settings?.autoApiService const serviceKey = find(apiService?.service_api_keys ?? [], (key) => key.tags === 'service_role') @@ -99,16 +100,15 @@ const StorageLayout: FC = ({ title, children }) => { } } - // [Joshen TODO] Revisit this - // if (!isLoading && !canAccessStorage) { - // return ( - // - //
- // - //
- //
- // ) - // } + if (!isLoading && !canAccessStorage) { + return ( + +
+ +
+
+ ) + } return ( }> diff --git a/studio/localStores/storageExplorer/StorageExplorerStore.js b/studio/localStores/storageExplorer/StorageExplorerStore.js index 77d26ceda79..6fee62250c0 100644 --- a/studio/localStores/storageExplorer/StorageExplorerStore.js +++ b/studio/localStores/storageExplorer/StorageExplorerStore.js @@ -44,6 +44,7 @@ const EMPTY_FOLDER_PLACEHOLDER_FILE_NAME = '.emptyFolderPlaceholder' const STORAGE_PROGRESS_INFO_TEXT = "Please do not close the browser until it's completed" class StorageExplorerStore { + isReadOnly = false projectRef = '' loaded = false view = STORAGE_VIEWS.COLUMNS From d2e28ca1667b568893249c5d439c468c422d5444 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 10 Feb 2023 16:12:06 +0800 Subject: [PATCH 07/75] Add read only locks for wrappers --- .../Database/Wrappers/CreateWrapper.tsx | 3 +- .../Database/Wrappers/EditWrapper.tsx | 9 ++- .../Database/Wrappers/WrapperRow.tsx | 78 +++++++++++++++---- .../interfaces/Database/Wrappers/Wrappers.tsx | 2 +- .../Database/Wrappers/WrappersDropdown.tsx | 30 ++++++- .../project/[ref]/database/wrappers/[id].tsx | 1 + .../project/[ref]/database/wrappers/index.tsx | 2 +- .../project/[ref]/database/wrappers/new.tsx | 6 +- 8 files changed, 107 insertions(+), 24 deletions(-) diff --git a/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx b/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx index dbe8fe10c83..242345a01f7 100644 --- a/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx +++ b/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx @@ -36,7 +36,7 @@ const CreateWrapper = () => { const [selectedTableToEdit, setSelectedTableToEdit] = useState() const [formErrors, setFormErrors] = useState<{ [k: string]: string }>({}) - const canCreateWrapper = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'extensions') + const canCreateWrapper = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'wrappers') const wrapperMeta = WRAPPERS.find((wrapper) => wrapper.name === type) const initialValues = @@ -164,6 +164,7 @@ const CreateWrapper = () => { isSubmitting={isSubmitting} hasChanges={hasChanges} handleReset={handleReset} + disabled={!canCreateWrapper} helper={ !canCreateWrapper ? 'You need additional permissions to create a foreign data wrapper' diff --git a/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx b/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx index 2286ec04c7f..fdcec1aa4b8 100644 --- a/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx +++ b/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx @@ -65,7 +65,7 @@ const EditWrapper = () => { const [selectedTableToEdit, setSelectedTableToEdit] = useState() const [formErrors, setFormErrors] = useState<{ [k: string]: string }>({}) - const canCreateWrapper = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'extensions') + const canUpdateWrapper = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'wrappers') const initialValues = wrapperMeta !== undefined @@ -248,7 +248,7 @@ const EditWrapper = () => { return ( { handleReset() setWrapperTables(initialTables) }} + disabled={!canUpdateWrapper} helper={ - !canCreateWrapper - ? 'You need additional permissions to create a foreign data wrapper' + !canUpdateWrapper + ? 'You need additional permissions to edit a foreign data wrapper' : undefined } /> diff --git a/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx b/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx index 35b7efbed34..896cd11d45e 100644 --- a/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx +++ b/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx @@ -1,10 +1,12 @@ import Link from 'next/link' import Image from 'next/image' import { FC } from 'react' -import { Collapsible, IconChevronUp, Button, IconExternalLink, IconTrash, IconEdit } from 'ui' import { partition } from 'lodash' +import * as Tooltip from '@radix-ui/react-tooltip' +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { Collapsible, IconChevronUp, Button, IconExternalLink, IconTrash, IconEdit } from 'ui' -import { useParams, useStore } from 'hooks' +import { useParams, useStore, checkPermissions } from 'hooks' import { WrapperMeta } from './Wrappers.types' import { FDW } from 'data/fdw/fdws-query' import { useFDWDeleteMutation } from 'data/fdw/fdw-delete-mutation' @@ -24,6 +26,8 @@ const WrapperRow: FC = ({ wrappers = [], wrapperMeta, isOpen, onOpen }) = const { project } = useProjectContext() const { mutateAsync: deleteFDW } = useFDWDeleteMutation() + const canManageWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'wrappers') + const onDeleteWrapper = (wrapper: any) => { confirmAlert({ title: `Confirm to disable ${wrapper.name}`, @@ -156,21 +160,69 @@ const WrapperRow: FC = ({ wrappers = [], wrapperMeta, isOpen, onOpen }) =
- - + {canManageWrappers ? ( + + +
) diff --git a/studio/components/interfaces/Database/Wrappers/Wrappers.tsx b/studio/components/interfaces/Database/Wrappers/Wrappers.tsx index d05146b786d..ad5e35a095f 100644 --- a/studio/components/interfaces/Database/Wrappers/Wrappers.tsx +++ b/studio/components/interfaces/Database/Wrappers/Wrappers.tsx @@ -1,4 +1,5 @@ import Link from 'next/link' +import { groupBy } from 'lodash' import { useState } from 'react' import { Button, IconExternalLink } from 'ui' import { observer } from 'mobx-react-lite' @@ -11,7 +12,6 @@ import ShimmeringLoader from 'components/ui/ShimmeringLoader' import WrappersDropdown from './WrappersDropdown' import WrappersDisabledState from './WrappersDisabledState' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -import { groupBy } from 'lodash' const Wrappers = () => { const { meta } = useStore() diff --git a/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx b/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx index b74c9a9fffb..affb01fb608 100644 --- a/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx +++ b/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx @@ -3,8 +3,10 @@ import Image from 'next/image' import { FC, Fragment } from 'react' import { observer } from 'mobx-react-lite' import { Button, Dropdown, IconPlus } from 'ui' +import * as Tooltip from '@radix-ui/react-tooltip' +import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useParams } from 'hooks' +import { useParams, checkPermissions } from 'hooks' import { WRAPPERS } from './Wrappers.constants' interface Props { @@ -14,6 +16,32 @@ interface Props { const WrapperDropdown: FC = ({ buttonText = 'Add wrapper', align = 'end' }) => { const { ref } = useParams() + const canManageWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') + + if (!canManageWrappers) { + return ( + + + + + + +
+ + You need additional permissions to add wrappers + +
+
+
+ ) + } return ( { const canReadWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'wrappers') + if (!canReadWrappers) { return } diff --git a/studio/pages/project/[ref]/database/wrappers/index.tsx b/studio/pages/project/[ref]/database/wrappers/index.tsx index ead5fba1e25..d8df5feae34 100644 --- a/studio/pages/project/[ref]/database/wrappers/index.tsx +++ b/studio/pages/project/[ref]/database/wrappers/index.tsx @@ -9,7 +9,7 @@ import NoPermission from 'components/ui/NoPermission' import { FormsContainer } from 'components/ui/Forms' const DatabaseWrappers: NextPageWithLayout = () => { - const canReadWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'wrappers') + const canReadWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'tables') if (!canReadWrappers) { return } diff --git a/studio/pages/project/[ref]/database/wrappers/new.tsx b/studio/pages/project/[ref]/database/wrappers/new.tsx index 4db0245131f..225b4051123 100644 --- a/studio/pages/project/[ref]/database/wrappers/new.tsx +++ b/studio/pages/project/[ref]/database/wrappers/new.tsx @@ -8,9 +8,9 @@ import NoPermission from 'components/ui/NoPermission' import { CreateWrapper } from 'components/interfaces/Database' const DatabaseWrappersNew: NextPageWithLayout = () => { - const canReadWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'wrappers') - if (!canReadWrappers) { - return + const canCreateWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') + if (!canCreateWrappers) { + return } return From 1469cb111e7a8fdd1468d88d02a5d16a8206d499 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 10 Feb 2023 16:16:49 +0800 Subject: [PATCH 08/75] read only lock storage settings --- .../to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx b/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx index 65662c22665..a604849edf0 100644 --- a/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx +++ b/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx @@ -142,7 +142,7 @@ const StorageConfig = ({ config, projectRef }: any) => { id="fileSizeLimit" name="fileSizeLimit" type="number" - disabled={isFreeTier} + disabled={isFreeTier || !canUpdateStorageSettings} step={1} onKeyPress={(event) => { if (event.charCode < 48 || event.charCode > 57) { From f5dade6b67dc35adcdb331df153c256de2f9d3d7 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 10 Feb 2023 17:21:55 +0800 Subject: [PATCH 09/75] read only lock pitr --- .../Database/Backups/PITR/PITRNotice.tsx | 38 +++++++++++++++---- .../Database/Backups/PITR/PITRStatus.tsx | 32 +++++++++++++++- 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/studio/components/interfaces/Database/Backups/PITR/PITRNotice.tsx b/studio/components/interfaces/Database/Backups/PITR/PITRNotice.tsx index cd43b19d452..daab4106406 100644 --- a/studio/components/interfaces/Database/Backups/PITR/PITRNotice.tsx +++ b/studio/components/interfaces/Database/Backups/PITR/PITRNotice.tsx @@ -1,15 +1,22 @@ import Link from 'next/link' import { Button, IconCalendar } from 'ui' import { FormPanel } from 'components/ui/Forms' -import { useParams } from 'hooks' +import * as Tooltip from '@radix-ui/react-tooltip' +import { checkPermissions, useParams } from 'hooks' import { useProjectSubscriptionQuery } from 'data/subscriptions/project-subscription-query' import { getPITRRetentionDuration } from './PITR.utils' +import { PermissionAction } from '@supabase/shared-types/out/constants' const PITRNotice = ({}) => { const { ref: projectRef } = useParams() const { data: subscription } = useProjectSubscriptionQuery({ projectRef }) const retentionPeriod = getPITRRetentionDuration(subscription?.addons ?? []) + const canUpdateSubscription = checkPermissions( + PermissionAction.BILLING_WRITE, + 'stripe.subscriptions' + ) + return ( { You can also increase your recovery retention period updating your PITR add-on - - - - - + + {!canUpdateSubscription && ( + + +
+ + You need additional permissions to amend subscriptions + +
+
+ )} + } > diff --git a/studio/components/interfaces/Database/Backups/PITR/PITRStatus.tsx b/studio/components/interfaces/Database/Backups/PITR/PITRStatus.tsx index 4b47575c1d4..b634e1365c7 100644 --- a/studio/components/interfaces/Database/Backups/PITR/PITRStatus.tsx +++ b/studio/components/interfaces/Database/Backups/PITR/PITRStatus.tsx @@ -2,11 +2,13 @@ import dayjs from 'dayjs' import { FC } from 'react' import { observer } from 'mobx-react-lite' import { Button, IconAlertCircle } from 'ui' +import * as Tooltip from '@radix-ui/react-tooltip' -import { useStore } from 'hooks' +import { checkPermissions, useStore } from 'hooks' import { Timezone } from './PITR.types' import { FormPanel } from 'components/ui/Forms' import TimezoneSelection from './TimezoneSelection' +import { PermissionAction } from '@supabase/shared-types/out/constants' interface Props { selectedTimezone: Timezone @@ -29,6 +31,11 @@ const PITRStatus: FC = ({ selectedTimezone, onUpdateTimezone, onSetConfig .tz(selectedTimezone?.utc[0]) .format('DD MMM YYYY, HH:mm:ss') + const canTriggerPhysicalBackup = checkPermissions( + PermissionAction.INFRA_EXECUTE, + 'queue_job.walg.prepare_restore' + ) + return ( <> = ({ selectedTimezone, onUpdateTimezone, onSetConfig You'll be able to pick the right date and time when you begin - + + + + + {!canTriggerPhysicalBackup && ( + + +
+ + You need additional permissions to trigger a PITR recovery + +
+
+ )} +
} > From 958559b2f78cf616f7d7038b932011a849f5f4d5 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Thu, 16 Feb 2023 15:38:36 +1100 Subject: [PATCH 10/75] move graphiql to react-query --- studio/package.json | 2 +- studio/pages/project/[ref]/graphiql/index.tsx | 27 ++++++++++--------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/studio/package.json b/studio/package.json index 15a7c89b7fd..ffa23069e4d 100644 --- a/studio/package.json +++ b/studio/package.json @@ -44,7 +44,7 @@ "dayjs": "^1.11.0", "file-saver": "^2.0.5", "generate-password": "^1.7.0", - "graphiql": "^2.0.13", + "graphiql": "^2.2.0", "graphql": "^16.6.0", "html-to-image": "^1.10.8", "immutability-helper": "^3.1.1", diff --git a/studio/pages/project/[ref]/graphiql/index.tsx b/studio/pages/project/[ref]/graphiql/index.tsx index b7382665d15..e5a6538d890 100644 --- a/studio/pages/project/[ref]/graphiql/index.tsx +++ b/studio/pages/project/[ref]/graphiql/index.tsx @@ -6,14 +6,14 @@ import 'graphiql/graphiql.css' import ExtensionCard from 'components/interfaces/Database/Extensions/ExtensionCard' import { ProjectLayoutWithAuth } from 'components/layouts' import Connecting from 'components/ui/Loading/Loading' -import { useParams, useProjectSettings, useStore } from 'hooks' -import { DEFAULT_PROJECT_API_SERVICE_ID } from 'lib/constants' +import { useParams, useStore } from 'hooks' +import { useProjectApiQuery } from 'data/config/project-api-query' import { useEffect, useMemo } from 'react' import { NextPageWithLayout } from 'types' import { IconAlertCircle } from 'ui' const GraphiQLPage: NextPageWithLayout = () => { - const { ref } = useParams() + const { ref: projectRef } = useParams() const { ui, meta } = useStore() const isExtensionsLoading = meta.extensions.isLoading @@ -28,25 +28,26 @@ const GraphiQLPage: NextPageWithLayout = () => { }, [ui.selectedProject?.ref]) const { - services, + data: settings, isError: isProjectSettingsError, isLoading: isProjectSettingsLoading, - } = useProjectSettings(ref) + } = useProjectApiQuery({ + projectRef, + }) - const apiService = (services ?? []).find((x: any) => x.app.id == DEFAULT_PROJECT_API_SERVICE_ID) - const apiConfig = apiService?.app_config ?? {} - const apiKeys = apiService?.service_api_keys ?? [] + const apiService = settings?.autoApiService + const anonKey = apiService?.service_api_keys.find((x) => x.name === 'anon key') + ? apiService.defaultApiKey + : undefined + const endpoint = settings?.autoApiService.app_config.endpoint ?? '' - const graphqlUrl = `https://${apiConfig.endpoint}/graphql/v1` - const anonKey = apiKeys.find((key: any) => key.tags === 'anon')?.api_key + const graphqlUrl = `https://${endpoint}/graphql/v1` const fetcher = useMemo(() => { return createGraphiQLFetcher({ url: graphqlUrl, fetch, - headers: { - apikey: anonKey, - }, + ...(anonKey && { headers: { apikey: anonKey } }), }) }, [graphqlUrl, anonKey]) From 90b54f839acbf725554f9bdf3a89545548e00cf5 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Thu, 16 Feb 2023 15:44:53 +1100 Subject: [PATCH 11/75] update package-lock.json --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index f04276790a3..d3e4e45e973 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39461,7 +39461,7 @@ "dayjs": "^1.11.0", "file-saver": "^2.0.5", "generate-password": "^1.7.0", - "graphiql": "^2.0.13", + "graphiql": "^2.2.0", "graphql": "^16.6.0", "html-to-image": "^1.10.8", "immutability-helper": "^3.1.1", @@ -66717,7 +66717,7 @@ "dayjs": "^1.11.0", "file-saver": "^2.0.5", "generate-password": "^1.7.0", - "graphiql": "^2.0.13", + "graphiql": "^2.2.0", "graphql": "^16.6.0", "html-to-image": "^1.10.8", "immutability-helper": "^3.1.1", From 09f3fd0c62a5052a57962b6d8b5e8ca61eb52a2f Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Fri, 17 Feb 2023 16:59:46 +1100 Subject: [PATCH 12/75] adds custom graphiql layout --- package-lock.json | 36 ++ .../interfaces/GraphQL/GraphiQL.tsx | 607 ++++++++++++++++++ .../interfaces/GraphQL/graphiql.module.css | 4 + studio/package.json | 3 + studio/pages/project/[ref]/graphiql/index.tsx | 3 +- 5 files changed, 651 insertions(+), 2 deletions(-) create mode 100644 studio/components/interfaces/GraphQL/GraphiQL.tsx create mode 100644 studio/components/interfaces/GraphQL/graphiql.module.css diff --git a/package-lock.json b/package-lock.json index d3e4e45e973..c6da4ad6636 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19915,6 +19915,17 @@ "graphql": "^15.5.0 || ^16.0.0" } }, + "node_modules/graphql-ws": { + "version": "5.11.3", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.11.3.tgz", + "integrity": "sha512-fU8zwSgAX2noXAsuFiCZ8BtXeXZOzXyK5u1LloCdacsVth4skdBMPO74EG51lBoWSIZ8beUocdpV8+cQHBODnQ==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": ">=0.11 <=16" + } + }, "node_modules/gray-matter": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", @@ -39432,6 +39443,7 @@ "studio": { "version": "0.0.9", "dependencies": { + "@graphiql/react": "^0.15.0", "@graphiql/toolkit": "^0.8.0", "@hcaptcha/react-hcaptcha": "^1.4.4", "@headlessui/react": "^1.4.1", @@ -39455,6 +39467,7 @@ "awesome-debounce-promise": "^2.1.0", "blueimp-md5": "^2.19.0", "clipboard": "^2.0.8", + "clsx": "^1.2.1", "common": "*", "config": "*", "configcat-js": "^7.0.0", @@ -39463,6 +39476,7 @@ "generate-password": "^1.7.0", "graphiql": "^2.2.0", "graphql": "^16.6.0", + "graphql-ws": "^5.11.3", "html-to-image": "^1.10.8", "immutability-helper": "^3.1.1", "ip-address": "^8.1.0", @@ -39724,6 +39738,14 @@ "postcss": "^8.1.0" } }, + "studio/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "studio/node_modules/configcat-common": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/configcat-common/-/configcat-common-7.0.1.tgz", @@ -55037,6 +55059,12 @@ "vscode-languageserver-types": "^3.17.1" } }, + "graphql-ws": { + "version": "5.11.3", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.11.3.tgz", + "integrity": "sha512-fU8zwSgAX2noXAsuFiCZ8BtXeXZOzXyK5u1LloCdacsVth4skdBMPO74EG51lBoWSIZ8beUocdpV8+cQHBODnQ==", + "requires": {} + }, "gray-matter": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", @@ -66653,6 +66681,7 @@ "version": "file:studio", "requires": { "@babel/core": "^7.17.5", + "@graphiql/react": "*", "@graphiql/toolkit": "^0.8.0", "@hcaptcha/react-hcaptcha": "^1.4.4", "@headlessui/react": "^1.4.1", @@ -66711,6 +66740,7 @@ "babel-loader": "^8.2.3", "blueimp-md5": "^2.19.0", "clipboard": "^2.0.8", + "clsx": "^1.2.1", "common": "*", "config": "*", "configcat-js": "^7.0.0", @@ -66719,6 +66749,7 @@ "generate-password": "^1.7.0", "graphiql": "^2.2.0", "graphql": "^16.6.0", + "graphql-ws": "*", "html-to-image": "^1.10.8", "immutability-helper": "^3.1.1", "ip-address": "^8.1.0", @@ -66878,6 +66909,11 @@ "postcss-value-parser": "^4.2.0" } }, + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + }, "configcat-common": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/configcat-common/-/configcat-common-7.0.1.tgz", diff --git a/studio/components/interfaces/GraphQL/GraphiQL.tsx b/studio/components/interfaces/GraphQL/GraphiQL.tsx new file mode 100644 index 00000000000..dbeb0661c07 --- /dev/null +++ b/studio/components/interfaces/GraphQL/GraphiQL.tsx @@ -0,0 +1,607 @@ +/* Based on https://github.com/graphql/graphiql/blob/main/packages/graphiql/src/components/GraphiQL.tsx */ + +import { + Button, + ChevronDownIcon, + ChevronUpIcon, + CopyIcon, + Dialog, + ExecuteButton, + GraphiQLProvider, + HeaderEditor, + KeyboardShortcutIcon, + MergeIcon, + PlusIcon, + PrettifyIcon, + QueryEditor, + ReloadIcon, + ResponseEditor, + SettingsIcon, + Spinner, + Tab, + Tabs, + ToolbarButton, + Tooltip, + UnStyledButton, + useCopyQuery, + useDragResize, + useEditorContext, + useExecutionContext, + useMergeQuery, + usePluginContext, + usePrettifyEditors, + useSchemaContext, + useStorageContext, + useTheme, + VariableEditor, +} from '@graphiql/react' +import { Fetcher } from '@graphiql/toolkit' +import clsx from 'clsx' +import 'graphiql/graphiql.css' +import { PropsWithChildren, useState } from 'react' +import styles from './graphiql.module.css' + +export type GraphiQLProps = { + fetcher: Fetcher +} + +const GraphiQL = ({ fetcher }: GraphiQLProps) => { + // Ensure props are correct + if (typeof fetcher !== 'function') { + throw new TypeError( + 'The `GraphiQL` component requires a `fetcher` function to be passed as prop.' + ) + } + + return ( + + + + ) +} + +export function GraphiQLInterface() { + const editorContext = useEditorContext({ nonNull: true }) + const executionContext = useExecutionContext({ nonNull: true }) + const schemaContext = useSchemaContext({ nonNull: true }) + const storageContext = useStorageContext() + const pluginContext = usePluginContext() + + const copy = useCopyQuery() + const merge = useMergeQuery() + const prettify = usePrettifyEditors() + + const { theme, setTheme } = useTheme() + + const PluginContent = pluginContext?.visiblePlugin?.content + + const pluginResize = useDragResize({ + defaultSizeRelation: 1 / 3, + direction: 'horizontal', + initiallyHidden: pluginContext?.visiblePlugin ? undefined : 'second', + onHiddenElementChange: (resizableElement) => { + if (resizableElement === 'first') { + pluginContext?.setVisiblePlugin(null) + } + }, + sizeThresholdSecond: 200, + storageKey: 'docExplorerFlex', + }) + const editorResize = useDragResize({ + direction: 'horizontal', + storageKey: 'editorFlex', + }) + const editorToolsResize = useDragResize({ + defaultSizeRelation: 3, + direction: 'vertical', + initiallyHidden: (() => { + return editorContext.initialVariables || editorContext.initialHeaders ? undefined : 'second' + })(), + sizeThresholdSecond: 60, + storageKey: 'secondaryEditorFlex', + }) + + const [activeSecondaryEditor, setActiveSecondaryEditor] = useState<'variables' | 'headers'>( + () => { + return !editorContext.initialVariables && editorContext.initialHeaders + ? 'headers' + : 'variables' + } + ) + const [showDialog, setShowDialog] = useState<'settings' | 'short-keys' | null>(null) + const [clearStorageStatus, setClearStorageStatus] = useState<'success' | 'error' | null>(null) + + const logo = + + const toolbar = ( + <> + prettify()} label="Prettify query (Shift-Ctrl-P)"> + + merge()} label="Merge fragments into query (Shift-Ctrl-M)"> + + copy()} label="Copy query (Shift-Ctrl-C)"> + + + ) + + const onClickReference = () => { + if (pluginResize.hiddenElement === 'second') { + pluginResize.setHiddenElement(null) + } + } + + const modifier = + window.navigator.platform.toLowerCase().indexOf('mac') === 0 ? ( + Cmd + ) : ( + Ctrl + ) + + return ( +
+
+
+
+
+ + {editorContext.tabs.length > 1 ? ( + <> + {editorContext.tabs.map((tab, index) => ( + + { + executionContext.stop() + editorContext.changeTab(index) + }} + > + {tab.title} + + { + if (editorContext.activeTabIndex === index) { + executionContext.stop() + } + editorContext.closeTab(index) + }} + /> + + ))} +
+ + editorContext.addTab()} + aria-label="Add tab" + > + + +
+ + ) : null} +
+
+ {editorContext.tabs.length === 1 ? ( +
+ + editorContext.addTab()} + aria-label="Add tab" + > + + +
+ ) : null} + {logo} +
+
+
+
+
+
+
+
+ +
+
+ + {toolbar} +
+
+
+
+
+
+ { + if (editorToolsResize.hiddenElement === 'second') { + editorToolsResize.setHiddenElement(null) + } + setActiveSecondaryEditor('variables') + }} + > + Variables + + + { + if (editorToolsResize.hiddenElement === 'second') { + editorToolsResize.setHiddenElement(null) + } + setActiveSecondaryEditor('headers') + }} + > + Headers + +
+ + { + editorToolsResize.setHiddenElement( + editorToolsResize.hiddenElement === 'second' ? null : 'second' + ) + }} + aria-label={ + editorToolsResize.hiddenElement === 'second' + ? 'Show editor tools' + : 'Hide editor tools' + } + > + {editorToolsResize.hiddenElement === 'second' ? ( + + +
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+ {executionContext.isFetching ? : null} + +
+
+
+
+
+ +
+ {pluginContext?.visiblePlugin ?
: null} +
+ +
+
{PluginContent ? : null}
+
+
+
+
+ {pluginContext?.plugins.map((plugin) => { + const isVisible = plugin === pluginContext.visiblePlugin + const label = `${isVisible ? 'Hide' : 'Show'} ${plugin.title}` + const Icon = plugin.icon + return ( + + { + if (isVisible) { + pluginContext.setVisiblePlugin(null) + pluginResize.setHiddenElement('second') + } else { + pluginContext.setVisiblePlugin(plugin) + pluginResize.setHiddenElement(null) + } + }} + aria-label={label} + > + + + ) + })} +
+
+ + schemaContext.introspect()} + aria-label="Re-fetch GraphQL schema" + > + + + + setShowDialog('short-keys')} + aria-label="Open short keys dialog" + > + + + + setShowDialog('settings')} + aria-label="Open settings dialog" + > + + +
+
+ setShowDialog(null)}> +
+
Short Keys
+ setShowDialog(null)} /> +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Short keyFunction
+ {modifier} + {' + '} + F + Search in editor
+ {modifier} + {' + '} + K + Search in documentation
+ {modifier} + {' + '} + Enter + Execute query
+ Ctrl + {' + '} + Shift + {' + '} + P + Prettify editors
+ Ctrl + {' + '} + Shift + {' + '} + M + Merge fragments definitions into operation definition
+ Ctrl + {' + '} + Shift + {' + '} + C + Copy query
+ Ctrl + {' + '} + Shift + {' + '} + R + Re-fetch schema using introspection
+

+ The editors use{' '} + + CodeMirror Key Maps + {' '} + that add more short keys. This instance of GraphiQL uses sublime + . +

+
+
+
+ { + setShowDialog(null) + setClearStorageStatus(null) + }} + > +
+
Settings
+ { + setShowDialog(null) + setClearStorageStatus(null) + }} + /> +
+ + {/*
+
+
Theme
+
+ Adjust how the interface looks like. +
+
+
+ + + + + +
+
*/} + {storageContext ? ( +
+
+
Clear storage
+
+ Remove all locally stored data and start fresh. +
+
+
+ +
+
+ ) : null} +
+
+ ) +} + +// Configure the UI by providing this Component as a child of GraphiQL. +function GraphiQLLogo(props: PropsWithChildren) { + return ( +
+ {props.children || ( + + Graph + i + QL + + )} +
+ ) +} + +GraphiQLLogo.displayName = 'GraphiQLLogo' + +export default GraphiQL diff --git a/studio/components/interfaces/GraphQL/graphiql.module.css b/studio/components/interfaces/GraphQL/graphiql.module.css new file mode 100644 index 00000000000..87caa2eb2e2 --- /dev/null +++ b/studio/components/interfaces/GraphQL/graphiql.module.css @@ -0,0 +1,4 @@ +.graphiqlContainer .graphiqlSessions { + margin-left: var(--px-16); + margin-right: 0; +} diff --git a/studio/package.json b/studio/package.json index ffa23069e4d..c135a48e5ea 100644 --- a/studio/package.json +++ b/studio/package.json @@ -15,6 +15,7 @@ "prettier:write": "prettier --write ." }, "dependencies": { + "@graphiql/react": "^0.15.0", "@graphiql/toolkit": "^0.8.0", "@hcaptcha/react-hcaptcha": "^1.4.4", "@headlessui/react": "^1.4.1", @@ -38,6 +39,7 @@ "awesome-debounce-promise": "^2.1.0", "blueimp-md5": "^2.19.0", "clipboard": "^2.0.8", + "clsx": "^1.2.1", "common": "*", "config": "*", "configcat-js": "^7.0.0", @@ -46,6 +48,7 @@ "generate-password": "^1.7.0", "graphiql": "^2.2.0", "graphql": "^16.6.0", + "graphql-ws": "^5.11.3", "html-to-image": "^1.10.8", "immutability-helper": "^3.1.1", "ip-address": "^8.1.0", diff --git a/studio/pages/project/[ref]/graphiql/index.tsx b/studio/pages/project/[ref]/graphiql/index.tsx index e5a6538d890..3a6102c80b1 100644 --- a/studio/pages/project/[ref]/graphiql/index.tsx +++ b/studio/pages/project/[ref]/graphiql/index.tsx @@ -1,9 +1,8 @@ import { observer } from 'mobx-react-lite' import { createGraphiQLFetcher } from '@graphiql/toolkit' -import { GraphiQL } from 'graphiql' -import 'graphiql/graphiql.css' import ExtensionCard from 'components/interfaces/Database/Extensions/ExtensionCard' +import GraphiQL from 'components/interfaces/GraphQL/GraphiQL' import { ProjectLayoutWithAuth } from 'components/layouts' import Connecting from 'components/ui/Loading/Loading' import { useParams, useStore } from 'hooks' From 14bb72b340460d146d6a6018264db0b95e869d54 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Fri, 17 Feb 2023 17:13:42 +1100 Subject: [PATCH 13/75] update package-lock.json --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index c6da4ad6636..f39429b25f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66681,7 +66681,7 @@ "version": "file:studio", "requires": { "@babel/core": "^7.17.5", - "@graphiql/react": "*", + "@graphiql/react": "^0.15.0", "@graphiql/toolkit": "^0.8.0", "@hcaptcha/react-hcaptcha": "^1.4.4", "@headlessui/react": "^1.4.1", @@ -66749,7 +66749,7 @@ "generate-password": "^1.7.0", "graphiql": "^2.2.0", "graphql": "^16.6.0", - "graphql-ws": "*", + "graphql-ws": "^5.11.3", "html-to-image": "^1.10.8", "immutability-helper": "^3.1.1", "ip-address": "^8.1.0", From 747cb3d02e7f7bff141c866e168bbcca0447b762 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 17 Feb 2023 16:54:10 +0800 Subject: [PATCH 14/75] Update edge functions home page --- .../Functions/EdgeFunctionsList.tsx | 123 ++++++++++++++++++ .../Functions/TerminalInstructions.tsx | 15 +-- .../components/interfaces/Functions/index.ts | 3 +- studio/components/layouts/FunctionsLayout.tsx | 72 +++++++--- studio/pages/_app.tsx | 2 + .../pages/project/[ref]/functions/index.tsx | 49 +------ 6 files changed, 193 insertions(+), 71 deletions(-) create mode 100644 studio/components/interfaces/Functions/EdgeFunctionsList.tsx diff --git a/studio/components/interfaces/Functions/EdgeFunctionsList.tsx b/studio/components/interfaces/Functions/EdgeFunctionsList.tsx new file mode 100644 index 00000000000..0ff6d7aa81b --- /dev/null +++ b/studio/components/interfaces/Functions/EdgeFunctionsList.tsx @@ -0,0 +1,123 @@ +import dayjs from 'dayjs' +import { FC, useState } from 'react' +import * as Tooltip from '@radix-ui/react-tooltip' + +import { useParams, useStore } from 'hooks' +import Table from 'components/to-be-cleaned/Table' +import FunctionsListItem from './FunctionsListItem' +import { Function } from 'components/interfaces/Functions/Functions.types' +import { IconUpload, IconCheck, IconClipboard, IconClock } from 'ui' +import { useRouter } from 'next/router' + +interface Props {} + +const EdgeFunctionsList: FC = () => { + const router = useRouter() + const { ref } = useParams() + const { ui, functions: functionsStore } = useStore() + const functions: Function[] = functionsStore.list() ?? [] + + const restUrl = ui.selectedProject?.restUrl + const restUrlTld = new URL(restUrl as string).hostname.split('.').pop() + + return ( +
+
+ {functions.map((fn) => { + const [isCopied, setIsCopied] = useState(false) + const functionUrl = `https://${ref}.functions.supabase.${restUrlTld}/${fn.slug}` + + return ( +
router.push(`/project/${ref}/functions/${fn.id}`)} + > +
+

{fn.name}

+
+

{functionUrl}

+ +
+
+
+ + +

Deployment {fn.version}

+
+ + +
+ + Deployment history coming soon + +
+
+
+ + +
+ +

{dayjs(fn.updated_at).fromNow()}

+
+
+ + +
+ + Last updated on {dayjs(fn.updated_at).format('DD MMM, YYYY HH:mm')} + +
+
+
+
+
+ ) + })} +
+
+ ) +} + +export default EdgeFunctionsList diff --git a/studio/components/interfaces/Functions/TerminalInstructions.tsx b/studio/components/interfaces/Functions/TerminalInstructions.tsx index f8f30b523ab..16a2e2eea21 100644 --- a/studio/components/interfaces/Functions/TerminalInstructions.tsx +++ b/studio/components/interfaces/Functions/TerminalInstructions.tsx @@ -10,19 +10,16 @@ import { Commands } from './Functions.types' interface Props { closable?: boolean + removeBorder?: boolean } -const TerminalInstructions: FC = ({ closable = false }) => { +const TerminalInstructions: FC = ({ closable = false, removeBorder = false }) => { const router = useRouter() const { ref: projectRef } = useParams() - const [showInstructions, setShowInstructions] = useState(!closable) - const { data: tokens } = useAccessTokensQuery() + const { data: settings } = useProjectApiQuery({ projectRef }) - const { data: settings } = useProjectApiQuery({ - projectRef, - }) const apiService = settings?.autoApiService const anonKey = apiService?.service_api_keys.find((x) => x.name === 'anon key') ? apiService.defaultApiKey @@ -86,7 +83,9 @@ const TerminalInstructions: FC = ({ closable = false }) => { return (
@@ -107,7 +106,7 @@ const TerminalInstructions: FC = ({ closable = false }) => {
)}
-
+
diff --git a/studio/components/interfaces/Functions/index.ts b/studio/components/interfaces/Functions/index.ts index b096ff5e604..e2d3d9dda40 100644 --- a/studio/components/interfaces/Functions/index.ts +++ b/studio/components/interfaces/Functions/index.ts @@ -1,5 +1,6 @@ +import EdgeFunctionsList from './EdgeFunctionsList' import FunctionsListItem from './FunctionsListItem' import FunctionsEmptyState from './FunctionsEmptyState' import TerminalInstructions from './TerminalInstructions' -export { FunctionsListItem, FunctionsEmptyState, TerminalInstructions } +export { EdgeFunctionsList, FunctionsListItem, FunctionsEmptyState, TerminalInstructions } diff --git a/studio/components/layouts/FunctionsLayout.tsx b/studio/components/layouts/FunctionsLayout.tsx index 511d401da95..67d65717dea 100644 --- a/studio/components/layouts/FunctionsLayout.tsx +++ b/studio/components/layouts/FunctionsLayout.tsx @@ -1,14 +1,15 @@ import Link from 'next/link' import { useRouter } from 'next/router' -import { FC, useEffect, ReactNode } from 'react' +import { FC, useEffect, ReactNode, useState } from 'react' import { observer } from 'mobx-react-lite' -import { IconCode } from 'ui' +import { Button, IconExternalLink, IconCode, Modal, IconTerminal } from 'ui' -import { checkPermissions, useStore, withAuth } from 'hooks' +import { checkPermissions, useParams, useStore, withAuth } from 'hooks' import FunctionsNav from '../interfaces/Functions/FunctionsNav' import BaseLayout from 'components/layouts' import NoPermission from 'components/ui/NoPermission' import { PermissionAction } from '@supabase/shared-types/out/constants' +import { TerminalInstructions } from 'components/interfaces/Functions' interface Props { title?: string @@ -16,10 +17,10 @@ interface Props { } const FunctionsLayout: FC = ({ title, children }) => { - const { functions, ui } = useStore() const router = useRouter() - - const { id, ref } = router.query + const { id, ref } = useParams() + const { functions, ui } = useStore() + const [showTerminalInstructions, setShowTerminalInstructions] = useState(false) useEffect(() => { if (ui.selectedProjectRef) functions.load() @@ -87,7 +88,7 @@ const FunctionsLayout: FC = ({ title, children }) => { ].join(' ')} >
-
+
= ({ title, children }) => {
-
- -

- Edge Functions -

- -

Beta

+
+
+ +

+ Edge Functions +

+ +

Beta

+
+
+ + + + + + +
- {name && ( + {/* {name && ( <> = ({ title, children }) => {
{name}
- )} + )} */}
{item && } @@ -140,6 +162,24 @@ const FunctionsLayout: FC = ({ title, children }) => {
)} + + setShowTerminalInstructions(false)} + header={

Deploying an edge function to your project

} + customFooter={ +
+ +
+ } + > +
+ +
+
) } diff --git a/studio/pages/_app.tsx b/studio/pages/_app.tsx index d97675d69ef..f542d30ca75 100644 --- a/studio/pages/_app.tsx +++ b/studio/pages/_app.tsx @@ -16,6 +16,7 @@ import dayjs from 'dayjs' import customParseFormat from 'dayjs/plugin/customParseFormat' import timezone from 'dayjs/plugin/timezone' import utc from 'dayjs/plugin/utc' +import relativeTime from 'dayjs/plugin/relativeTime' // @ts-ignore import Prism from 'prism-react-renderer/prism' @@ -44,6 +45,7 @@ import useAutoAuthRedirect from 'hooks/misc/useAutoAuthRedirect' dayjs.extend(customParseFormat) dayjs.extend(utc) dayjs.extend(timezone) +dayjs.extend(relativeTime) dart(Prism) diff --git a/studio/pages/project/[ref]/functions/index.tsx b/studio/pages/project/[ref]/functions/index.tsx index f8778eb09f5..7848cff03fe 100644 --- a/studio/pages/project/[ref]/functions/index.tsx +++ b/studio/pages/project/[ref]/functions/index.tsx @@ -1,50 +1,8 @@ import { observer } from 'mobx-react-lite' - -import { NextPageWithLayout } from 'types' import { useStore } from 'hooks' +import { NextPageWithLayout } from 'types' import FunctionsLayout from 'components/layouts/FunctionsLayout' - -import Table from 'components/to-be-cleaned/Table' -import { Function } from 'components/interfaces/Functions/Functions.types' -import { - FunctionsListItem, - FunctionsEmptyState, - TerminalInstructions, -} from 'components/interfaces/Functions' - -const FunctionsList = ({ functions }: { functions: Function[] }) => { - return ( - <> -
-
- {`${functions.length} function${ - functions.length > 1 ? 's' : '' - } deployed`} -
-
- - Name - URL - Created - Last updated - Version - Status - - } - body={ - <> - {functions.length > 0 && - functions.map((item: any) => )} - - } - /> - - - - ) -} +import { EdgeFunctionsList, FunctionsEmptyState } from 'components/interfaces/Functions' const PageLayout: NextPageWithLayout = () => { const { functions } = useStore() @@ -52,8 +10,7 @@ const PageLayout: NextPageWithLayout = () => { return hasFunctions ? (
- - +
) : ( From c5f162a8e699dd816bda64e5608bb3554919a7a5 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 17 Feb 2023 18:27:06 +0800 Subject: [PATCH 15/75] Shift edge functions to RQ and implement delete function --- .../Functions/EdgeFunctionDetails.tsx | 296 ++++++++++++++++++ .../Functions/EdgeFunctionsList.tsx | 123 -------- .../interfaces/Functions/EdgeFunctionsRow.tsx | 111 +++++++ .../Functions/FunctionsListItem.tsx | 62 ---- .../interfaces/Functions/FunctionsNav.tsx | 4 +- .../components/interfaces/Functions/index.ts | 5 +- studio/components/layouts/FunctionsLayout.tsx | 41 ++- .../edge-functions-delete-mutation.ts | 43 +++ .../edge-functions/edge-functions-query.ts | 57 ++++ studio/data/edge-functions/keys.ts | 3 + .../project/[ref]/functions/[id]/details.tsx | 231 +------------- .../pages/project/[ref]/functions/index.tsx | 27 +- 12 files changed, 559 insertions(+), 444 deletions(-) create mode 100644 studio/components/interfaces/Functions/EdgeFunctionDetails.tsx delete mode 100644 studio/components/interfaces/Functions/EdgeFunctionsList.tsx create mode 100644 studio/components/interfaces/Functions/EdgeFunctionsRow.tsx delete mode 100644 studio/components/interfaces/Functions/FunctionsListItem.tsx create mode 100644 studio/data/edge-functions/edge-functions-delete-mutation.ts create mode 100644 studio/data/edge-functions/edge-functions-query.ts create mode 100644 studio/data/edge-functions/keys.ts diff --git a/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx b/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx new file mode 100644 index 00000000000..4f25d522982 --- /dev/null +++ b/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx @@ -0,0 +1,296 @@ +import dayjs from 'dayjs' +import { useRouter } from 'next/router' +import { FC, useState, useEffect } from 'react' +import * as Tooltip from '@radix-ui/react-tooltip' +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { Alert, IconGlobe, IconTerminal, IconMinimize2, IconMaximize2, Button, Modal } from 'ui' + +import { useStore, useParams, checkPermissions } from 'hooks' +import Panel from 'components/ui/Panel' +import CommandRender from './CommandRender' +import { useProjectApiQuery } from 'data/config/project-api-query' +import { useEdgeFunctionDeleteMutation } from 'data/edge-functions/edge-functions-delete-mutation' + +interface Props {} + +const EdgeFunctionDetails: FC = () => { + const router = useRouter() + const { functions, ui } = useStore() + const { ref: projectRef, id } = useParams() + const [showDeleteModal, setShowDeleteModal] = useState(false) + const [showInstructions, setShowInstructions] = useState(false) + const [selectedFunction, setSelectedFunction] = useState(null) + + const canDeleteEdgeFunction = checkPermissions(PermissionAction.FUNCTIONS_WRITE, '*') + const { mutateAsync: deleteEdgeFunction, isLoading: isDeleting } = useEdgeFunctionDeleteMutation() + + useEffect(() => { + setSelectedFunction(functions.byId(id)) + }, [functions.isLoaded, ui.selectedProject]) + + const { data: settings } = useProjectApiQuery({ projectRef }) + const functionUrl = `https://${projectRef}.functions.supabase.co/${selectedFunction?.slug}` + + // get the .co or .net TLD from the restUrl + const restUrl = ui.selectedProject?.restUrl + const restUrlTld = new URL(restUrl as string).hostname.split('.').pop() + + // Get the API service + const apiService = settings?.autoApiService + const anonKey = apiService?.service_api_keys.find((x) => x.name === 'anon key') + ? apiService.defaultApiKey + : undefined + + const endpoint = apiService?.app_config.endpoint ?? '' + const endpointSections = endpoint.split('.') + const functionsEndpoint = [ + ...endpointSections.slice(0, 1), + 'functions', + ...endpointSections.slice(1), + ].join('.') + + const onConfirmDelete = async () => { + if (!projectRef) return console.error('Project ref is required') + if (selectedFunction === undefined) return console.error('No edge function selected') + + try { + await deleteEdgeFunction({ projectRef, slug: selectedFunction.slug }) + ui.setNotification({ + category: 'success', + message: `Succesfully deleted "${selectedFunction.name}"`, + }) + router.push(`/project/${projectRef}/functions`) + } catch (error: any) { + ui.setNotification({ + category: 'error', + message: `Failed to delete function: ${error.message}`, + }) + } + } + + const managementCommands: any = [ + { + command: `supabase functions deploy ${selectedFunction?.slug}`, + description: 'This will overwrite the deployed function with your new function', + jsx: () => { + return ( + <> + supabase functions deploy{' '} + {selectedFunction?.slug} + + ) + }, + comment: 'Deploy a new version', + }, + { + command: `supabase functions delete ${selectedFunction?.slug}`, + description: 'This will remove the function and all the logs associated with it', + jsx: () => { + return ( + <> + supabase functions delete{' '} + {selectedFunction?.slug} + + ) + }, + comment: 'Delete the function', + }, + ] + + const secretCommands: any = [ + { + command: `supabase secrets list`, + description: 'This will list all the secrets for your project', + jsx: () => { + return ( + <> + supabase secrets list + + ) + }, + comment: 'View all secrets', + }, + { + command: `supabase secrets set NAME1=VALUE1 NAME2=VALUE2`, + description: 'This will set secrets for your project', + jsx: () => { + return ( + <> + supabase secrets set NAME1=VALUE1 NAME2=VALUE2 + + ) + }, + comment: 'Set secrets for your project', + }, + { + command: `supabase secrets unset NAME1 NAME2 `, + description: 'This will delete secrets for your project', + jsx: () => { + return ( + <> + supabase secrets unset NAME1 NAME2 + + ) + }, + comment: 'Unset secrets for your project', + }, + ] + + const invokeCommands: any = [ + { + command: `curl -L -X POST 'https://${projectRef}.functions.supabase.${restUrlTld}/${ + selectedFunction?.slug + }' -H 'Authorization: Bearer ${anonKey ?? '[YOUR ANON KEY]'}' --data '{"name":"Functions"}'`, + description: 'Invokes the hello function', + jsx: () => { + return ( + <> + curl -L -X POST 'https://{functionsEndpoint}/ + {selectedFunction?.slug}' -H 'Authorization: Bearer [YOUR ANON KEY]'{' '} + {`--data '{"name":"Functions"}'`} + + ) + }, + comment: 'Invoke your function', + }, + ] + + return ( + <> +
+
+
+

Function Name

+

{selectedFunction?.name}

+
+ +
+

Endpoint URL

+

{functionUrl}

+
+ +
+

Created At

+

+ {selectedFunction?.created_at && + dayjs(selectedFunction.created_at).format('dddd, MMMM D, YYYY h:mm A')} +

+
+ +
+

Last Updated At

+

+ {selectedFunction?.updated_at && + dayjs(selectedFunction.updated_at).format('dddd, MMMM D, YYYY h:mm A')} +

+
+ +
+ Deployment +
{selectedFunction?.version}
+
+ +
+ Regions +
+
+ + Earth +
+ All functions are deployed globally +
+
+
+ +
+
+
+
+ +
+

Command line access

+
+
setShowInstructions(!showInstructions)}> + {showInstructions ? ( + + ) : ( + + )} +
+
+ +
Deployment management
+ +
Invoke
+ +
Secrets management
+ +
+ + Delete Edge Function

}> + + +

+ Make sure you have made a backup if you want to restore your edge function +

+ + + + + {!canDeleteEdgeFunction && ( + + +
+ + You need additional permissions to delete an edge function + +
+
+ )} +
+
+
+
+
+ + Confirm to delete {selectedFunction?.name}} + visible={showDeleteModal} + loading={isDeleting} + onCancel={() => setShowDeleteModal(false)} + onConfirm={onConfirmDelete} + > +
+ + + Ensure that you have made a backup if you want to restore your edge function + + +
+
+ + ) +} + +export default EdgeFunctionDetails diff --git a/studio/components/interfaces/Functions/EdgeFunctionsList.tsx b/studio/components/interfaces/Functions/EdgeFunctionsList.tsx deleted file mode 100644 index 0ff6d7aa81b..00000000000 --- a/studio/components/interfaces/Functions/EdgeFunctionsList.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import dayjs from 'dayjs' -import { FC, useState } from 'react' -import * as Tooltip from '@radix-ui/react-tooltip' - -import { useParams, useStore } from 'hooks' -import Table from 'components/to-be-cleaned/Table' -import FunctionsListItem from './FunctionsListItem' -import { Function } from 'components/interfaces/Functions/Functions.types' -import { IconUpload, IconCheck, IconClipboard, IconClock } from 'ui' -import { useRouter } from 'next/router' - -interface Props {} - -const EdgeFunctionsList: FC = () => { - const router = useRouter() - const { ref } = useParams() - const { ui, functions: functionsStore } = useStore() - const functions: Function[] = functionsStore.list() ?? [] - - const restUrl = ui.selectedProject?.restUrl - const restUrlTld = new URL(restUrl as string).hostname.split('.').pop() - - return ( -
-
- {functions.map((fn) => { - const [isCopied, setIsCopied] = useState(false) - const functionUrl = `https://${ref}.functions.supabase.${restUrlTld}/${fn.slug}` - - return ( -
router.push(`/project/${ref}/functions/${fn.id}`)} - > -
-

{fn.name}

-
-

{functionUrl}

- -
-
-
- - -

Deployment {fn.version}

-
- - -
- - Deployment history coming soon - -
-
-
- - -
- -

{dayjs(fn.updated_at).fromNow()}

-
-
- - -
- - Last updated on {dayjs(fn.updated_at).format('DD MMM, YYYY HH:mm')} - -
-
-
-
-
- ) - })} -
-
- ) -} - -export default EdgeFunctionsList diff --git a/studio/components/interfaces/Functions/EdgeFunctionsRow.tsx b/studio/components/interfaces/Functions/EdgeFunctionsRow.tsx new file mode 100644 index 00000000000..185f01348e3 --- /dev/null +++ b/studio/components/interfaces/Functions/EdgeFunctionsRow.tsx @@ -0,0 +1,111 @@ +import dayjs from 'dayjs' +import { useRouter } from 'next/router' +import { FC, useState } from 'react' +import * as Tooltip from '@radix-ui/react-tooltip' +import { IconUpload, IconCheck, IconClipboard } from 'ui' + +import { useParams, useStore } from 'hooks' +import { EdgeFunctionsResponse } from 'data/edge-functions/edge-functions-query' + +interface Props { + fn: EdgeFunctionsResponse +} + +const EdgeFunctionsRow: FC = ({ fn }) => { + const router = useRouter() + const { ref } = useParams() + const { ui } = useStore() + const [isCopied, setIsCopied] = useState(false) + + const restUrl = ui.selectedProject?.restUrl + const restUrlTld = new URL(restUrl as string).hostname.split('.').pop() + const functionUrl = `https://${ref}.functions.supabase.${restUrlTld}/${fn.slug}` + + return ( +
router.push(`/project/${ref}/functions/${fn.id}`)} + > +
+

{fn.name}

+
+

{functionUrl}

+ +
+
+
+ + +

Deployment {fn.version}

+
+ + +
+ Deployment history coming soon +
+
+
+ + +
+ +

{dayjs(fn.updated_at).fromNow()}

+
+
+ + +
+ + Last updated on {dayjs(fn.updated_at).format('DD MMM, YYYY HH:mm')} + +
+
+
+
+
+ ) +} + +export default EdgeFunctionsRow diff --git a/studio/components/interfaces/Functions/FunctionsListItem.tsx b/studio/components/interfaces/Functions/FunctionsListItem.tsx deleted file mode 100644 index f2b9045d417..00000000000 --- a/studio/components/interfaces/Functions/FunctionsListItem.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import dayjs from 'dayjs' -import { FC } from 'react' -import { useRouter } from 'next/router' -import { observer } from 'mobx-react-lite' - -import { useStore } from 'hooks' -import Table from 'components/to-be-cleaned/Table' - -interface Props { - function: any -} - -const FunctionsListItem: FC = ({ function: item }) => { - const { ui } = useStore() - const is_functionConfirmed = item.email_confirmed_at || item.phone_confirmed_at - - const router = useRouter() - const ref = ui?.selectedProject?.ref - - // get the .co or .net TLD from the restUrl - const restUrl = ui.selectedProject?.restUrl - const restUrlTld = new URL(restUrl as string).hostname.split('.').pop() - - return ( - { - router.push(`/project/${ref}/functions/${item.id}`) - }} - > - -
- {item.name} -
-
- -
- {`https://${ref}.functions.supabase.${restUrlTld}/${item.slug}`} - {`/${item.name}`} -
-
- - - {dayjs(item.created_at).format('DD MMM, YYYY HH:mm')} - - - - - {dayjs(item.updated_at).format('DD MMM, YYYY HH:mm')} - - - - v{item.version} - - - {item.status} - -
- ) -} - -export default observer(FunctionsListItem) diff --git a/studio/components/interfaces/Functions/FunctionsNav.tsx b/studio/components/interfaces/Functions/FunctionsNav.tsx index dfe6bd5ab13..992a11474d0 100644 --- a/studio/components/interfaces/Functions/FunctionsNav.tsx +++ b/studio/components/interfaces/Functions/FunctionsNav.tsx @@ -1,8 +1,7 @@ import { Tabs } from 'ui' import { useRouter } from 'next/router' -// @ts-ignore -const FunctionsNav = ({ item }) => { +const FunctionsNav = ({ item }: any) => { const router = useRouter() const activeRoute = router.pathname.split('/')[5] const { ref } = router.query @@ -19,7 +18,6 @@ const FunctionsNav = ({ item }) => { > - {/* */} diff --git a/studio/components/interfaces/Functions/index.ts b/studio/components/interfaces/Functions/index.ts index e2d3d9dda40..af628ba5d81 100644 --- a/studio/components/interfaces/Functions/index.ts +++ b/studio/components/interfaces/Functions/index.ts @@ -1,6 +1,5 @@ -import EdgeFunctionsList from './EdgeFunctionsList' -import FunctionsListItem from './FunctionsListItem' +import EdgeFunctionDetails from './EdgeFunctionDetails' import FunctionsEmptyState from './FunctionsEmptyState' import TerminalInstructions from './TerminalInstructions' -export { EdgeFunctionsList, FunctionsListItem, FunctionsEmptyState, TerminalInstructions } +export { EdgeFunctionDetails, FunctionsEmptyState, TerminalInstructions } diff --git a/studio/components/layouts/FunctionsLayout.tsx b/studio/components/layouts/FunctionsLayout.tsx index 67d65717dea..0dc242cd836 100644 --- a/studio/components/layouts/FunctionsLayout.tsx +++ b/studio/components/layouts/FunctionsLayout.tsx @@ -106,6 +106,26 @@ const FunctionsLayout: FC = ({ title, children }) => {

Beta

+ {name && ( +
+ + + + + +
{name}
+
+ )}
- - {/* {name && ( - <> - - - - - -
{name}
- - )} */} {item && } diff --git a/studio/data/edge-functions/edge-functions-delete-mutation.ts b/studio/data/edge-functions/edge-functions-delete-mutation.ts new file mode 100644 index 00000000000..23552bb4e48 --- /dev/null +++ b/studio/data/edge-functions/edge-functions-delete-mutation.ts @@ -0,0 +1,43 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { delete_ } from 'lib/common/fetch' +import { API_ADMIN_URL } from 'lib/constants' +import { edgeFunctionsKeys } from './keys' + +export type EdgeFunctionsDeleteVariables = { + projectRef: string + slug: string +} + +export async function deleteEdgeFunction({ projectRef, slug }: EdgeFunctionsDeleteVariables) { + if (!projectRef) throw new Error('projectRef is required') + + const response = await delete_(`${API_ADMIN_URL}/projects/${projectRef}/functions/${slug}`, {}) + if (response.error) { + throw response.error + } + + return response +} + +type EdgeFunctionsDeleteData = Awaited> + +export const useEdgeFunctionDeleteMutation = ({ + onSuccess, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation( + (vars) => deleteEdgeFunction(vars), + { + async onSuccess(data, variables, context) { + await queryClient.invalidateQueries(edgeFunctionsKeys.list('edge-functions')) + await onSuccess?.(data, variables, context) + }, + ...options, + } + ) +} diff --git a/studio/data/edge-functions/edge-functions-query.ts b/studio/data/edge-functions/edge-functions-query.ts new file mode 100644 index 00000000000..28bc3f8c649 --- /dev/null +++ b/studio/data/edge-functions/edge-functions-query.ts @@ -0,0 +1,57 @@ +import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' +import { get } from 'lib/common/fetch' +import { API_ADMIN_URL } from 'lib/constants' +import { useCallback } from 'react' +import { edgeFunctionsKeys } from './keys' + +export type EdgeFunctionsVariables = { projectRef?: string } + +export type EdgeFunctionsResponse = { + id: string + name: string + slug: string + status: string + version: number + created_at: number + updated_at: number + verify_jwt: boolean + import_map: boolean +} + +export async function getEdgeFunctions( + { projectRef }: EdgeFunctionsVariables, + signal?: AbortSignal +) { + if (!projectRef) throw new Error('projectRef is required') + + const response = await get(`${API_ADMIN_URL}/projects/${projectRef}/functions`, { + signal, + }) + if (response.error) throw response.error + return response as EdgeFunctionsResponse[] +} + +export type EdgeFunctionsData = Awaited> +export type EdgeFunctionsError = unknown + +export const useEdgeFunctionsQuery = ( + { projectRef }: EdgeFunctionsVariables, + { enabled = true, ...options }: UseQueryOptions = {} +) => + useQuery( + edgeFunctionsKeys.list(projectRef), + ({ signal }) => getEdgeFunctions({ projectRef }, signal), + { enabled: enabled && typeof projectRef !== 'undefined', ...options } + ) + +export const useEdgeFunctionsPrefetch = ({ projectRef }: EdgeFunctionsVariables) => { + const client = useQueryClient() + + return useCallback(() => { + if (projectRef) { + client.prefetchQuery(edgeFunctionsKeys.list(projectRef), ({ signal }) => + getEdgeFunctions({ projectRef }, signal) + ) + } + }, [projectRef]) +} diff --git a/studio/data/edge-functions/keys.ts b/studio/data/edge-functions/keys.ts new file mode 100644 index 00000000000..dacd8d4a8dd --- /dev/null +++ b/studio/data/edge-functions/keys.ts @@ -0,0 +1,3 @@ +export const edgeFunctionsKeys = { + list: (projectRef: string | undefined) => ['projects', projectRef, 'edge-functions'] as const, +} diff --git a/studio/pages/project/[ref]/functions/[id]/details.tsx b/studio/pages/project/[ref]/functions/[id]/details.tsx index 613833e4f31..debcd5ebe99 100644 --- a/studio/pages/project/[ref]/functions/[id]/details.tsx +++ b/studio/pages/project/[ref]/functions/[id]/details.tsx @@ -1,244 +1,21 @@ -import dayjs from 'dayjs' -import { useEffect, useState } from 'react' import { observer } from 'mobx-react-lite' -import { IconGlobe, IconTerminal } from 'ui' import { PermissionAction } from '@supabase/shared-types/out/constants' import { NextPageWithLayout } from 'types' -import { checkPermissions, useParams, useStore } from 'hooks' -import { useProjectApiQuery } from 'data/config/project-api-query' +import { checkPermissions, useParams } from 'hooks' import FunctionsLayout from 'components/layouts/FunctionsLayout' -import CommandRender from 'components/interfaces/Functions/CommandRender' import NoPermission from 'components/ui/NoPermission' +import { EdgeFunctionDetails } from 'components/interfaces/Functions' const PageLayout: NextPageWithLayout = () => { - const { ref: projectRef, id } = useParams() - - const { functions, ui } = useStore() - - const [selectedFunction, setSelectedFunction] = useState(null) - - useEffect(() => { - setSelectedFunction(functions.byId(id)) - }, [functions.isLoaded, ui.selectedProject]) - - const { data: settings } = useProjectApiQuery({ - projectRef, - }) - - // get the .co or .net TLD from the restUrl - const restUrl = ui.selectedProject?.restUrl - const restUrlTld = new URL(restUrl as string).hostname.split('.').pop() - - const managementCommands: any = [ - { - command: `supabase functions deploy ${selectedFunction?.slug}`, - description: 'This will overwrite the deployed function with your new function', - jsx: () => { - return ( - <> - supabase functions deploy{' '} - {selectedFunction?.slug} - - ) - }, - comment: 'Deploy a new version', - }, - { - command: `supabase functions delete ${selectedFunction?.slug}`, - description: 'This will remove the function and all the logs associated with it', - jsx: () => { - return ( - <> - supabase functions delete{' '} - {selectedFunction?.slug} - - ) - }, - comment: 'Delete the function', - }, - ] - - const secretCommands: any = [ - { - command: `supabase secrets list`, - description: 'This will list all the secrets for your project', - jsx: () => { - return ( - <> - supabase secrets list - - ) - }, - comment: 'View all secrets', - }, - { - command: `supabase secrets set NAME1=VALUE1 NAME2=VALUE2`, - description: 'This will set secrets for your project', - jsx: () => { - return ( - <> - supabase secrets set NAME1=VALUE1 NAME2=VALUE2 - - ) - }, - comment: 'Set secrets for your project', - }, - { - command: `supabase secrets unset NAME1 NAME2 `, - description: 'This will delete secrets for your project', - jsx: () => { - return ( - <> - supabase secrets unset NAME1 NAME2 - - ) - }, - comment: 'Unset secrets for your project', - }, - ] - - // Get the API service - const apiService = settings?.autoApiService - const anonKey = apiService?.service_api_keys.find((x) => x.name === 'anon key') - ? apiService.defaultApiKey - : undefined - - const endpoint = apiService?.app_config.endpoint ?? '' - const endpointSections = endpoint.split('.') - const functionsEndpoint = [ - ...endpointSections.slice(0, 1), - 'functions', - ...endpointSections.slice(1), - ].join('.') - - const invokeCommands: any = [ - { - command: `curl -L -X POST 'https://${projectRef}.functions.supabase.${restUrlTld}/${ - selectedFunction?.slug - }' -H 'Authorization: Bearer ${anonKey ?? '[YOUR ANON KEY]'}' --data '{"name":"Functions"}'`, - description: 'Invokes the hello function', - jsx: () => { - return ( - <> - curl -L -X POST 'https://{functionsEndpoint}/ - {selectedFunction?.slug}' -H 'Authorization: Bearer [YOUR ANON KEY]'{' '} - {`--data '{"name":"Functions"}'`} - - ) - }, - comment: 'Invoke your function', - }, - ] + const { id } = useParams() const canReadFunction = checkPermissions(PermissionAction.FUNCTIONS_READ, id as string) if (!canReadFunction) { return } - return ( -
-
-
-
-
- Function Name -
{selectedFunction?.name}
-
- -
- Status -
-
-
- {selectedFunction?.status} -
- - - - -
-
-
-
-
-
- -
- Endpoint URL -
- {`https://${projectRef}.functions.supabase.co/${selectedFunction?.slug}`} -
-
- -
- Created At -
- {selectedFunction?.created_at && - dayjs(selectedFunction.created_at).format('dddd, MMMM D, YYYY h:mm A')} -
-
- -
- Updated At -
- {selectedFunction?.updated_at && - dayjs(selectedFunction.updated_at).format('dddd, MMMM D, YYYY h:mm A')} -
-
- -
- Version -
v{selectedFunction?.version}
-
- -
- Regions -
-
- - Earth -
- All functions are deployed globally -
-
-
-
- -
-
-
-
- -
-

Command line access

-
- -
Deployment management
- -
Invoke
- -
Secrets management
- -
-
-
- ) + return } PageLayout.getLayout = (page) => {page} diff --git a/studio/pages/project/[ref]/functions/index.tsx b/studio/pages/project/[ref]/functions/index.tsx index 7848cff03fe..480e1700d87 100644 --- a/studio/pages/project/[ref]/functions/index.tsx +++ b/studio/pages/project/[ref]/functions/index.tsx @@ -1,16 +1,33 @@ import { observer } from 'mobx-react-lite' -import { useStore } from 'hooks' +import { useParams } from 'hooks' import { NextPageWithLayout } from 'types' import FunctionsLayout from 'components/layouts/FunctionsLayout' -import { EdgeFunctionsList, FunctionsEmptyState } from 'components/interfaces/Functions' +import { FunctionsEmptyState } from 'components/interfaces/Functions' +import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query' +import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import EdgeFunctionsRow from 'components/interfaces/Functions/EdgeFunctionsRow' const PageLayout: NextPageWithLayout = () => { - const { functions } = useStore() - const hasFunctions = functions.list().length > 0 + const { ref } = useParams() + const { isLoading, data: functions } = useEdgeFunctionsQuery({ projectRef: ref }) + + const hasFunctions = (functions ?? []).length > 0 + + if (isLoading || functions === undefined) { + return ( +
+ + + +
+ ) + } return hasFunctions ? (
- + {(functions ?? []).map((fn) => ( + + ))}
) : ( From 3873db7da4e1c7ac8711b8ff694768c01e9fea65 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 20 Feb 2023 12:02:43 +0800 Subject: [PATCH 16/75] Add comment --- studio/components/interfaces/Functions/EdgeFunctionDetails.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx b/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx index 4f25d522982..80b929e7324 100644 --- a/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx +++ b/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx @@ -13,6 +13,8 @@ import { useEdgeFunctionDeleteMutation } from 'data/edge-functions/edge-function interface Props {} +// [Joshen] Next - additional configs: Verify jwt + import maps + const EdgeFunctionDetails: FC = () => { const router = useRouter() const { functions, ui } = useStore() From 0247e1b8c32cd272696e57ba2e1fce25fdaa594b Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 20 Feb 2023 12:03:29 +0800 Subject: [PATCH 17/75] Fix typo --- studio/components/interfaces/Functions/EdgeFunctionDetails.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx b/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx index 80b929e7324..72b73e3e117 100644 --- a/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx +++ b/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx @@ -59,7 +59,7 @@ const EdgeFunctionDetails: FC = () => { await deleteEdgeFunction({ projectRef, slug: selectedFunction.slug }) ui.setNotification({ category: 'success', - message: `Succesfully deleted "${selectedFunction.name}"`, + message: `Successfully deleted "${selectedFunction.name}"`, }) router.push(`/project/${projectRef}/functions`) } catch (error: any) { From dbebd83937c8623df0bda4c50dd6c5a1cdd5f394 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 20 Feb 2023 14:21:51 +0800 Subject: [PATCH 18/75] Small style fix --- studio/components/interfaces/Functions/EdgeFunctionsRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/studio/components/interfaces/Functions/EdgeFunctionsRow.tsx b/studio/components/interfaces/Functions/EdgeFunctionsRow.tsx index 185f01348e3..e9df8387450 100644 --- a/studio/components/interfaces/Functions/EdgeFunctionsRow.tsx +++ b/studio/components/interfaces/Functions/EdgeFunctionsRow.tsx @@ -26,7 +26,7 @@ const EdgeFunctionsRow: FC = ({ fn }) => { key={fn.id} className={[ 'border border-scale-500 border-t-0 first:rounded-t first:border-t last:rounded-b', - 'bg-scale-100 dark:bg-scale-300 py-6 px-6 flex items-center justify-between', + 'bg-scale-100 dark:bg-scale-300 py-4 px-6 flex items-center justify-between', 'hover:border-t hover:-mt-[1px] first:!mt-0 cursor-pointer', 'hover:bg-scale-200 dark:hover:bg-scale-500 hover:border-scale-500 dark:hover:border-scale-700', ].join(' ')} From 7d4721f3e297e09fcd772099cffdd75e340012f4 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 20 Feb 2023 14:48:27 +0800 Subject: [PATCH 19/75] Fixes based on feedback --- .../Functions/EdgeFunctionDetails.tsx | 52 +++++++++++++++---- .../interfaces/Functions/EdgeFunctionsRow.tsx | 2 +- .../edge-functions-delete-mutation.ts | 2 +- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx b/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx index 72b73e3e117..f2f21e65c04 100644 --- a/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx +++ b/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx @@ -3,7 +3,17 @@ import { useRouter } from 'next/router' import { FC, useState, useEffect } from 'react' import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { Alert, IconGlobe, IconTerminal, IconMinimize2, IconMaximize2, Button, Modal } from 'ui' +import { + Alert, + IconGlobe, + IconTerminal, + IconMinimize2, + IconMaximize2, + IconCheck, + IconClipboard, + Button, + Modal, +} from 'ui' import { useStore, useParams, checkPermissions } from 'hooks' import Panel from 'components/ui/Panel' @@ -19,6 +29,7 @@ const EdgeFunctionDetails: FC = () => { const router = useRouter() const { functions, ui } = useStore() const { ref: projectRef, id } = useParams() + const [isCopied, setIsCopied] = useState(false) const [showDeleteModal, setShowDeleteModal] = useState(false) const [showInstructions, setShowInstructions] = useState(false) const [selectedFunction, setSelectedFunction] = useState(null) @@ -31,11 +42,6 @@ const EdgeFunctionDetails: FC = () => { }, [functions.isLoaded, ui.selectedProject]) const { data: settings } = useProjectApiQuery({ projectRef }) - const functionUrl = `https://${projectRef}.functions.supabase.co/${selectedFunction?.slug}` - - // get the .co or .net TLD from the restUrl - const restUrl = ui.selectedProject?.restUrl - const restUrlTld = new URL(restUrl as string).hostname.split('.').pop() // Get the API service const apiService = settings?.autoApiService @@ -50,6 +56,7 @@ const EdgeFunctionDetails: FC = () => { 'functions', ...endpointSections.slice(1), ].join('.') + const functionUrl = `https://${functionsEndpoint}/${selectedFunction?.slug}` const onConfirmDelete = async () => { if (!projectRef) return console.error('Project ref is required') @@ -140,9 +147,9 @@ const EdgeFunctionDetails: FC = () => { const invokeCommands: any = [ { - command: `curl -L -X POST 'https://${projectRef}.functions.supabase.${restUrlTld}/${ - selectedFunction?.slug - }' -H 'Authorization: Bearer ${anonKey ?? '[YOUR ANON KEY]'}' --data '{"name":"Functions"}'`, + command: `curl -L -X POST '${functionUrl}' -H 'Authorization: Bearer ${ + anonKey ?? '[YOUR ANON KEY]' + }' --data '{"name":"Functions"}'`, description: 'Invokes the hello function', jsx: () => { return ( @@ -169,6 +176,33 @@ const EdgeFunctionDetails: FC = () => {

Endpoint URL

{functionUrl}

+
diff --git a/studio/components/interfaces/Functions/EdgeFunctionsRow.tsx b/studio/components/interfaces/Functions/EdgeFunctionsRow.tsx index e9df8387450..a263b6aff26 100644 --- a/studio/components/interfaces/Functions/EdgeFunctionsRow.tsx +++ b/studio/components/interfaces/Functions/EdgeFunctionsRow.tsx @@ -58,7 +58,7 @@ const EdgeFunctionsRow: FC = ({ fn }) => { ) : (
- +
)} diff --git a/studio/data/edge-functions/edge-functions-delete-mutation.ts b/studio/data/edge-functions/edge-functions-delete-mutation.ts index 23552bb4e48..f37faa08a37 100644 --- a/studio/data/edge-functions/edge-functions-delete-mutation.ts +++ b/studio/data/edge-functions/edge-functions-delete-mutation.ts @@ -34,7 +34,7 @@ export const useEdgeFunctionDeleteMutation = ({ (vars) => deleteEdgeFunction(vars), { async onSuccess(data, variables, context) { - await queryClient.invalidateQueries(edgeFunctionsKeys.list('edge-functions')) + await queryClient.invalidateQueries(edgeFunctionsKeys.list(variables.projectRef)) await onSuccess?.(data, variables, context) }, ...options, From 3a5f555426885d9f85f534f43fbe041f38b4126d Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 20 Feb 2023 15:37:57 +0800 Subject: [PATCH 20/75] Fix conflict --- .../Settings/Database/SSLConfiguration.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx b/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx index f5e57942243..20935a97d97 100644 --- a/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx +++ b/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx @@ -1,16 +1,10 @@ import Link from 'next/link' import { useState, useEffect } from 'react' -<<<<<<< HEAD -import { checkPermissions, useParams, useStore } from 'hooks' -import * as Tooltip from '@radix-ui/react-tooltip' -import { Button, IconDownload, Toggle, IconExternalLink, IconLoader, Alert } from 'ui' -import { PermissionAction } from '@supabase/shared-types/out/constants' - -======= -import { useParams, useStore, useFlag } from 'hooks' import * as Tooltip from '@radix-ui/react-tooltip' import { Button, IconDownload, Toggle, IconLoader, Alert } from 'ui' ->>>>>>> bb5d57da53dbdacb4c51df21a8c357d864ba7a0e +import { PermissionAction } from '@supabase/shared-types/out/constants' + +import { checkPermissions, useParams, useStore, useFlag } from 'hooks' import { FormHeader, FormPanel, From f4e69433ee66bc87b88f598553f6afdaa03043ec Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 20 Feb 2023 17:11:17 +0800 Subject: [PATCH 21/75] Remove unnecessary change in storage explorer store --- studio/localStores/storageExplorer/StorageExplorerStore.js | 1 - 1 file changed, 1 deletion(-) diff --git a/studio/localStores/storageExplorer/StorageExplorerStore.js b/studio/localStores/storageExplorer/StorageExplorerStore.js index 6fee62250c0..77d26ceda79 100644 --- a/studio/localStores/storageExplorer/StorageExplorerStore.js +++ b/studio/localStores/storageExplorer/StorageExplorerStore.js @@ -44,7 +44,6 @@ const EMPTY_FOLDER_PLACEHOLDER_FILE_NAME = '.emptyFolderPlaceholder' const STORAGE_PROGRESS_INFO_TEXT = "Please do not close the browser until it's completed" class StorageExplorerStore { - isReadOnly = false projectRef = '' loaded = false view = STORAGE_VIEWS.COLUMNS From cb49607cd89efc11454edd2f3753ff75619c330c Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 20 Feb 2023 21:12:51 +0800 Subject: [PATCH 22/75] Reinstate table component for functions list --- ...tionsRow.tsx => EdgeFunctionsListItem.tsx} | 78 +++++++++---------- .../components/interfaces/Functions/index.ts | 3 +- .../pages/project/[ref]/functions/index.tsx | 34 ++++++-- 3 files changed, 64 insertions(+), 51 deletions(-) rename studio/components/interfaces/Functions/{EdgeFunctionsRow.tsx => EdgeFunctionsListItem.tsx} (54%) diff --git a/studio/components/interfaces/Functions/EdgeFunctionsRow.tsx b/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx similarity index 54% rename from studio/components/interfaces/Functions/EdgeFunctionsRow.tsx rename to studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx index a263b6aff26..0f8a00ecb8b 100644 --- a/studio/components/interfaces/Functions/EdgeFunctionsRow.tsx +++ b/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx @@ -1,41 +1,44 @@ import dayjs from 'dayjs' -import { useRouter } from 'next/router' import { FC, useState } from 'react' +import { useRouter } from 'next/router' +import { observer } from 'mobx-react-lite' +import { IconCheck, IconClipboard } from 'ui' import * as Tooltip from '@radix-ui/react-tooltip' -import { IconUpload, IconCheck, IconClipboard } from 'ui' import { useParams, useStore } from 'hooks' +import Table from 'components/to-be-cleaned/Table' import { EdgeFunctionsResponse } from 'data/edge-functions/edge-functions-query' interface Props { - fn: EdgeFunctionsResponse + function: EdgeFunctionsResponse } -const EdgeFunctionsRow: FC = ({ fn }) => { +const EdgeFunctionsListItem: FC = ({ function: item }) => { const router = useRouter() - const { ref } = useParams() const { ui } = useStore() + const { ref } = useParams() const [isCopied, setIsCopied] = useState(false) + // get the .co or .net TLD from the restUrl const restUrl = ui.selectedProject?.restUrl const restUrlTld = new URL(restUrl as string).hostname.split('.').pop() - const functionUrl = `https://${ref}.functions.supabase.${restUrlTld}/${fn.slug}` + const functionUrl = `https://${ref}.functions.supabase.${restUrlTld}/${item.slug}` return ( -
router.push(`/project/${ref}/functions/${fn.id}`)} + { + router.push(`/project/${ref}/functions/${item.id}`) + }} > -
-

{fn.name}

-
-

{functionUrl}

+ +
+

{item.name}

+
+
+ +
+

{functionUrl}

-
-
- - -

Deployment {fn.version}

-
- - -
- Deployment history coming soon -
-
-
+ + +

{dayjs(item.created_at).format('DD MMM, YYYY HH:mm')}

+
+
- -

{dayjs(fn.updated_at).fromNow()}

+

{dayjs(item.updated_at).fromNow()}

@@ -98,14 +87,17 @@ const EdgeFunctionsRow: FC = ({ fn }) => { ].join(' ')} > - Last updated on {dayjs(fn.updated_at).format('DD MMM, YYYY HH:mm')} + Last updated on {dayjs(item.updated_at).format('DD MMM, YYYY HH:mm')}
-
-
+ + +

{item.version}

+
+ ) } -export default EdgeFunctionsRow +export default observer(EdgeFunctionsListItem) diff --git a/studio/components/interfaces/Functions/index.ts b/studio/components/interfaces/Functions/index.ts index af628ba5d81..003d5feaca6 100644 --- a/studio/components/interfaces/Functions/index.ts +++ b/studio/components/interfaces/Functions/index.ts @@ -1,5 +1,6 @@ import EdgeFunctionDetails from './EdgeFunctionDetails' +import EdgeFunctionsListItem from './EdgeFunctionsListItem' import FunctionsEmptyState from './FunctionsEmptyState' import TerminalInstructions from './TerminalInstructions' -export { EdgeFunctionDetails, FunctionsEmptyState, TerminalInstructions } +export { EdgeFunctionDetails, EdgeFunctionsListItem, FunctionsEmptyState, TerminalInstructions } diff --git a/studio/pages/project/[ref]/functions/index.tsx b/studio/pages/project/[ref]/functions/index.tsx index 480e1700d87..ae427eed957 100644 --- a/studio/pages/project/[ref]/functions/index.tsx +++ b/studio/pages/project/[ref]/functions/index.tsx @@ -1,11 +1,12 @@ import { observer } from 'mobx-react-lite' + import { useParams } from 'hooks' import { NextPageWithLayout } from 'types' +import Table from 'components/to-be-cleaned/Table' import FunctionsLayout from 'components/layouts/FunctionsLayout' -import { FunctionsEmptyState } from 'components/interfaces/Functions' -import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query' +import { EdgeFunctionsListItem, FunctionsEmptyState } from 'components/interfaces/Functions' import ShimmeringLoader from 'components/ui/ShimmeringLoader' -import EdgeFunctionsRow from 'components/interfaces/Functions/EdgeFunctionsRow' +import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query' const PageLayout: NextPageWithLayout = () => { const { ref } = useParams() @@ -24,10 +25,29 @@ const PageLayout: NextPageWithLayout = () => { } return hasFunctions ? ( -
- {(functions ?? []).map((fn) => ( - - ))} +
+
+ {`${functions.length} function${ + functions.length > 1 ? 's' : '' + } deployed`} +
+
+ Name + URL + Created + Last updated + Deployment + + } + body={ + <> + {functions.length > 0 && + functions.map((item: any) => )} + + } + /> ) : ( From 986b529625e665dd037cced0cf588146e595b417 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 20 Feb 2023 21:33:25 +0800 Subject: [PATCH 23/75] Fix from feedback --- .../interfaces/Database/Wrappers/WrappersDropdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx b/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx index affb01fb608..76a5ce5b6b4 100644 --- a/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx +++ b/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx @@ -16,7 +16,7 @@ interface Props { const WrapperDropdown: FC = ({ buttonText = 'Add wrapper', align = 'end' }) => { const { ref } = useParams() - const canManageWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') + const canManageWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'wrappers') if (!canManageWrappers) { return ( From c9a6695c7023f1da510353cd41238172c47a7c73 Mon Sep 17 00:00:00 2001 From: JD <31992827+jdshaeffer@users.noreply.github.com> Date: Mon, 20 Feb 2023 09:38:01 -0500 Subject: [PATCH 24/75] fix: update sveltekit tutorial - This small change from dynamic public env var to static allows the env vars to be used for deployment, particularly in Vercel. - For some reason, when deploying to Vercel and using `$env/dynamic/public`, the env vars will return `undefined` and you'll get an error when the Supabase client attempts to startup: `supaBase url is required` - This change makes a successful client connection to Supabase --- .../pages/guides/getting-started/tutorials/with-sveltekit.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-sveltekit.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-sveltekit.mdx index 3be4542731a..1daf9a881ec 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-sveltekit.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-sveltekit.mdx @@ -50,9 +50,9 @@ These variables will be exposed on the browser, and that's completely fine since ```js title=src/lib/supabaseClient.ts import { createClient } from '@supabase/auth-helpers-sveltekit' -import { env } from '$env/dynamic/public' +import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public' -export const supabase = createClient(env.PUBLIC_SUPABASE_URL, env.PUBLIC_SUPABASE_ANON_KEY) +export const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY) ``` Optionally, update `src/routes/styles.css` with the [CSS from the example](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/svelte-user-management/src/app.css). From e8c834b82b88e03ecd08225d61717322b34ddfd7 Mon Sep 17 00:00:00 2001 From: Dhanush P Sukruth Date: Thu, 16 Feb 2023 13:52:26 +0530 Subject: [PATCH 25/75] Fix #12466 Adding viewport must solve mobile usability issues --- examples/user-management/sveltekit-user-management/src/app.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/user-management/sveltekit-user-management/src/app.html b/examples/user-management/sveltekit-user-management/src/app.html index b555f2469a3..8ce87909fb7 100644 --- a/examples/user-management/sveltekit-user-management/src/app.html +++ b/examples/user-management/sveltekit-user-management/src/app.html @@ -3,7 +3,7 @@ - + %sveltekit.head% From 4e3e3d7bb5c127a02c996a1347628ae58a19ec6a Mon Sep 17 00:00:00 2001 From: Dhanush P Sukruth Date: Fri, 17 Feb 2023 02:00:20 +0530 Subject: [PATCH 26/75] Fix #12466 added viewport for mobile usability problem. --- apps/docs/components/DocSearch.tsx | 1 + apps/docs/components/Favicons.tsx | 1 + apps/docs/layouts/DefaultLayout.tsx | 2 +- apps/docs/layouts/tutorials/TutorialLayout.tsx | 2 +- apps/docs/pages/_document.tsx | 4 +++- apps/www/pages/_app.tsx | 2 +- apps/www/pages/beta.tsx | 1 + 7 files changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/docs/components/DocSearch.tsx b/apps/docs/components/DocSearch.tsx index 7ff1a5ece2a..e956935ac50 100644 --- a/apps/docs/components/DocSearch.tsx +++ b/apps/docs/components/DocSearch.tsx @@ -48,6 +48,7 @@ export function SearchProvider({ children }: any) { <> + { {/* */} + diff --git a/apps/docs/layouts/DefaultLayout.tsx b/apps/docs/layouts/DefaultLayout.tsx index 3e610896b8b..783ed7f7187 100644 --- a/apps/docs/layouts/DefaultLayout.tsx +++ b/apps/docs/layouts/DefaultLayout.tsx @@ -21,7 +21,7 @@ const Layout: FC = (props: Props) => { {props.meta?.title} | Supabase Docs - + diff --git a/apps/docs/layouts/tutorials/TutorialLayout.tsx b/apps/docs/layouts/tutorials/TutorialLayout.tsx index fc1a2fa8ea9..2f58a612cca 100644 --- a/apps/docs/layouts/tutorials/TutorialLayout.tsx +++ b/apps/docs/layouts/tutorials/TutorialLayout.tsx @@ -50,7 +50,7 @@ const Layout: FC = (props: Props) => { {props.meta?.title} | Supabase - + diff --git a/apps/docs/pages/_document.tsx b/apps/docs/pages/_document.tsx index 98acbb79912..d7c31f22258 100644 --- a/apps/docs/pages/_document.tsx +++ b/apps/docs/pages/_document.tsx @@ -3,7 +3,9 @@ import { Html, Head, Main, NextScript } from 'next/document' export default function Document() { return ( - + + +
diff --git a/apps/www/pages/_app.tsx b/apps/www/pages/_app.tsx index b4fb7a031ce..9698060202f 100644 --- a/apps/www/pages/_app.tsx +++ b/apps/www/pages/_app.tsx @@ -38,7 +38,7 @@ export default function MyApp({ Component, pageProps }: AppProps) { return ( <> - + { {site_title} + From 961faf1035fc8360fad47b73cd12cda501e1d621 Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Sun, 19 Feb 2023 17:31:04 +0100 Subject: [PATCH 27/75] Update apps/docs/components/DocSearch.tsx --- apps/docs/components/DocSearch.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/docs/components/DocSearch.tsx b/apps/docs/components/DocSearch.tsx index e956935ac50..7ff1a5ece2a 100644 --- a/apps/docs/components/DocSearch.tsx +++ b/apps/docs/components/DocSearch.tsx @@ -48,7 +48,6 @@ export function SearchProvider({ children }: any) { <> - Date: Sun, 19 Feb 2023 17:31:28 +0100 Subject: [PATCH 28/75] Update apps/docs/components/Favicons.tsx --- apps/docs/components/Favicons.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/docs/components/Favicons.tsx b/apps/docs/components/Favicons.tsx index cb88257b6d9..5c65346f0fa 100644 --- a/apps/docs/components/Favicons.tsx +++ b/apps/docs/components/Favicons.tsx @@ -25,7 +25,6 @@ const Favicons = () => { {/* */} - From 6642d6446cc02796d36112ea3db7934cd97204a1 Mon Sep 17 00:00:00 2001 From: egor-romanov Date: Mon, 20 Feb 2023 19:50:01 +0300 Subject: [PATCH 29/75] fix prettier errors --- apps/docs/layouts/DefaultLayout.tsx | 2 +- apps/docs/pages/_document.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/layouts/DefaultLayout.tsx b/apps/docs/layouts/DefaultLayout.tsx index 783ed7f7187..0a6b48c5547 100644 --- a/apps/docs/layouts/DefaultLayout.tsx +++ b/apps/docs/layouts/DefaultLayout.tsx @@ -21,7 +21,7 @@ const Layout: FC = (props: Props) => { {props.meta?.title} | Supabase Docs - + diff --git a/apps/docs/pages/_document.tsx b/apps/docs/pages/_document.tsx index d7c31f22258..ed9a8a70c10 100644 --- a/apps/docs/pages/_document.tsx +++ b/apps/docs/pages/_document.tsx @@ -4,7 +4,7 @@ export default function Document() { return ( - +
From 4918207adf74be1a46c8fb6eaaab772e2c1f7c34 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 21 Feb 2023 12:03:20 +0800 Subject: [PATCH 30/75] Clicking into edge functions will lead to details page --- studio/components/interfaces/Functions/EdgeFunctionDetails.tsx | 2 +- .../components/interfaces/Functions/EdgeFunctionsListItem.tsx | 2 +- studio/components/interfaces/Functions/FunctionsNav.tsx | 2 +- studio/pages/project/[ref]/functions/index.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx b/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx index f2f21e65c04..42b4986fc64 100644 --- a/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx +++ b/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx @@ -222,7 +222,7 @@ const EdgeFunctionDetails: FC = () => {
- Deployment + Deployments
{selectedFunction?.version}
diff --git a/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx b/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx index 0f8a00ecb8b..0d9ce1f727b 100644 --- a/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx +++ b/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx @@ -28,7 +28,7 @@ const EdgeFunctionsListItem: FC = ({ function: item }) => { { - router.push(`/project/${ref}/functions/${item.id}`) + router.push(`/project/${ref}/functions/${item.id}/details`) }} > diff --git a/studio/components/interfaces/Functions/FunctionsNav.tsx b/studio/components/interfaces/Functions/FunctionsNav.tsx index 992a11474d0..af24e28d29f 100644 --- a/studio/components/interfaces/Functions/FunctionsNav.tsx +++ b/studio/components/interfaces/Functions/FunctionsNav.tsx @@ -16,8 +16,8 @@ const FunctionsNav = ({ item }: any) => { router.push(`/project/${ref}/functions/${item.id}/${e === 'metrics' ? '' : e}`) } > - + diff --git a/studio/pages/project/[ref]/functions/index.tsx b/studio/pages/project/[ref]/functions/index.tsx index ae427eed957..e78b41e0996 100644 --- a/studio/pages/project/[ref]/functions/index.tsx +++ b/studio/pages/project/[ref]/functions/index.tsx @@ -38,7 +38,7 @@ const PageLayout: NextPageWithLayout = () => { URL Created Last updated - Deployment + Deployments } body={ From 9e7623eb1773847a5490245a20f05fc013874f49 Mon Sep 17 00:00:00 2001 From: Isaiah Hamilton Date: Mon, 20 Feb 2023 23:11:05 -0500 Subject: [PATCH 31/75] fix: docs sidebar --- .../Navigation/NavigationMenu/NavigationMenuGuideList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx index 7e52d424832..1223868ffb7 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx @@ -38,7 +38,7 @@ const NavigationMenuGuideList: React.FC = ({ id, active }) => { // disabled // level !== 'home' && level !== id ? '-ml-8' : '', - !active ? 'opacity-0 invisible absolute' : '', + !active ? 'opacity-0 hidden absolute' : '', ].join(' ')} > From 33f1c837107da1ae3e0c486d44320584850ed487 Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Tue, 21 Feb 2023 14:23:10 +0800 Subject: [PATCH 32/75] Update NavigationMenuGuideList.tsx --- .../Navigation/NavigationMenu/NavigationMenuGuideList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx index 1223868ffb7..c55ec3b6f0d 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx @@ -38,7 +38,7 @@ const NavigationMenuGuideList: React.FC = ({ id, active }) => { // disabled // level !== 'home' && level !== id ? '-ml-8' : '', - !active ? 'opacity-0 hidden absolute' : '', + !active ? 'opacity-0 invisible absolute h-0 overflow-hidden' : '', ].join(' ')} > From ddb0a33c0d1375d943ce17f3b2ce27814f746011 Mon Sep 17 00:00:00 2001 From: thorwebdev Date: Tue, 21 Feb 2023 14:26:06 +0800 Subject: [PATCH 33/75] chore: add openai example. --- .../NavigationMenu.constants.ts | 2 + .../examples/connect-to-postgres.mdx | 1 + .../guides/functions/examples/openai.mdx | 84 ++++++++++++++++++ .../img/integrations/logos/openai_logo.png | Bin 0 -> 48063 bytes 4 files changed, 87 insertions(+) create mode 100644 apps/docs/pages/guides/functions/examples/openai.mdx create mode 100644 apps/docs/public/img/integrations/logos/openai_logo.png diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 7385afd2d3d..3a0cd161cca 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -555,6 +555,7 @@ export const functions = { }, { name: 'GitHub Actions', url: '/guides/functions/examples/github-actions', items: [] }, { name: 'OG Image', url: '/guides/functions/examples/og-image', items: [] }, + { name: 'OpenAI', url: '/guides/functions/examples/openai', items: [] }, { name: 'Storage Caching', url: '/guides/functions/examples/storage-caching', items: [] }, { name: 'Stripe Webhooks', url: '/guides/functions/examples/stripe-webhooks', items: [] }, { name: 'Telegram Bot', url: '/guides/functions/examples/telegram-bot', items: [] }, @@ -821,6 +822,7 @@ export const integrations = { url: undefined, items: [ { name: 'Estuary', url: '/guides/integrations/estuary', items: [] }, + { name: 'OpenAI', url: '/guides/functions/examples/openai', items: [] }, { name: 'pgMustard', url: '/guides/integrations/pgmustard', items: [] }, { name: 'Prisma', url: '/guides/integrations/prisma', items: [] }, { name: 'Sequin', url: '/guides/integrations/sequin', items: [] }, diff --git a/apps/docs/pages/guides/functions/examples/connect-to-postgres.mdx b/apps/docs/pages/guides/functions/examples/connect-to-postgres.mdx index 3b66a4affd9..de9f2378359 100644 --- a/apps/docs/pages/guides/functions/examples/connect-to-postgres.mdx +++ b/apps/docs/pages/guides/functions/examples/connect-to-postgres.mdx @@ -4,6 +4,7 @@ export const meta = { id: 'examples-postgres-on-the-edge', title: 'Connect to Postgres', description: 'Connecting to Postgres from Edge Functions.', + video: 'https://www.youtube.com/v/cl7EuF1-RsY', }
diff --git a/apps/docs/pages/guides/functions/examples/openai.mdx b/apps/docs/pages/guides/functions/examples/openai.mdx new file mode 100644 index 00000000000..bc24d6a151a --- /dev/null +++ b/apps/docs/pages/guides/functions/examples/openai.mdx @@ -0,0 +1,84 @@ +import Layout from '~/layouts/DefaultGuideLayout' + +export const meta = { + id: 'examples-openai', + title: 'OpenAI', + description: 'Using OpenAI in Edge Functions.', + video: 'https://www.youtube.com/v/29p8kIqyU_Y', +} + +
+ +
+ +Use the [OpenAI completions API](https://platform.openai.com/docs/api-reference/completions) in Supabase Edge Functions. + +```ts index.ts +import 'xhr_polyfill' +import { serve } from 'std/server' +import { CreateCompletionRequest } from 'openai' + +serve(async (req) => { + const { query } = await req.json() + + const completionConfig: CreateCompletionRequest = { + model: 'text-davinci-003', + prompt: query, + max_tokens: 256, + temperature: 0, + stream: true, + } + + return fetch('https://api.openai.com/v1/completions', { + method: 'POST', + headers: { + Authorization: `Bearer ${Deno.env.get('OPENAI_API_KEY')}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(completionConfig), + }) +}) +``` + +## Run locally + +```bash +supabase functions serve --env-file ./supabase/.env.local --no-verify-jwt +``` + +Use cURL or Postman to make a POST request to http://localhost:54321/functions/v1/openai. + +```bash +curl -i --location --request POST http://localhost:54321/functions/v1/openai \ + --header 'Content-Type: application/json' \ + --data '{"query":"What is Supabase?"}' +``` + +## Deploy + +```bash +supabase functions deploy --no-verify-jwt openai +supabase secrets set --env-file ./supabase/.env.local +``` + +## Go deeper + +If you're interesting in learning how to use this to build your own ChatGPT, read [the blog post](/blog/chatgpt-supabase-docs) and check out the video: + +
+ +
+ +export const Page = ({ children }) => + +export default Page diff --git a/apps/docs/public/img/integrations/logos/openai_logo.png b/apps/docs/public/img/integrations/logos/openai_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c5596878b0b39bd3dff02db3ad6c2733345d1f7d GIT binary patch literal 48063 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

)wPS90WhetGP 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 KC>JT3y!$_5swKk! literal 0 HcmV?d00001 From 805358d0da7b33139f50683145e2a3378a3e1d97 Mon Sep 17 00:00:00 2001 From: thorwebdev Date: Tue, 21 Feb 2023 14:56:05 +0800 Subject: [PATCH 34/75] chore: sync all examples. --- .../NavigationMenu.constants.ts | 1 + apps/docs/pages/guides/functions.mdx | 69 ++++++- apps/docs/pages/guides/functions/auth.mdx | 2 +- apps/docs/pages/guides/functions/cors.mdx | 2 +- .../examples/cloudflare-turnstile.mdx | 2 +- .../examples/connect-to-postgres.mdx | 2 +- .../guides/functions/examples/discord-bot.mdx | 178 ++++++++++++++++++ .../guides/functions/examples/og-image.mdx | 2 +- .../pages/guides/integrations/onesignal.mdx | 68 +++---- 9 files changed, 277 insertions(+), 49 deletions(-) create mode 100644 apps/docs/pages/guides/functions/examples/discord-bot.mdx diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 3a0cd161cca..88c2d614951 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -553,6 +553,7 @@ export const functions = { url: '/guides/functions/examples/connect-to-postgres', items: [], }, + { name: 'Discord Bot', url: '/guides/functions/examples/discord-bot', items: [] }, { name: 'GitHub Actions', url: '/guides/functions/examples/github-actions', items: [] }, { name: 'OG Image', url: '/guides/functions/examples/og-image', items: [] }, { name: 'OpenAI', url: '/guides/functions/examples/openai', items: [] }, diff --git a/apps/docs/pages/guides/functions.mdx b/apps/docs/pages/guides/functions.mdx index a382f7ccde1..b19ddb81872 100644 --- a/apps/docs/pages/guides/functions.mdx +++ b/apps/docs/pages/guides/functions.mdx @@ -43,12 +43,12 @@ export const examples = [ { name: 'With supabase-js', description: 'Use the Supabase client inside your Edge Function.', - href: 'https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts', + href: '/guides/functions/auth', }, { name: 'With CORS headers', description: 'Send CORS headers for invoking from the browser.', - href: 'https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/browser-with-cors/index.ts', + href: '/guides/functions/cors', }, { name: 'React Native with Stripe', @@ -86,6 +86,71 @@ export const examples = [ description: `Get user location data from user's IP address.`, href: 'https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/location', }, + { + name: 'Cloudflare Turnstile', + description: `Protecting Forms with Cloudflare Turnstile.`, + href: '/guides/functions/examples/cloudflare-turnstile', + }, + { + name: 'Connect to Postgres', + description: `Connecting to Postgres from Edge Functions.`, + href: '/guides/functions/examples/connect-to-postgres', + }, + { + name: 'Github Actions', + description: `Deploying Edge Functions with GitHub Actions.`, + href: '/guides/functions/examples/github-actions', + }, + { + name: 'Oak Server Middleware', + description: `Request Routing with Oak server middleware.`, + href: 'https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/oak-server', + }, + { + name: 'OpenAI', + description: `Using OpenAI in Edge Functions.`, + href: '/guides/functions/examples/openai', + }, + { + name: 'Stripe Webhooks', + description: `Handling signed Stripe Webhooks with Edge Functions.`, + href: '/guides/functions/examples/stripe-webhooks', + }, + { + name: 'Send emails', + description: `Send emails in Edge Functions.`, + href: 'https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/send-email-smtp', + }, + { + name: 'Web Stream', + description: `Server-Sent Events in Edge Functions.`, + href: 'https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/streams', + }, + { + name: 'Puppeteer', + description: `Generate screenshots with Puppeteer.`, + href: 'https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/puppeteer', + }, + { + name: 'Discord Bot', + description: `Building a Slash Command Discord Bot with Edge Functions.`, + href: '/guides/functions/examples/discord-bot', + }, + { + name: 'Telegram Bot', + description: `Building a Telegram Bot with Edge Functions.`, + href: '/guides/functions/examples/telegram-bot', + }, + { + name: 'Upload File', + description: `Process multipart/form-data.`, + href: 'https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/file-upload-storage', + }, + { + name: 'Upstash Redis', + description: `Build an Edge Functions Counter with Upstash Redis.`, + href: '/guides/functions/examples/upstash-redis', + }, ] export const Page = ({ children }) => diff --git a/apps/docs/pages/guides/functions/auth.mdx b/apps/docs/pages/guides/functions/auth.mdx index 871c16bd6f2..29ad1505034 100644 --- a/apps/docs/pages/guides/functions/auth.mdx +++ b/apps/docs/pages/guides/functions/auth.mdx @@ -20,7 +20,7 @@ By creating a supabase client with the auth context from the function, you can d 2. Run queries in the context of the user with [Row Level Security (RLS)](/docs/guides/auth/row-level-security) policies enforced. ```js lines=14,17-19,22-23 title=supabase/functions/select-from-table-with-auth-rls/index.ts -import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' +import { serve } from 'https://deno.land/std@0.177.0/http/server.ts' import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' serve(async (req: Request) => { diff --git a/apps/docs/pages/guides/functions/cors.mdx b/apps/docs/pages/guides/functions/cors.mdx index 766da74ae86..7274d810b02 100644 --- a/apps/docs/pages/guides/functions/cors.mdx +++ b/apps/docs/pages/guides/functions/cors.mdx @@ -24,7 +24,7 @@ export const corsHeaders = { You can then import and use the CORS headers within your functions: ```ts index.ts -import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' +import { serve } from 'https://deno.land/std@0.177.0/http/server.ts' import { corsHeaders } from '../_shared/cors.ts' console.log(`Function "browser-with-cors" up and running!`) diff --git a/apps/docs/pages/guides/functions/examples/cloudflare-turnstile.mdx b/apps/docs/pages/guides/functions/examples/cloudflare-turnstile.mdx index ef38552855f..830e91723c8 100644 --- a/apps/docs/pages/guides/functions/examples/cloudflare-turnstile.mdx +++ b/apps/docs/pages/guides/functions/examples/cloudflare-turnstile.mdx @@ -34,7 +34,7 @@ supabase functions new cloudflare-turnstile And add the code to the `index.ts` file: ```ts index.ts -import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' +import { serve } from 'https://deno.land/std@0.177.0/http/server.ts' import { corsHeaders } from '../_shared/cors.ts' console.log('Hello from Cloudflare Trunstile!') diff --git a/apps/docs/pages/guides/functions/examples/connect-to-postgres.mdx b/apps/docs/pages/guides/functions/examples/connect-to-postgres.mdx index de9f2378359..d99931b58e8 100644 --- a/apps/docs/pages/guides/functions/examples/connect-to-postgres.mdx +++ b/apps/docs/pages/guides/functions/examples/connect-to-postgres.mdx @@ -20,7 +20,7 @@ Supabase Edge Functions allow you to go beyond HTTP and can connect to your Post ```ts index.ts import * as postgres from 'https://deno.land/x/postgres@v0.14.2/mod.ts' -import { serve } from 'https://deno.land/std@0.168.0/http/server.ts' +import { serve } from 'https://deno.land/std@0.177.0/http/server.ts' // Get the connection string from the environment variable "DATABASE_URL" const databaseUrl = Deno.env.get('DATABASE_URL')! diff --git a/apps/docs/pages/guides/functions/examples/discord-bot.mdx b/apps/docs/pages/guides/functions/examples/discord-bot.mdx new file mode 100644 index 00000000000..281a2ac0e3e --- /dev/null +++ b/apps/docs/pages/guides/functions/examples/discord-bot.mdx @@ -0,0 +1,178 @@ +import Layout from '~/layouts/DefaultGuideLayout' + +export const meta = { + id: 'examples-discord-bot', + title: 'Discord Bot', + description: 'Building a Slash Command Discord Bot with Edge Functions.', + video: 'https://www.youtube.com/v/J24Bvo_m7DM', +} + +

+ +
+ +## Create an application on Discord Developer Portal + +1. Go to [https://discord.com/developers/applications](https://discord.com/developers/applications) (login using your discord account if required). +2. Click on **New Application** button available at left side of your profile picture. +3. Name your application and click on **Create**. +4. Go to **Bot** section, click on **Add Bot**, and finally on **Yes, do it!** to confirm. + +That's it. A new application is created which will hold our Slash Command. Don't close the tab as we need information from this application page throughout our development. + +Before we can write some code, we need to curl a discord endpoint to register a Slash Command in our app. + +Fill `DISCORD_BOT_TOKEN` with the token available in the **Bot** section and `CLIENT_ID` with the ID available on the **General Information** section of the page and run the command on your terminal. + +```bash +BOT_TOKEN='replace_me_with_bot_token' +CLIENT_ID='replace_me_with_client_id' +curl -X POST \ +-H 'Content-Type: application/json' \ +-H "Authorization: Bot $BOT_TOKEN" \ +-d '{"name":"hello","description":"Greet a person","options":[{"name":"name","description":"The name of the person","type":3,"required":true}]}' \ +"https://discord.com/api/v8/applications/$CLIENT_ID/commands" +``` + +This will register a Slash Command named `hello` that accepts a parameter named `name` of type string. + +## Code + +```ts index.ts +// Sift is a small routing library that abstracts away details like starting a +// listener on a port, and provides a simple function (serve) that has an API +// to invoke a function for a specific path. +import { json, serve, validateRequest } from 'sift' +// TweetNaCl is a cryptography library that we use to verify requests +// from Discord. +import nacl from 'nacl' + +enum DiscordCommandType { + Ping = 1, + ApplicationCommand = 2, +} + +// For all requests to "/" endpoint, we want to invoke home() handler. +serve({ + '/discord-bot': home, +}) + +// The main logic of the Discord Slash Command is defined in this function. +async function home(request: Request) { + // validateRequest() ensures that a request is of POST method and + // has the following headers. + const { error } = await validateRequest(request, { + POST: { + headers: ['X-Signature-Ed25519', 'X-Signature-Timestamp'], + }, + }) + if (error) { + return json({ error: error.message }, { status: error.status }) + } + + // verifySignature() verifies if the request is coming from Discord. + // When the request's signature is not valid, we return a 401 and this is + // important as Discord sends invalid requests to test our verification. + const { valid, body } = await verifySignature(request) + if (!valid) { + return json( + { error: 'Invalid request' }, + { + status: 401, + } + ) + } + + const { type = 0, data = { options: [] } } = JSON.parse(body) + // Discord performs Ping interactions to test our application. + // Type 1 in a request implies a Ping interaction. + if (type === DiscordCommandType.Ping) { + return json({ + type: 1, // Type 1 in a response is a Pong interaction response type. + }) + } + + // Type 2 in a request is an ApplicationCommand interaction. + // It implies that a user has issued a command. + if (type === DiscordCommandType.ApplicationCommand) { + const { value } = data.options.find( + (option: { name: string; value: string }) => option.name === 'name' + ) + return json({ + // Type 4 responds with the below message retaining the user's + // input at the top. + type: 4, + data: { + content: `Hello, ${value}!`, + }, + }) + } + + // We will return a bad request error as a valid Discord request + // shouldn't reach here. + return json({ error: 'bad request' }, { status: 400 }) +} + +/** Verify whether the request is coming from Discord. */ +async function verifySignature(request: Request): Promise<{ valid: boolean; body: string }> { + const PUBLIC_KEY = Deno.env.get('DISCORD_PUBLIC_KEY')! + // Discord sends these headers with every request. + const signature = request.headers.get('X-Signature-Ed25519')! + const timestamp = request.headers.get('X-Signature-Timestamp')! + const body = await request.text() + const valid = nacl.sign.detached.verify( + new TextEncoder().encode(timestamp + body), + hexToUint8Array(signature), + hexToUint8Array(PUBLIC_KEY) + ) + + return { valid, body } +} + +/** Converts a hexadecimal string to Uint8Array. */ +function hexToUint8Array(hex: string) { + return new Uint8Array(hex.match(/.{1,2}/g)!.map((val) => parseInt(val, 16))) +} +``` + +## Deploy the Slash Command Handler + +```bash +supabase functions deploy discord-bot --no-verify-jwt +supabase secrets set DISCORD_PUBLIC_KEY=your_public_key +``` + +Navigate to your Function details in the Supabase Dashboard to get your Endpoint URL. + +### Configure Discord application to use our URL as interactions endpoint URL + +1. Go back to your application (Greeter) page on Discord Developer Portal +2. Fill **INTERACTIONS ENDPOINT URL** field with the URL and click on **Save Changes**. + +The application is now ready. Let's proceed to the next section to install it. + +## Install the Slash Command on your Discord server + +So to use the `hello` Slash Command, we need to install our Greeter application on our Discord server. Here are the steps: + +1. Go to **OAuth2** section of the Discord application page on Discord Developer Portal +2. Select `applications.commands` scope and click on the **Copy** button below. +3. Now paste and visit the URL on your browser. Select your server and click on **Authorize**. + +Open Discord, type `/Promise` and press **Enter**. + +## Run locally + +```bash +supabase functions serve discord-bot --no-verify-jwt --env-file ./supabase/.env.local +ngrok http 54321 +``` + +export const Page = ({ children }) => + +export default Page diff --git a/apps/docs/pages/guides/functions/examples/og-image.mdx b/apps/docs/pages/guides/functions/examples/og-image.mdx index efa8eb99e59..be4f4dc46d2 100644 --- a/apps/docs/pages/guides/functions/examples/og-image.mdx +++ b/apps/docs/pages/guides/functions/examples/og-image.mdx @@ -50,7 +50,7 @@ export default function handler(req: Request) { Create an `index.ts` file to execute the handler on incoming requests: ```ts index.ts -import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' +import { serve } from 'https://deno.land/std@0.177.0/http/server.ts' import handler from './handler.tsx' console.log('Hello from og-image Function!') diff --git a/apps/docs/pages/guides/integrations/onesignal.mdx b/apps/docs/pages/guides/integrations/onesignal.mdx index 3f270c6f60e..bf4922e749f 100644 --- a/apps/docs/pages/guides/integrations/onesignal.mdx +++ b/apps/docs/pages/guides/integrations/onesignal.mdx @@ -8,7 +8,7 @@ export const meta = { video: 'https://www.youtube.com/v/mw0DLwItue4', } -[OneSignal](https://onesignal.com/) is a tool that allows you to send messages across different channels such as the following to keep your users engaged. +[OneSignal](https://onesignal.com/) is a tool that allows you to send messages across different channels such as the following to keep your users engaged. - Push notifications - SMS @@ -36,7 +36,6 @@ You can find the complete example app along with the edge functions code to send ![Ordering app UI](/docs/img/guides/integrations/onesignal/app-ui.png) - ## Step 1: Getting started Before we dive into the code, this guide assumes that you have the following ready @@ -80,8 +79,7 @@ const supabase = createClient(supabaseUrl, supabaseAnonKey) const Home: NextPage = () => { const [user, setUser] = useState(null) - const [oneSignalInitialized, setOneSignalInitialized] = - useState(false) + const [oneSignalInitialized, setOneSignalInitialized] = useState(false) /** * Initializes OneSignal SDK for a given Supabase User ID @@ -123,9 +121,7 @@ const Home: NextPage = () => { const { price } = Object.fromEntries(new FormData(event.currentTarget)) if (typeof price !== 'string') return - const { error } = await supabase - .from('orders') - .insert({ price: Number(price) }) + const { error } = await supabase.from('orders').insert({ price: Number(price) }) if (error) { alert(error.message) } @@ -142,15 +138,13 @@ const Home: NextPage = () => { initialize() - const authListener = supabase.auth.onAuthStateChange( - async (event, session) => { - const user = session?.user ?? null - setUser(user) - if (user) { - initializeOneSignal(user.id) - } + const authListener = supabase.auth.onAuthStateChange(async (event, session) => { + const user = session?.user ?? null + setUser(user) + if (user) { + initializeOneSignal(user.id) } - ) + }) return () => { authListener.data.subscription.unsubscribe() @@ -175,10 +169,7 @@ const Home: NextPage = () => { - @@ -190,10 +181,7 @@ const Home: NextPage = () => { name="email" placeholder="Email" /> - @@ -211,7 +199,7 @@ There is quite a bit of stuff going on here, but basically, it’s creating a si Notice that inside the `initializeOneSignal()` function, we are setting the Supabase user ID as an [external user ID of OneSignal](https://documentation.onesignal.com/docs/external-user-ids). This allows us to later send push notifications to the user using their Supabase user ID from the backend, which is very handy. ```tsx -await OneSignal.setExternalUserId(uid); +await OneSignal.setExternalUserId(uid) ``` The front-end side of things is done here. Let’s get into the backend. @@ -237,7 +225,7 @@ supabase functions new notify Replace the contents of `supabase/functions/notify/index.ts` with the following ```tsx -import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' +import { serve } from 'https://deno.land/std@0.177.0/http/server.ts' import * as OneSignal from 'https://esm.sh/@onesignal/node-onesignal@1.0.0-beta7' const _OnesignalAppId_ = Deno.env.get('ONESIGNAL_APP_ID')! @@ -263,12 +251,9 @@ serve(async (req) => { } const onesignalApiRes = await onesignal.createNotification(notification) - return new Response( - JSON.stringify({ onesignalResponse: onesignalApiRes }), - { - headers: { 'Content-Type': 'application/json' }, - } - ) + return new Response(JSON.stringify({ onesignalResponse: onesignalApiRes }), { + headers: { 'Content-Type': 'application/json' }, + }) } catch (err) { console.error('Failed to create OneSignal notification', err) return new Response('Server error.', { @@ -295,7 +280,6 @@ ONESIGNAL_REST_API_KEY=YOUR_ONESIGNAL_REST_API_KEY ![Where to find OneSignal User Auth Key](/docs/img/guides/integrations/onesignal/onesignal-api-key.png) - Once your environment variables are filled in, you can run the following command to set the environment variable. ```bash @@ -313,35 +297,35 @@ supabase functions deploy notify --no-verify-jwt Finally, we get to set up the database! Run the following SQL to set up the `orders` table. ```sql -create table if not exists public.orders ( - id uuid not null primary key default uuid_generate_v4(), - created_at timestamptz not null default now(), - user_id uuid not null default auth.uid(), +create table + if not exists public.orders ( + id uuid not null primary key default uuid_generate_v4 (), + created_at timestamptz not null default now (), + user_id uuid not null default auth.uid (), price int8 not null -); + ); ``` As you can see, the `orders` table has 4 columns and 3 of them have default values. That means all we need to send from the front-end app is the price. That is why our insert statement looked very simple. ```tsx const { error } = await supabase.from('orders').insert({ - price: 100, - }) + price: 100, +}) ``` Let’s also set up the webhook so that whenever a new row is inserted in the `orders` table, it calls the edge function. Go to `Database > Webhooks` and create a new Database Webhook. The table should be set to `orders` and Events should be inserted. The type should be HTTP Request, the HTTP method should be POST, and the URL should be the URL of your edge function. Hit confirm to save the webhook configuration. ![Supabase Webhooks configuration](/docs/img/guides/integrations/onesignal/webhook.png) -At this point, the app should be complete! Run your app locally with `npm run dev`, or deploy your app to a hosting service and see how you receive a push notification when you place an order! +At this point, the app should be complete! Run your app locally with `npm run dev`, or deploy your app to a hosting service and see how you receive a push notification when you place an order! Remember that if you decide to deploy your app to a hosting service, you would need to create another OneSignal app configured for your local address. ![Ordering app UI](/docs/img/guides/integrations/onesignal/app-ui.png) - ## Resources -This particular example was using Next.js, but you can apply the same principles to implement send push notification, SMS, Emails, and in-app-notifications on other platforms as well. +This particular example was using Next.js, but you can apply the same principles to implement send push notification, SMS, Emails, and in-app-notifications on other platforms as well. - [OneSignal + Flutter + Supabase example](https://github.com/OneSignalDevelopers/onesignal-supabase-sample-integration-supabase) - [OneSignal Mobile Quickstart](https://documentation.onesignal.com/docs/mobile-sdk-setup) From 6e60cf087d059a6f395b0c06ea3d2357a0daf756 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 21 Feb 2023 14:57:56 +0800 Subject: [PATCH 35/75] Add check permissions for database roles --- .../interfaces/Database/Roles/RolesList.tsx | 48 ++++++++++++++----- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/studio/components/interfaces/Database/Roles/RolesList.tsx b/studio/components/interfaces/Database/Roles/RolesList.tsx index 956fb1af750..c5059c26e05 100644 --- a/studio/components/interfaces/Database/Roles/RolesList.tsx +++ b/studio/components/interfaces/Database/Roles/RolesList.tsx @@ -1,17 +1,18 @@ +import { partition } from 'lodash' import { FC, useState, useEffect } from 'react' import { observer } from 'mobx-react-lite' import * as Tooltip from '@radix-ui/react-tooltip' import { PostgresRole } from '@supabase/postgres-meta' import { Button, Input, IconPlus, IconSearch, IconX, Badge } from 'ui' +import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useStore } from 'hooks' +import { checkPermissions, useStore } from 'hooks' import SparkBar from 'components/ui/SparkBar' import { FormHeader } from 'components/ui/Forms' import RoleRow from './RoleRow' -import DeleteRoleModal from './DeleteRoleModal' import { SUPABASE_ROLES } from './Roles.constants' -import { partition } from 'lodash' import CreateRolePanel from './CreateRolePanel' +import DeleteRoleModal from './DeleteRoleModal' interface Props { onSelectRole: (role: any) => void @@ -26,6 +27,8 @@ const RolesList: FC = ({}) => { const [isCreatingRole, setIsCreatingRole] = useState(false) const [selectedRoleToDelete, setSelectedRoleToDelete] = useState() + const canUpdateRoles = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'roles') + useEffect(() => { const getMaxConnectionLimit = async () => { const res = await meta.query('show max_connections') @@ -149,13 +152,31 @@ const RolesList: FC = ({}) => {
- + + + + + {!canUpdateRoles && ( + + +
+ You need additional permissions to add a new role +
+
+ )} +
@@ -184,7 +205,12 @@ const RolesList: FC = ({}) => { )} {otherRoles.map((role: PostgresRole, i: number) => ( - + ))} From d1a7465f99f2f34634c2a42002791a023742f307 Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Tue, 21 Feb 2023 16:55:59 +0800 Subject: [PATCH 36/75] Update index.tsx --- apps/www/pages/pricing/index.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/www/pages/pricing/index.tsx b/apps/www/pages/pricing/index.tsx index 9b209dc901e..44f6ef9030a 100644 --- a/apps/www/pages/pricing/index.tsx +++ b/apps/www/pages/pricing/index.tsx @@ -127,8 +127,8 @@ export default function IndexPage() { `Private Slack channel`, `Uptime SLA`, ], - priceLabel: 'Custom quotas', - priceMonthly: 'Exclusive pricing', + priceLabel: '', + priceMonthly: 'Contact us', preface: 'These apply to all projects within the organization:', scale: '', shutdown: '', @@ -750,13 +750,15 @@ export default function IndexPage() { <> {tier.name !== 'Enterprise' && '$'} {tier.priceMonthly} -

per project per month

+ {tier.priceMonthly && !isNaN(Number(tier.priceMonthly)) && ( +

per project per month

+ )} {tier.warning && ( From 04d75e86e7baff703508998d9f8b4da29d7edd7a Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Tue, 21 Feb 2023 17:15:18 +0800 Subject: [PATCH 37/75] Update index.tsx --- apps/www/pages/pricing/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/www/pages/pricing/index.tsx b/apps/www/pages/pricing/index.tsx index 44f6ef9030a..2724d7bc5e1 100644 --- a/apps/www/pages/pricing/index.tsx +++ b/apps/www/pages/pricing/index.tsx @@ -319,7 +319,7 @@ export default function IndexPage() {

{tier.name !== 'Enterprise' && '$'} From 05da078e0102ba845f354ea499e1bbef6e56b167 Mon Sep 17 00:00:00 2001 From: Div Arora Date: Tue, 21 Feb 2023 17:28:28 +0800 Subject: [PATCH 38/75] chore: remove note offering support for inter-project migrations --- .../pages/guides/platform/migrating-and-upgrading-projects.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/pages/guides/platform/migrating-and-upgrading-projects.mdx b/apps/docs/pages/guides/platform/migrating-and-upgrading-projects.mdx index 1b657450546..9b035e1264b 100644 --- a/apps/docs/pages/guides/platform/migrating-and-upgrading-projects.mdx +++ b/apps/docs/pages/guides/platform/migrating-and-upgrading-projects.mdx @@ -66,7 +66,7 @@ Replication for Realtime is disabled for all tables in your new project. On the ### Migrate Storage objects -The new project has the old project's Storage buckets, but the Storage objects need to be migrated manually. Use this script to move storage objects from one project to another. If you have more than 10k objects, we can move the objects for you. Just contact us at [support@supabase.com](mailto:support@supabase.com). +The new project has the old project's Storage buckets, but the Storage objects need to be migrated manually. Use this script to move storage objects from one project to another. ```js // npm install @supabase/supabase-js@1 From 97934c6607b93a38456fa2f36bf67c243a5316a9 Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Tue, 21 Feb 2023 18:05:32 +0800 Subject: [PATCH 39/75] fix accordian types. remove social buttons for pricing questions --- apps/www/pages/pricing/index.tsx | 71 ++++--------------- .../ui/src/components/Accordion/Accordion.tsx | 9 ++- 2 files changed, 18 insertions(+), 62 deletions(-) diff --git a/apps/www/pages/pricing/index.tsx b/apps/www/pages/pricing/index.tsx index 2724d7bc5e1..550ab106c57 100644 --- a/apps/www/pages/pricing/index.tsx +++ b/apps/www/pages/pricing/index.tsx @@ -863,66 +863,12 @@ export default function IndexPage() {

Frequently asked questions

-

- Can't find the answer to your question, ask someone in the community either on - our Discord or GitHub. -

- -
- {/* @ts-ignore */} +
@@ -942,6 +888,17 @@ export default function IndexPage() { })}
+

+ Can't find the answer to your question, you can{' '} + + open a support ticket + {' '} + and our team of experts will be able to help. +

diff --git a/packages/ui/src/components/Accordion/Accordion.tsx b/packages/ui/src/components/Accordion/Accordion.tsx index 4a802c44ed6..11ca83de563 100644 --- a/packages/ui/src/components/Accordion/Accordion.tsx +++ b/packages/ui/src/components/Accordion/Accordion.tsx @@ -37,14 +37,13 @@ interface AccordionProps { defaultActiveId?: (string | number)[] icon?: React.ReactNode iconPosition?: Align - bordered: boolean onChange?: (item: string | string[]) => void openBehaviour: 'single' | 'multiple' type: Type size: Size defaultValue?: string | string[] | undefined - justified: Boolean - chevronAlign: Align + justified?: Boolean + chevronAlign?: Align } function Accordion({ @@ -58,8 +57,8 @@ function Accordion({ type = 'default', // size, // TO DO defaultValue = undefined, - justified = true, - chevronAlign, + justified = false, + chevronAlign = 'left', }: AccordionProps) { // const [currentItems, setCurrentItems] = useState(defaultValue || []) From 41dc9158291f9e859e508044a8923483ac35fd0e Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Tue, 21 Feb 2023 18:09:31 +0800 Subject: [PATCH 40/75] Update index.tsx --- apps/www/pages/pricing/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/www/pages/pricing/index.tsx b/apps/www/pages/pricing/index.tsx index 550ab106c57..bf161b4b65e 100644 --- a/apps/www/pages/pricing/index.tsx +++ b/apps/www/pages/pricing/index.tsx @@ -756,7 +756,7 @@ export default function IndexPage() { {tier.name !== 'Enterprise' && '$'} {tier.priceMonthly} - {tier.priceMonthly && !isNaN(Number(tier.priceMonthly)) && ( + {tier.name !== 'Enterprise' && (

per project per month

)} From ea6862fef216ed38069559bd0b903329250967ab Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Tue, 21 Feb 2023 22:28:21 +1100 Subject: [PATCH 41/75] sync graphiql theme to dashboard theme --- .../components/interfaces/GraphQL/GraphiQL.tsx | 18 +++++++++++++----- studio/pages/project/[ref]/graphiql/index.tsx | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/studio/components/interfaces/GraphQL/GraphiQL.tsx b/studio/components/interfaces/GraphQL/GraphiQL.tsx index dbeb0661c07..1f961603bd8 100644 --- a/studio/components/interfaces/GraphQL/GraphiQL.tsx +++ b/studio/components/interfaces/GraphQL/GraphiQL.tsx @@ -38,14 +38,15 @@ import { import { Fetcher } from '@graphiql/toolkit' import clsx from 'clsx' import 'graphiql/graphiql.css' -import { PropsWithChildren, useState } from 'react' +import { PropsWithChildren, useEffect, useState } from 'react' import styles from './graphiql.module.css' export type GraphiQLProps = { fetcher: Fetcher + theme?: 'dark' | 'light' } -const GraphiQL = ({ fetcher }: GraphiQLProps) => { +const GraphiQL = ({ fetcher, theme = 'dark' }: GraphiQLProps) => { // Ensure props are correct if (typeof fetcher !== 'function') { throw new TypeError( @@ -55,12 +56,16 @@ const GraphiQL = ({ fetcher }: GraphiQLProps) => { return ( - + ) } -export function GraphiQLInterface() { +type GraphiQLInterfaceProps = { + theme: 'dark' | 'light' +} + +export const GraphiQLInterface = ({ theme }: GraphiQLInterfaceProps) => { const editorContext = useEditorContext({ nonNull: true }) const executionContext = useExecutionContext({ nonNull: true }) const schemaContext = useSchemaContext({ nonNull: true }) @@ -71,7 +76,10 @@ export function GraphiQLInterface() { const merge = useMergeQuery() const prettify = usePrettifyEditors() - const { theme, setTheme } = useTheme() + const { setTheme } = useTheme() + useEffect(() => { + setTheme(theme) + }, [theme]) const PluginContent = pluginContext?.visiblePlugin?.content diff --git a/studio/pages/project/[ref]/graphiql/index.tsx b/studio/pages/project/[ref]/graphiql/index.tsx index 3a6102c80b1..ed285275815 100644 --- a/studio/pages/project/[ref]/graphiql/index.tsx +++ b/studio/pages/project/[ref]/graphiql/index.tsx @@ -81,7 +81,7 @@ const GraphiQLPage: NextPageWithLayout = () => { ) } - return + return } GraphiQLPage.getLayout = (page) => ( From f8aa24fec90422fe494ec909f08ae41d8e3a4763 Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Tue, 21 Feb 2023 19:54:04 +0800 Subject: [PATCH 42/75] Update index.tsx --- apps/www/pages/pricing/index.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/www/pages/pricing/index.tsx b/apps/www/pages/pricing/index.tsx index bf161b4b65e..6dd5e19f7b3 100644 --- a/apps/www/pages/pricing/index.tsx +++ b/apps/www/pages/pricing/index.tsx @@ -899,6 +899,17 @@ export default function IndexPage() { {' '} and our team of experts will be able to help.

+

+ For enterprise enquries, + + you can contact the team here + + . +

From 992029383eb80ca1ddf8f8c626b0ac6e45ab5bfe Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Tue, 21 Feb 2023 23:03:32 +1100 Subject: [PATCH 43/75] updates graphiql theme --- .../interfaces/GraphQL/GraphiQL.tsx | 4 +-- .../interfaces/GraphQL/graphiql.module.css | 27 +++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/studio/components/interfaces/GraphQL/GraphiQL.tsx b/studio/components/interfaces/GraphQL/GraphiQL.tsx index 1f961603bd8..de419530f14 100644 --- a/studio/components/interfaces/GraphQL/GraphiQL.tsx +++ b/studio/components/interfaces/GraphQL/GraphiQL.tsx @@ -215,12 +215,12 @@ export const GraphiQLInterface = ({ theme }: GraphiQLInterfaceProps) => {
diff --git a/studio/components/interfaces/GraphQL/graphiql.module.css b/studio/components/interfaces/GraphQL/graphiql.module.css index 87caa2eb2e2..dc574beed61 100644 --- a/studio/components/interfaces/GraphQL/graphiql.module.css +++ b/studio/components/interfaces/GraphQL/graphiql.module.css @@ -1,4 +1,27 @@ .graphiqlContainer .graphiqlSessions { - margin-left: var(--px-16); - margin-right: 0; + margin: 0; + border-radius: 0; +} + +.graphiqlContainer .graphiqlSession { + padding: 0; +} + +.graphiqlContainer .graphiqlEditors { + border-radius: 0; +} + +.graphiqlContainer .graphiqlEditors:global(.full-height) { + margin-top: calc(0px - var(--session-header-height)); +} + +/* Colors */ +:global(body.graphiql-dark) .graphiqlContainer { + --color-base: 0, 0%, 11%; + --color-primary: 153, 50%, 50%; +} + +:global(body.graphiql-light) .graphiqlContainer { + --color-base: 210, 17%, 98%; + --color-primary: 153, 50%, 50%; } From 21990fede29600737c11a4e54b6c95e5616410ad Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Tue, 21 Feb 2023 08:35:17 -0330 Subject: [PATCH 44/75] Cleanup --- apps/www/pages/pricing/index.tsx | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/apps/www/pages/pricing/index.tsx b/apps/www/pages/pricing/index.tsx index 6dd5e19f7b3..af6bd0ae0a8 100644 --- a/apps/www/pages/pricing/index.tsx +++ b/apps/www/pages/pricing/index.tsx @@ -83,34 +83,6 @@ export default function IndexPage() { additional: '', cta: 'Get Started', }, - // { - // name: 'Team', - // nameBadge: 'New', - // costUnit: 'per month per org', - // href: 'https://app.supabase.com/new/new-project', - // from: true, - // priceLabel: 'Starting from', - // priceMonthly: 599, - // description: 'For scaling teams with permissions & access controls', - // warning: '+ any additional usage', - // features: [ - // 'Usage-based pricing', - // 'Organization member roles (ABAC)', - // 'SOC2', - // 'Priority email support & SLAs', - // '14 day backups', - // '100,000 monthly active users included', - // 'Standardized Security Questionnaire', - // 'SSO for Supabase Dashboard', - // '1 XS compute instance', - // '28 day log retention', - // ], - // scale: '', - // shutdown: '', - // preface: 'The following features will apply to all projects within the organization:', - // additional: '', - // cta: 'Get Started', - // }, { name: 'Enterprise', href: 'https://forms.supabase.com/enterprise', @@ -900,7 +872,7 @@ export default function IndexPage() { and our team of experts will be able to help.

- For enterprise enquries, + For enterprise enquries,{' '} Date: Tue, 21 Feb 2023 09:11:07 -0330 Subject: [PATCH 45/75] Remove unused prop --- .../Logs/LogsErrorRenderers/ResourcesExceededErrorRenderer.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/studio/components/interfaces/Settings/Logs/LogsErrorRenderers/ResourcesExceededErrorRenderer.tsx b/studio/components/interfaces/Settings/Logs/LogsErrorRenderers/ResourcesExceededErrorRenderer.tsx index 2473f64aa19..c8b76284adc 100644 --- a/studio/components/interfaces/Settings/Logs/LogsErrorRenderers/ResourcesExceededErrorRenderer.tsx +++ b/studio/components/interfaces/Settings/Logs/LogsErrorRenderers/ResourcesExceededErrorRenderer.tsx @@ -19,7 +19,6 @@ const ResourcesExceededErrorRenderer: React.FC = ({ error, i type="default" chevronAlign="left" size="small" - bordered={false} iconPosition="left" > From 7102791900f649a05a99254975a154ea2813c32b Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Wed, 22 Feb 2023 00:07:59 +1100 Subject: [PATCH 46/75] move graphiql to api docs section --- .../layouts/DocsLayout/DocsLayout.tsx | 2 ++ .../layouts/DocsLayout/DocsLayout.utils.tsx | 20 +++++++++++++++++++ .../NavigationBar/NavigationBar.utils.tsx | 14 ------------- .../{graphiql/index.tsx => api/graphiql.tsx} | 6 ++---- 4 files changed, 24 insertions(+), 18 deletions(-) rename studio/pages/project/[ref]/{graphiql/index.tsx => api/graphiql.tsx} (94%) diff --git a/studio/components/layouts/DocsLayout/DocsLayout.tsx b/studio/components/layouts/DocsLayout/DocsLayout.tsx index c172f2c7a88..f4d677f2669 100644 --- a/studio/components/layouts/DocsLayout/DocsLayout.tsx +++ b/studio/components/layouts/DocsLayout/DocsLayout.tsx @@ -14,6 +14,8 @@ function DocsLayout({ title, children }: { title: string; children: ReactElement const { data, isLoading, error } = meta.openApi const getPage = () => { + if (router.pathname.endsWith('graphiql')) return 'graphiql' + const { page, resource } = router.query if (!page && !resource) return 'introduction' return (page || resource || '') as string diff --git a/studio/components/layouts/DocsLayout/DocsLayout.utils.tsx b/studio/components/layouts/DocsLayout/DocsLayout.utils.tsx index c97284cf3e1..13b4c097599 100644 --- a/studio/components/layouts/DocsLayout/DocsLayout.utils.tsx +++ b/studio/components/layouts/DocsLayout/DocsLayout.utils.tsx @@ -1,4 +1,5 @@ import { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types' +import SVG from 'react-inlinesvg' import { IconBook, IconBookOpen } from 'ui' export const generateDocsMenu = ( @@ -58,6 +59,25 @@ export const generateDocsMenu = ( }), ], }, + { + title: 'GraphQL', + items: [ + { + name: 'GraphiQL', + key: 'graphiql', + url: `/project/${ref}/api/graphiql`, + icon: ( + code.replace(/svg/, 'svg class="m-auto text-color-inherit"')} + /> + ), + items: [], + }, + ], + }, { title: 'More Resources', items: [ diff --git a/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx b/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx index c16b3374c95..5a2c1a7bac5 100644 --- a/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx +++ b/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx @@ -50,20 +50,6 @@ export const generateToolRoutes = (ref?: string, project?: ProjectBase): Route[] ref && (isProjectPaused ? homeUrl : isProjectBuilding ? buildingUrl : `/project/${ref}/sql`), }, - { - key: 'graphiql', - label: 'GraphiQL', - icon: ( - code.replace(/svg/, 'svg class="m-auto text-color-inherit"')} - /> - ), - link: - ref && - (isProjectPaused ? homeUrl : isProjectBuilding ? buildingUrl : `/project/${ref}/graphiql`), - }, ] } export const generateProductRoutes = (ref?: string, project?: ProjectBase): Route[] => { diff --git a/studio/pages/project/[ref]/graphiql/index.tsx b/studio/pages/project/[ref]/api/graphiql.tsx similarity index 94% rename from studio/pages/project/[ref]/graphiql/index.tsx rename to studio/pages/project/[ref]/api/graphiql.tsx index ed285275815..4e11726744f 100644 --- a/studio/pages/project/[ref]/graphiql/index.tsx +++ b/studio/pages/project/[ref]/api/graphiql.tsx @@ -3,7 +3,7 @@ import { createGraphiQLFetcher } from '@graphiql/toolkit' import ExtensionCard from 'components/interfaces/Database/Extensions/ExtensionCard' import GraphiQL from 'components/interfaces/GraphQL/GraphiQL' -import { ProjectLayoutWithAuth } from 'components/layouts' +import { DocsLayout } from 'components/layouts' import Connecting from 'components/ui/Loading/Loading' import { useParams, useStore } from 'hooks' import { useProjectApiQuery } from 'data/config/project-api-query' @@ -84,8 +84,6 @@ const GraphiQLPage: NextPageWithLayout = () => { return } -GraphiQLPage.getLayout = (page) => ( - {page} -) +GraphiQLPage.getLayout = (page) => {page} export default observer(GraphiQLPage) From dee866ca9c5ef9bed29901bf7478463e00b2684d Mon Sep 17 00:00:00 2001 From: Kasper Laursen Date: Sun, 19 Feb 2023 11:52:29 +0100 Subject: [PATCH 47/75] Update Types path + Add comment for help I got a little confused when trying to get type generation to work with SvelteKit. After finding that [at least one other person had the same](https://github.com/supabase/supabase/discussions/11811) I thought a small PR might be helpful. I changed the path in the example to the one used in the Type generation guide: https://supabase.com/docs/guides/database/api/generating-types **Note:** I think the documentation for this `@supabase/auth-helpers-sveltekit` is a bit too decoupled from the SvelteKit and maybe Type Generation documentation, had a hard time finding it even when i knew it existed :) --- apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx b/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx index 7598037b2d8..8d73f79e224 100644 --- a/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx +++ b/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx @@ -137,7 +137,8 @@ In order to get the most out of TypeScript and it´s intellisense, you should im // and what to do when importing types declare namespace App { interface Supabase { - Database: import('./DatabaseDefinitions').Database + // Use the path to where you generated the types using the Supbase CLI. + Database: import('../types/supabase').Database; SchemaName: 'public' } From e2767ca87a6855eaf5db3cd252d06694a8d050c4 Mon Sep 17 00:00:00 2001 From: Yann Aufray <70212709+yannaufray@users.noreply.github.com> Date: Tue, 21 Feb 2023 16:53:31 +0100 Subject: [PATCH 48/75] Update nuxtjs.mdx --- apps/docs/pages/guides/getting-started/quickstarts/nuxtjs.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/pages/guides/getting-started/quickstarts/nuxtjs.mdx b/apps/docs/pages/guides/getting-started/quickstarts/nuxtjs.mdx index 366120f35e9..f9259185926 100644 --- a/apps/docs/pages/guides/getting-started/quickstarts/nuxtjs.mdx +++ b/apps/docs/pages/guides/getting-started/quickstarts/nuxtjs.mdx @@ -126,7 +126,7 @@ The fastest way to get started with supabase and Nuxt.js is to use the supabase- - Next, in your Next.js app, create a file called supabase-client.js and add the following code to initialize the Supabase client and set your project's credentials: + Next, in your Nuxt.js app, create a file called supabase-client.js and add the following code to initialize the Supabase client and set your project's credentials: From 1f9158342d51a672a9a9dea4775e4d5dc8eb340e Mon Sep 17 00:00:00 2001 From: Onur Usluca <51825926+onurusluca@users.noreply.github.com> Date: Wed, 22 Feb 2023 00:55:30 +0900 Subject: [PATCH 49/75] docs: fix email form logic user management tutorial guide for vue 3 Without a "required" property, form will try to make a request for empty string. --- apps/docs/pages/guides/getting-started/tutorials/with-vue-3.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-vue-3.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-vue-3.mdx index 5480df2d4aa..0cbdd921ca2 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-vue-3.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-vue-3.mdx @@ -99,7 +99,7 @@ const handleLogin = async () => {

Supabase + Vue 3

Sign in via magic link with your email below

- +
Date: Tue, 21 Feb 2023 15:06:00 -0330 Subject: [PATCH 50/75] Update pitr price --- apps/www/pages/pricing/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/www/pages/pricing/index.tsx b/apps/www/pages/pricing/index.tsx index af6bd0ae0a8..bb359df65a1 100644 --- a/apps/www/pages/pricing/index.tsx +++ b/apps/www/pages/pricing/index.tsx @@ -136,7 +136,7 @@ export default function IndexPage() { name: 'Point in Time Recovery', heroImg: 'addons-pitr-hero', icon: 'pitr-upgrade', - price: 'Starts from $5', + price: 'Starts from $100', description: 'Roll back to any specific point in time and ensure that data is not lost.', leftCtaText: 'Documentation', leftCtaLink: 'https://supabase.com/docs/guides/platform/backups', From b8670c38ea957033e383f860bb5d7edd39cc511d Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Tue, 21 Feb 2023 16:08:39 -0330 Subject: [PATCH 51/75] Notify users when their db is in read-only mode --- .../ProjectUsageBars/ProjectUsageBars.tsx | 73 ++++++++++++++++++- .../LayoutHeader/LayoutHeader.tsx | 38 ++++++++++ 2 files changed, 107 insertions(+), 4 deletions(-) diff --git a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx b/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx index a7d3134706e..4e29da3c019 100644 --- a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx +++ b/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx @@ -1,5 +1,14 @@ -import { FC, useEffect } from 'react' -import { Badge, Button, IconAlertCircle, IconInfo, Loading, IconExternalLink } from 'ui' +import { FC, useEffect, useState } from 'react' +import { + Badge, + Button, + IconAlertCircle, + IconInfo, + Loading, + IconExternalLink, + Alert, + IconBookOpen, +} from 'ui' import { useStore } from 'hooks' import { formatBytes } from 'lib/helpers' @@ -11,6 +20,9 @@ import { USAGE_BASED_PRODUCTS } from 'components/interfaces/Billing/Billing.cons import { ProjectUsageResponseUsageKeys, useProjectUsageQuery } from 'data/usage/project-usage-query' import { useRouter } from 'next/router' import * as Tooltip from '@radix-ui/react-tooltip' +import Link from 'next/link' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { executeSql } from 'data/sql/execute-sql-query' interface Props { projectRef?: string @@ -21,8 +33,10 @@ const ProjectUsage: FC = ({ projectRef }) => { const { data: usage, error, isLoading } = useProjectUsageQuery({ projectRef }) const router = useRouter() - const subscriptionTier = ui.selectedProject?.subscription_tier + const { project } = useProjectContext() + const [isReadOnlyMode, setIsReadOnlyMode] = useState(false) + const subscriptionTier = ui.selectedProject?.subscription_tier const projectHasNoLimits = subscriptionTier === PRICING_TIER_PRODUCT_IDS.PAYG || subscriptionTier === PRICING_TIER_PRODUCT_IDS.TEAM || @@ -59,6 +73,20 @@ const ProjectUsage: FC = ({ projectRef }) => { /> ) } + // [Terry] + // temporary solution to check if project is in read only mode + // until we get an api endpoint for this + + async function checkForReadOnlyMode() { + const sql = `show default_transaction_read_only;` + const connectionString = project?.connectionString + const { result: readOnlyStatus } = await executeSql({ projectRef, connectionString, sql }) + if (readOnlyStatus[0]?.default_transaction_read_only === 'on') setIsReadOnlyMode(true) + } + + useEffect(() => { + checkForReadOnlyMode() + }) const isPaidTier = subscriptionTier !== PRICING_TIER_PRODUCT_IDS.FREE @@ -127,7 +155,6 @@ const ProjectUsage: FC = ({ projectRef }) => {
- {/* Line items */} {usage === undefined ? (
@@ -253,6 +280,44 @@ const ProjectUsage: FC = ({ projectRef }) => {
)}
+ + {!isReadOnlyMode && product.title === 'Database' && ( + + )}
) })} diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx index cf8789a80e2..bfb11815157 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx @@ -11,19 +11,43 @@ import HelpPopover from './HelpPopover' import NotificationsPopover from './NotificationsPopover' import { getResourcesExceededLimits } from 'components/ui/OveragesBanner/OveragesBanner.utils' import { useProjectUsageQuery } from 'data/usage/project-usage-query' +import { Badge } from 'ui' +import { executeSql } from 'data/sql/execute-sql-query' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' + +import { useEffect, useState } from 'react' const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = true }: any) => { const { ui } = useStore() const { selectedOrganization, selectedProject } = ui const { ref: projectRef } = useParams() + const { project } = useProjectContext() + + const [isReadOnlyMode, setIsReadOnlyMode] = useState(false) + + // [Terry] + // temporary solution to check if project is in read only mode + // until we get an api endpoint for this + async function checkForReadOnlyMode() { + const sql = `show default_transaction_read_only;` + const connectionString = project?.connectionString + const { result: readOnlyStatus } = await executeSql({ projectRef, connectionString, sql }) + if (readOnlyStatus[0]?.default_transaction_read_only === 'on') setIsReadOnlyMode(true) + } + + useEffect(() => { + checkForReadOnlyMode() + }) const { data: usage } = useProjectUsageQuery({ projectRef }) const resourcesExceededLimits = getResourcesExceededLimits(usage) + const projectHasNoLimits = ui.selectedProject?.subscription_tier === PRICING_TIER_PRODUCT_IDS.PAYG || ui.selectedProject?.subscription_tier === PRICING_TIER_PRODUCT_IDS.ENTERPRISE || ui.selectedProject?.subscription_tier === PRICING_TIER_PRODUCT_IDS.TEAM + const showOverUsageBadge = selectedProject?.subscription_tier !== undefined && !projectHasNoLimits && @@ -63,6 +87,20 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = {/* Project Dropdown */} + {/* [Terry] Temporary until we figure out how we want to display this permanently */} + {/* context: https://www.notion.so/supabase/DB-Disk-Size-Free-tier-Read-only-Critical-f2b8937c13a149e3ac769fe5888f6db0*/} + {!isReadOnlyMode && ( + + )} + {/* [Joshen TODO] Temporarily hidden until usage endpoint is sorted out */} {/* {showOverUsageBadge && (
From 2f3c67a56e243477238b3e7c24f7cfa8d4b965fd Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Wed, 22 Feb 2023 09:06:00 +1100 Subject: [PATCH 52/75] moves apiKey header to graphiql headers section --- studio/components/interfaces/GraphQL/GraphiQL.tsx | 5 +++-- studio/pages/project/[ref]/api/graphiql.tsx | 7 +++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/studio/components/interfaces/GraphQL/GraphiQL.tsx b/studio/components/interfaces/GraphQL/GraphiQL.tsx index de419530f14..f52a378157d 100644 --- a/studio/components/interfaces/GraphQL/GraphiQL.tsx +++ b/studio/components/interfaces/GraphQL/GraphiQL.tsx @@ -44,9 +44,10 @@ import styles from './graphiql.module.css' export type GraphiQLProps = { fetcher: Fetcher theme?: 'dark' | 'light' + apiKey?: string } -const GraphiQL = ({ fetcher, theme = 'dark' }: GraphiQLProps) => { +const GraphiQL = ({ fetcher, theme = 'dark', apiKey }: GraphiQLProps) => { // Ensure props are correct if (typeof fetcher !== 'function') { throw new TypeError( @@ -55,7 +56,7 @@ const GraphiQL = ({ fetcher, theme = 'dark' }: GraphiQLProps) => { } return ( - + ) diff --git a/studio/pages/project/[ref]/api/graphiql.tsx b/studio/pages/project/[ref]/api/graphiql.tsx index 4e11726744f..8b449994aee 100644 --- a/studio/pages/project/[ref]/api/graphiql.tsx +++ b/studio/pages/project/[ref]/api/graphiql.tsx @@ -46,9 +46,8 @@ const GraphiQLPage: NextPageWithLayout = () => { return createGraphiQLFetcher({ url: graphqlUrl, fetch, - ...(anonKey && { headers: { apikey: anonKey } }), }) - }, [graphqlUrl, anonKey]) + }, [graphqlUrl]) if (isProjectSettingsLoading || (isExtensionsLoading && !pgGraphqlExtension)) { return @@ -70,7 +69,7 @@ const GraphiQLPage: NextPageWithLayout = () => {

Enable the GraphQL Extension

- Toggle the switch below to enable the GraphQL extension. Then you can use the GraphQL + Toggle the switch below to enable the GraphQL extension. You can then use the GraphQL API with your Supabase Database.

@@ -81,7 +80,7 @@ const GraphiQLPage: NextPageWithLayout = () => { ) } - return + return } GraphiQLPage.getLayout = (page) => {page} From 5691531290c7c70aec1a9a26702c7bc7b3350255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20Gal=C3=A1n?= Date: Wed, 22 Feb 2023 02:26:56 +0100 Subject: [PATCH 53/75] Docs: improve python client docs (#12532) * Document insert functionality in python client spec * Document select functionality in python client spec --- spec/supabase_py_v2.yml | 354 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 348 insertions(+), 6 deletions(-) diff --git a/spec/supabase_py_v2.yml b/spec/supabase_py_v2.yml index a3af18a5acd..486e429f3e1 100644 --- a/spec/supabase_py_v2.yml +++ b/spec/supabase_py_v2.yml @@ -281,18 +281,360 @@ functions: name: Getting your data code: | ``` - r = supabase - .from('countries') - .select("*").execute() + response = supabase.table('countries').select("*").execute() + ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + response: | + ``` + APIResponse(data=[{'id': 1, 'name': 'Afghanistan'}, + {'id': 2, 'name': 'Albania'}, + {'id': 3, 'name': 'Algeria'}], + count=None) ``` - id: selecting-specific-columns name: Selecting specific columns code: | ``` - r = await supabase - .from('countries') - .select('name').execute() + response = supabase.table('countries').select('name').execute() ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + response: | + ``` + APIResponse(data=[{'name': 'Afghanistan'}, + {'name': 'Albania'}, + {'name': 'Algeria'}], + count=None) + ``` + - id: query-foreign-tables + name: Query foreign tables + description: | + If your database has foreign key relationships, you can query related tables too. + code: | + ``` + response = supabase.table('countries').select('name, cities(name)').execute() + ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + create table + cities ( + id int8 primary key, + country_id int8 not null references countries, + name text + ); + + insert into + countries (id, name) + values + (1, 'Germany'), + (2, 'Indonesia'); + insert into + cities (id, country_id, name) + values + (1, 2, 'Bali'), + (2, 1, 'Munich'); + ``` + response: | + ``` + APIResponse(data=[{'name': 'Germany', 'cities': [{'name': 'Munich'}]}, + {'name': 'Indonesia', 'cities': [{'name': 'Bali'}]}], + count=None) + ``` + - id: query-foreign-tables-through-a-join-table + name: Query foreign tables through a join table + code: | + ``` + response = supabase.table('users').select('name, teams(name)').execute() + ``` + data: + sql: | + ```sql + create table + users ( + id int8 primary key, + name text + ); + create table + teams ( + id int8 primary key, + name text + ); + -- join table + create table + users_teams ( + user_id int8 not null references users, + team_id int8 not null references teams, + -- both foreign keys must be part of a composite primary key + primary key (user_id, team_id) + ); + + insert into + users (id, name) + values + (1, 'Kiran'), + (2, 'Evan'); + insert into + teams (id, name) + values + (1, 'Green'), + (2, 'Blue'); + insert into + users_teams (user_id, team_id) + values + (1, 1), + (1, 2), + (2, 2); + ``` + response: | + ``` + APIResponse(data=[ + {'name': 'Kiran', 'teams': [{'name': 'Green'}, {'name': 'Blue'}]}, + {'name': 'Evan', 'teams': [{'name': 'Blue'}]} + ], + count=None) + ``` + description: | + If you're in a situation where your tables are **NOT** directly + related, but instead are joined by a _join table_, you can still use + the `select()` method to query the related data. The join table needs + to have the foreign keys as part of its composite primary key. + hideCodeBlock: true + - id: query-the-same-foreign-table-multiple-times + name: Query the same foreign table multiple times + code: | + ``` + response = supabase.table('messages').select('content,from:sender_id(name),to:receiver_id(name)').execute() + ``` + data: + sql: | + ```sql + create table + users (id int8 primary key, name text); + + create table + messages ( + sender_id int8 not null references users, + receiver_id int8 not null references users, + content text + ); + + insert into + users (id, name) + values + (1, 'Kiran'), + (2, 'Evan'); + + insert into + messages (sender_id, receiver_id, content) + values + (1, 2, '👋'); + ``` + response: | + ``` + APIResponse(data=[{'content': '👋', + 'from': {'name': 'Kiran'}, + 'to': {'name': 'Evan'}} + ], + count=None) + ``` + description: | + If you need to query the same foreign table twice, use the name of the + joined column to identify which join to use. You can also give each + column an alias. + hideCodeBlock: true + - id: filtering-through-foreign-tables + name: Filtering through foreign tables + code: | + ``` + response = supabase.table('cities').select('name, countries(*)').eq('countries.name', 'Estonia').execute() + ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + create table + cities ( + id int8 primary key, + country_id int8 not null references countries, + name text + ); + + insert into + countries (id, name) + values + (1, 'Germany'), + (2, 'Indonesia'); + insert into + cities (id, country_id, name) + values + (1, 2, 'Bali'), + (2, 1, 'Munich'); + ``` + response: | + ``` + APIResponse(data=[{'name': 'Bali', 'countries': None}, + {'name': 'Munich', 'countries': None}], + count=None) + ``` + description: | + NOT WORKING CURRENTLY + If the filter on a foreign table's column is not satisfied, the foreign + table returns `[]` or `null` but the parent table is not filtered out. + If you want to filter out the parent table rows, use the `!inner` hint + hideCodeBlock: true + - id: querying-foreign-table-with-count + name: Querying foreign table with count + code: | + ``` + response = supabase.table('countries').select('*, cities(count)').execute() + ``` + data: + sql: | + ```sql + create table countries ( + "id" "uuid" primary key default "extensions"."uuid_generate_v4"() not null, + "name" text + ); + + create table cities ( + "id" "uuid" primary key default "extensions"."uuid_generate_v4"() not null, + "name" text, + "country_id" "uuid" references public.countries on delete cascade + ); + + with country as ( + insert into countries (name) + values ('united kingdom') returning id + ) + insert into cities (name, country_id) values + ('London', (select id from country)), + ('Manchester', (select id from country)), + ('Liverpool', (select id from country)), + ('Bristol', (select id from country)); + ``` + response: | + ``` + APIResponse(data=[{'id': 1, 'name': 'Germany', 'cities': [{'count': 1}]}, + {'id': 2, 'name': 'Indonesia', 'cities': [{'count': 1}]}], + count=None) + ``` + description: | + You can get the number of rows in a related table by using the + **count** property. + hideCodeBlock: true + - id: querying-with-count-option + name: Querying with count option + code: | + ``` + response = supabase.table('countries').select('*', count='exact').execute() + ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + response: | + ``` + APIResponse(data=[{'id': 1, 'name': 'Germany'}, + {'id': 2, 'name': 'Indonesia'}], + count=2) + ``` + description: | + You can get the number of rows by using the + *count* parameter in the select query. + hideCodeBlock: true + - id: querying-json-data + name: Querying JSON data + code: | + ``` + response = supabase.table('users').select('id, name, address->city').execute() + ``` + data: + sql: | + ```sql + create table + users ( + id int8 primary key, + name text, + address jsonb + ); + + insert into + users (id, name, address) + values + (1, 'Avdotya', '{"city":"Saint Petersburg"}'); + ``` + response: | + ``` + APIResponse(data=[{'id': 1, 'name': 'Avdotya', 'city': 'Saint Petersburg'}], count=None) + ``` + description: | + You can select and filter data inside of + [JSON](/docs/guides/database/json) columns. Postgres offers some + [operators](/docs/guides/database/json#query-the-jsonb-data) for + querying JSON data. + hideCodeBlock: true + + - id: insert + title: 'Create data: insert()' + $ref: '@supabase/postgrest-js.PostgrestQueryBuilder.insert' + examples: + - id: create-a-record + name: Create a record + code: | + ``` + data, count = supabase + .table('countries') + .insert({"id": 1, "name": "Denmark"}) + .execute() + ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + ``` + response: | + ``` + APIResponse(data=[{'id': 1, 'name': 'Denmark'}], count=None) + ``` + hideCodeBlock: true + isSpotlight: true - id: invoke title: 'invoke()' From a2e980f04c73a13c06d1b3044beadcd695b8ec29 Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Wed, 22 Feb 2023 11:11:28 +0800 Subject: [PATCH 54/75] Update ProjectUsageBars.tsx --- .../ProjectUsageBars/ProjectUsageBars.tsx | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx b/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx index 4e29da3c019..2ec8272f340 100644 --- a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx +++ b/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx @@ -281,21 +281,39 @@ const ProjectUsage: FC = ({ projectRef }) => { )} - {!isReadOnlyMode && product.title === 'Database' && ( + {isReadOnlyMode && product.title === 'Database' && (
+

Your disk has reached 95% capacity and has entered Read Only mode.

{isPaidTier ? ( -

- Your disk has reached 90% capacity. For Pro and Enterprise projects, - disk size expands ~1.5x automatically (e.g., 8GB to 12GB), but can only - occur once every six hours.{' '} -

+ <> +

+ For Pro and Enterprise projects, Disk Size expands ~1.5x automatically + (e.g., 8GB to 12GB) when it reaches 90% capacity, but can only occur{' '} + once every six hours. +

+

+ If the disk size has already expanded within 6 hours, and then reaches + 95% capacity, your disk will enter read only mode until it can resize + again after 6 hours. +

+

+ If you require help or need your disk changed so you can delete data, + + you can contact the support team + + . +

+ ) : (

- This project is on the Free tier. Consider{' '} + This project is on the Free tier. Your disk size cannot auto scale. + Consider{' '} - upgrading + upgrading this project {' '} to resume service as usual.

From d5da9083231e442ed25d653efbafbe536e458593 Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Wed, 22 Feb 2023 11:39:48 +0800 Subject: [PATCH 55/75] added guards in place because projectRef can be undefined on new page load --- .../ProjectUsageBars/ProjectUsageBars.tsx | 30 +++++++++++++------ .../LayoutHeader/LayoutHeader.tsx | 4 +-- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx b/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx index 2ec8272f340..e291b7fcc1a 100644 --- a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx +++ b/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx @@ -85,8 +85,8 @@ const ProjectUsage: FC = ({ projectRef }) => { } useEffect(() => { - checkForReadOnlyMode() - }) + if (projectRef) checkForReadOnlyMode() + }, [projectRef]) const isPaidTier = subscriptionTier !== PRICING_TIER_PRODUCT_IDS.FREE @@ -289,19 +289,27 @@ const ProjectUsage: FC = ({ projectRef }) => { {isPaidTier ? ( <>

- For Pro and Enterprise projects, Disk Size expands ~1.5x automatically + For Pro and Enterprise projects,{' '} + + Disk Size expands ~1.5x automatically + (e.g., 8GB to 12GB) when it reaches 90% capacity, but can only occur{' '} once every six hours.

If the disk size has already expanded within 6 hours, and then reaches - 95% capacity, your disk will enter read only mode until it can resize - again after 6 hours. + 95% capacity,{' '} + + your disk will enter read only mode + {' '} + until it can resize again after 6 hours.

If you require help or need your disk changed so you can delete data, you can contact the support team @@ -312,14 +320,18 @@ const ProjectUsage: FC = ({ projectRef }) => {

This project is on the Free tier. Your disk size cannot auto scale. Consider{' '} - + upgrading this project {' '} to resume service as usual.

)} -

+ {/*

-

+

*/}
diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx index bfb11815157..14a04890a6c 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx @@ -37,8 +37,8 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = } useEffect(() => { - checkForReadOnlyMode() - }) + if (projectRef) checkForReadOnlyMode() + }, [projectRef]) const { data: usage } = useProjectUsageQuery({ projectRef }) const resourcesExceededLimits = getResourcesExceededLimits(usage) From 22835d79319431406e7f7d988c6877f68a827ca7 Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Wed, 22 Feb 2023 12:00:06 +0800 Subject: [PATCH 56/75] Update LayoutHeader.tsx --- .../layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx index 14a04890a6c..4b2a129b33d 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx @@ -89,7 +89,7 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = {/* [Terry] Temporary until we figure out how we want to display this permanently */} {/* context: https://www.notion.so/supabase/DB-Disk-Size-Free-tier-Read-only-Critical-f2b8937c13a149e3ac769fe5888f6db0*/} - {!isReadOnlyMode && ( + {isReadOnlyMode && (
From 0961562be1af485e7c0b6748136deef929e83fff Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Wed, 22 Feb 2023 13:21:50 +0800 Subject: [PATCH 57/75] updating text --- .../ProjectUsageBars/ProjectUsageBars.tsx | 99 +++++++++++++------ .../LayoutHeader/LayoutHeader.tsx | 6 +- 2 files changed, 72 insertions(+), 33 deletions(-) diff --git a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx b/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx index e291b7fcc1a..edb4ab3b1cf 100644 --- a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx +++ b/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx @@ -281,26 +281,45 @@ const ProjectUsage: FC = ({ projectRef }) => { )} - {isReadOnlyMode && product.title === 'Database' && ( + {!isReadOnlyMode && product.title === 'Database' && (
- +
-

Your disk has reached 95% capacity and has entered Read Only mode.

+

+ Your disk has reached 95% capacity and has entered{' '} + + read-only mode + + . +

{isPaidTier ? ( <>

For Pro and Enterprise projects,{' '} - - Disk Size expands ~1.5x automatically - - (e.g., 8GB to 12GB) when it reaches 90% capacity, but can only occur{' '} + + Disk Size expands automatically + {' '} + when it reaches 90% capacity, but can only occur{' '} once every six hours.

- If the disk size has already expanded within 6 hours, and then reaches - 95% capacity,{' '} - - your disk will enter read only mode + If the disk size has already expanded and then reaches 95% capacity + within 6 hours, then{' '} + + your disk will enter read-only mode {' '} until it can resize again after 6 hours.

@@ -311,39 +330,61 @@ const ProjectUsage: FC = ({ projectRef }) => { projectRef ? projectRef : '' }&category=Database_unresponsive&Subject=Read%20only%20mode%20issue`} > - you can contact the support team + + you can contact the support team + .

) : (

- This project is on the Free tier. Your disk size cannot auto scale. - Consider{' '} + This project is on the Free tier. Your disk size cannot auto scale to + avoid read-only mode. You can either{' '} + + reduce your disk usage + {' '} + or{' '} - upgrading this project - {' '} - to resume service as usual. + + upgrade your project + + + .

)} - {/*

- + ) : ( + -

*/} + + Database storage management + + + )} +

diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx index 4b2a129b33d..2d53a284b4a 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx @@ -89,13 +89,11 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = {/* [Terry] Temporary until we figure out how we want to display this permanently */} {/* context: https://www.notion.so/supabase/DB-Disk-Size-Free-tier-Read-only-Critical-f2b8937c13a149e3ac769fe5888f6db0*/} - {isReadOnlyMode && ( + {!isReadOnlyMode && ( From dda1206cea566fcde8108813fe85e8688b6afb94 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 22 Feb 2023 13:27:54 +0800 Subject: [PATCH 58/75] Updates based on feedback --- .../components/interfaces/Functions/EdgeFunctionDetails.tsx | 2 +- studio/data/edge-functions/edge-functions-delete-mutation.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx b/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx index 42b4986fc64..c93c1fefb66 100644 --- a/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx +++ b/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx @@ -56,7 +56,7 @@ const EdgeFunctionDetails: FC = () => { 'functions', ...endpointSections.slice(1), ].join('.') - const functionUrl = `https://${functionsEndpoint}/${selectedFunction?.slug}` + const functionUrl = `${apiService?.protocol}://${functionsEndpoint}/${selectedFunction?.slug}` const onConfirmDelete = async () => { if (!projectRef) return console.error('Project ref is required') diff --git a/studio/data/edge-functions/edge-functions-delete-mutation.ts b/studio/data/edge-functions/edge-functions-delete-mutation.ts index f37faa08a37..d642758cd9c 100644 --- a/studio/data/edge-functions/edge-functions-delete-mutation.ts +++ b/studio/data/edge-functions/edge-functions-delete-mutation.ts @@ -34,7 +34,10 @@ export const useEdgeFunctionDeleteMutation = ({ (vars) => deleteEdgeFunction(vars), { async onSuccess(data, variables, context) { - await queryClient.invalidateQueries(edgeFunctionsKeys.list(variables.projectRef)) + const { projectRef } = variables + await queryClient.invalidateQueries(edgeFunctionsKeys.list(projectRef), { + refetchType: 'all', + }) await onSuccess?.(data, variables, context) }, ...options, From a8cb08728d48cfa4a70137b6cebe2f76be1e6f1d Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Wed, 22 Feb 2023 13:54:44 +0800 Subject: [PATCH 59/75] change text --- .../ProjectUsageBars/ProjectUsageBars.tsx | 182 +++++++++--------- .../LayoutHeader/LayoutHeader.tsx | 2 +- 2 files changed, 96 insertions(+), 88 deletions(-) diff --git a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx b/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx index edb4ab3b1cf..769579c4d4a 100644 --- a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx +++ b/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx @@ -91,7 +91,7 @@ const ProjectUsage: FC = ({ projectRef }) => { const isPaidTier = subscriptionTier !== PRICING_TIER_PRODUCT_IDS.FREE const featureFootnotes: Record = { - db_size: ( + db_size: isPaidTier ? (
{usage?.disk_volume_size_gb && Disk Size: {usage.disk_volume_size_gb} GB} @@ -105,6 +105,8 @@ const ProjectUsage: FC = ({ projectRef }) => {
+ ) : ( + <> ), } @@ -284,108 +286,114 @@ const ProjectUsage: FC = ({ projectRef }) => { {!isReadOnlyMode && product.title === 'Database' && (
-
-

- Your disk has reached 95% capacity and has entered{' '} - - read-only mode - - . -

+

{isPaidTier ? ( <> -

- For Pro and Enterprise projects,{' '} - - Disk Size expands automatically - {' '} - when it reaches 90% capacity, but can only occur{' '} - once every six hours. -

-

- If the disk size has already expanded and then reaches 95% capacity - within 6 hours, then{' '} - - your disk will enter read-only mode - {' '} - until it can resize again after 6 hours. -

-

- If you require help or need your disk changed so you can delete data, - - - you can contact the support team - - - . -

- - ) : ( -

- This project is on the Free tier. Your disk size cannot auto scale to - avoid read-only mode. You can either{' '} + Your disk has reached 95% capacity and has entered{' '} - reduce your disk usage + read-only mode + + . + + ) : ( + <> + You have exceeded the 500mb Database size limit and your project is now + in{' '} + + read-only mode + + . + + )} +

+ {isPaidTier ? ( + <> +

+ For Pro and Enterprise projects,{' '} + + Disk Size expands automatically {' '} - or{' '} + when it reaches 90% capacity, but can only occur once every six hours. + If the disk size has already expanded and then reaches 95% capacity + within 6 hours, then{' '} + + your disk will enter read-only mode + {' '} + until it can resize again after 6 hours. +

+

+ If you require help or need your disk changed to read/write mode so you + can delete data,{' '} - upgrade your project + you can contact the support team .

- )} - + + ) : (

- {!isPaidTier ? ( - - ) : ( - - )} + You can either{' '} + + reduce your disk usage below 500mb + {' '} + or{' '} + + + upgrade your project + + + .

-
+ )} + + {!isPaidTier ? ( + + ) : ( + + )}
)} diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx index 2d53a284b4a..b3b3c01e9ea 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx @@ -93,7 +93,7 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = From eb073f085666391f1d341db90c43ae911dcf8c16 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Wed, 22 Feb 2023 16:58:44 +1100 Subject: [PATCH 60/75] remove unused comments --- .../interfaces/GraphQL/GraphiQL.tsx | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/studio/components/interfaces/GraphQL/GraphiQL.tsx b/studio/components/interfaces/GraphQL/GraphiQL.tsx index f52a378157d..4780cf40d25 100644 --- a/studio/components/interfaces/GraphQL/GraphiQL.tsx +++ b/studio/components/interfaces/GraphQL/GraphiQL.tsx @@ -522,39 +522,6 @@ export const GraphiQLInterface = ({ theme }: GraphiQLInterfaceProps) => { />
- {/*
-
-
Theme
-
- Adjust how the interface looks like. -
-
-
- - - - - -
-
*/} {storageContext ? (
From c2f62c2ef162662ef2d03af3807603166f41148d Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Wed, 22 Feb 2023 14:04:56 +0800 Subject: [PATCH 61/75] hiding disk size --- .../pages/project/[ref]/reports/database.tsx | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/studio/pages/project/[ref]/reports/database.tsx b/studio/pages/project/[ref]/reports/database.tsx index 20ba14361d2..934c9af1246 100644 --- a/studio/pages/project/[ref]/reports/database.tsx +++ b/studio/pages/project/[ref]/reports/database.tsx @@ -103,24 +103,26 @@ const DatabaseUsage: FC = () => { />
-
-
- {usage?.disk_volume_size_gb && ( - Disk Size: {usage.disk_volume_size_gb} GB - )} + {isPaidTier && ( +
+
+ {usage?.disk_volume_size_gb && ( + Disk Size: {usage.disk_volume_size_gb} GB + )} - {isPaidTier && Auto-Scaling} + {isPaidTier && Auto-Scaling} +
+ +
- - -
+ )}
From 3dc80725742ab6298f70b21799901f912f780ae4 Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Wed, 22 Feb 2023 14:05:20 +0800 Subject: [PATCH 62/75] Update database.tsx --- studio/pages/project/[ref]/reports/database.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/studio/pages/project/[ref]/reports/database.tsx b/studio/pages/project/[ref]/reports/database.tsx index 934c9af1246..51ff8dfb946 100644 --- a/studio/pages/project/[ref]/reports/database.tsx +++ b/studio/pages/project/[ref]/reports/database.tsx @@ -109,8 +109,7 @@ const DatabaseUsage: FC = () => { {usage?.disk_volume_size_gb && ( Disk Size: {usage.disk_volume_size_gb} GB )} - - {isPaidTier && Auto-Scaling} + Auto-Scaling