Merge branch 'master' into hi/fix-project-kpsversion

This commit is contained in:
Hieu Pham authored and GitHub committed 2022-04-12 05:44:14 +07:00
commit 65bf440db8
55 files changed
+1947 -2790

No files matched your search

+2 -2
View File
@@ -92,7 +92,7 @@ services:
realtime:
container_name: supabase-realtime
image: supabase/realtime:v0.21.0
image: supabase/realtime:v0.22.4
depends_on:
- db
restart: unless-stopped
@@ -151,7 +151,7 @@ services:
db:
container_name: supabase-db
image: supabase/postgres:14.1.0.19
image: supabase/postgres:14.1.0.21
command: postgres -c config_file=/etc/postgresql/postgresql.conf
restart: unless-stopped
ports:
+38 -17
View File
@@ -17,13 +17,19 @@ as $$
IF server_version >= 14 THEN
RETURN jsonb_build_object(
'data', null::jsonb,
'errors', array['pg_graphql extension is not enabled.']
'errors', jsonb_build_array(
jsonb_build_object(
'message', 'pg_graphql extension is not enabled.'
)
)
);
ELSE
RETURN jsonb_build_object(
'data', null::jsonb,
'errors', array['pg_graphql is only available on projects running Postgres 14 onwards.']
'errors', jsonb_build_array(
jsonb_build_object(
'message', 'pg_graphql is only available on projects running Postgres 14 onwards.'
)
)
);
END IF;
END;
@@ -42,13 +48,13 @@ alter default privileges for user supabase_admin in schema graphql_public grant
on functions to postgres, anon, authenticated, service_role;
-- Trigger upon enabling pg_graphql
CREATE OR REPLACE FUNCTION extensions.grant_pg_graphql_access()
RETURNS event_trigger
LANGUAGE plpgsql
create or replace function extensions.grant_pg_graphql_access()
returns event_trigger
language plpgsql
AS $func$
DECLARE
DECLARE
func_is_graphql_resolve bool;
BEGIN
BEGIN
func_is_graphql_resolve = (
SELECT n.proname = 'resolve'
FROM pg_event_trigger_ddl_commands() AS ev
@@ -65,23 +71,29 @@ AS $func$
alter default privileges in schema graphql grant all on functions to postgres, anon, authenticated, service_role;
alter default privileges in schema graphql grant all on sequences to postgres, anon, authenticated, service_role;
-- Update public wrapper to pass all arguments through to the pg_graphql resolve func
create or replace function graphql_public.graphql(
"operationName" text default null,
query text default null,
variables jsonb default null,
extensions jsonb default null
extensions jsonb default null
)
returns jsonb
language sql
as $$
SELECT graphql.resolve(query, coalesce(variables, '{}'));
select graphql.resolve(
query := query,
variables := coalesce(variables, '{}'),
"operationName" := "operationName",
extensions := extensions
);
$$;
grant select on graphql.field, graphql.type, graphql.enum_value to postgres, anon, authenticated, service_role;
grant execute on function graphql.resolve to postgres, anon, authenticated, service_role;
END IF;
END;
END;
$func$;
CREATE EVENT TRIGGER issue_pg_graphql_access ON ddl_command_end WHEN TAG in ('CREATE FUNCTION')
@@ -120,13 +132,19 @@ AS $func$
IF server_version >= 14 THEN
RETURN jsonb_build_object(
'data', null::jsonb,
'errors', array['pg_graphql extension is not enabled.']
'errors', jsonb_build_array(
jsonb_build_object(
'message', 'pg_graphql extension is not enabled.'
)
)
);
ELSE
RETURN jsonb_build_object(
'data', null::jsonb,
'errors', array['pg_graphql is only available on projects running Postgres 14 onwards.']
'errors', jsonb_build_array(
jsonb_build_object(
'message', 'pg_graphql is only available on projects running Postgres 14 onwards.'
)
)
);
END IF;
END;
@@ -138,4 +156,7 @@ $func$;
CREATE EVENT TRIGGER issue_graphql_placeholder ON sql_drop WHEN TAG in ('DROP EXTENSION')
EXECUTE PROCEDURE extensions.set_graphql_placeholder();
COMMENT ON FUNCTION extensions.set_graphql_placeholder IS 'Reintroduces placeholder function for graphql_public.graphql';
COMMENT ON FUNCTION extensions.set_graphql_placeholder IS 'Reintroduces placeholder function for graphql_public.graphql';
drop extension if exists pg_graphql;
create extension if not exists pg_graphql;
+6 -6
View File
@@ -3606,9 +3606,9 @@
}
},
"node_modules/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
},
"node_modules/mississippi": {
"version": "3.0.0",
@@ -10124,9 +10124,9 @@
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
},
"mississippi": {
"version": "3.0.0",
+2 -2
View File
@@ -1988,8 +1988,8 @@ minimatch@^3.0.4:
brace-expansion "^1.1.7"
minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5:
version "1.2.5"
resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz"
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
mississippi@^3.0.0:
version "3.0.0"
+6 -6
View File
@@ -2202,9 +2202,9 @@
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
},
"node_modules/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
},
"node_modules/ms": {
"version": "2.0.0",
@@ -5152,9 +5152,9 @@
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
},
"ms": {
"version": "2.0.0",
+3 -3
View File
@@ -648,9 +648,9 @@
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
},
"ms": {
"version": "2.0.0",
+3 -3
View File
@@ -1733,9 +1733,9 @@
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
},
"ms": {
"version": "2.0.0",
+6 -6
View File
@@ -1192,9 +1192,9 @@
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
},
"node_modules/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
},
"node_modules/ms": {
"version": "2.0.0",
@@ -2486,9 +2486,9 @@
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
},
"ms": {
"version": "2.0.0",
+6 -6
View File
@@ -2364,9 +2364,9 @@
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
},
"node_modules/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
},
"node_modules/ms": {
"version": "2.0.0",
@@ -5460,9 +5460,9 @@
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
},
"ms": {
"version": "2.0.0",
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -6191,9 +6191,9 @@
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
},
"minipass": {
"version": "3.1.3",
+6 -6
View File
@@ -13123,9 +13123,9 @@
}
},
"node_modules/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
},
"node_modules/minipass": {
"version": "3.1.3",
@@ -31734,9 +31734,9 @@
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
},
"minipass": {
"version": "3.1.3",
+6 -6
View File
@@ -3892,9 +3892,9 @@
}
},
"node_modules/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true
},
"node_modules/minimist-options": {
@@ -8442,9 +8442,9 @@
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true
},
"minimist-options": {
+3 -3
View File
@@ -9762,9 +9762,9 @@
"dev": true
},
"moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==",
"version": "2.29.2",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz",
"integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==",
"dev": true
},
"ms": {
+6 -6
View File
@@ -9524,9 +9524,9 @@
}
},
"node_modules/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true
},
"node_modules/minipass": {
@@ -23742,9 +23742,9 @@
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true
},
"minipass": {
+3 -3
View File
@@ -5415,9 +5415,9 @@
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true
},
"mixin-deep": {
+1
View File
@@ -0,0 +1 @@
14
@@ -15,7 +15,30 @@ const RouteValidationWrapper: FC = ({ children }) => {
const projectRef = router.query.ref
const orgSlug = router.query.slug
/**
* Array of urls/routes that should be ignored
*/
const excemptUrls: string[] = [
// project creation route, allows the page to self determine it's own route, it will redirect to the first org
// or prompt the user to create an organaization
// this is used by database.dev, usually as /new/new-project
'/new/[slug]',
]
/**
* Map through all the urls that are excluded
* from route validation check
*
* @returns a boolean
*/
function isExceptUrl() {
return excemptUrls.includes(router?.pathname)
}
useEffect(() => {
// check if current route is excempted from route validation check
if (isExceptUrl()) return
if (orgsInitialized && orgSlug) {
// Check validity of organization that user is trying to access
const organizations = app.organizations.list()
@@ -30,6 +53,9 @@ const RouteValidationWrapper: FC = ({ children }) => {
}, [orgsInitialized])
useEffect(() => {
// check if current route is excempted from route validation check
if (isExceptUrl()) return
if (projectsInitialized && projectRef) {
// Check validity of project that the user is trying to access
const projects = app.projects.list()
@@ -27,7 +27,13 @@ import { USAGE_COLORS } from 'components/ui/Charts/Charts.constants'
import { EndpointResponse, PathsDatum, StatusCodesDatum } from './ChartData.types'
const CHART_INTERVALS = [
{ key: 'minutely', label: '60 minutes', startValue: 1, startUnit: 'hour', format: 'MMM D, h:mma' },
{
key: 'minutely',
label: '60 minutes',
startValue: 1,
startUnit: 'hour',
format: 'MMM D, h:mma',
},
{ key: 'hourly', label: '24 hours', startValue: 24, startUnit: 'hour', format: 'MMM D, ha' },
{ key: 'daily', label: '7 days', startValue: 7, startUnit: 'day', format: 'MMM D' },
]
@@ -37,7 +43,6 @@ interface Props {
const ProjectUsage: FC<Props> = ({ project }) => {
const logsUsageCodesPaths = useFlag('logsUsageCodesPaths')
const logsUsageChartIntervals = useFlag('logsUsageChartIntervals')
const [interval, setInterval] = useState<string>('hourly')
const router = useRouter()
const { ref } = router.query
@@ -59,9 +64,7 @@ const ProjectUsage: FC<Props> = ({ project }) => {
get
)
const selectedInterval = logsUsageChartIntervals
? CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1]
: CHART_INTERVALS[2]
const selectedInterval = CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1]
const startDate = dayjs()
.subtract(selectedInterval.startValue, selectedInterval.startUnit)
.format(DATE_FORMAT)
@@ -85,25 +88,23 @@ const ProjectUsage: FC<Props> = ({ project }) => {
return (
<div className="mx-6 space-y-6">
<div className="flex flex-row items-center gap-2">
{logsUsageChartIntervals && (
<Dropdown
side="bottom"
align="start"
overlay={
<Dropdown.RadioGroup value={interval} onChange={setInterval}>
{CHART_INTERVALS.map((i) => (
<Dropdown.Radio key={i.key} value={i.key}>
{i.label}
</Dropdown.Radio>
))}
</Dropdown.RadioGroup>
}
>
<Button as="span" type="default" iconRight={<IconChevronDown />}>
{selectedInterval.label}
</Button>
</Dropdown>
)}
<Dropdown
side="bottom"
align="start"
overlay={
<Dropdown.RadioGroup value={interval} onChange={setInterval}>
{CHART_INTERVALS.map((i) => (
<Dropdown.Radio key={i.key} value={i.key}>
{i.label}
</Dropdown.Radio>
))}
</Dropdown.RadioGroup>
}
>
<Button as="span" type="default" iconRight={<IconChevronDown />}>
{selectedInterval.label}
</Button>
</Dropdown>
<span className="text-scale-1000 text-xs">
Statistics for past {selectedInterval.label}
</span>
@@ -1,4 +1,4 @@
import { LogTemplate } from '.'
import { FilterTableSet, LogTemplate } from '.'
export const TEMPLATES: LogTemplate[] = [
{
@@ -185,58 +185,21 @@ export const LOG_TYPE_LABEL_MAPPING: { [k: string]: string } = {
database: 'Database',
}
export const genDefaultQuery = (table: LogsTableName, where: string | undefined) => {
switch (table) {
case 'edge_logs':
return `select id, timestamp, event_message, metadata, request, response, request.method, request.path, response.status_code
from ${table}
cross join unnest(metadata) as m
cross join unnest(m.request) as request
cross join unnest(m.response) as response
${where}
limit 100
`
break
case 'postgres_logs':
return `select postgres_logs.timestamp, id, event_message, metadata, metadataparsed.error_severity from ${table}
cross join unnest(metadata) as m
cross join unnest(m.parsed) as metadataparsed
${where}
limit 100
`
break
case 'function_logs':
return `select id, ${table}.timestamp, event_message, metadata.event_type, metadata.function_id, metadata.level, metadata from ${table}
cross join unnest(metadata) as metadata
${where}
limit 100
`
break
case 'function_edge_logs':
return `select id, ${table}.timestamp, event_message, response.status_code, response, request, request.method, m.function_id, m.execution_time_ms, m.deployment_id, m.version from ${table}
cross join unnest(metadata) as m
cross join unnest(m.response) as response
cross join unnest(m.request) as request
${where}
limit 100
`
default:
return ""
break
}
const _SQL_FILTER_COMMON = {
'search_query': (value: string) => `regexp_contains(event_message, '${value}')`
}
export const SQL_FILTER_TEMPLATES: any = {
postgres_logs: {
..._SQL_FILTER_COMMON,
'severity.error': `metadataParsed.error_severity in ('ERROR', 'FATAL', 'PANIC')`,
'severity.noError': `metadataParsed.error_severity not in ('ERROR', 'FATAL', 'PANIC')`,
'severity.log': `metadataParsed.error_severity = 'LOG'`,
},
edge_logs: {
..._SQL_FILTER_COMMON,
'status_code.error': `response.status_code between 500 and 599`,
'status_code.success': `response.status_code between 200 and 299`,
'status_code.warning': `response.status_code between 400 and 499`,
@@ -252,11 +215,13 @@ export const SQL_FILTER_TEMPLATES: any = {
'method.options': `request.method = 'OPTIONS'`,
},
function_edge_logs: {
..._SQL_FILTER_COMMON,
'status_code.error': `response.status_code between 500 and 599`,
'status_code.success': `response.status_code between 200 and 299`,
'status_code.warning': `response.status_code between 400 and 499`,
},
function_logs: {
..._SQL_FILTER_COMMON,
'severity.error': `metadata.level = 'error'`,
'severity.notError': `metadata.level != 'error'`,
'severity.log': `metadata.level = 'log'`,
@@ -265,12 +230,6 @@ export const SQL_FILTER_TEMPLATES: any = {
},
}
// export const genDefaultQuery = (table: string, where: string = ''): string => `SELECT
// id, timestamp, event_message, metadata
// FROM
// ${table}${where ? ' WHERE\n ' + where : ''}
// LIMIT 100
// `
// export const cleanQuery = (str: string) => str.replaceAll(/\n/g, ' ')
export const cleanQuery = (str: string) => str.replace(/\n/g, ' ')
// .replace(/\n.*\-\-.*(\n)?$?/, "")
@@ -309,23 +268,7 @@ export const genQueryParams = (params: { [k: string]: string }) => {
const qs = new URLSearchParams(params).toString()
return qs
}
interface Filter {
label: string
key: string
options: {
key: string
label: string
description?: string
}[]
}
type FilterOptions = {
[table: string]: {
[filterName: string]: Filter
}
}
export const FILTER_OPTIONS: FilterOptions = {
export const FILTER_OPTIONS: FilterTableSet = {
// Postgres logs
postgres_logs: {
severity: {
@@ -1,141 +0,0 @@
import { Button, Checkbox, Form, Popover } from '@supabase/ui'
import { pluckObjectFields } from 'lib/helpers'
import { useState } from 'react'
export const LogsFilter = ({ options, filtersState, dispatchFilters, classes }: any) => {
const [open, setOpen] = useState(false)
function handleReset() {
// console.log('handleReset ran!')
const empty = {
[options.key]: [],
}
dispatchFilters({ ...empty })
setOpen(!open)
}
// console.log('filters', filters)
// console.log('item in filter', options)
// console.log('options.key', options.key)
const filteredKey = filtersState[options.key]
// loop through options in saved filters
// and check if any options are active
function checkIsActive() {
let active = false
options.options.map((x: any) => {
if (filteredKey && filteredKey.includes(x.key)) {
active = true
}
})
return active
}
const isActive = checkIsActive()
function initialValues() {
let compiledOptions = {}
options.options.map((x: any) => {
compiledOptions = {
...compiledOptions,
[x.key]: filteredKey && filteredKey.includes(x.key) ? true : false,
}
})
return compiledOptions
}
return (
<div className="flex items-center">
<Popover
open={open}
align="end"
size="medium"
onOpenChange={() => setOpen(!open)}
overlay={
<>
<Form
initialValues={initialValues()}
onSubmit={(values: any, { setSubmitting, handleReset }: any) => {
setSubmitting(true)
setOpen(false)
// console.log('values in Logs.filter submit', values)
const payload: any = {
[options.key]: [],
}
Object.keys(values).map((filterKey) => {
if (values[filterKey]) {
payload[options.key].push(filterKey)
}
})
console.log('payload', payload)
// console.log('submit form ran')
dispatchFilters({ ...payload })
// handleReset()
}}
>
{({ isSubmitting }: { isSubmitting: boolean }) => (
<>
<div className="py-6 space-y-4">
{options.options.map((x: any, i: number) => {
return (
<>
<Checkbox
id={x.key}
className="px-3"
label={x.label}
description={x.description}
size="medium"
/>
{i !== options.options.length - 1 && <Popover.Seperator />}
</>
)
})}
</div>
<div
className="flex items-center justify-end gap-2
bg-scale-200 dark:bg-scale-400 py-2 px-3
border-t border-scale-400 dark:border-scale-500
"
>
<Button
size="tiny"
type="default"
onClick={() => handleReset()}
htmlType="button"
>
Clear
</Button>
<Button loading={isSubmitting} type="primary" htmlType="submit">
Save
</Button>
</div>
</>
)}
</Form>
</>
}
portalled
showClose
side="bottom"
>
<Button
as="span"
type={isActive ? 'secondary' : 'default'}
onClick={() => setOpen(!open)}
className={classes}
>
{options.label}
</Button>
</Popover>
</div>
)
}
@@ -1,78 +0,0 @@
import { LogsTableName, SQL_FILTER_TEMPLATES } from './Logs.constants'
import { FilterObject, Override } from './Logs.types'
export function filterSqlWhereBuilder(
filters: FilterObject | any,
table: LogsTableName,
override?: Override,
search?: string
) {
// remove any filter arrays that are empty
const filtersSanitized: any = Object.values(filters).filter((x: any) => x && x.length > 0)
const keys = Object.keys(filters)
const hasOverride = override && override.value && override.key
// build array of where statements
// first item is `where` if there are any filters
let whereArray: string[] =
(filtersSanitized.length > 0 || hasOverride) && !search ? ['where'] : search ? ['and'] : []
if (filtersSanitized.length === 0 && !hasOverride) return whereArray
if (hasOverride) {
// count = +1
const sql = `(${override?.key}='${override?.value}')`
whereArray.push(sql)
if (filtersSanitized.length > 0) whereArray.push('and')
}
if (filtersSanitized.length === 0) return whereArray
keys.map((x: string, i) => {
let count = 0
// do not parse empty key
if (!x) return
const filterKeyArray: string[] = []
filters[x].map((value: string, i: number) => {
// first line should be WHERE
const last = i === filters[x].length - 1
if (count === 0) {
count = +1
// the first argument begins with a `(`
// if this is also the last argument, it is closed with a `)`
const sql = `( ${SQL_FILTER_TEMPLATES[table][`${x}.${value}`]}${last ? ' ) ' : ''}`
filterKeyArray.push(sql)
return sql
} else {
count = +1
// remaining lines use `or` as standard
// if this is also the last argument, it is closed with a `)`
const sql = ` or ${SQL_FILTER_TEMPLATES[table][`${x}.${value}`]}${last ? ' ) ' : ''}`
filterKeyArray.push(sql)
return sql
}
})
whereArray.push(...filterKeyArray)
// if there are multiple filters in an 'and' sequence
// then `and` is inserted between them
if (filtersSanitized.length > 0 && i < filtersSanitized.length - 1) {
whereArray.push(' and ')
}
return filterKeyArray
})
return whereArray
}
export function filterReducer(state: FilterObject, action: any) {
const oldState = { ...state }
const newState = { ...oldState, ...action }
return newState
}
@@ -65,6 +65,28 @@ export interface FilterObject {
te?: string
}
export interface FilterSet {
label: string
key: string
options: FilterOption[]
}
export interface FilterOption {
key: string
label: string
description?: string
}
export type FilterTableSet = {
[table: string]: {
[filterName: string]: FilterSet
}
}
export interface Filters {
[key: string]: string | string[] | boolean | undefined | Filters
}
export type Override = {
key: string
value: string | string[] | undefined
@@ -0,0 +1,135 @@
import { get } from "lodash"
import { Filters, LogsTableName, SQL_FILTER_TEMPLATES } from "."
/**
* Recursively retrieve all nested object key paths.
*
* TODO: move to utils
*
* @param obj any object
* @param parent a string representing the parent key
* @returns string[] all dot paths for keys.
*/
const getDotKeys = (obj: { [k: string]: unknown }, parent?: string): string[] => {
const keys = Object.keys(obj).filter(k => obj[k])
return keys.flatMap(k => {
const currKey = parent ? `${parent}.${k}` : k
if (typeof obj[k] === 'object') {
return getDotKeys(obj[k] as any, currKey)
} else {
return [currKey]
}
})
}
/**
* Root keys in the filter object are considered to be AND filters.
* Nested keys under a root key are considered to be OR filters.
*
* For example:
* ```
* {my_value: 'something', nested: {id: 123, test: 123 }}
* ```
* This would be converted into `WHERE (my_value = 'something') and (id = 123 or test = 123)
*
* The template of the filter determines the actual filter statement. If no template is provided, a generic equality statement will be used.
* This only applies for root keys of the filter.
* For example:
* ```
* {'my.nested.value': 123}
* ```
* with no template, it will be converted into `WHERE (my.nested.value = 123)
*
* @returns a whewhere statement with WHERE clause.
*/
const _genWhereStatement = (table: LogsTableName, filters: Filters) => {
const keys = Object.keys(filters)
const filterTemplates = SQL_FILTER_TEMPLATES[table]
const _resolveTemplateToStatement = (dotKey: string): string | null => {
const template = filterTemplates[dotKey]
const value = get(filters, dotKey)
if (value !== undefined && typeof template === 'function') {
return template(value)
} else if (template === undefined) {
// resolve unknwon filters (possibly from filter overrides)
// no template, set a default
if (typeof value === 'string') {
return `${dotKey} = '${value}'`
} else {
return `${dotKey} = ${value}`
}
} else if (value === undefined && typeof template === 'function') {
return null
} else {
return template
}
}
const statement = keys.map(rootKey => {
if (typeof filters[rootKey] === 'object') {
// join all statements with an OR
const nestedStatement = getDotKeys(filters[rootKey] as Filters, rootKey).map(_resolveTemplateToStatement).filter(Boolean).join(" or ")
return `(${nestedStatement})`
} else {
const nestedStatement = _resolveTemplateToStatement(rootKey)
if (nestedStatement === null) return null
return `(${nestedStatement})`
}
}).filter(Boolean)
// join all root statements with AND
.join(" and ")
if (statement) {
return 'where ' + statement
} else {
return ''
}
}
export const genDefaultQuery = (table: LogsTableName, filters: Filters) => {
const where = _genWhereStatement(table, filters)
switch (table) {
case 'edge_logs':
return `select id, timestamp, event_message, metadata, request, response, request.method, request.path, response.status_code
from ${table}
cross join unnest(metadata) as m
cross join unnest(m.request) as request
cross join unnest(m.response) as response
${where}
limit 100
`
break
case 'postgres_logs':
return `select postgres_logs.timestamp, id, event_message, metadata, metadataparsed.error_severity from ${table}
cross join unnest(metadata) as m
cross join unnest(m.parsed) as metadataparsed
${where}
limit 100
`
break
case 'function_logs':
return `select id, ${table}.timestamp, event_message, metadata.event_type, metadata.function_id, metadata.level, metadata from ${table}
cross join unnest(metadata) as metadata
${where}
limit 100
`
break
case 'function_edge_logs':
return `select id, ${table}.timestamp, event_message, response.status_code, response, request, request.method, m.function_id, m.execution_time_ms, m.deployment_id, m.version from ${table}
cross join unnest(metadata) as m
cross join unnest(m.response) as response
cross join unnest(m.request) as request
${where}
limit 100
`
default:
return ""
break
}
}
@@ -0,0 +1,96 @@
import { Button, Checkbox, Form, Popover } from '@supabase/ui'
import React, { useState } from 'react'
import { Filters, FilterSet } from '.'
interface Props {
options: FilterSet
filters: Filters
onFiltersChange: (filters: Filters) => void
buttonClassName: string
}
const LogsFilterPopover: React.FC<Props> = ({
options,
filters,
onFiltersChange,
buttonClassName,
}) => {
const [open, setOpen] = useState(false)
const handleReset = () => {
onFiltersChange({})
setOpen(false)
}
const handleToggle = () => setOpen(!open)
const checkIsActive = () => {
const filter = filters[options.key]
if (typeof filter === 'object' && Object.values(filter).some(Boolean)) {
return true
}
return false
}
return (
<Popover
className="flex items-center"
open={open}
align="end"
size="medium"
onOpenChange={handleToggle}
overlay={
<Form
className="w-full"
initialValues={filters}
onSubmit={(values, { setSubmitting }) => {
setSubmitting(true)
onFiltersChange(values)
setOpen(false)
}}
>
{({ isSubmitting }: { isSubmitting: boolean }) => (
<>
<div className="py-6 space-y-4">
{options.options.map((x, i: number) => (
<React.Fragment key={x.key}>
<Checkbox
value="true"
id={`${options.key}.${x.key}`}
className="px-3"
label={x.label}
description={x.description}
size="medium"
defaultChecked={(filters?.[options.key] as Filters)?.[x.key] as boolean}
/>
{i !== options.options.length - 1 && <Popover.Seperator />}
</React.Fragment>
))}
</div>
<div className="flex items-center justify-end gap-2 bg-scale-200 dark:bg-scale-400 py-2 px-3 border-t border-scale-400 dark:border-scale-500 ">
<Button size="tiny" type="default" onClick={handleReset} htmlType="button">
Clear
</Button>
<Button loading={isSubmitting} type="primary" htmlType="submit">
Save
</Button>
</div>
</>
)}
</Form>
}
portalled
showClose
side="bottom"
>
<Button
as="span"
type={checkIsActive() ? 'secondary' : 'default'}
onClick={handleToggle}
className={buttonClassName}
>
{options.label}
</Button>
</Popover>
)
}
export default LogsFilterPopover
@@ -2,21 +2,23 @@ import { Tabs } from '@supabase/ui'
import { useFlag } from 'hooks'
import { useRouter } from 'next/router'
interface TabItem {
label: string
id: string
}
const LogsNavigation = () => {
const router = useRouter()
const activeRoute = router.pathname.split('/')[4]
const { ref } = router.query
const indexRoute = 'query'
const logsSourcesPage = useFlag('logsSourcesPage')
const logsSavedQueries = useFlag('logsSavedQueries')
const tabs = [
{ id: 'query', label: 'Query' },
logsSourcesPage ? { id: 'sources', label: 'Sources' } : null,
logsSavedQueries ? { id: 'saved', label: 'Saved' } : null,
{ id: 'templates', label: 'Templates' },
].filter(Boolean)
].filter(Boolean) as TabItem[]
return (
<Tabs
@@ -28,9 +30,8 @@ const LogsNavigation = () => {
router.push(`/project/${ref}/logs-explorer/${e === indexRoute ? '' : e}`)
}}
>
{/* TODO: type properly */}
{(tabs as any[]).map(({ id, label }) => (
<Tabs.Panel id={id} label={label} />
{tabs.map((item) => (
<Tabs.Panel key={item.id} id={item.id} label={item.label} />
))}
</Tabs>
)
@@ -1,14 +1,6 @@
import React, { useEffect, useReducer, useState } from 'react'
import { useRouter } from 'next/router'
import {
Typography,
IconLoader,
IconAlertCircle,
IconRewind,
Button,
Card,
Input,
} from '@supabase/ui'
import { Typography, IconAlertCircle, IconRewind, Button, Card, Input } from '@supabase/ui'
import {
LogTable,
@@ -16,10 +8,10 @@ import {
TEMPLATES,
LogSearchCallback,
LogsTableName,
filterSqlWhereBuilder,
filterReducer,
QueryType,
LogEventChart,
FilterObject,
Filters,
} from 'components/interfaces/Settings/Logs'
import dayjs from 'dayjs'
import useLogsPreview from 'hooks/analytics/useLogsPreview'
@@ -27,7 +19,6 @@ import PreviewFilterPanel from 'components/interfaces/Settings/Logs/PreviewFilte
import { LOGS_TABLES } from './Logs.constants'
import { Override } from './Logs.types'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import ShimmerLine from 'components/ui/ShimmerLine'
import LoadingOpacity from 'components/ui/LoadingOpacity'
@@ -44,14 +35,14 @@ import LoadingOpacity from 'components/ui/LoadingOpacity'
interface Props {
projectRef: string
queryType: QueryType
override?: Override
filterOverride?: Filters
condensedLayout?: boolean
tableName?: LogsTableName
}
export const LogsPreviewer: React.FC<Props> = ({
projectRef,
queryType,
override,
filterOverride,
condensedLayout = false,
tableName,
}) => {
@@ -59,22 +50,12 @@ export const LogsPreviewer: React.FC<Props> = ({
const { s, te, ts } = router.query
const [showChart, setShowChart] = useState(true)
const [whereFilters, dispatchWhereFilters] = useReducer(filterReducer, {})
const table = !tableName ? LOGS_TABLES[queryType] : tableName
const [
{ error, logData, params, newCount, filters, isLoading, oldestTimestamp },
{ loadOlder, setFilters, refresh, setTo, setFrom },
] = useLogsPreview(projectRef as string, table, {
initialFilters: { search_query: s as string },
whereStatementFactory: (filterObj) => `${
filterObj.search_query
? `where REGEXP_CONTAINS(event_message, '${filterObj.search_query}')`
: ''
}
${filterSqlWhereBuilder(whereFilters, table, override, filterObj.search_query).join('\n')}`,
})
] = useLogsPreview(projectRef as string, table, filterOverride)
useEffect(() => {
setFilters((prev) => ({ ...prev, search_query: s as string }))
@@ -91,14 +72,9 @@ export const LogsPreviewer: React.FC<Props> = ({
}, [s, te, ts])
const onSelectTemplate = (template: LogTemplate) => {
setFilters((prev) => ({ ...prev, search_query: template.searchString }))
setFilters((prev: any) => ({ ...prev, search_query: template.searchString }))
}
useEffect(() => {
// runs when any of the filters change
handleRefresh()
}, [whereFilters])
const handleRefresh = () => {
refresh()
router.push({
@@ -135,7 +111,6 @@ export const LogsPreviewer: React.FC<Props> = ({
},
})
}
return (
<div className="h-full flex flex-col flex-grow">
<PreviewFilterPanel
@@ -147,7 +122,7 @@ export const LogsPreviewer: React.FC<Props> = ({
)}
onRefresh={handleRefresh}
onSearch={handleSearch}
defaultSearchValue={filters.search_query}
defaultSearchValue={filters.search_query as string}
defaultToValue={
params.timestamp_end ? dayjs(Number(params.timestamp_end) / 1000).toISOString() : ''
}
@@ -162,8 +137,8 @@ export const LogsPreviewer: React.FC<Props> = ({
router.push(`/project/${projectRef}/logs-explorer?q=${encodeURIComponent(params.rawSql)}`)
}}
onSelectTemplate={onSelectTemplate}
dispatchWhereFilters={dispatchWhereFilters}
whereFilters={whereFilters}
filters={filters}
onFiltersChange={setFilters}
table={table}
condensedLayout={condensedLayout}
isShowingEventChart={showChart}
@@ -182,7 +157,7 @@ export const LogsPreviewer: React.FC<Props> = ({
<LogEventChart
data={!isLoading ? logData : undefined}
onBarClick={(timestampMicro) => {
handleSearch({ query: filters.search_query, toMicro: timestampMicro })
handleSearch({ query: filters.search_query as string, toMicro: timestampMicro })
}}
/>
)}
@@ -9,11 +9,11 @@ import {
IconEye,
IconEyeOff,
} from '@supabase/ui'
import { LogSearchCallback, LogTemplate } from '.'
import { Filters, LogSearchCallback, LogTemplate } from '.'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import { FILTER_OPTIONS, LogsTableName } from './Logs.constants'
import { LogsFilter } from './Logs.filter'
import LogsFilterPopover from './LogsFilterPopover'
import DatePickers from './Logs.DatePickers'
import CSVButton from 'components/ui/CSVButton'
@@ -28,13 +28,13 @@ interface Props {
onSearch?: LogSearchCallback
onExploreClick?: () => void
onSelectTemplate: (template: LogTemplate) => void
dispatchWhereFilters: (x: any) => void
whereFilters: any
table: LogsTableName
condensedLayout: Boolean
isShowingEventChart: boolean
onToggleEventChart: () => void
csvData?: unknown[]
onFiltersChange: (filters: Filters) => void
filters: Filters
}
dayjs.extend(utc)
@@ -51,13 +51,13 @@ const PreviewFilterPanel: FC<Props> = ({
defaultToValue = '',
defaultFromValue = '',
onExploreClick,
dispatchWhereFilters,
whereFilters: filters,
table,
condensedLayout,
isShowingEventChart,
onToggleEventChart,
csvData,
onFiltersChange,
filters,
table,
}) => {
const [search, setSearch] = useState('')
@@ -174,12 +174,12 @@ const PreviewFilterPanel: FC<Props> = ({
}
return (
<LogsFilter
classes={classes.join(' ')}
<LogsFilterPopover
buttonClassName={classes.join(' ')}
key={`${x.key}-filter`}
options={x}
dispatchFilters={dispatchWhereFilters}
filtersState={filters}
onFiltersChange={onFiltersChange}
filters={filters}
/>
)
})}
@@ -1,8 +1,8 @@
export * from './Logs.types'
export * from './Logs.constants'
export * from './Logs.helpers'
export * from './Logs.filter'
export * from './Logs.utils'
export { default as LogsSavedQueriesItem } from './Logs.SavedQueriesItem'
export { default as LogsQueryPanel } from './LogsQueryPanel'
export { default as LogTable } from './LogTable'
export { default as LogEventChart } from './LogEventChart'
export { default as LogsFilterPopover } from "./LogsFilterPopover"
@@ -82,12 +82,11 @@ export const generateProductRoutes = (ref: string, project?: ProjectBase): Route
}
export const generateOtherRoutes = (ref: string, project?: ProjectBase) => {
const logsExplorer = useFlag('logsExplorer')
const isProjectBuilding = project?.status !== PROJECT_STATUS.ACTIVE_HEALTHY
const buildingUrl = `/project/${ref}/building`
return [
...(IS_PLATFORM && logsExplorer
...(IS_PLATFORM
? [
{
key: 'logsExplorer',
+56 -35
View File
@@ -1,6 +1,7 @@
import {
cleanQuery,
Count,
Filters,
genCountQuery,
genDefaultQuery,
genQueryParams,
@@ -15,50 +16,36 @@ import useSWRInfinite, { SWRInfiniteKeyLoader } from 'swr/infinite'
import { API_URL } from 'lib/constants'
import { get } from 'lib/common/fetch'
interface Data<T> {
interface Data {
logData: LogData[]
error: string | Object | null
newCount: number
isLoading: boolean
pageSize: number
filters: T
filters: Filters
params: LogsEndpointParams
oldestTimestamp?: string
}
interface Handlers<T> {
interface Handlers {
loadOlder: () => void
refresh: () => void
setFilters: Dispatch<SetStateAction<T>>
setFilters: (filters: Filters | ((previous: Filters) => Filters)) => void
setFrom: (value: string) => void
setTo: (value: string) => void
}
interface Options<Filters> {
initialFilters: Filters
whereStatementFactory: WhereStatementFactory<Filters>
}
type WhereStatementFactory<T> = (filters: T) => string
function useLogsPreview<Filters>(
function useLogsPreview(
projectRef: string,
table: LogsTableName,
opts?: Partial<Options<Filters>>
): [Data<Filters>, Handlers<Filters>] {
const options: Options<Filters> = {
initialFilters: {} as Filters,
whereStatementFactory: () => '',
...(opts ?? {}),
}
filterOverride?: Filters
): [Data, Handlers] {
const [latestRefresh, setLatestRefresh] = useState<string>(new Date().toISOString())
const [filters, setFilters] = useState<Filters>(options.initialFilters)
const [filters, setFilters] = useState<Filters>({ ...filterOverride })
const [params, setParams] = useState<LogsEndpointParams>({
project: projectRef,
sql: cleanQuery(genDefaultQuery(table, options.whereStatementFactory(filters))),
rawSql: genDefaultQuery(table, options.whereStatementFactory(filters)),
sql: '',
rawSql: '',
period_start: '',
period_end: '',
timestamp_start: '',
@@ -74,6 +61,14 @@ function useLogsPreview<Filters>(
// handle url generation for log pagination
const getKeyLogs: SWRInfiniteKeyLoader = (_pageIndex: number, prevPageData: Logs) => {
let queryParams
// cancel request if no sql provided
if(!params.sql) {
// return null to restrict unnecessary requests to api
// https://swr.vercel.app/docs/conditional-fetching#conditional
return null
}
// if prev page data is 100 items, could possibly have more records that are not yet fetched within this interval
if (prevPageData === null) {
// reduce interval window limit by using the timestamp of the last log
@@ -96,22 +91,36 @@ function useLogsPreview<Filters>(
isValidating,
size,
setSize,
} = useSWRInfinite<Logs>(getKeyLogs, get, { revalidateOnFocus: false })
} = useSWRInfinite<Logs>(getKeyLogs, get, { revalidateOnFocus: false, dedupingInterval: 3000 })
let logData: LogData[] = []
const countUrl = `${API_URL}/projects/${projectRef}/analytics/endpoints/logs.all?${genQueryParams(
{
...params,
sql: genCountQuery(table),
period_start: String(latestRefresh),
} as any
)}`
const countUrl = () => {
// cancel request if no sql provided
if(!params.sql) {
// return null to restrict unnecessary requests to api
// https://swr.vercel.app/docs/conditional-fetching#conditional
return null
}
const { data: countData } = useSWR<Count>(countUrl, get, { refreshInterval: 5000 })
return `${API_URL}/projects/${projectRef}/analytics/endpoints/logs.all?${genQueryParams(
{
...params,
sql: genCountQuery(table),
period_start: String(latestRefresh),
} as any
)}`
}
const { data: countData } = useSWR<Count>(countUrl, get, {
revalidateOnFocus: false,
dedupingInterval: 5000,
refreshInterval: 5000,
})
const newCount = countData?.result?.[0]?.count ?? 0
const refresh = async () => {
const generatedSql = genDefaultQuery(table, options.whereStatementFactory(filters))
const generatedSql = genDefaultQuery(table, filters)
setParams((prev) => ({ ...prev, sql: cleanQuery(generatedSql), rawSql: generatedSql }))
setLatestRefresh(new Date().toISOString())
setSize(1)
@@ -126,7 +135,19 @@ function useLogsPreview<Filters>(
error = response.error
}
})
const oldestTimestamp = logData[logData.length - 1]?.timestamp
const handleSetFilters: Handlers['setFilters'] = (newFilters) => {
if (typeof newFilters === 'function') {
setFilters((prev) => {
const resolved = newFilters(prev)
return { ...resolved, ...filterOverride }
})
} else {
setFilters((prev) => ({ ...prev, ...newFilters, ...filterOverride }))
}
}
return [
{
newCount,
@@ -141,7 +162,7 @@ function useLogsPreview<Filters>(
{
setFrom: (value) => setParams((prev) => ({ ...prev, timestamp_start: value })),
setTo: (value) => setParams((prev) => ({ ...prev, timestamp_end: value })),
setFilters,
setFilters: handleSetFilters,
refresh,
loadOlder: () => setSize((prev) => prev + 1),
},
+3 -3
View File
@@ -6143,9 +6143,9 @@
}
},
"@supabase/ui": {
"version": "0.37.0-alpha.50",
"resolved": "https://registry.npmjs.org/@supabase/ui/-/ui-0.37.0-alpha.50.tgz",
"integrity": "sha512-d5jFxIVCRrZ6cHQ6qpuvyaexTmqMnxDWm0hg7vj9D2x5qcmySRCTC42uhpKXz5fKre2TUl38pFT+UpscWw/OyA==",
"version": "0.37.0-alpha.51",
"resolved": "https://registry.npmjs.org/@supabase/ui/-/ui-0.37.0-alpha.51.tgz",
"integrity": "sha512-9N5k6m3iEwR8HYyH7Ekz6EPI2VW2CZZh0TOZuvaKIXSfccgp2w5kcNcRoyGv/t4c7j+8VAyS+H9cRHHSLiwLzQ==",
"requires": {
"@headlessui/react": "^1.0.0",
"@mertasan/tailwindcss-variables": "^2.0.1",
+1 -1
View File
@@ -26,7 +26,7 @@
"@supabase/react-data-grid": "^7.1.0-beta.7",
"@supabase/shared-types": "^0.1.15",
"@supabase/supabase-js": "^1.29.1",
"@supabase/ui": "0.37.0-alpha.50",
"@supabase/ui": "0.37.0-alpha.51",
"ajv": "^8.6.3",
"analytics-node": "^3.5.0",
"clipboard": "^2.0.8",
@@ -16,7 +16,7 @@ export const LogPage: NextPageWithLayout = () => {
<LogsPreviewer
projectRef={ref as string}
queryType={'fn_edge'}
override={{ key: 'function_id', value: id }}
filterOverride={{function_id: id}}
/>
)
}
@@ -16,7 +16,7 @@ export const LogPage: NextPageWithLayout = () => {
<LogsPreviewer
projectRef={ref as string}
queryType={'functions'}
override={{ key: 'metadata.function_id', value: id }}
filterOverride={{ 'metadata.function_id': id }}
/>
)
}
@@ -139,7 +139,6 @@ export const LogsExplorerPage: NextPage = () => {
try {
const { data: query, error } = await content.create(payload)
if (error) throw error
console.log(query)
setSubmitting(false)
setSaveModalOpen(false)
toast.success(`Saved "${values.name}" log query`)
@@ -1,49 +0,0 @@
import React from 'react'
import { NextPage } from 'next'
import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import { IconCode, Badge, IconDatabase, Icon, IconCloud } from '@supabase/ui'
import { withAuth } from 'hooks'
import { LogsTableName, LOGS_SOURCE_DESCRIPTION } from 'components/interfaces/Settings/Logs'
import LogsExplorerLayout from 'components/layouts/LogsExplorerLayout/LogsExplorerLayout'
import CardButton from 'components/ui/CardButton'
export const LogsExplorerPage: NextPage = () => {
const router = useRouter()
const { ref } = router.query
// @ts-ignore
const sources = Object.values(LogsTableName).sort((a, b) => a - b)
return (
<LogsExplorerLayout>
<div className="grid grid-cols-3 gap-6">
{sources.map((source) => (
<CardButton
title={source}
icon={
<div
className="h-6 w-6 text-scale-100 flex items-center justify-center rounded
bg-scale-1200
transition-colors
duration-400
group-hover:bg-brand-900
group-hover:text-brand-1200
"
>
<div className="scale-100 group-hover:scale-110">
{source === LogsTableName.POSTGRES && <IconDatabase size={12} strokeWidth={2} />}
{source === LogsTableName.EDGE && <IconCloud size={12} strokeWidth={2} />}
{source === LogsTableName.FUNCTIONS && <IconCode size={12} strokeWidth={2} />}
{source === LogsTableName.FN_EDGE && <IconCode size={12} strokeWidth={2} />}
</div>
</div>
}
linkHref={`/project/${ref}/logs-explorer/sources/${source}`}
description={LOGS_SOURCE_DESCRIPTION[source]}
/>
))}
</div>
</LogsExplorerLayout>
)
}
export default withAuth(observer(LogsExplorerPage))
@@ -1,35 +0,0 @@
import React from 'react'
import { NextPage } from 'next'
import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import { withAuth } from 'hooks'
import { LogsExplorerLayout } from 'components/layouts/'
import { LogsTableName } from 'components/interfaces/Settings/Logs'
import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer'
export const LogPage: NextPage = () => {
const router = useRouter()
const { ref, source } = router.query
// quick fix to get correct queryType
const queryType = {
edge_logs: 'api',
postgres_logs: 'database',
function_logs: 'functions',
function_edge_logs: 'fn_edge',
}
return (
<LogsExplorerLayout subtitle={<span className="font-mono">{source}</span>}>
<LogsPreviewer
projectRef={ref as string}
condensedLayout={false}
tableName={source as LogsTableName}
// @ts-ignore
queryType={queryType[source]}
/>
</LogsExplorerLayout>
)
}
export default withAuth(observer(LogPage))
+1 -1
View File
@@ -52,7 +52,7 @@ const coolGray = {
module.exports = ui({
darkMode: 'class', // or 'media' or 'class'
purge: [
content: [
// purge styles from app
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
@@ -0,0 +1,41 @@
import { genDefaultQuery, LogsTableName } from 'components/interfaces/Settings/Logs'
describe.each(Object.values(LogsTableName))('%s', (table) => {
describe.each([
{ filter: { search_query: '' } },
{ filter: { search_query: undefined } },
{ filter: {} },
{ filter: { search_query: '123test' }, contains: ['123test'] },
// test behaviour with overrides
{
filter: { search_query: '123test', override: 'something' },
contains: ['123test', 'override', 'something'],
},
{
filter: { search_query: '', override: 'something' },
contains: ['override', 'something'],
},
{
filter: { search_query: undefined, override: 'something' },
contains: ['override', 'something'],
},
{
filter: { override: 'something' },
contains: ['override', 'something'],
},
{
filter: { 'override.nested': 'something' },
contains: ['override.nested', 'something'],
},
])('generates sql for filter $filter', ({ filter, contains = [] }) => {
test('generates query correctly', async () => {
const generated = genDefaultQuery(table, filter)
// should not contain functions
expect(generated).not.toMatch(/function[ A-Za-z_-]+\(.+\).+\{.+return.+\}/)
expect(generated).not.toMatch(/\=\>|\$\{.+\}/)
expect(generated).not.toContain('return')
contains.forEach((str) => expect(generated).toContain(str))
})
})
})
@@ -291,4 +291,33 @@ test("bug: nav to explorer preserves newlines", async ()=>{
const router = useRouter()
userEvent.click(await screen.findByText(/Explore/))
await expect(router.push).toBeCalledWith(expect.stringContaining(encodeURIComponent("\n")))
}
}
test('filters alter generated query', async () => {
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
userEvent.click(await screen.findByRole('button', { name: 'Status' }))
userEvent.click(await screen.findByText(/500 error codes/))
userEvent.click(await screen.findByText(/200 codes/))
userEvent.click(await screen.findByText(/Save/))
await waitFor(() => {
expect(get).toHaveBeenCalledWith(expect.stringContaining('select'))
expect(get).toHaveBeenCalledWith(expect.stringContaining('500'))
expect(get).toHaveBeenCalledWith(expect.stringContaining('200'))
expect(get).toHaveBeenCalledWith(expect.stringContaining('where'))
expect(get).toHaveBeenCalledWith(expect.stringContaining('and'))
})
})
test('filters accept filterOverride', async () => {
render(
<LogsPreviewer
projectRef="123"
tableName={LogsTableName.FUNCTIONS}
filterOverride={{ 'my.nestedkey': 'myvalue' }}
/>
)
await waitFor(() => {
expect(get).toHaveBeenCalledWith(expect.stringContaining('my.nestedkey'))
expect(get).toHaveBeenCalledWith(expect.stringContaining('myvalue'))
})
})
@@ -1,287 +0,0 @@
// mock the fetch function
jest.mock('lib/common/fetch')
import { get } from 'lib/common/fetch'
// mock mobx
jest.mock('mobx-react-lite')
import { observer } from 'mobx-react-lite'
observer.mockImplementation((v) => v)
// mock the router
jest.mock('next/router')
import { useRouter } from 'next/router'
const defaultRouterMock = () => {
const router = jest.fn()
router.query = {}
router.push = jest.fn()
router.pathname = 'logs/path'
return router
}
useRouter.mockReturnValue(defaultRouterMock())
// mock monaco editor
jest.mock('@monaco-editor/react')
import Editor, { useMonaco } from '@monaco-editor/react'
Editor = jest.fn()
Editor.mockImplementation((props) => {
return (
<textarea className="monaco-editor" onChange={(e) => props.onChange(e.target.value)}></textarea>
)
})
useMonaco.mockImplementation((v) => v)
// mock usage flags
jest.mock('components/ui/Flag/Flag')
import Flag from 'components/ui/Flag/Flag'
Flag.mockImplementation(({ children }) => <>{children}</>)
jest.mock('hooks')
import { useFlag } from 'hooks'
useFlag.mockReturnValue(true)
import { SWRConfig } from 'swr'
jest.mock('components/interfaces/Settings/Logs/LogsPreviewer')
import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer'
LogsPreviewer.mockImplementation((props) => {
const Comp = jest.requireActual('components/interfaces/Settings/Logs/LogsPreviewer').default
// wrap with SWR to reset the cache each time
return (
<SWRConfig
value={{
provider: () => new Map(),
shouldRetryOnError: false,
}}
>
<Comp {...props} />
</SWRConfig>
)
})
import { render, fireEvent, waitFor, screen, act } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { getToggleByText } from '../../helpers'
import { wait } from '@testing-library/user-event/dist/utils'
import { logDataFixture } from '../../fixtures'
import { LogsTableName } from 'components/interfaces/Settings/Logs'
import { find } from 'lodash'
beforeEach(() => {
// reset mocks between tests
get.mockReset()
useRouter.mockReset()
useRouter.mockReturnValue(defaultRouterMock())
})
test('can display log data and metadata', async () => {
get.mockResolvedValue({
result: [
logDataFixture({
id: 'some-event-happened-id',
metadata: {
my_key: 'something_value',
},
}),
],
})
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
fireEvent.click(await screen.findByText(/some-event-happened-id/))
await screen.findByText(/my_key/)
await screen.findByText(/something_value/)
})
test('Refresh page', async () => {
get.mockImplementation((url) => {
if (url.includes('count')) return { result: { count: 0 } }
return {
result: [
logDataFixture({
id: 'some-event-id',
metadata: { my_key: 'something_value' },
}),
],
}
})
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
const row = await screen.findByText(/some-event-id/)
fireEvent.click(row)
await screen.findByText(/my_key/)
})
test('Search will trigger a log refresh', async () => {
get.mockImplementation((url) => {
if (url.includes('something')) {
return {
result: [logDataFixture({ id: 'some-event-id' })],
}
}
return { result: [] }
})
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
userEvent.type(screen.getByPlaceholderText(/Search events/), 'something{enter}')
await waitFor(
() => {
expect(get).toHaveBeenCalledWith(expect.stringContaining('something'))
// updates router query params
const router = useRouter()
expect(router.push).toHaveBeenCalledWith(
expect.objectContaining({
pathname: expect.any(String),
query: expect.objectContaining({
s: expect.stringContaining('something'),
}),
})
)
},
{ timeout: 1500 }
)
await screen.findByText(/some-event-id/)
})
test('poll count for new messages', async () => {
get.mockImplementation((url) => {
if (url.includes('count')) {
return { result: [{ count: 125 }] }
}
return {
result: [logDataFixture({ id: 'some-uuid123' })],
}
})
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
await waitFor(() => screen.queryByText(/some-uuid123/) === null)
// should display new logs count
await waitFor(() => screen.getByText(/125/))
userEvent.click(screen.getByText(/Refresh/))
await waitFor(() => screen.queryByText(/125/) === null)
await screen.findByText(/some-uuid123/)
})
test('s= query param will populate the search bar', async () => {
const router = defaultRouterMock()
router.query = { ...router.query, s: 'someSearch' }
useRouter.mockReturnValue(router)
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
// should populate search input with the search param
await screen.findByDisplayValue('someSearch')
expect(get).toHaveBeenCalledWith(expect.stringContaining('someSearch'))
})
test('te= query param will populate the timestamp to input', async () => {
// get time 20 mins before
const newDate = new Date()
newDate.setMinutes(new Date().getMinutes() - 20)
const isoString = newDate.toISOString()
const unixMicro = newDate.getTime() * 1000 //microseconds
const router = defaultRouterMock()
router.query = { ...router.query, te: unixMicro }
useRouter.mockReturnValue(router)
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
await waitFor(() => {
expect(get).toHaveBeenCalledWith(
expect.stringContaining(`timestamp_end=${encodeURIComponent(unixMicro)}`)
)
})
userEvent.click(await screen.findByText('Custom'))
expect(get).toHaveBeenCalledWith(expect.stringContaining('timestamp_end=' + unixMicro))
})
test('ts= query param will populate the timestamp from input', async () => {
// get time 20 mins before
const newDate = new Date()
newDate.setMinutes(new Date().getMinutes() - 20)
const isoString = newDate.toISOString()
const unixMicro = newDate.getTime() * 1000 //microseconds
const router = defaultRouterMock()
router.query = { ...router.query, ts: unixMicro }
useRouter.mockReturnValue(router)
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
await waitFor(() => {
expect(get).toHaveBeenCalledWith(
expect.stringContaining(`timestamp_start=${encodeURIComponent(unixMicro)}`)
)
})
userEvent.click(await screen.findByText('Custom'))
await screen.findByText(new RegExp(newDate.getFullYear()))
await screen.findByText(/Apply/)
expect(get).toHaveBeenCalledWith(expect.stringContaining('timestamp_start=' + unixMicro))
})
test('load older btn will fetch older logs', async () => {
get.mockImplementation((url) => {
if (url.includes('count')) {
return {}
}
return {
result: [logDataFixture({ id: 'first event' })],
}
})
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
// should display first log but not second
await waitFor(() => screen.getByText('first event'))
await expect(screen.findByText('second event')).rejects.toThrow()
get.mockResolvedValueOnce({
result: [logDataFixture({ id: 'second event' })],
})
// should display first and second log
userEvent.click(await screen.findByText('Load older'))
await screen.findByText('first event')
await screen.findByText('second event')
expect(get).toHaveBeenCalledWith(expect.stringContaining('timestamp_end='))
})
test('bug: load older btn does not error out when previous page is empty', async () => {
// bugfix for https://sentry.io/organizations/supabase/issues/2903331460/?project=5459134&referrer=slack
get.mockImplementation((url) => {
if (url.includes('count')) {
return {}
}
return { result: [] }
})
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
userEvent.click(await screen.findByText('Load older'))
// NOTE: potential race condition, since we are asserting that something DOES NOT EXIST
// wait for 500s to make sure all ui logic is complete
// need to wrap in act because internal react state is changing during this time.
await act(async () => await wait(100))
// clicking load older multiple times should not give error
await waitFor(() => {
expect(screen.queryByText(/Sorry/)).toBeNull()
expect(screen.queryByText(/An error occured/)).toBeNull()
expect(screen.queryByText(/undefined/)).toBeNull()
})
})
test('log event chart hide', async () => {
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
await screen.findByText('Events')
const toggle = await screen.findByText(/Chart/)
userEvent.click(toggle)
await expect(screen.findByText('Events')).rejects.toThrow()
})
test('bug: nav backwards with params change results in ui changing', async () => {
// bugfix for https://sentry.io/organizations/supabase/issues/2903331460/?project=5459134&referrer=slack
get.mockImplementation((url) => {
if (url.includes('count')) {
return {}
}
return { data: [] }
})
const { container, rerender } = render(
<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />
)
await expect(screen.findByDisplayValue('simple-query')).rejects.toThrow()
const router = defaultRouterMock()
router.query = { ...router.query, s: 'simple-query' }
useRouter.mockReturnValue(router)
rerender(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
await screen.findByDisplayValue('simple-query')
})
@@ -91,7 +91,6 @@ test('can display log data', async () => {
userEvent.click(await screen.findByText(/Run/))
const row = await screen.findByText("some-event-happened")
screen.debug()
userEvent.click(row)
await screen.findByText(/something_value/)
})
+8 -17
View File
@@ -54,11 +54,6 @@ GraphQL is in Beta, and may have breaking changes. It is only available on self-
:::
GraphQL in Supabase works through [pg_graphql](/blog/2021/12/03/pg-graphql), an open source PostgreSQL extension for GraphQL.
To get started with GraphQL you must enable the `pg_graphql` extension in your project.
<video width="99%" muted playsInline controls="true">
<source src="/docs/videos/api/api-enable-graphql.mp4" type="video/mp4" muted playsInline />
</video>
### Realtime API
@@ -240,7 +235,8 @@ JS Reference: [select()](/docs/reference/javascript/select),
:::note
While pg_graphql is in beta, we have disabled GraphQL auto-refreshing. If you want to refresh your GraphQL introspection, call `select graphql.rebuild_schema();` to rebuild your GraphQL schema.
To rebuild your GraphQL schema from the SQL schema, call `select graphql.rebuild_schema();`.
Be sure to rebuild the GraphQL schema after altering the SQL schema.
:::
@@ -426,19 +422,14 @@ If they detect any keys with service_role privileges being pushed to GitHub, the
### Safeguards towards accidental deletes and updates
For new projects, by default, the Postgres extension [safeupdate](https://github.com/eradman/pg-safeupdate) is enabled for all queries coming from the API.
For all projects, by default, the Postgres extension [safeupdate](https://github.com/eradman/pg-safeupdate) is enabled for all queries coming from the API.
This ensures that any `delete()` or `update()` would fail if there are no accompanying filters provided.
This can be disabled by running the following query on the SQL editor on the dashboard:
To confirm that safeupdate is enabled for queries going through the API of your project, the following query could be run:
```sql
alter role "authenticator"
set "session_preload_libraries" to default;
select usename,useconfig from pg_shadow where usename = 'authenticator' ;
```
To reenable it :
```sql
alter role "authenticator"
set "session_preload_libraries" = 'safeupdate';
The expected value for `useconfig` should be:
```
The above can also be run to enable it for existing projects. Settings are applied immediately and as such no restarts are needed after running any of the two.
["session_preload_libraries=supautils, safeupdate"]
```
+1 -1
View File
@@ -27,7 +27,7 @@ There are two parts to every Auth system:
- **Authorization:** once they are in, what are they allowed to do?
Supabase Auth is designed to work either as a standalone product, or deeply integrated with the other Supabase products.
Postgres is at the heart of everything we do, and the Auth system follows this principle. We leverage Postgres' build-in Auth functionality wherever possible.
Postgres is at the heart of everything we do, and the Auth system follows this principle. We leverage Postgres' built-in Auth functionality wherever possible.
## Authentication
+4 -4
View File
@@ -87,9 +87,9 @@ async function signInWithEmail() {
<TabItem value="dart">
```dart
function SignInWithEmail() {
Future<void> signInWithEmail() async {
await supabase.auth.signIn(
email: 'example@email.com',
email: 'example@email.com',
password: 'example-password'
);
}
@@ -120,8 +120,8 @@ async function signOut() {
<TabItem value="dart">
```dart
function SignOut() {
await supabase.auth.signOut();
Future<void> signOut() async {
await supabase.auth.signOut();
}
```
+1 -1
View File
@@ -24,7 +24,7 @@ Setting up LinkedIn logins for your application consists of 3 parts:
- Go to [LinkedIn Developer Dashboard](https://www.linkedin.com/developers/apps).
- Log in (if necessary.)
![LinkedIn Developer Portal](/img/auth-linkedin/linkedin_developers_page.png)
![LinkedIn Developer Portal](/img/guides/auth-linkedin/linkedin_developers_page.png)
### Find your callback URL
+3 -5
View File
@@ -83,10 +83,8 @@ async function signInWithEmail() {
<TabItem value="dart">
```dart
function SignInWithEmail() {
await supabase.auth.signIn(
email: 'example@email.com'
);
Future<void> signInWithEmail() async {
await supabase.auth.signIn(email: 'example@email.com');
}
```
@@ -115,7 +113,7 @@ async function signOut() {
<TabItem value="dart">
```dart
function SignOut() {
Future<void> signOut() async {
await supabase.auth.signOut();
}
```
@@ -107,7 +107,7 @@ rollback;
API:
- [`hasTable()`](https://pgtap.org/documentation.html#has_table)
- [`has_table()`](https://pgtap.org/documentation.html#has_table)
### Testing columns
@@ -130,4 +130,4 @@ API:
## Resources
- Official [`pgTAP` documentation](https://pgtap.org/).
- Official [`pgTAP` documentation](https://pgtap.org/).
+1 -1
View File
@@ -17,7 +17,7 @@ pgMustard requires plans to be in json format, and the buffers, verbose, and set
So a good prefix for your query would be:
```jsx
explain (analyze, format json, buffers, verbose, settings)`
explain (analyze, format json, buffers, verbose, settings)
```
Run the query, and copy the output.
+3
View File
@@ -419,6 +419,9 @@ And then open the browser to [localhost:3000](http://localhost:3000) and you sho
Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like photos and videos.
### Add @reach/visually-hidden
The upload widget uses one additional npm library. You can install it like so: `npm install @reach/visually-hidden`.
### Create an upload widget
Let's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component:
Binary file not shown.
+1 -1
View File
@@ -1001,7 +1001,7 @@ const Beta = (props: Props) => {
<div className="flex items-center justify-between px-5 py-5 shadow-lg xl:px-20 bg-scale-1200 dark:bg-scale-300">
<Link href="/">
<a>
<Image src={`${basePath}/images/logo-dark.png`} height={20} width={20} />
<Image src={`${basePath}/images/logo-dark.png`} height={24} width={120} />
</a>
</Link>
<HamburgerMenu openMenu={() => setMenuOpen(!menuOpen)} />
+1 -1
View File
@@ -3,7 +3,7 @@ const svgToDataUri = require('mini-svg-data-uri')
module.exports = ui({
mode: 'JIT',
purge: [
content: [
'./components/**/*.tsx',
'./pages/**/*.tsx',
'./_blog/*.mdx',