mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
feat: RLS policies for realtime channels (#27490)
* Add queries and mutations for realtime channels. * Change the layout, add a link for policies and modal for creating a channel. * Add a page for policies. * Add templates for realtime tables. * Fix the spacing in the PolicyDetails component. Add better types. * Pass templateData to the template generating functions. * Various CSS fixes. * Add components for rendering the realtime policies page. * Fix the editing policy flow. * Fix refetching when adding or deleting policies. * Minor various fixes. * Remove an old console.log. * More fixes for templates. * Make the realtime policies open by default when clicking on realtime. * Update the realtime policy templates. * Use a dropdown button for the three types of policies. * Fix a copy-paste mistake. * Fix icons. * Fix the toast. * Clean up the previous components. * Clean up more unneeded code. * Remove templateData prop. * Remove unneeded queries and mutations. * More cleanup. * Reuse the components from the Auth Policies. * Minor fixes for the new components. * Add a feature flag for AuthZ. * Revert some changes. * Fix a build error. * Fix all definitions. * Fix an issue with copying large lines which from templates which are word-wrapped. * Addressed various comments by Joshen. * Revert the UI changes in the Policy Editor. * Update the template names. * Small style fix --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
1 parent
553f2aa7fc
commit
ee8f901bfd
14 files changed
+312
-41
No files matched your search
+9
-2
@@ -1,9 +1,10 @@
|
||||
import { Check, ChevronsUpDown } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { UseFormReturn } from 'react-hook-form'
|
||||
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query'
|
||||
import { useTablesQuery } from 'data/tables/tables-query'
|
||||
import { Check, ChevronsUpDown } from 'lucide-react'
|
||||
import {
|
||||
Button,
|
||||
CommandEmpty_Shadcn_,
|
||||
@@ -37,7 +38,13 @@ interface PolicyDetailsV2Props {
|
||||
searchString?: string
|
||||
selectedTable?: string
|
||||
isEditing: boolean
|
||||
form: any
|
||||
form: UseFormReturn<{
|
||||
name: string
|
||||
table: string
|
||||
behavior: string
|
||||
command: string
|
||||
roles: string
|
||||
}>
|
||||
onUpdateCommand: (command: string) => void
|
||||
}
|
||||
|
||||
|
||||
+10
-3
@@ -8,7 +8,10 @@ import SimpleCodeBlock from 'components/to-be-cleaned/SimpleCodeBlock'
|
||||
import CardButton from 'components/ui/CardButton'
|
||||
import CopyButton from 'components/ui/CopyButton'
|
||||
import NoSearchResults from 'components/ui/NoSearchResults'
|
||||
import { getGeneralPolicyTemplates } from '../PolicyEditorModal/PolicyEditorModal.constants'
|
||||
import {
|
||||
getGeneralPolicyTemplates,
|
||||
getRealtimePolicyTemplates,
|
||||
} from '../PolicyEditorModal/PolicyEditorModal.constants'
|
||||
|
||||
interface PolicyTemplatesProps {
|
||||
schema: string
|
||||
@@ -26,7 +29,11 @@ export const PolicyTemplates = ({
|
||||
onSelectTemplate,
|
||||
}: PolicyTemplatesProps) => {
|
||||
const [search, setSearch] = useState('')
|
||||
const templates = getGeneralPolicyTemplates(schema, table.length > 0 ? table : 'table_name')
|
||||
|
||||
const templates =
|
||||
schema === 'realtime'
|
||||
? getRealtimePolicyTemplates()
|
||||
: getGeneralPolicyTemplates(schema, table.length > 0 ? table : 'table_name')
|
||||
|
||||
const baseTemplates =
|
||||
selectedPolicy !== undefined
|
||||
@@ -68,7 +75,7 @@ export const PolicyTemplates = ({
|
||||
title={template.name}
|
||||
titleClass="text-sm"
|
||||
className={cn(
|
||||
'transition',
|
||||
'transition w-full',
|
||||
template.id === selectedTemplate
|
||||
? '!border-stronger bg-surface-200 hover:!border-stronger'
|
||||
: ''
|
||||
|
||||
+8
-2
@@ -4,9 +4,9 @@ import { MutableRefObject, useEffect, useRef } from 'react'
|
||||
import { cn } from 'ui'
|
||||
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
import { noop } from 'lodash'
|
||||
import { formatQuery } from 'data/sql/format-sql-query'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { formatQuery } from 'data/sql/format-sql-query'
|
||||
import { noop } from 'lodash'
|
||||
|
||||
// [Joshen] Is there a way we can just have one single MonacoEditor component that's shared across the dashboard?
|
||||
// Feels like we're creating multiple copies of Editor. I'm keen to make this one the defacto as well so lets make sure
|
||||
@@ -119,6 +119,12 @@ const RLSCodeEditor = ({
|
||||
onChange()
|
||||
}
|
||||
|
||||
// when the value has changed, trigger the onChange callback so that the height of the container can be adjusted.
|
||||
// Happens when the value wordwraps and is updated via a template.
|
||||
useEffect(() => {
|
||||
onChange()
|
||||
}, [value])
|
||||
|
||||
async function formatPgsql(value: any) {
|
||||
try {
|
||||
const formatted = await formatQuery({
|
||||
|
||||
@@ -195,9 +195,9 @@ export const AIPolicyEditorPanel = memo(function ({
|
||||
}, [chatMessages, debugThread])
|
||||
|
||||
const { mutate: executeMutation, isLoading: isExecuting } = useExecuteSqlMutation({
|
||||
onSuccess: () => {
|
||||
onSuccess: async () => {
|
||||
// refresh all policies
|
||||
queryClient.invalidateQueries(databasePoliciesKeys.list(ref))
|
||||
await queryClient.invalidateQueries(databasePoliciesKeys.list(ref))
|
||||
toast.success('Successfully created new policy')
|
||||
onSelectCancel()
|
||||
},
|
||||
|
||||
@@ -145,7 +145,7 @@ const Policies = ({
|
||||
<section key={table.id}>
|
||||
<PolicyTableRow
|
||||
table={table}
|
||||
isLocked={isLocked}
|
||||
isLocked={schema === 'realtime' ? true : isLocked}
|
||||
onSelectToggleRLS={onSelectToggleRLS}
|
||||
onSelectCreatePolicy={() => onSelectCreatePolicy(table.name)}
|
||||
onSelectEditPolicy={onSelectEditPolicy}
|
||||
|
||||
+138
@@ -173,3 +173,141 @@ for select using (
|
||||
roles: [],
|
||||
},
|
||||
]
|
||||
|
||||
export const getRealtimePolicyTemplates = (): PolicyTemplate[] => {
|
||||
const results = [
|
||||
{
|
||||
id: 'policy-broadcast-1',
|
||||
preview: false,
|
||||
templateName: 'Allow listening for broadcasts for authenticated users only',
|
||||
description: 'This policy allows listening for broadcasts for authenticated users only.',
|
||||
statement: `
|
||||
create policy "Allow listening for broadcasts for authenticated users only"
|
||||
on realtime.messages for select
|
||||
to authenticated
|
||||
using ( realtime.messages.extension = 'broadcast' );`.trim(),
|
||||
name: 'Allow listening for broadcasts for authenticated users only',
|
||||
definition: "realtime.messages.extension = 'broadcast'",
|
||||
check: '',
|
||||
command: 'SELECT',
|
||||
roles: ['authenticated'],
|
||||
},
|
||||
{
|
||||
id: 'policy-broadcast-2',
|
||||
preview: false,
|
||||
templateName: 'Allow pushing broadcasts for authenticated users only',
|
||||
description: 'This policy allows pushing broadcasts for authenticated users only.',
|
||||
statement: `
|
||||
create policy "Allow pushing broadcasts for authenticated users only"
|
||||
ON realtime.messages for update
|
||||
TO authenticated
|
||||
using ( realtime.messages.extension = 'broadcast' )
|
||||
with check ( realtime.messages.extension = 'broadcast' );`.trim(),
|
||||
name: 'Allow pushing broadcasts for authenticated users only',
|
||||
definition: "realtime.messages.extension = 'broadcast'",
|
||||
check: "realtime.messages.extension = 'broadcast'",
|
||||
command: 'UPDATE',
|
||||
roles: ['authenticated'],
|
||||
},
|
||||
{
|
||||
id: 'policy-broadcast-3',
|
||||
preview: false,
|
||||
templateName: 'Allow listening for broadcasts from a specific channel',
|
||||
description: 'This policy allows listening for broadcasts from a specific channel.',
|
||||
statement: `
|
||||
create policy "Allow listening for broadcasts from a specific channel"
|
||||
on realtime.messages for select
|
||||
using ( realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name' );`.trim(),
|
||||
name: 'Allow listening for broadcasts from a specific channel',
|
||||
definition: `realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name'`,
|
||||
check: '',
|
||||
command: 'SELECT',
|
||||
roles: [],
|
||||
},
|
||||
{
|
||||
id: 'policy-broadcast-4',
|
||||
preview: false,
|
||||
templateName: 'Allow pushing broadcasts to specific channel',
|
||||
description: 'This policy allow pushing broadcasts to specific channel.',
|
||||
statement: `
|
||||
create policy "Allow pushing broadcasts to specific channel"
|
||||
ON realtime.messages for update
|
||||
using ( realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name' )
|
||||
with check ( realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name' );`.trim(),
|
||||
name: 'Allow pushing broadcasts to specific channel',
|
||||
definition: `realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name'`,
|
||||
check: `realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name'`,
|
||||
command: 'UPDATE',
|
||||
roles: [],
|
||||
},
|
||||
{
|
||||
id: 'policy-presences-1',
|
||||
preview: false,
|
||||
templateName: 'Allow listening for presences on all channels for authenticated users only',
|
||||
description:
|
||||
'This policy enables listening for presences on all channels for all authenticated users only.',
|
||||
statement: `
|
||||
create policy "Allow listening for presences on all channels for authenticated users only"
|
||||
on realtime.messages for select
|
||||
to authenticated
|
||||
using ( realtime.messages.extension = 'presence' );`.trim(),
|
||||
name: 'Allow listening for presences on all channels for authenticated users only',
|
||||
definition: "realtime.messages.extension = 'presence'",
|
||||
check: '',
|
||||
command: 'SELECT',
|
||||
roles: ['authenticated'],
|
||||
},
|
||||
{
|
||||
id: 'policy-presences-2',
|
||||
preview: false,
|
||||
templateName: 'Allow broadcasting presences on all channels for authenticated users only',
|
||||
description:
|
||||
'This policy enables broadcasting presences on all channels for all authenticated users only.',
|
||||
statement: `
|
||||
create policy "Allow broadcasting presences on all channels for authenticated users only"
|
||||
ON realtime.messages for update
|
||||
TO authenticated
|
||||
using ( realtime.messages.extension = 'presence' )
|
||||
with check ( realtime.messages.extension = 'presence' );
|
||||
;`.trim(),
|
||||
name: 'Allow broadcasting presences on all channels for authenticated users only',
|
||||
definition: "realtime.messages.extension = 'presence'",
|
||||
check: "realtime.messages.extension = 'presence'",
|
||||
command: 'UPDATE',
|
||||
roles: ['authenticated'],
|
||||
},
|
||||
{
|
||||
id: 'policy-presences-3',
|
||||
preview: false,
|
||||
templateName: 'Allow listening for presences from a specific channel',
|
||||
description: 'This policy enables listening for presences from a specific channel.',
|
||||
statement: `
|
||||
create policy "Allow listening for presences from a specific channel"
|
||||
on realtime.messages for select
|
||||
using ( realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name' );`.trim(),
|
||||
name: 'Allow listening for presences from a specific channel',
|
||||
definition: `realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name'`,
|
||||
check: '',
|
||||
command: 'SELECT',
|
||||
roles: [],
|
||||
},
|
||||
{
|
||||
id: 'policy-presences-4',
|
||||
preview: false,
|
||||
templateName: 'Publish presence to a specific channel',
|
||||
description: 'This policy allows publishing presence to a specific channel.',
|
||||
statement: `
|
||||
create policy "Publish presence to a specific channel"
|
||||
ON realtime.messages for update
|
||||
using ( realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name' )
|
||||
with check ( realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name' );
|
||||
;`.trim(),
|
||||
name: 'Publish presence to a specific channel',
|
||||
definition: `realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name'`,
|
||||
check: `realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name'`,
|
||||
command: 'UPDATE',
|
||||
roles: [],
|
||||
},
|
||||
] as PolicyTemplate[]
|
||||
return results
|
||||
}
|
||||
+24
-20
@@ -8,7 +8,7 @@ import { useRouter } from 'next/router'
|
||||
|
||||
import { useIsRLSAIAssistantEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { useCheckPermissions } from 'hooks'
|
||||
import { Badge, Button } from 'ui'
|
||||
import { Badge, Button, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_, Tooltip_Shadcn_ } from 'ui'
|
||||
|
||||
interface PolicyTableRowHeaderProps {
|
||||
table: PostgresTable
|
||||
@@ -29,6 +29,10 @@ const PolicyTableRowHeader = ({
|
||||
const isAiAssistantEnabled = useIsRLSAIAssistantEnabled()
|
||||
const canToggleRLS = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
|
||||
|
||||
const isRealtimeSchema = table.schema === 'realtime'
|
||||
const isRealtimeMessagesTable = isRealtimeSchema && table.name === 'messages'
|
||||
const isTableLocked = isRealtimeSchema ? !isRealtimeMessagesTable : isLocked
|
||||
|
||||
return (
|
||||
<div id={table.id.toString()} className="flex w-full items-center justify-between">
|
||||
<div className="flex gap-x-4 text-left">
|
||||
@@ -36,7 +40,7 @@ const PolicyTableRowHeader = ({
|
||||
<h4 className="m-0">{table.name}</h4>
|
||||
</Link>
|
||||
<div className="flex items-center gap-x-2">
|
||||
{isLocked && (
|
||||
{isTableLocked && (
|
||||
<Badge>
|
||||
<span className="flex gap-2 items-center text-xs uppercase text-foreground-lighter">
|
||||
<Lock size={12} /> Locked
|
||||
@@ -48,22 +52,22 @@ const PolicyTableRowHeader = ({
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
{!isLocked && (
|
||||
{!isTableLocked && (
|
||||
<div className="flex-1">
|
||||
<div className="flex flex-row justify-end gap-x-2">
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
type="default"
|
||||
disabled={!canToggleRLS}
|
||||
onClick={() => onSelectToggleRLS(table)}
|
||||
>
|
||||
{table.rls_enabled ? 'Disable RLS' : 'Enable RLS'}
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{!canToggleRLS && (
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="bottom">
|
||||
{!isRealtimeMessagesTable ? (
|
||||
<Tooltip_Shadcn_ delayDuration={0}>
|
||||
<TooltipTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
type="default"
|
||||
disabled={!canToggleRLS}
|
||||
onClick={() => onSelectToggleRLS(table)}
|
||||
>
|
||||
{table.rls_enabled ? 'Disable RLS' : 'Enable RLS'}
|
||||
</Button>
|
||||
</TooltipTrigger_Shadcn_>
|
||||
{!canToggleRLS && (
|
||||
<TooltipContent_Shadcn_ side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
@@ -75,10 +79,10 @@ const PolicyTableRowHeader = ({
|
||||
You need additional permissions to toggle RLS
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
</TooltipContent_Shadcn_>
|
||||
)}
|
||||
</Tooltip_Shadcn_>
|
||||
) : null}
|
||||
{!isAiAssistantEnabled && (
|
||||
<Button
|
||||
type="default"
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { PostgresPolicy } from '@supabase/postgres-meta'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Policies } from 'components/interfaces/Auth/Policies'
|
||||
import { AIPolicyEditorPanel } from 'components/interfaces/Auth/Policies/AIPolicyEditorPanel'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { useTablesQuery } from 'data/tables/tables-query'
|
||||
import { FormHeader } from 'components/ui/Forms'
|
||||
|
||||
export const RealtimePolicies = () => {
|
||||
const { project } = useProjectContext()
|
||||
|
||||
const [showPolicyEditor, setShowPolicyEditor] = useState(false)
|
||||
const [selectedPolicyToEdit, setSelectedPolicyToEdit] = useState<PostgresPolicy>()
|
||||
|
||||
const {
|
||||
data: tables,
|
||||
isLoading,
|
||||
isSuccess,
|
||||
isError,
|
||||
error,
|
||||
} = useTablesQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
schema: 'realtime',
|
||||
})
|
||||
|
||||
const filteredTables = (tables ?? []).filter((table) => table.name === 'messages')
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full w-full flex-col p-4 gap-y-4">
|
||||
<FormHeader
|
||||
className="!mb-0"
|
||||
title="Realtime policies"
|
||||
description="You can use RLS policies to control access to Realtime Channels"
|
||||
/>
|
||||
|
||||
{isLoading && <GenericSkeletonLoader />}
|
||||
|
||||
{isError && <AlertError error={error} subject="Failed to retrieve tables" />}
|
||||
|
||||
{isSuccess && (
|
||||
<div className="space-y-4">
|
||||
<Policies
|
||||
schema="realtime"
|
||||
tables={filteredTables}
|
||||
hasTables
|
||||
isLocked={false}
|
||||
onSelectCreatePolicy={() => {
|
||||
setSelectedPolicyToEdit(undefined)
|
||||
setShowPolicyEditor(true)
|
||||
}}
|
||||
onSelectEditPolicy={(policy) => {
|
||||
setSelectedPolicyToEdit(policy)
|
||||
setShowPolicyEditor(true)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AIPolicyEditorPanel
|
||||
visible={showPolicyEditor}
|
||||
searchString="messages"
|
||||
schema="realtime"
|
||||
selectedPolicy={selectedPolicyToEdit}
|
||||
onSelectCancel={() => setShowPolicyEditor(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,25 +2,29 @@ import { useRouter } from 'next/router'
|
||||
import { PropsWithChildren } from 'react'
|
||||
|
||||
import { ProductMenu } from 'components/ui/ProductMenu'
|
||||
import { useSelectedProject, withAuth } from 'hooks'
|
||||
import { useFlag, useSelectedProject, withAuth } from 'hooks'
|
||||
import { ProjectLayout } from '../'
|
||||
import { generateRealtimeMenu } from './RealtimeMenu.utils'
|
||||
|
||||
export interface RealtimeLayoutProps {
|
||||
title?: string
|
||||
title: string
|
||||
}
|
||||
|
||||
const RealtimeLayout = ({ children }: PropsWithChildren<RealtimeLayoutProps>) => {
|
||||
const RealtimeLayout = ({ title, children }: PropsWithChildren<RealtimeLayoutProps>) => {
|
||||
const project = useSelectedProject()
|
||||
|
||||
const router = useRouter()
|
||||
const page = router.pathname.split('/')[4]
|
||||
|
||||
const authzEnabled = useFlag('authzRealtime')
|
||||
|
||||
return (
|
||||
<ProjectLayout
|
||||
isLoading={false}
|
||||
title={title}
|
||||
product="Realtime"
|
||||
productMenu={<ProductMenu page={page} menu={generateRealtimeMenu(project)} />}
|
||||
productMenu={
|
||||
<ProductMenu page={page} menu={generateRealtimeMenu(project!, { authzEnabled })} />
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</ProjectLayout>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import type { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types'
|
||||
import type { Project } from 'data/projects/project-detail-query'
|
||||
|
||||
export const generateRealtimeMenu = (project?: Project): ProductMenuGroup[] => {
|
||||
export const generateRealtimeMenu = (
|
||||
project: Project,
|
||||
{ authzEnabled }: { authzEnabled: boolean }
|
||||
): ProductMenuGroup[] => {
|
||||
const ref = project?.ref ?? 'default'
|
||||
|
||||
return [
|
||||
{
|
||||
title: 'Realtime',
|
||||
title: 'Tools',
|
||||
items: [
|
||||
{
|
||||
name: 'Inspector',
|
||||
@@ -16,5 +19,20 @@ export const generateRealtimeMenu = (project?: Project): ProductMenuGroup[] => {
|
||||
},
|
||||
],
|
||||
},
|
||||
...(authzEnabled
|
||||
? [
|
||||
{
|
||||
title: 'Configuration',
|
||||
items: [
|
||||
{
|
||||
name: 'Policies',
|
||||
key: 'policies',
|
||||
url: `/project/${ref}/realtime/policies`,
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export const databasePoliciesKeys = {
|
||||
list: (projectRef: string | undefined, schema?: string | undefined) =>
|
||||
['projects', projectRef, 'database-policies', schema] as const,
|
||||
['projects', projectRef, 'database-policies', schema].filter(Boolean),
|
||||
}
|
||||
@@ -73,8 +73,9 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
const [protectedSchemas] = partition(schemas, (schema) =>
|
||||
EXCLUDED_SCHEMAS.includes(schema?.name ?? '')
|
||||
const [protectedSchemas] = partition(
|
||||
schemas,
|
||||
(schema) => schema?.name !== 'realtime' && EXCLUDED_SCHEMAS.includes(schema?.name ?? '')
|
||||
)
|
||||
const selectedSchema = schemas?.find((s) => s.name === schema)
|
||||
const isLocked = protectedSchemas.some((s) => s.id === selectedSchema?.id)
|
||||
|
||||
@@ -16,6 +16,8 @@ export const InspectorPage: NextPageWithLayout = () => {
|
||||
return <RealtimeInspector />
|
||||
}
|
||||
|
||||
InspectorPage.getLayout = (page) => <RealtimeLayout title="Realtime">{page}</RealtimeLayout>
|
||||
InspectorPage.getLayout = (page) => (
|
||||
<RealtimeLayout title="Realtime Inspector">{page}</RealtimeLayout>
|
||||
)
|
||||
|
||||
export default InspectorPage
|
||||
@@ -0,0 +1,12 @@
|
||||
import { RealtimePolicies } from 'components/interfaces/Realtime/Policies'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
|
||||
import RealtimeLayout from 'components/layouts/RealtimeLayout/RealtimeLayout'
|
||||
|
||||
const RealtimePoliciesPage: NextPageWithLayout = () => {
|
||||
return <RealtimePolicies />
|
||||
}
|
||||
|
||||
RealtimePoliciesPage.getLayout = (page) => <RealtimeLayout title="Policies">{page}</RealtimeLayout>
|
||||
|
||||
export default RealtimePoliciesPage
|
||||
Reference in new issue
Block a user