diff --git a/apps/studio/components/interfaces/Auth/Policies/Policies.tsx b/apps/studio/components/interfaces/Auth/Policies/Policies.tsx index 047b511de69..59382c015e3 100644 --- a/apps/studio/components/interfaces/Auth/Policies/Policies.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/Policies.tsx @@ -1,7 +1,6 @@ import type { PostgresPolicy } from '@supabase/postgres-meta' import { isEmpty } from 'lodash' -import { HelpCircle } from 'lucide-react' -import { useRouter } from 'next/router' +import Link from 'next/link' import { useState } from 'react' import { toast } from 'sonner' @@ -11,32 +10,34 @@ import { PolicyTableRowProps, } from 'components/interfaces/Auth/Policies/PolicyTableRow' import { ProtectedSchemaWarning } from 'components/interfaces/Database/ProtectedSchemaWarning' -import NoSearchResults from 'components/to-be-cleaned/NoSearchResults' -import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' -import InformationBox from 'components/ui/InformationBox' +import { NoSearchResults } from 'components/ui/NoSearchResults' import { useDatabasePolicyDeleteMutation } from 'data/database-policies/database-policy-delete-mutation' import { useTableUpdateMutation } from 'data/tables/table-update-mutation' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { Button, Card, CardContent } from 'ui' import ConfirmModal from 'ui-patterns/Dialogs/ConfirmDialog' interface PoliciesProps { + search?: string schema: string tables: PolicyTableRowProps['table'][] hasTables: boolean isLocked: boolean onSelectCreatePolicy: (table: string) => void onSelectEditPolicy: (policy: PostgresPolicy) => void + onResetSearch?: () => void } -const Policies = ({ +export const Policies = ({ + search, schema, tables, hasTables, isLocked, onSelectCreatePolicy, onSelectEditPolicy: onSelectEditPolicyAI, + onResetSearch, }: PoliciesProps) => { - const router = useRouter() const { ref } = useParams() const { data: project } = useSelectedProjectQuery() @@ -115,40 +116,21 @@ const Policies = ({ }) } - if (tables.length === 0) { + if (!hasTables) { return ( -
- router.push(`/project/${ref}/editor`)} - > -
- } - description={ -
-

- Policies restrict, on a per-user basis, which rows can be returned by normal - queries, or inserted, updated, or deleted by data modification commands. -

-

- This is also known as Row-Level Security (RLS). Each policy is attached to a - table, and the policy is executed each time its accessed. -

-
- } - /> -

- Create a table in this schema first before creating a policy. -

-
-
-
+ + +

No tables to create policies for

+ +

+ RLS Policies control per-user access to table rows. Create a table in this schema first + before creating a policy. +

+ +
+
) } @@ -170,7 +152,7 @@ const Policies = ({ )) ) : hasTables ? ( - + ) : null} @@ -202,5 +184,3 @@ const Policies = ({ ) } - -export default Policies diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx index c58cc46fa17..82a90858614 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx @@ -4,7 +4,6 @@ import { noop } from 'lodash' import { Edit, MoreVertical, Trash } from 'lucide-react' import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip' -import Panel from 'components/ui/Panel' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' @@ -13,6 +12,8 @@ import { Badge, Button, cn, + TableRow, + TableCell, DropdownMenu, DropdownMenuContent, DropdownMenuItem, @@ -57,58 +58,50 @@ const PolicyRow = ({ (policy.roles.includes('authenticated') || policy.roles.includes('public')) return ( - -
-
-

- {policy.command} -

- -
- -
-
- Applied to:{' '} - {policy.roles.slice(0, 3).map((role, i) => ( - - {role} - {i < Math.min(policy.roles.length, 3) - 1 ? ', ' : ' '} - - ))} - {policy.roles.length > 1 ? 'roles' : 'role'} -
- {policy.roles.length > 3 && ( - - - - + {policy.roles.length - 3} more roles - - - - {policy.roles.slice(3).join(', ')} - - - )} -
-
- + + +
+ {appliesToAnonymousUsers ? ( Applies to anonymous users ) : null}
-
-
+ + + {policy.command} + + +
+
+ {policy.roles.slice(0, 3).map((role, i) => ( + + {role} + {i < Math.min(policy.roles.length, 3) - 1 ? ', ' : ' '} + + ))} + {policy.roles.length > 1 ? 'roles' : 'role'} +
+ {policy.roles.length > 3 && ( + + + + + {policy.roles.length - 3} more roles + + + + {policy.roles.slice(3).join(', ')} + + + )} +
+
+ {!isLocked && ( @@ -127,9 +120,9 @@ const PolicyRow = ({ name: `Update policy ${policy.name}`, open: true, sqlSnippets: [sql], - initialInput: `Update the policy with name "${policy.name}" in the ${policy.schema} schema on the ${policy.table} table. It should...`, + initialInput: `Update the policy with name \"${policy.name}\" in the ${policy.schema} schema on the ${policy.table} table. It should...`, suggestions: { - title: `I can help you make a change to the policy "${policy.name}" in the ${policy.schema} schema on the ${policy.table} table, here are a few example prompts to get you started:`, + title: `I can help you make a change to the policy \"${policy.name}\" in the ${policy.schema} schema on the ${policy.table} table, here are a few example prompts to get you started:`, prompts: [ { label: 'Improve Policy', @@ -169,8 +162,8 @@ const PolicyRow = ({ )} -
- + + ) } diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx index 6858191349a..dfa72075deb 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx @@ -1,13 +1,13 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { noop } from 'lodash' -import { Lock, Unlock } from 'lucide-react' +import { Lock, Table } from 'lucide-react' import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id' import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' -import { AiIconAnimation, Badge } from 'ui' +import { AiIconAnimation, Badge, CardTitle } from 'ui' interface PolicyTableRowHeaderProps { table: { @@ -54,28 +54,19 @@ const PolicyTableRowHeader = ({ - {table.rls_enabled ? ( -
- -
- ) : ( -
- -
- )} -

{table.name}

+ + {table.name} + {!table.rls_enabled && RLS Disabled} -
- {isTableLocked && ( - - - Locked - - - )} -
+ {isTableLocked && ( + + + Locked + + + )} {!isTableLocked && (
diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx index 8732566cc23..e6199df9fa6 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx @@ -1,14 +1,24 @@ import type { PostgresPolicy } from '@supabase/postgres-meta' import { noop } from 'lodash' -import { Info } from 'lucide-react' import AlertError from 'components/ui/AlertError' -import Panel from 'components/ui/Panel' import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query' import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query' import { useTableRolesAccessQuery } from 'data/tables/table-roles-access-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' +import { + Alert_Shadcn_, + AlertDescription_Shadcn_, + Card, + CardContent, + CardHeader, + cn, + Table, + TableBody, + TableHead, + TableHeader, + TableRow, +} from 'ui' import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' import PolicyRow from './PolicyRow' import PolicyTableRowHeader from './PolicyTableRowHeader' @@ -73,85 +83,86 @@ export const PolicyTableRow = ({ .filter((policy) => policy.schema === table.schema && policy.table === table.name) .sort((a, b) => a.name.localeCompare(b.name)) const rlsEnabledNoPolicies = isRLSEnabled && policies.length === 0 + const isRealtimeSchema = table.schema === 'realtime' + const isRealtimeMessagesTable = isRealtimeSchema && table.name === 'messages' + const isTableLocked = isRealtimeSchema ? !isRealtimeMessagesTable : isLocked return ( - + - } - > + + {(isPubliclyReadableWritable || rlsEnabledNoPolicies) && ( -
-
- - {isPubliclyReadableWritable ? 'Warning' : 'Note'}: - {' '} - + {isPubliclyReadableWritable - ? 'Row Level Security is disabled. Your table is publicly readable and writable.' - : 'Row Level Security is enabled, but no policies exist. No data will be selectable via Supabase APIs.'} - - {isPubliclyReadableWritable && ( - - - - - - Anyone with the project's anonymous key can modify or delete your data. Enable RLS - and create access policies to keep your data secure. - - - )} -
+ ? "Anyone with your project's anonymous key can read, modify, or delete your data." + : 'No data will be selectable via Supabase APIs because RLS is enabled but no policies have been created yet.'} + + )} + {isLoading && ( -
+ -
+ )} + {isError && ( - + + + )} + {isSuccess && ( - <> - {policies.length === 0 && ( -
-

No policies created yet

-
+ + {policies.length === 0 ? ( +

No policies created yet

+ ) : ( +
+ + + Name + Command + Applied to + + Actions + + + + + {policies.map((policy) => ( + + ))} + +
)} - {policies?.map((policy) => ( - - ))} - + )} - + ) } diff --git a/apps/studio/components/interfaces/Realtime/Policies.tsx b/apps/studio/components/interfaces/Realtime/Policies.tsx index cffdf03c235..8c96c07a41a 100644 --- a/apps/studio/components/interfaces/Realtime/Policies.tsx +++ b/apps/studio/components/interfaces/Realtime/Policies.tsx @@ -1,7 +1,7 @@ import { PostgresPolicy } from '@supabase/postgres-meta' import { useState } from 'react' -import Policies from 'components/interfaces/Auth/Policies/Policies' +import { Policies } from 'components/interfaces/Auth/Policies/Policies' import { PolicyEditorPanel } from 'components/interfaces/Auth/Policies/PolicyEditorPanel' import AlertError from 'components/ui/AlertError' import { FormHeader } from 'components/ui/Forms/FormHeader' diff --git a/apps/studio/components/to-be-cleaned/NoSearchResults.tsx b/apps/studio/components/to-be-cleaned/NoSearchResults.tsx index 5cf4e9d5d3a..4214b8a92f8 100644 --- a/apps/studio/components/to-be-cleaned/NoSearchResults.tsx +++ b/apps/studio/components/to-be-cleaned/NoSearchResults.tsx @@ -1,8 +1,9 @@ -import SVG from 'react-inlinesvg' import { BASE_PATH } from 'lib/constants' +import SVG from 'react-inlinesvg' -// [Joshen] To be deprecated in favor of NoSearchResults in components/ui - +/** + * To be deprecated in favor of NoSearchResults in components/ui + */ export const NoSearchResults = () => { return (
diff --git a/apps/studio/components/ui/NoSearchResults.tsx b/apps/studio/components/ui/NoSearchResults.tsx index 71eba41ab86..d307c8535d5 100644 --- a/apps/studio/components/ui/NoSearchResults.tsx +++ b/apps/studio/components/ui/NoSearchResults.tsx @@ -6,7 +6,11 @@ export interface NoSearchResultsProps { className?: string } -const NoSearchResults = ({ searchString, onResetFilter, className }: NoSearchResultsProps) => { +export const NoSearchResults = ({ + searchString, + onResetFilter, + className, +}: NoSearchResultsProps) => { return (
void onSelectCreateSchema?: () => void portal?: boolean + align?: 'start' | 'end' } const SchemaSelector = ({ @@ -48,6 +49,7 @@ const SchemaSelector = ({ onSelectSchema, onSelectCreateSchema, portal = true, + align = 'start', }: SchemaSelectorProps) => { const [open, setOpen] = useState(false) const { can: canCreateSchemas } = useAsyncCheckProjectPermissions( @@ -130,7 +132,7 @@ const SchemaSelector = ({ diff --git a/apps/studio/pages/project/[ref]/auth/policies.tsx b/apps/studio/pages/project/[ref]/auth/policies.tsx index f379c6d457a..8a602e108d8 100644 --- a/apps/studio/pages/project/[ref]/auth/policies.tsx +++ b/apps/studio/pages/project/[ref]/auth/policies.tsx @@ -3,14 +3,15 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { Search } from 'lucide-react' import { useState } from 'react' -import { useParams } from 'common' import { useIsInlineEditorEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' -import Policies from 'components/interfaces/Auth/Policies/Policies' +import { Policies } from 'components/interfaces/Auth/Policies/Policies' import { getGeneralPolicyTemplates } from 'components/interfaces/Auth/Policies/PolicyEditorModal/PolicyEditorModal.constants' import { PolicyEditorPanel } from 'components/interfaces/Auth/Policies/PolicyEditorPanel' import { generatePolicyUpdateSQL } from 'components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRow.utils' import AuthLayout from 'components/layouts/AuthLayout/AuthLayout' import DefaultLayout from 'components/layouts/DefaultLayout' +import { PageLayout } from 'components/layouts/PageLayout/PageLayout' +import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import AlertError from 'components/ui/AlertError' import { DocsButton } from 'components/ui/DocsButton' import { EditorPanel } from 'components/ui/EditorPanel/EditorPanel' @@ -24,7 +25,7 @@ import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useUrlState } from 'hooks/ui/useUrlState' import { useIsProtectedSchema } from 'hooks/useProtectedSchemas' import type { NextPageWithLayout } from 'types' -import { Input } from 'ui' +import { Input } from 'ui-patterns/DataInputs/Input' /** * Filter tables by table name and policy name @@ -62,7 +63,6 @@ const onFilterTables = ( } const AuthPoliciesPage: NextPageWithLayout = () => { - const { ref } = useParams() const [params, setParams] = useUrlState<{ schema?: string search?: string @@ -106,140 +106,144 @@ const AuthPoliciesPage: NextPageWithLayout = () => { } return ( -
-
-
+ + +
+ { + const str = e.target.value + setParams({ ...params, search: str === '' ? undefined : str }) + }} + icon={} + /> { setParams({ ...params, search: undefined, schema }) }} /> -
- { - const str = e.target.value - setParams({ ...params, search: str === '' ? undefined : str }) - }} - icon={} - /> - -
-
- {isLoading && } + {isLoading && } - {isError && } + {isError && } - {isSuccess && ( - 0} + isLocked={isSchemaLocked} + onSelectCreatePolicy={(table: string) => { + setSelectedTable(table) + setSelectedPolicyToEdit(undefined) + if (isInlineEditorEnabled) { + setEditorPanelOpen(true) + } else { + setShowPolicyAiEditor(true) + } + }} + onSelectEditPolicy={(policy) => { + setSelectedPolicyToEdit(policy) + setSelectedTable(undefined) + if (isInlineEditorEnabled) { + setEditorPanelOpen(true) + } else { + setShowPolicyAiEditor(true) + } + }} + onResetSearch={() => setParams({ ...params, search: undefined })} + /> + )} + + 0} - isLocked={isSchemaLocked} - onSelectCreatePolicy={(table: string) => { - setSelectedTable(table) - setSelectedPolicyToEdit(undefined) - if (isInlineEditorEnabled) { - setEditorPanelOpen(true) - } else { - setShowPolicyAiEditor(true) - } - }} - onSelectEditPolicy={(policy) => { - setSelectedPolicyToEdit(policy) + searchString={searchString} + selectedTable={selectedTable} + selectedPolicy={selectedPolicyToEdit} + onSelectCancel={() => { setSelectedTable(undefined) - if (isInlineEditorEnabled) { - setEditorPanelOpen(true) - } else { - setShowPolicyAiEditor(true) - } + setShowPolicyAiEditor(false) + setSelectedPolicyToEdit(undefined) }} + authContext="database" /> - )} - { - setSelectedTable(undefined) - setShowPolicyAiEditor(false) - setSelectedPolicyToEdit(undefined) - }} - authContext="database" - /> - - { - setEditorPanelOpen(false) - setSelectedPolicyToEdit(undefined) - setSelectedTable(undefined) - }} - onRunSuccess={() => { - setEditorPanelOpen(false) - setSelectedPolicyToEdit(undefined) - setSelectedTable(undefined) - }} - initialValue={ - selectedPolicyToEdit - ? generatePolicyUpdateSQL(selectedPolicyToEdit) - : selectedTable - ? `create policy "replace_with_policy_name" - on ${schema}.${selectedTable} - for select - to authenticated - using ( - true -- Write your policy condition here -);` - : '' - } - label={ - selectedPolicyToEdit - ? 'RLS policies are just SQL statements that you can alter' - : selectedTable - ? `Create new RLS policy on "${selectedTable}"` - : '' - } - initialPrompt={ - selectedPolicyToEdit - ? `Update the policy with name "${selectedPolicyToEdit.name}" in the ${selectedPolicyToEdit.schema} schema on the ${selectedPolicyToEdit.table} table. It should...` - : selectedTable - ? `Create and name a entirely new RLS policy for the "${selectedTable}" table in the ${schema} schema. The policy should...` - : '' - } - templates={ - selectedPolicyToEdit - ? getGeneralPolicyTemplates( - selectedPolicyToEdit.schema, - selectedPolicyToEdit.table - ).map((template) => ({ - name: template.templateName, - description: template.description, - content: template.statement, - })) - : [] - } - /> -
+ { + setEditorPanelOpen(false) + setSelectedPolicyToEdit(undefined) + setSelectedTable(undefined) + }} + onRunSuccess={() => { + setEditorPanelOpen(false) + setSelectedPolicyToEdit(undefined) + setSelectedTable(undefined) + }} + initialValue={ + selectedPolicyToEdit + ? generatePolicyUpdateSQL(selectedPolicyToEdit) + : selectedTable + ? `create policy "replace_with_policy_name"\n on ${schema}.${selectedTable}\n for select\n to authenticated\n using (\n true -- Write your policy condition here\n);` + : '' + } + label={ + selectedPolicyToEdit + ? 'RLS policies are just SQL statements that you can alter' + : selectedTable + ? `Create new RLS policy on "${selectedTable}"` + : '' + } + initialPrompt={ + selectedPolicyToEdit + ? `Update the policy with name "${selectedPolicyToEdit.name}" in the ${selectedPolicyToEdit.schema} schema on the ${selectedPolicyToEdit.table} table. It should...` + : selectedTable + ? `Create and name a entirely new RLS policy for the "${selectedTable}" table in the ${schema} schema. The policy should...` + : '' + } + templates={ + selectedPolicyToEdit + ? getGeneralPolicyTemplates( + selectedPolicyToEdit.schema, + selectedPolicyToEdit.table + ).map((template) => ({ + name: template.templateName, + description: template.description, + content: template.statement, + })) + : [] + } + /> + + ) } AuthPoliciesPage.getLayout = (page) => ( -
{page}
+ + } + size="large" + > + {page} +
)