From 2e4cc9c2e93aa4312e0a3fc5b6fff45293a0762a Mon Sep 17 00:00:00 2001 From: Beng Eu Date: Fri, 27 May 2022 18:34:56 +0800 Subject: [PATCH] feat: add permissions to routes and support adding and removing roles --- .../interfaces/Authentication/Templates.tsx | 18 +- .../Billing/Subscription/Subscription.tsx | 13 +- .../interfaces/TableGridEditor/EmptyState.tsx | 14 +- .../TableGridEditor/TableGridEditor.tsx | 17 +- .../TableEditorLayout/TableEditorMenu.tsx | 29 ++-- .../ModalsDeprecated/InviteMemberModal.js | 27 ++- .../to-be-cleaned/ProductEmptyState.tsx | 4 +- studio/hooks/queries/useOrganizationDetail.ts | 5 +- studio/pages/org/[slug]/settings.tsx | 155 +++++++++++++----- studio/pages/project/[ref]/auth/settings.tsx | 137 +++++++++++++--- studio/pages/project/[ref]/reports/[id].tsx | 30 +++- studio/pages/project/[ref]/reports/index.tsx | 16 +- .../pages/project/[ref]/settings/general.tsx | 95 +++++++---- 13 files changed, 409 insertions(+), 151 deletions(-) diff --git a/studio/components/interfaces/Authentication/Templates.tsx b/studio/components/interfaces/Authentication/Templates.tsx index 681df42e284..ccfd22ed6b4 100644 --- a/studio/components/interfaces/Authentication/Templates.tsx +++ b/studio/components/interfaces/Authentication/Templates.tsx @@ -7,15 +7,20 @@ import { Typography } from '@supabase/ui' import { AutoField, LongTextField } from 'uniforms-bootstrap4' import { API_URL, IS_PLATFORM } from 'lib/constants' -import { useProjectAuthConfig, useStore } from 'hooks' +import { usePermissions, useProjectAuthConfig, useStore } from 'hooks' import { pluckJsonSchemaFields } from 'lib/helpers' import { patch } from 'lib/common/fetch' import { authConfig } from 'stores/jsonSchema' import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel' +import { PermissionAction } from '@supabase/shared-types/out/constants' const Templates: FC = ({ project }) => { const router = useRouter() const { ui } = useStore() + const canUpdate = usePermissions( + PermissionAction.SQL_UPDATE, + 'postgres.public.custom_config_gotrue' + ) const magicLinkEnable = semver.gte( // @ts-ignore semver.coerce(toJS(project?.kpsVersion) || 'kps-v0.0.1'), @@ -67,6 +72,7 @@ const Templates: FC = ({ project }) => { readOnly={!IS_PLATFORM} inputClassName="font-mono" errorMessage="Please enter an sms body." + disabled={!canUpdate} /> @@ -88,6 +94,7 @@ const Templates: FC = ({ project }) => { name="MAILER_SUBJECTS_CONFIRMATION" readOnly={!IS_PLATFORM} errorMessage="Please enter a subject." + disabled={!canUpdate} /> = ({ project }) => { readOnly={!IS_PLATFORM} inputClassName="font-mono" errorMessage="Please enter an email body." + disabled={!canUpdate} /> @@ -117,6 +125,7 @@ const Templates: FC = ({ project }) => { name="MAILER_SUBJECTS_RECOVERY" readOnly={!IS_PLATFORM} errorMessage="Please enter a subject." + disabled={!canUpdate} /> = ({ project }) => { readOnly={!IS_PLATFORM} inputClassName="font-mono" errorMessage="Please enter an email body." + disabled={!canUpdate} /> @@ -147,6 +157,7 @@ const Templates: FC = ({ project }) => { name="MAILER_SUBJECTS_MAGIC_LINK" readOnly={!IS_PLATFORM} errorMessage="Please enter a subject." + disabled={!canUpdate} /> = ({ project }) => { readOnly={!IS_PLATFORM} inputClassName="font-mono" errorMessage="Please enter an email body." + disabled={!canUpdate} /> @@ -177,6 +189,7 @@ const Templates: FC = ({ project }) => { name="MAILER_SUBJECTS_EMAIL_CHANGE" readOnly={!IS_PLATFORM} errorMessage="Please enter a subject." + disabled={!canUpdate} /> = ({ project }) => { readOnly={!IS_PLATFORM} inputClassName="font-mono" errorMessage="Please enter an email body." + disabled={!canUpdate} /> @@ -206,6 +220,7 @@ const Templates: FC = ({ project }) => { name="MAILER_SUBJECTS_INVITE" readOnly={!IS_PLATFORM} errorMessage="Please enter a subject." + disabled={!canUpdate} /> = ({ project }) => { readOnly={!IS_PLATFORM} inputClassName="font-mono" errorMessage="Please enter an email body." + disabled={!canUpdate} /> diff --git a/studio/components/interfaces/Billing/Subscription/Subscription.tsx b/studio/components/interfaces/Billing/Subscription/Subscription.tsx index 2c1dcb66f09..9b9f5cdc85b 100644 --- a/studio/components/interfaces/Billing/Subscription/Subscription.tsx +++ b/studio/components/interfaces/Billing/Subscription/Subscription.tsx @@ -2,17 +2,17 @@ import { FC } from 'react' import dayjs from 'dayjs' import { sum } from 'lodash' import { useRouter } from 'next/router' -import { Loading, Button } from '@supabase/ui' -import { Dictionary } from '@supabase/grid' +import { Button, Loading } from '@supabase/ui' import { formatBytes } from 'lib/helpers' import { STRIPE_PRODUCT_IDS } from 'lib/constants' -import { useStore, useSubscriptionStats } from 'hooks' +import { usePermissions, useStore, useSubscriptionStats } from 'hooks' import CostBreakdownRow from './CostBreakdownRow' import { StripeSubscription } from './Subscription.types' import { deriveFeatureCost, deriveProductCost } from '../PAYGUsage/PAYGUsage.utils' import { chargeableProducts } from '../PAYGUsage/PAYGUsage.constants' import { PaygStats, ProductFeature } from '../PAYGUsage/PAYGUsage.types' +import { PermissionAction } from '@supabase/shared-types/out/constants' interface Props { project: any @@ -37,6 +37,11 @@ const Subscription: FC = ({ const { ui } = useStore() const isOrgOwner = ui.selectedOrganization?.is_owner + const canChangeSubscription = usePermissions( + PermissionAction.BILLING_WRITE, + 'stripe.subscriptions' + ) + const isPayg = subscription?.tier.prod_id === STRIPE_PRODUCT_IDS.PAYG const addOns = subscription?.addons ?? [] const paid = subscription && subscription.tier.unit_amount > 0 @@ -64,7 +69,7 @@ const Subscription: FC = ({
} + {selectedSchema === 'public' && ( + + )}
) } @@ -28,7 +34,9 @@ const EmptyState: FC = ({ selectedSchema, onAddTable }) => { ) : (

Select a table or create a new one

- +
)} diff --git a/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx index 7adaa5c79af..d8c26f34f2c 100644 --- a/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx @@ -1,16 +1,16 @@ import { FC, useRef } from 'react' import { useRouter } from 'next/router' import { observer } from 'mobx-react-lite' -import { isUndefined, find } from 'lodash' -import { SupabaseGrid, SupabaseGridRef, parseSupaTable, Dictionary } from '@supabase/grid' -import { PostgresTable, PostgresColumn } from '@supabase/postgres-meta' +import { find, isUndefined } from 'lodash' +import { Dictionary, parseSupaTable, SupabaseGrid, SupabaseGridRef } from '@supabase/grid' +import { PostgresColumn, PostgresTable } from '@supabase/postgres-meta' -import { useStore } from 'hooks' +import { usePermissions, useStore } from 'hooks' import NotFoundState from './NotFoundState' import GridHeaderActions from './GridHeaderActions' import SidePanelEditor from './SidePanelEditor' import { SchemaView } from 'components/layouts/TableEditorLayout/TableEditorLayout.types' -import { SidePanel } from '@supabase/ui' +import { PermissionAction } from '@supabase/shared-types/out/constants' interface Props { /** Theme for the editor */ @@ -64,6 +64,11 @@ const TableGridEditor: FC = ({ } const tableId = selectedTable?.id + const canAdminWrite = usePermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, String(tableId)) + const canInsert = usePermissions(PermissionAction.TENANT_SQL_INSERT, String(tableId)) + const canUpdate = usePermissions(PermissionAction.TENANT_SQL_UPDATE, String(tableId)) + // const canEdit = canAdminWrite && canInsert && canUpdate + const canEdit = true const isViewSelected = Object.keys(selectedTable).length === 2 const gridTable = !isViewSelected @@ -135,7 +140,7 @@ const TableGridEditor: FC = ({ theme={theme} gridProps={{ height: '100%' }} storageRef={projectRef} - editable={!isViewSelected && selectedTable.schema === 'public'} + editable={!isViewSelected && selectedTable.schema === 'public' && canEdit} schema={selectedTable.schema} table={gridTable} headerActions={ diff --git a/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx b/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx index c9c3218672a..95e4d6ecf50 100644 --- a/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx +++ b/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx @@ -1,29 +1,30 @@ -import { FC, useState, useEffect } from 'react' +import { FC, useEffect, useState } from 'react' import { useRouter } from 'next/router' import Link from 'next/link' import { + Alert, Button, Dropdown, - Typography, - Listbox, - Menu, - Input, - IconCopy, IconChevronDown, + IconCopy, IconEdit, - IconTrash, - IconSearch, - IconX, IconLoader, IconRefreshCw, - Alert, + IconSearch, + IconTrash, + IconX, + Input, + Listbox, + Menu, + Typography, } from '@supabase/ui' import { PostgresSchema, PostgresTable } from '@supabase/postgres-meta' import Base64 from 'lib/base64' -import { useStore } from 'hooks' +import { usePermissions, useStore } from 'hooks' import { SchemaView } from './TableEditorLayout.types' import ProductMenuItem from 'components/ui/ProductMenu/ProductMenuItem' +import { PermissionAction } from '@supabase/shared-types/out/constants' interface Props { selectedSchema?: string @@ -50,6 +51,7 @@ const TableEditorMenu: FC = ({ const [searchText, setSearchText] = useState('') const [schemaViews, setSchemaViews] = useState([]) const [isRefreshing, setIsRefreshing] = useState(false) + const canCreate = usePermissions(PermissionAction.TENANT_SQL_CREATE_TABLE, 'postgres.public.*') // We may need to shift this to the schema store and do something like meta.schema.loadViews() // I don't need we need a separate store for views @@ -116,7 +118,8 @@ const TableEditorMenu: FC = ({ // @ts-ignore label={ <> - schema {schema.name} + schema + {schema.name} } > @@ -129,7 +132,7 @@ const TableEditorMenu: FC = ({
- {selectedSchema === 'public' && ( + {selectedSchema === 'public' && canCreate && (
{/* Add new table button */} {infoButtonUrl && infoButtonLabel ? ( diff --git a/studio/hooks/queries/useOrganizationDetail.ts b/studio/hooks/queries/useOrganizationDetail.ts index 5941957cfd8..51a5bf0eb4b 100644 --- a/studio/hooks/queries/useOrganizationDetail.ts +++ b/studio/hooks/queries/useOrganizationDetail.ts @@ -4,9 +4,9 @@ import { Member } from 'types' import { API_URL } from 'lib/constants' export function useOrganizationDetail(slug: string) { - const url = `${API_URL}/props/org/${slug}` + const url = `${API_URL}/props/org/${slug}?member_roles` const { data, error } = useSWR(url, get) - const { members, products } = data ?? [] + const members = data?.members ?? [] const anyError = data?.error || error function mutateOrgMembers(updatedMembers: Member[], revalidate?: boolean) { @@ -15,7 +15,6 @@ export function useOrganizationDetail(slug: string) { return { members: members as Member[], - products, isLoading: !anyError && !data, isError: !!anyError, mutateOrgMembers, diff --git a/studio/pages/org/[slug]/settings.tsx b/studio/pages/org/[slug]/settings.tsx index 5c25e938271..8a34ef49981 100644 --- a/studio/pages/org/[slug]/settings.tsx +++ b/studio/pages/org/[slug]/settings.tsx @@ -1,4 +1,4 @@ -import { createContext, useEffect, useContext, useState } from 'react' +import { createContext, useContext, useEffect, useState } from 'react' import { useRouter } from 'next/router' import { observer, useLocalObservable } from 'mobx-react-lite' import { toJS } from 'mobx' @@ -6,23 +6,23 @@ import { pluckJsonSchemaFields, pluckObjectFields, timeout } from 'lib/helpers' import { AutoField } from 'uniforms-bootstrap4' import { organizations } from 'stores/jsonSchema' import { - Loading, + Alert, Button, + Dropdown, + Form, IconMoreHorizontal, + IconSearch, + IconTrash, + Input, + Loading, + Modal, Tabs, Typography, - IconTrash, - Alert, - Input, - Dropdown, - Modal, - IconSearch, - Form, } from '@supabase/ui' import { API_URL } from 'lib/constants' -import { useOrganizationDetail, useStore, withAuth } from 'hooks' -import { post, delete_, patch } from 'lib/common/fetch' +import { useOrganizationDetail, usePermissions, useStore, withAuth } from 'hooks' +import { delete_, get, patch, post } from 'lib/common/fetch' import { AccountLayoutWithoutAuth } from 'components/layouts' import { BillingSettings, InvoicesSettings } from 'components/interfaces/Organization' @@ -33,6 +33,8 @@ import InviteMemberModal from 'components/to-be-cleaned/ModalsDeprecated/InviteM import TextConfirmModal from 'components/ui/Modals/TextConfirmModal' import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel' import { NextPageWithLayout, Project } from 'types' +import useSWR from 'swr' +import { PermissionAction } from '@supabase/shared-types/out/constants' // [Joshen] Low prio refactor: Bring out general and team settings into their own components too @@ -47,22 +49,20 @@ const OrgSettingsLayout = withAuth( organization: {}, projects: [], members: [], - products: [], membersFilterString: '', get isOrgOwner() { - return ( - this.members.find((x: any) => x.profile.id === this.user?.id && x.is_owner) != undefined - ) + return this.members.find((x: any) => x.user_id === this.user?.id && x.is_owner) != undefined }, get filteredMembers() { - const temp = this.members.filter((x: any) => { - let profile = x.profile - return ( - profile.username.includes(this.membersFilterString) || - profile.primary_email.includes(this.membersFilterString) - ) - }) - return temp.sort((a: any, b: any) => a.profile.username.localeCompare(b.profile.username)) + const temp = this.members.filter( + ({ username, primary_email }: { username: string; primary_email: string }) => { + return ( + username.includes(this.membersFilterString) || + primary_email.includes(this.membersFilterString) + ) + } + ) + return temp.sort((a: any, b: any) => a.username.localeCompare(b.username)) }, initData(organization: any, user: any, projects: any) { this.organization = organization @@ -123,18 +123,15 @@ export default observer(OrgSettings) const OrganizationSettings = observer(() => { const PageState: any = useContext(PageContext) const { ui } = useStore() - const { - members, - products, - isError: isOrgDetailError, - } = useOrganizationDetail(ui.selectedOrganization?.slug || '') + const { members, isError: isOrgDetailError } = useOrganizationDetail( + ui.selectedOrganization?.slug || '' + ) useEffect(() => { if (!isOrgDetailError) { PageState.members = members ?? [] - PageState.products = products ?? [] } - }, [members, products, isOrgDetailError]) + }, [members, isOrgDetailError]) if (!PageState.organization) return
@@ -198,6 +195,10 @@ const TabsView = observer(() => { const GeneralSettings = observer(() => { const PageState: any = useContext(PageContext) const { ui } = useStore() + const canUpdateOrganization = usePermissions( + PermissionAction.SQL_UPDATE, + 'postgres.public.organizations' + ) const formModel = toJS(PageState.organization) // remove warning null value for controlled input @@ -214,7 +215,10 @@ const GeneralSettings = observer(() => { } else { const updatedOrg = response PageState.onOrgUpdated(updatedOrg) - ui.setNotification({ category: 'success', message: 'Successfully saved settings' }) + ui.setNotification({ + category: 'success', + message: 'Successfully saved settings', + }) } } @@ -231,11 +235,13 @@ const GeneralSettings = observer(() => { name="name" showInlineError errorMessage="Please enter an organization name" + disabled={!canUpdateOrganization} /> @@ -410,7 +416,10 @@ const TeamSettings = observer(() => { }, }) } catch (error: any) { - ui.setNotification({ category: 'error', message: `Error leaving: ${error?.message}` }) + ui.setNotification({ + category: 'error', + message: `Error leaving: ${error?.message}`, + }) } finally { setIsLeaving(false) } @@ -466,6 +475,10 @@ const MembersFilterInput = observer(() => { const MembersView = observer(() => { const PageState: any = useContext(PageContext) + const { data: roles, error: rolesError } = useSWR( + `${API_URL}/organizations/${PageState.organization.slug}/roles`, + get + ) return (
@@ -478,32 +491,36 @@ const MembersView = observer(() => { ]} body={[ PageState.filteredMembers.map((x: any) => ( - +
- {x.profile.username} + {x.username}
- {x.profile.primary_email} + {x.primary_email}
- {x.is_owner ? 'Owner' : 'Developer'} + {roles && + x.role_ids + .map((roleId) => roles.find(({ id }) => roleId === id).name) + .sort() + .join(', ')} {PageState.isOrgOwner && !x.is_owner && ( // @ts-ignore - + )}
@@ -530,7 +547,7 @@ const MembersView = observer(() => { ) }) -const OwnerDropdown = observer(({ members, member }: any) => { +const OwnerDropdown = observer(({ members, member, roles }: any) => { const PageState: any = useContext(PageContext) const { ui } = useStore() const { mutateOrgMembers } = useOrganizationDetail(ui.selectedOrganization?.slug || '') @@ -552,11 +569,11 @@ const OwnerDropdown = observer(({ members, member }: any) => { confirmAlert({ title: 'Confirm to remove', - message: `This is permanent! Are you sure you want to remove ${member.profile.username} ?`, + message: `This is permanent! Are you sure you want to remove ${member.username} ?`, onAsyncConfirm: async () => { setLoading(true) const response = await delete_(`${API_URL}/organizations/${orgSlug}/members/remove`, { - member_id: member.id, + member_id: member.member_id, }) if (response.error) { ui.setNotification({ @@ -567,7 +584,10 @@ const OwnerDropdown = observer(({ members, member }: any) => { } else { const updatedMembers = members.filter((x: any) => x.id !== member.id) mutateOrgMembers(updatedMembers) - ui.setNotification({ category: 'success', message: 'Successfully removed member' }) + ui.setNotification({ + category: 'success', + message: 'Successfully removed member', + }) } }, }) @@ -595,10 +615,43 @@ const OwnerDropdown = observer(({ members, member }: any) => { if (newOwner) newOwner.is_owner = true mutateOrgMembers(updatedMembers) setOwnerTransferIsVisble(false) - ui.setNotification({ category: 'success', message: 'Successfully transfered organization' }) + ui.setNotification({ + category: 'success', + message: 'Successfully transfered organization', + }) } } + async function handleRoleChecked(checked: boolean, roleId: number) { + setLoading(true) + const response = await (checked ? post : delete_)( + `${API_URL}/users/${member.gotrue_id}/roles/${roleId}`, + {} + ) + if (response.error) { + ui.setNotification({ + category: 'error', + message: `Failed to ${checked ? 'add' : 'remove'} member's role: ${response.error.message}`, + }) + } else { + const updatedMembers = [...members] + const updatedMember = updatedMembers.find((x) => x.id == member.id) + if (checked) { + updatedMember.role_ids.push(roleId) + } else { + updatedMember.role_ids = updatedMember.role_ids.filter( + (role_id: number) => role_id != roleId + ) + } + mutateOrgMembers(updatedMembers) + ui.setNotification({ + category: 'success', + message: `Successfully ${checked ? 'added' : 'removed'} member's role`, + }) + } + setLoading(false) + } + return (
{ align="end" overlay={ <> + Roles + {roles + ?.filter(({ name }: { name: string }) => name !== 'Owner') + .map(({ id, name }: { id: number; name: string }) => ( + handleRoleChecked(checked, id)} + > + {name} + + ))} + setOwnerTransferIsVisble(!ownerTransferIsVisble)}>

Make owner

@@ -641,8 +706,8 @@ const OwnerDropdown = observer(({ members, member }: any) => { text={ By transferring this organization, it will be solely owned by{' '} - {member.profile.username}, they - will also be able to remove you from the organization as a member + {member.username}, they will also + be able to remove you from the organization as a member } /> diff --git a/studio/pages/project/[ref]/auth/settings.tsx b/studio/pages/project/[ref]/auth/settings.tsx index 21dcb237ec4..e410659e1cd 100644 --- a/studio/pages/project/[ref]/auth/settings.tsx +++ b/studio/pages/project/[ref]/auth/settings.tsx @@ -1,24 +1,24 @@ import useSWR from 'swr' import { observer } from 'mobx-react-lite' -import { useState, useEffect } from 'react' +import { useEffect, useState } from 'react' import { useRouter } from 'next/router' import { AutoField, NumField } from 'uniforms-bootstrap4' -import { Button, Typography, Toggle as UIToggle } from '@supabase/ui' +import { Button, Toggle as UIToggle, Typography } from '@supabase/ui' import Divider from 'components/ui/Divider' import { API_URL } from 'lib/constants' -import { useStore } from 'hooks' +import { usePermissions, useStore } from 'hooks' import { authConfig } from 'stores/jsonSchema' import { get, patch } from 'lib/common/fetch' import { pluckJsonSchemaFields } from 'lib/helpers' import { AuthLayout } from 'components/layouts' import Table from 'components/to-be-cleaned/Table' import Panel from 'components/to-be-cleaned/Panel' -import Toggle from 'components/to-be-cleaned/forms/Toggle' import ToggleField from 'components/to-be-cleaned/forms/ToggleField' import SecretField from 'components/to-be-cleaned/forms/SecretField' import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel' import { NextPageWithLayout } from 'types' +import { PermissionAction } from '@supabase/shared-types/out/constants' const Auth: NextPageWithLayout = () => { return ( @@ -42,6 +42,10 @@ const Settings = () => { const [isCustomSMTPEnabled, setCustomSMTP] = useState(false) const URL = `${API_URL}/auth/${router.query.ref}/config` const { data: config, error }: any = useSWR(URL, get) + const canUpdate = usePermissions( + PermissionAction.SQL_UPDATE, + 'postgres.public.custom_config_gotrue' + ) const { ref: projectRef } = router.query if (error) { @@ -104,7 +108,10 @@ const Settings = () => { setModel({ ...updates }) ui.setNotification({ category: 'success', message: 'Settings saved' }) } catch (error: any) { - ui.setNotification({ category: 'error', message: `Update config failed: ${error.message}` }) + ui.setNotification({ + category: 'error', + message: `Update config failed: ${error.message}`, + }) } } @@ -140,16 +147,21 @@ const Settings = () => { name="SITE_URL" showInlineError errorMessage="Please enter a valid site url." + disabled={!canUpdate} /> - - - - + + + +
@@ -190,6 +202,7 @@ const Settings = () => { }} checked={model.EXTERNAL_EMAIL_ENABLED} descriptionText={authConfig.properties.EXTERNAL_EMAIL_ENABLED.help} + disabled={!canUpdate} /> { }} checked={model.MAILER_SECURE_EMAIL_CHANGE_ENABLED} descriptionText={authConfig.properties.MAILER_SECURE_EMAIL_CHANGE_ENABLED.help} + disabled={!canUpdate} /> { }} checked={!model.MAILER_AUTOCONFIRM} descriptionText={authConfig.properties.MAILER_AUTOCONFIRM.help} + disabled={!canUpdate} /> { } setCustomSMTP(!isCustomSMTPEnabled) }} + disabled={!canUpdate} /> {isCustomSMTPEnabled && ( <> @@ -246,21 +262,43 @@ const Settings = () => { name="SMTP_ADMIN_EMAIL" showInlineError errorMessage="Please enter from email address." + disabled={!canUpdate} + /> + + + + - - - - )} @@ -309,6 +347,7 @@ const Settings = () => { }} checked={model.EXTERNAL_PHONE_ENABLED} descriptionText={authConfig.properties.EXTERNAL_PHONE_ENABLED.help} + disabled={!canUpdate} /> {model.EXTERNAL_PHONE_ENABLED && ( @@ -317,6 +356,7 @@ const Settings = () => { name="SMS_PROVIDER" showInlineError errorMessage="Please enter the phone provider." + disabled={!canUpdate} /> {smsProviderModel?.SMS_PROVIDER === 'messagebird' ? ( <> @@ -324,11 +364,13 @@ const Settings = () => { name="SMS_MESSAGEBIRD_ACCESS_KEY" showInlineError errorMessage="Please enter the messagebird access key." + disabled={!canUpdate} /> ) : smsProviderModel?.SMS_PROVIDER === 'textlocal' ? ( @@ -337,11 +379,13 @@ const Settings = () => { name="SMS_TEXTLOCAL_API_KEY" showInlineError errorMessage="Please enter the vonage account sid." + disabled={!canUpdate} /> ) : smsProviderModel?.SMS_PROVIDER === 'vonage' ? ( @@ -350,16 +394,19 @@ const Settings = () => { name="SMS_VONAGE_API_KEY" showInlineError errorMessage="Please enter the vonage account sid." + disabled={!canUpdate} /> ) : ( @@ -368,16 +415,19 @@ const Settings = () => { name="SMS_TWILIO_ACCOUNT_SID" showInlineError errorMessage="Please enter the twilio account sid." + disabled={!canUpdate} /> )} @@ -394,6 +444,7 @@ const Settings = () => { // checked={!model.SMS_AUTOCONFIRM} descriptionText={authConfig.properties.SMS_AUTOCONFIRM.help} + disabled={!canUpdate} /> @@ -530,6 +581,7 @@ const Settings = () => { ) } + disabled={!canUpdate} /> {externalProvidersModel.EXTERNAL_APPLE_ENABLED && ( <> @@ -537,11 +589,13 @@ const Settings = () => { name="EXTERNAL_APPLE_CLIENT_ID" showInlineError errorMessage="Please enter the client id." + disabled={!canUpdate} /> )} @@ -559,6 +613,7 @@ const Settings = () => { ) } + disabled={!canUpdate} /> {externalProvidersModel.EXTERNAL_AZURE_ENABLED && ( <> @@ -566,16 +621,19 @@ const Settings = () => { name="EXTERNAL_AZURE_CLIENT_ID" showInlineError errorMessage="Please enter the client id." + disabled={!canUpdate} /> )} @@ -593,6 +651,7 @@ const Settings = () => { ) } + disabled={!canUpdate} /> {externalProvidersModel.EXTERNAL_BITBUCKET_ENABLED && ( <> @@ -600,11 +659,13 @@ const Settings = () => { name="EXTERNAL_BITBUCKET_CLIENT_ID" showInlineError errorMessage="Please enter the client id." + disabled={!canUpdate} /> )} @@ -622,6 +683,7 @@ const Settings = () => { ) } + disabled={!canUpdate} /> {externalProvidersModel.EXTERNAL_DISCORD_ENABLED && ( <> @@ -629,11 +691,13 @@ const Settings = () => { name="EXTERNAL_DISCORD_CLIENT_ID" showInlineError errorMessage="Please enter the client id." + disabled={!canUpdate} /> )} @@ -651,6 +715,7 @@ const Settings = () => { ) } + disabled={!canUpdate} /> {externalProvidersModel.EXTERNAL_FACEBOOK_ENABLED && ( <> @@ -658,11 +723,13 @@ const Settings = () => { name="EXTERNAL_FACEBOOK_CLIENT_ID" showInlineError errorMessage="Please enter the client id." + disabled={!canUpdate} /> )} @@ -680,6 +747,7 @@ const Settings = () => { ) } + disabled={!canUpdate} /> {externalProvidersModel.EXTERNAL_GITHUB_ENABLED && ( <> @@ -687,11 +755,13 @@ const Settings = () => { name="EXTERNAL_GITHUB_CLIENT_ID" showInlineError errorMessage="Please enter the client id." + disabled={!canUpdate} /> )} @@ -709,6 +779,7 @@ const Settings = () => { ) } + disabled={!canUpdate} /> {externalProvidersModel.EXTERNAL_GITLAB_ENABLED && ( <> @@ -716,11 +787,13 @@ const Settings = () => { name="EXTERNAL_GITLAB_CLIENT_ID" showInlineError errorMessage="Please enter the client id." + disabled={!canUpdate} /> )} @@ -738,6 +811,7 @@ const Settings = () => { ) } + disabled={!canUpdate} /> {externalProvidersModel.EXTERNAL_GOOGLE_ENABLED && ( <> @@ -745,11 +819,13 @@ const Settings = () => { name="EXTERNAL_GOOGLE_CLIENT_ID" showInlineError errorMessage="Please enter the client id." + disabled={!canUpdate} /> )} @@ -797,6 +873,7 @@ const Settings = () => { ) } + disabled={!canUpdate} /> {externalProvidersModel.EXTERNAL_LINKEDIN_ENABLED && ( <> @@ -804,11 +881,13 @@ const Settings = () => { name="EXTERNAL_LINKEDIN_CLIENT_ID" showInlineError errorMessage="Please enter the client id." + disabled={!canUpdate} /> )} @@ -826,6 +905,7 @@ const Settings = () => { ) } + disabled={!canUpdate} /> {externalProvidersModel.EXTERNAL_NOTION_ENABLED && ( <> @@ -833,11 +913,13 @@ const Settings = () => { name="EXTERNAL_NOTION_CLIENT_ID" showInlineError errorMessage="Please enter the client id." + disabled={!canUpdate} /> )} @@ -855,6 +937,7 @@ const Settings = () => { ) } + disabled={!canUpdate} /> {externalProvidersModel.EXTERNAL_TWITCH_ENABLED && ( <> @@ -862,11 +945,13 @@ const Settings = () => { name="EXTERNAL_TWITCH_CLIENT_ID" showInlineError errorMessage="Please enter the client id." + disabled={!canUpdate} /> )} @@ -884,6 +969,7 @@ const Settings = () => { ) } + disabled={!canUpdate} /> {externalProvidersModel.EXTERNAL_TWITTER_ENABLED && ( <> @@ -891,11 +977,13 @@ const Settings = () => { name="EXTERNAL_TWITTER_CLIENT_ID" showInlineError errorMessage="Please enter the client id." + disabled={!canUpdate} /> )} @@ -913,6 +1001,7 @@ const Settings = () => { ) } + disabled={!canUpdate} /> {externalProvidersModel.EXTERNAL_SLACK_ENABLED && ( <> @@ -920,11 +1009,13 @@ const Settings = () => { name="EXTERNAL_SLACK_CLIENT_ID" showInlineError errorMessage="Please enter the client id." + disabled={!canUpdate} /> )} @@ -942,6 +1033,7 @@ const Settings = () => { ) } + disabled={!canUpdate} /> {externalProvidersModel.EXTERNAL_SPOTIFY_ENABLED && ( <> @@ -949,11 +1041,13 @@ const Settings = () => { name="EXTERNAL_SPOTIFY_CLIENT_ID" showInlineError errorMessage="Please enter the client id." + disabled={!canUpdate} /> )} @@ -1005,6 +1099,7 @@ const Settings = () => { ) } + disabled={!canUpdate} /> {externalProvidersModel.EXTERNAL_ZOOM_ENABLED && ( <> @@ -1012,11 +1107,13 @@ const Settings = () => { name="EXTERNAL_ZOOM_CLIENT_ID" showInlineError errorMessage="Please enter the client id." + disabled={!canUpdate} /> )} @@ -1041,15 +1138,11 @@ const AuditLog = ({ interval, projectRef }: any) => { setIsLoading(true) try { let query = ` - SELECT - * - FROM - auth.audit_log_entries - WHERE - created_at > CURRENT_TIMESTAMP - interval '${interval}' - ORDER BY - created_at DESC - LIMIT 1000; + select * + from auth.audit_log_entries + where created_at > current_timestamp - interval '${interval}' + order by created_at desc + limit 1000; ` const response = await meta.query(query) if (response.error) throw response.error diff --git a/studio/pages/project/[ref]/reports/[id].tsx b/studio/pages/project/[ref]/reports/[id].tsx index 9881271b182..5ed213b4eda 100644 --- a/studio/pages/project/[ref]/reports/[id].tsx +++ b/studio/pages/project/[ref]/reports/[id].tsx @@ -6,19 +6,20 @@ import { groupBy, isNull } from 'lodash' import { toJS } from 'mobx' import dayjs from 'dayjs' import { - Dropdown, - Button, - IconSave, Badge, - IconSettings, + Button, + Dropdown, IconArrowRight, - IconHome, IconChevronRight, + IconHome, IconPlus, + IconSave, + IconSettings, } from '@supabase/ui' +import { usePermissions } from 'hooks' import { uuidv4 } from 'lib/helpers' -import { METRICS, METRIC_CATEGORIES, TIME_PERIODS_REPORTS } from 'lib/constants' +import { METRIC_CATEGORIES, METRICS, TIME_PERIODS_REPORTS } from 'lib/constants' import { useProjectContentStore } from 'stores/projectContentStore' import { ProjectLayoutWithAuth } from 'components/layouts' import Loading from 'components/ui/Loading' @@ -26,6 +27,7 @@ import EditReportModal from 'components/to-be-cleaned/Reports/EditReportModal' import ChartHandler from 'components/to-be-cleaned/Charts/ChartHandler' import DateRangePicker from 'components/to-be-cleaned/DateRangePicker' import { NextPageWithLayout } from 'types' +import { PermissionAction } from '@supabase/shared-types/out/constants' const ReactGridLayout = WidthProvider(RGL) @@ -64,6 +66,13 @@ const Reports = () => { const [startDate, setStartDate] = useState(null) const [endDate, setEndDate] = useState(null) + const canSaveReport = usePermissions( + PermissionAction.SQL_UPDATE, + 'postgres.public.user_content', + { + resource: { type: 'report' }, + } + ) const contentStore = useProjectContentStore(ref) /* @@ -293,7 +302,12 @@ const Reports = () => { x.attribute === metric.key)} - onChange={(e) => handleChartSelection({ metric, value: e })} + onChange={(e) => + handleChartSelection({ + metric, + value: e, + }) + } >
{metric.label} @@ -354,7 +368,7 @@ const Reports = () => { There are unsaved changes
)} - {hasEdits && ( + {canSaveReport && hasEdits && (