mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Chore/rls assistant iteration templates (#21447)
* Add templates to RLS assistant * Update button * lint * Chore/rls assistant iteration templates styling (#21459) * Update index.tsx * update icon pack * add new classes * expose hover card * add slight shadow to scroll area * monaco editor updated * tabs border-b updated * message styling updated to use circle AI icon * templates side panel updated to use new tailwind classes and ScrollArea * Updated tab label to "Assistant" and adjusted background colors * header updated to use simpler open/close button * divide-y styling added to chat * fix tabs bg state * shadcn tooltip updated to use correct bg color and added default delay duration * updated styling of codeblocks in chat * updated open / close button. also added a tooltip * divide border styling * add 'slide in' animation option for Hover Card * update code block * switch to Card Button * Update CardButton to expose props and allow any props to be applied * init * Apply suggestions from code review * Add entry step when clicking on create new policy for rls ai * templates code replacement state highlight selected template * Ai message code replacement state highlight selected message * Opt to leave templates panel open * Fix templates to use template name in policy sql * Fixes * fix * Remove unused imports --------- Co-authored-by: Jonathan Summers-Muir <MildTomato@users.noreply.github.com>
This commit is contained in:
1 parent
c1bde9a1a7
commit
8deb0865c5
22 files changed
+587
-180
No files matched your search
+7
-4
@@ -25,14 +25,16 @@ import Message from './Message'
|
||||
|
||||
interface AIPolicyChatProps {
|
||||
messages: MessageWithDebug[]
|
||||
selectedMessage?: string
|
||||
loading: boolean
|
||||
onSubmit: (s: string) => void
|
||||
onDiff: (s: string) => void
|
||||
onDiff: (message: { id: string; content: string }) => void
|
||||
onChange: (value: boolean) => void
|
||||
}
|
||||
|
||||
export const AIPolicyChat = ({
|
||||
messages,
|
||||
selectedMessage,
|
||||
loading,
|
||||
onSubmit,
|
||||
onDiff,
|
||||
@@ -78,13 +80,13 @@ export const AIPolicyChat = ({
|
||||
|
||||
return (
|
||||
<div id={'ai-chat-assistant'} className="flex flex-col h-full max-w-full">
|
||||
<div className="overflow-auto flex-1">
|
||||
<div className="overflow-auto flex-1 divide-y divide-border">
|
||||
<Message
|
||||
key="zero"
|
||||
role="assistant"
|
||||
content={`Hi${
|
||||
name ? ' ' + name : ''
|
||||
}, how can I help you? I'm powered by AI, so surprises and mistakes are possible.
|
||||
}, I can help you to write RLS policies. I'm powered by AI, so surprises and mistakes are possible.
|
||||
Make sure to verify any generated code or suggestions, and share feedback so that we can
|
||||
learn and improve.`}
|
||||
>
|
||||
@@ -107,7 +109,8 @@ export const AIPolicyChat = ({
|
||||
content={m.content}
|
||||
createdAt={new Date(m.createdAt || new Date()).getTime()}
|
||||
isDebug={m.isDebug}
|
||||
onDiff={onDiff}
|
||||
isSelected={m.id === selectedMessage}
|
||||
onDiff={(content) => onDiff({ id: m.id, content })}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
+41
-34
@@ -1,12 +1,15 @@
|
||||
import { PostgresPolicy } from '@supabase/postgres-meta'
|
||||
import styles from '@ui/layout/ai-icon-animation/ai-icon-animation-style.module.css'
|
||||
import clsx from 'clsx'
|
||||
import { X } from 'lucide-react'
|
||||
import { AiIcon, Button, SheetClose_Shadcn_, SheetHeader_Shadcn_, SheetTitle_Shadcn_, cn } from 'ui'
|
||||
|
||||
import { subscriptionHasHipaaAddon } from 'components/interfaces/Billing/Subscription/Subscription.utils'
|
||||
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import { PanelLeftClose, PanelRightClose, X } from 'lucide-react'
|
||||
import {
|
||||
SheetClose_Shadcn_,
|
||||
SheetHeader_Shadcn_,
|
||||
SheetTitle_Shadcn_,
|
||||
TooltipContent_Shadcn_,
|
||||
TooltipTrigger_Shadcn_,
|
||||
Tooltip_Shadcn_,
|
||||
cn,
|
||||
} from 'ui'
|
||||
|
||||
export const AIPolicyHeader = ({
|
||||
selectedPolicy,
|
||||
@@ -17,51 +20,55 @@ export const AIPolicyHeader = ({
|
||||
assistantVisible: boolean
|
||||
setAssistantVisible: (v: boolean) => void
|
||||
}) => {
|
||||
// Customers on HIPAA plans should not have access to Supabase AI
|
||||
const organization = useSelectedOrganization()
|
||||
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug })
|
||||
const hasHipaaAddon = subscriptionHasHipaaAddon(subscription)
|
||||
|
||||
return (
|
||||
<SheetHeader_Shadcn_
|
||||
className={`${
|
||||
selectedPolicy !== undefined ? 'pt-3 pb-0' : 'py-3'
|
||||
} flex flex-row justify-between items-center`}
|
||||
className={cn(
|
||||
selectedPolicy !== undefined ? 'pt-3 pb-0' : 'py-3',
|
||||
'flex flex-row justify-between items-center'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-row gap-3 items-center max-w-[75%]">
|
||||
<SheetClose_Shadcn_
|
||||
className={clsx(
|
||||
'text-light hover:text ring-offset-background transition-opacity hover:opacity-100',
|
||||
'text-muted hover:text ring-offset-background transition-opacity hover:opacity-100',
|
||||
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
'disabled:pointer-events-none data-[state=open]:bg-secondary'
|
||||
'disabled:pointer-events-none data-[state=open]:bg-secondary',
|
||||
'transition'
|
||||
)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetClose_Shadcn_>
|
||||
<div className="h-[24px] w-[1px] bg-border" />
|
||||
<div className="h-[24px] w-[1px] bg-border-overlay" />
|
||||
<SheetTitle_Shadcn_ className="truncate">
|
||||
{selectedPolicy !== undefined
|
||||
? `Update policy: ${selectedPolicy.name}`
|
||||
: 'Create a new Row Level Security policy'}
|
||||
</SheetTitle_Shadcn_>
|
||||
</div>
|
||||
{!hasHipaaAddon && (
|
||||
<Button
|
||||
aria-expanded={assistantVisible}
|
||||
aria-controls="ai-chat-assistant"
|
||||
size="tiny"
|
||||
type="outline"
|
||||
className={cn('group', styles['ai-icon__container--allow-hover-effect'], 'px-3 py-0.5')}
|
||||
rounded
|
||||
icon={
|
||||
<AiIcon className="scale-75 [&>div>div]:border-foreground-light [&>div>div]:group-hover:border-foreground -mr-0.5" />
|
||||
}
|
||||
onClick={() => setAssistantVisible(!assistantVisible)}
|
||||
>
|
||||
{assistantVisible ? <>Close Assistant</> : <>Open Assistant</>}
|
||||
</Button>
|
||||
)}
|
||||
<Tooltip_Shadcn_>
|
||||
<TooltipTrigger_Shadcn_ asChild>
|
||||
<button
|
||||
aria-expanded={assistantVisible}
|
||||
aria-controls="ai-chat-assistant"
|
||||
className={cn(
|
||||
!assistantVisible ? 'text-foreground-lighter' : 'text-light',
|
||||
'hover:text-foreground',
|
||||
'transition'
|
||||
)}
|
||||
onClick={() => setAssistantVisible(!assistantVisible)}
|
||||
>
|
||||
{!assistantVisible ? (
|
||||
<PanelLeftClose size={19} strokeWidth={1} />
|
||||
) : (
|
||||
<PanelRightClose size={19} strokeWidth={1} />
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger_Shadcn_>
|
||||
<TooltipContent_Shadcn_ side="left">
|
||||
{assistantVisible ? 'Hide' : 'Show'} tools
|
||||
</TooltipContent_Shadcn_>
|
||||
</Tooltip_Shadcn_>
|
||||
</SheetHeader_Shadcn_>
|
||||
)
|
||||
}
|
||||
+22
-4
@@ -44,7 +44,7 @@ export const AIPolicyPre = ({ onDiff, children, className }: AAIPolicyPreProps)
|
||||
value={formatted}
|
||||
language="sql"
|
||||
className={cn(
|
||||
'!bg-transparent !py-3 !px-3.5 prose dark:prose-dark',
|
||||
'!py-3 !px-3.5 prose dark:prose-dark transition',
|
||||
// change the look of the code block. The flex hack is so that the code is wrapping since
|
||||
// every word is a separate span
|
||||
'[&>code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap'
|
||||
@@ -52,12 +52,25 @@ export const AIPolicyPre = ({ onDiff, children, className }: AAIPolicyPreProps)
|
||||
hideCopy
|
||||
hideLineNumbers
|
||||
/>
|
||||
<div className="absolute top-5 right-2 bg-surface-100 border-muted border rounded-lg h-[28px] hidden group-hover:block">
|
||||
<div
|
||||
className={cn(
|
||||
'absolute',
|
||||
'top-0 right-2',
|
||||
'bg-surface-300',
|
||||
'border border-strong',
|
||||
'rounded',
|
||||
'h-[28px]',
|
||||
'opacity-0 group-hover:opacity-100',
|
||||
'group-hover:top-2',
|
||||
'transition-all'
|
||||
)}
|
||||
>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
type="text"
|
||||
size="tiny"
|
||||
className={cn('text-foreground-lighter hover:text-foreground', 'transition')}
|
||||
onClick={() => {
|
||||
onDiff(formatted)
|
||||
Telemetry.sendEvent(
|
||||
@@ -71,7 +84,7 @@ export const AIPolicyPre = ({ onDiff, children, className }: AAIPolicyPreProps)
|
||||
)
|
||||
}}
|
||||
>
|
||||
<FileDiff className="h-4 w-4" />
|
||||
<FileDiff className={cn('h-4 w-4')} />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
@@ -93,6 +106,7 @@ export const AIPolicyPre = ({ onDiff, children, className }: AAIPolicyPreProps)
|
||||
<Button
|
||||
type="text"
|
||||
size="tiny"
|
||||
className={cn('text-foreground-lighter hover:text-foreground', 'transition')}
|
||||
onClick={() => {
|
||||
handleCopy(formatted)
|
||||
Telemetry.sendEvent(
|
||||
@@ -106,7 +120,11 @@ export const AIPolicyPre = ({ onDiff, children, className }: AAIPolicyPreProps)
|
||||
)
|
||||
}}
|
||||
>
|
||||
{copied ? <Check size={16} className="text-brand-600" /> : <Copy size={16} />}
|
||||
{copied ? (
|
||||
<Check size={16} className="text-brand-600" />
|
||||
) : (
|
||||
<Copy size={16} className={cn('h-4 w-4')} />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
|
||||
@@ -4,7 +4,7 @@ import Image from 'next/image'
|
||||
import { PropsWithChildren, memo, useMemo } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { AiIconAnimation, Badge, markdownComponents } from 'ui'
|
||||
import { AiIconAnimation, Badge, cn, markdownComponents } from 'ui'
|
||||
|
||||
import { useProfile } from 'lib/profile'
|
||||
import { AIPolicyPre } from './AIPolicyPre'
|
||||
@@ -15,6 +15,7 @@ interface MessageProps {
|
||||
content?: string
|
||||
createdAt?: number
|
||||
isDebug?: boolean
|
||||
isSelected?: boolean
|
||||
onDiff?: (s: string) => void
|
||||
}
|
||||
|
||||
@@ -24,6 +25,7 @@ const Message = memo(function Message({
|
||||
content,
|
||||
createdAt,
|
||||
isDebug,
|
||||
isSelected,
|
||||
onDiff = noop,
|
||||
children,
|
||||
}: PropsWithChildren<MessageProps>) {
|
||||
@@ -31,12 +33,21 @@ const Message = memo(function Message({
|
||||
|
||||
const icon = useMemo(() => {
|
||||
return role === 'assistant' ? (
|
||||
<AiIconAnimation
|
||||
loading={content === 'Thinking...'}
|
||||
className="[&>div>div]:border-black dark:[&>div>div]:border-white"
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'bg-foreground border border-foreground-light rounded-full',
|
||||
'w-8 h-8',
|
||||
'flex items-center justify-center'
|
||||
)}
|
||||
>
|
||||
<AiIconAnimation
|
||||
loading={content === 'Thinking...'}
|
||||
className={cn('scale-75', '[&>div>div]:border-background')}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative border shadow-lg w-8 h-8 rounded-full overflow-hidden">
|
||||
{/* // TODO: this only works for GitHub profiles */}
|
||||
<Image
|
||||
src={`https://github.com/${profile?.username}.png` || ''}
|
||||
width={30}
|
||||
@@ -51,10 +62,12 @@ const Message = memo(function Message({
|
||||
if (!content) return null
|
||||
|
||||
return (
|
||||
<div className="flex flex-col py-4 gap-4 border-t px-5 text-foreground-light text-sm">
|
||||
<div className="flex flex-col py-4 gap-4 px-5 text-foreground-light text-sm">
|
||||
<div className="flex flex-row gap-3 items-center">
|
||||
{icon}
|
||||
<span className="text-sm">{role === 'assistant' ? 'Assistant' : name ? name : 'You'}</span>
|
||||
<span className="text-foreground">
|
||||
{role === 'assistant' ? 'Assistant' : name ? name : 'You'}
|
||||
</span>
|
||||
{createdAt && (
|
||||
<span className="text-xs text-foreground-muted">{dayjs(createdAt).fromNow()}</span>
|
||||
)}
|
||||
@@ -67,7 +80,12 @@ const Message = memo(function Message({
|
||||
...markdownComponents,
|
||||
pre: (props: any) => {
|
||||
return (
|
||||
<AIPolicyPre onDiff={onDiff} className="pt-3">
|
||||
<AIPolicyPre
|
||||
onDiff={onDiff}
|
||||
className={
|
||||
isSelected ? '[&>div>pre]:!border-stronger [&>div>pre]:!bg-surface-200' : ''
|
||||
}
|
||||
>
|
||||
{props.children[0].props.children}
|
||||
</AIPolicyPre>
|
||||
)
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import { Search } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { Badge, HoverCard, HoverCardContent, HoverCardTrigger, Input, cn } from 'ui'
|
||||
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
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 { PolicyTemplate } from '../PolicyTemplates/PolicyTemplates.constants'
|
||||
|
||||
interface PolicyTemplatesProps {
|
||||
selectedTemplate?: string
|
||||
onSelectTemplate: (template: { id: string; content: string }) => void
|
||||
}
|
||||
|
||||
export const PolicyTemplates = ({ selectedTemplate, onSelectTemplate }: PolicyTemplatesProps) => {
|
||||
const [search, setSearch] = useState('')
|
||||
const templates = getGeneralPolicyTemplates('schema_name', 'table_name')
|
||||
const filteredTemplates =
|
||||
search.length > 0
|
||||
? templates.filter(
|
||||
(template) =>
|
||||
template.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
template.command.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
: templates
|
||||
|
||||
return (
|
||||
<div className="h-full px-content py-content flex flex-col gap-3">
|
||||
<label className="sr-only" htmlFor="template-search">
|
||||
Search templates
|
||||
</label>
|
||||
<Input
|
||||
size="small"
|
||||
id="template-search"
|
||||
icon={<Search size={16} className="text-foreground-muted" />}
|
||||
placeholder="Search templates"
|
||||
value={search}
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
/>
|
||||
|
||||
{search.length > 0 && filteredTemplates.length === 0 && (
|
||||
<NoSearchResults searchString={search} className="min-w-full" />
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{filteredTemplates.map((template) => {
|
||||
return (
|
||||
<HoverCard key={template.id} openDelay={100} closeDelay={0}>
|
||||
<HoverCardTrigger>
|
||||
<CardButton
|
||||
title={template.name}
|
||||
titleClass="text-sm"
|
||||
className={cn(
|
||||
'transition',
|
||||
template.id === selectedTemplate
|
||||
? '!border-stronger bg-surface-200 hover:!border-stronger'
|
||||
: ''
|
||||
)}
|
||||
key={template.id}
|
||||
onClick={() => onSelectTemplate({ id: template.id, content: template.statement })}
|
||||
hideChevron
|
||||
fixedHeight={false}
|
||||
icon={
|
||||
<div className="min-w-16">
|
||||
<Badge
|
||||
className="!rounded font-mono"
|
||||
color={
|
||||
template.command === 'ALL'
|
||||
? 'scale'
|
||||
: template.command === 'SELECT'
|
||||
? 'green'
|
||||
: template.command === 'UPDATE'
|
||||
? 'blue'
|
||||
: template.command === 'DELETE'
|
||||
? 'tomato'
|
||||
: 'yellow'
|
||||
}
|
||||
>
|
||||
{template.command}
|
||||
</Badge>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Markdown content={template.description} className="[&>p]:m-0 space-y-2" />
|
||||
</CardButton>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent
|
||||
hideWhenDetached
|
||||
side="left"
|
||||
align="center"
|
||||
className="w-96 flex"
|
||||
animate="slide-in"
|
||||
>
|
||||
<SimpleCodeBlock
|
||||
showCopy={false}
|
||||
className="sql"
|
||||
parentClassName="!p-0 [&>div>span]:text-xs"
|
||||
>
|
||||
{template.statement}
|
||||
</SimpleCodeBlock>
|
||||
<CopyButton
|
||||
iconOnly
|
||||
type="default"
|
||||
className="px-1 absolute top-1.5 right-1.5"
|
||||
text={template.statement}
|
||||
/>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -81,7 +81,6 @@ const RLSCodeEditor = ({
|
||||
readOnly: false,
|
||||
minimap: { enabled: false },
|
||||
wordWrap: 'on' as const,
|
||||
fixedOverflowWidgets: true,
|
||||
contextmenu: true,
|
||||
lineNumbers: undefined,
|
||||
glyphMargin: undefined,
|
||||
|
||||
@@ -8,7 +8,21 @@ import dynamic from 'next/dynamic'
|
||||
import { useRouter } from 'next/router'
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { Button, Modal, SheetContent_Shadcn_, SheetFooter_Shadcn_, Sheet_Shadcn_, cn } from 'ui'
|
||||
import {
|
||||
Button,
|
||||
IconEdit,
|
||||
IconGrid,
|
||||
Modal,
|
||||
ScrollArea,
|
||||
SheetContent_Shadcn_,
|
||||
SheetFooter_Shadcn_,
|
||||
Sheet_Shadcn_,
|
||||
TabsContent_Shadcn_,
|
||||
TabsList_Shadcn_,
|
||||
TabsTrigger_Shadcn_,
|
||||
Tabs_Shadcn_,
|
||||
cn,
|
||||
} from 'ui'
|
||||
|
||||
import {
|
||||
IStandaloneCodeEditor,
|
||||
@@ -35,6 +49,10 @@ import { AIPolicyHeader } from './AIPolicyHeader'
|
||||
import PolicyDetails from './PolicyDetails'
|
||||
import QueryError from './QueryError'
|
||||
import RLSCodeEditor from './RLSCodeEditor'
|
||||
import { PolicyTemplates } from './PolicyTemplates'
|
||||
import { subscriptionHasHipaaAddon } from 'components/interfaces/Billing/Subscription/Subscription.utils'
|
||||
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
|
||||
import CardButton from 'components/ui/CardButton'
|
||||
|
||||
const DiffEditor = dynamic(
|
||||
() => import('@monaco-editor/react').then(({ DiffEditor }) => DiffEditor),
|
||||
@@ -72,15 +90,20 @@ export const AIPolicyEditorPanel = memo(function ({
|
||||
const isOptedInToAI = selectedOrganization?.opt_in_tags?.includes(OPT_IN_TAGS.AI_SQL) ?? false
|
||||
|
||||
const [error, setError] = useState<QueryResponseError>()
|
||||
const [errorPanelOpen, setErrorPanelOpen] = useState(true)
|
||||
const [showDetails, setShowDetails] = useState(false)
|
||||
const [errorPanelOpen, setErrorPanelOpen] = useState<boolean>(true)
|
||||
const [showDetails, setShowDetails] = useState<boolean>(false)
|
||||
const [selectedDiff, setSelectedDiff] = useState<string>()
|
||||
// [Joshen] Separate state here as there's a delay between submitting and the API updating the loading status
|
||||
const [debugThread, setDebugThread] = useState<MessageWithDebug[]>([])
|
||||
const [assistantVisible, setAssistantPanel] = useState(false)
|
||||
const [isAssistantChatInputEmpty, setIsAssistantChatInputEmpty] = useState(true)
|
||||
const [assistantVisible, setAssistantPanel] = useState<boolean>(false)
|
||||
const [isAssistantChatInputEmpty, setIsAssistantChatInputEmpty] = useState<boolean>(true)
|
||||
const [incomingChange, setIncomingChange] = useState<string | undefined>(undefined)
|
||||
// used for confirmation when closing the panel with unsaved changes
|
||||
const [isClosingPolicyEditorPanel, setIsClosingPolicyEditorPanel] = useState(false)
|
||||
const [isClosingPolicyEditorPanel, setIsClosingPolicyEditorPanel] = useState<boolean>(false)
|
||||
|
||||
// Customers on HIPAA plans should not have access to Supabase AI
|
||||
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: selectedOrganization?.slug })
|
||||
const hasHipaaAddon = subscriptionHasHipaaAddon(subscription)
|
||||
|
||||
const { data: entities } = useEntityDefinitionsQuery(
|
||||
{
|
||||
@@ -222,6 +245,24 @@ export const AIPolicyEditorPanel = memo(function ({
|
||||
setDebugThread(cleanedMessages)
|
||||
}
|
||||
|
||||
const updateEditorWithCheckForDiff = (value: { id: string; content: string }) => {
|
||||
const editorModel = editorRef.current?.getModel()
|
||||
if (!editorModel) return
|
||||
|
||||
const existingValue = editorRef.current?.getValue() ?? ''
|
||||
if (existingValue.length === 0) {
|
||||
editorRef.current?.executeEdits('apply-template', [
|
||||
{
|
||||
text: value.content,
|
||||
range: editorModel.getFullModelRange(),
|
||||
},
|
||||
])
|
||||
} else {
|
||||
setSelectedDiff(value.id)
|
||||
setIncomingChange(value.content)
|
||||
}
|
||||
}
|
||||
|
||||
// when the panel is closed, reset all values
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
@@ -233,6 +274,9 @@ export const AIPolicyEditorPanel = memo(function ({
|
||||
setDebugThread([])
|
||||
setChatId(uuidv4())
|
||||
setShowDetails(false)
|
||||
setSelectedDiff(undefined)
|
||||
} else {
|
||||
setAssistantPanel(true)
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
@@ -251,8 +295,9 @@ export const AIPolicyEditorPanel = memo(function ({
|
||||
<SheetContent_Shadcn_
|
||||
size={assistantVisible ? 'lg' : 'default'}
|
||||
className={cn(
|
||||
'bg-surface-200',
|
||||
'p-0 flex flex-row gap-0',
|
||||
assistantVisible ? '!min-w-[1024px]' : '!min-w-[600px]'
|
||||
assistantVisible ? '!min-w-[1200px]' : '!min-w-[600px]'
|
||||
)}
|
||||
>
|
||||
<div className={cn('flex flex-col grow w-full', assistantVisible && 'w-[60%]')}>
|
||||
@@ -261,16 +306,14 @@ export const AIPolicyEditorPanel = memo(function ({
|
||||
assistantVisible={assistantVisible}
|
||||
setAssistantVisible={setAssistantPanel}
|
||||
/>
|
||||
|
||||
<PolicyDetails
|
||||
policy={selectedPolicy}
|
||||
showDetails={showDetails}
|
||||
toggleShowDetails={() => setShowDetails(!showDetails)}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col h-full w-full justify-between">
|
||||
{incomingChange ? (
|
||||
<div className="px-5 py-3 flex justify-between gap-3 bg-muted">
|
||||
<div className="px-5 py-3 flex justify-between gap-3 bg-surface-75">
|
||||
<div className="flex gap-2 items-center text-foreground-light">
|
||||
<FileDiff className="h-4 w-4" />
|
||||
<span className="text-sm">Accept changes from assistant</span>
|
||||
@@ -280,6 +323,7 @@ export const AIPolicyEditorPanel = memo(function ({
|
||||
type="default"
|
||||
onClick={() => {
|
||||
setIncomingChange(undefined)
|
||||
setSelectedDiff(undefined)
|
||||
Telemetry.sendEvent(
|
||||
{
|
||||
category: 'rls_editor',
|
||||
@@ -297,6 +341,7 @@ export const AIPolicyEditorPanel = memo(function ({
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
acceptChange()
|
||||
setSelectedDiff(undefined)
|
||||
Telemetry.sendEvent(
|
||||
{
|
||||
category: 'rls_editor',
|
||||
@@ -377,20 +422,66 @@ export const AIPolicyEditorPanel = memo(function ({
|
||||
</div>
|
||||
</div>
|
||||
{assistantVisible && (
|
||||
<div className={cn('flex border-l grow w-full', assistantVisible && 'w-[40%]')}>
|
||||
<AIPolicyChat
|
||||
messages={messages}
|
||||
onSubmit={(message) =>
|
||||
append({
|
||||
content: message,
|
||||
role: 'user',
|
||||
createdAt: new Date(),
|
||||
})
|
||||
}
|
||||
onDiff={setIncomingChange}
|
||||
onChange={setIsAssistantChatInputEmpty}
|
||||
loading={isLoading || isDebugSqlLoading}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'border-l shadow-[rgba(0,0,0,0.13)_-4px_0px_6px_0px] z-10',
|
||||
assistantVisible && 'w-[50%]',
|
||||
'bg-studio'
|
||||
)}
|
||||
>
|
||||
<Tabs_Shadcn_ defaultValue="templates" className="flex flex-col h-full w-full">
|
||||
<TabsList_Shadcn_ className="flex gap-4 px-content pt-2">
|
||||
<TabsTrigger_Shadcn_
|
||||
key="templates"
|
||||
value="templates"
|
||||
className="px-0 data-[state=active]:bg-transparent"
|
||||
>
|
||||
Templates
|
||||
</TabsTrigger_Shadcn_>
|
||||
{!hasHipaaAddon && (
|
||||
<TabsTrigger_Shadcn_
|
||||
key="conversation"
|
||||
value="conversation"
|
||||
className="px-0 data-[state=active]:bg-transparent"
|
||||
>
|
||||
Assistant
|
||||
</TabsTrigger_Shadcn_>
|
||||
)}
|
||||
</TabsList_Shadcn_>
|
||||
<TabsContent_Shadcn_
|
||||
value="templates"
|
||||
className={cn(
|
||||
'!mt-0 overflow-y-auto',
|
||||
'data-[state=active]:flex data-[state=active]:grow'
|
||||
)}
|
||||
>
|
||||
<ScrollArea className="h-full w-full">
|
||||
<PolicyTemplates
|
||||
selectedTemplate={selectedDiff}
|
||||
onSelectTemplate={updateEditorWithCheckForDiff}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</TabsContent_Shadcn_>
|
||||
<TabsContent_Shadcn_
|
||||
value="conversation"
|
||||
className="flex grow !mt-0 overflow-y-auto"
|
||||
>
|
||||
<AIPolicyChat
|
||||
messages={messages}
|
||||
selectedMessage={selectedDiff}
|
||||
onSubmit={(message) =>
|
||||
append({
|
||||
content: message,
|
||||
role: 'user',
|
||||
createdAt: new Date(),
|
||||
})
|
||||
}
|
||||
onDiff={updateEditorWithCheckForDiff}
|
||||
onChange={setIsAssistantChatInputEmpty}
|
||||
loading={isLoading || isDebugSqlLoading}
|
||||
/>
|
||||
</TabsContent_Shadcn_>
|
||||
</Tabs_Shadcn_>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -170,6 +170,43 @@ const Policies = ({
|
||||
})
|
||||
}
|
||||
|
||||
if (tables.length === 0) {
|
||||
return (
|
||||
<div className="flex-grow flex items-center justify-center">
|
||||
<ProductEmptyState
|
||||
size="large"
|
||||
title="Row-Level Security (RLS) Policies"
|
||||
ctaButtonLabel="Create a table"
|
||||
infoButtonLabel="What is RLS?"
|
||||
infoButtonUrl="https://supabase.com/docs/guides/auth/row-level-security"
|
||||
onClickCta={() => router.push(`/project/${ref}/editor`)}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<InformationBox
|
||||
title="What are policies?"
|
||||
icon={<IconHelpCircle strokeWidth={2} />}
|
||||
description={
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm">
|
||||
Policies restrict, on a per-user basis, which rows can be returned by normal
|
||||
queries, or inserted, updated, or deleted by data modification commands.
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Create a table in this schema first before creating a policy.
|
||||
</p>
|
||||
</div>
|
||||
</ProductEmptyState>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-y-4 pb-4">
|
||||
@@ -189,40 +226,7 @@ const Policies = ({
|
||||
))
|
||||
) : hasTables ? (
|
||||
<NoSearchResults />
|
||||
) : (
|
||||
<div className="flex-grow">
|
||||
<ProductEmptyState
|
||||
size="large"
|
||||
title="Row-Level Security (RLS) Policies"
|
||||
ctaButtonLabel="Create a table"
|
||||
infoButtonLabel="What is RLS?"
|
||||
infoButtonUrl="https://supabase.com/docs/guides/auth/row-level-security"
|
||||
onClickCta={() => router.push(`/project/${ref}/editor`)}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<InformationBox
|
||||
title="What are policies?"
|
||||
icon={<IconHelpCircle strokeWidth={2} />}
|
||||
description={
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm">
|
||||
Policies restrict, on a per-user basis, which rows can be returned by normal
|
||||
queries, or inserted, updated, or deleted by data modification commands.
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Create a table in this schema first before creating a policy.
|
||||
</p>
|
||||
</div>
|
||||
</ProductEmptyState>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<PolicyEditorModal
|
||||
|
||||
+92
-21
@@ -17,15 +17,14 @@ import { PolicyTemplate } from '../PolicyTemplates/PolicyTemplates.constants'
|
||||
export const getGeneralPolicyTemplates = (schema: string, table: string): PolicyTemplate[] => [
|
||||
{
|
||||
id: 'policy-1',
|
||||
preview: false,
|
||||
templateName: 'Enable read access to everyone',
|
||||
description:
|
||||
'This policy gives read access to your table for all users via the SELECT operation.',
|
||||
statement: `
|
||||
CREATE POLICY "policy_name"
|
||||
ON ${schema}.${table}
|
||||
FOR SELECT USING (
|
||||
true
|
||||
);`.trim(),
|
||||
create policy "Enable read access for all users"
|
||||
on ${schema}.${table}
|
||||
for select using (true);`.trim(),
|
||||
name: 'Enable read access for all users',
|
||||
definition: 'true',
|
||||
check: '',
|
||||
@@ -34,14 +33,14 @@ FOR SELECT USING (
|
||||
},
|
||||
{
|
||||
id: 'policy-2',
|
||||
preview: false,
|
||||
templateName: 'Enable insert access for authenticated users only',
|
||||
description: 'This policy gives insert access to your table for all authenticated users only.',
|
||||
statement: `
|
||||
CREATE POLICY "policy_name"
|
||||
ON ${schema}.${table}
|
||||
FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (true);`.trim(),
|
||||
create policy "Enable insert for authenticated users only"
|
||||
on ${schema}.${table}
|
||||
for insert to authenticated
|
||||
with check (true);`.trim(),
|
||||
name: 'Enable insert for authenticated users only',
|
||||
definition: '',
|
||||
check: 'true',
|
||||
@@ -50,15 +49,16 @@ WITH CHECK (true);`.trim(),
|
||||
},
|
||||
{
|
||||
id: 'policy-3',
|
||||
preview: false,
|
||||
templateName: 'Enable update access for users based on their email *',
|
||||
description:
|
||||
'This policy assumes that your table has a column "email", and allows users to update rows which the "email" column matches their email.',
|
||||
statement: `
|
||||
CREATE POLICY "policy_name"
|
||||
ON ${schema}.${table}
|
||||
FOR UPDATE USING (
|
||||
create policy "Enable update for users based on email"
|
||||
on ${schema}.${table}
|
||||
for update using (
|
||||
auth.jwt() ->> 'email' = email
|
||||
) WITH CHECK (
|
||||
) with check (
|
||||
auth.jwt() ->> 'email' = email
|
||||
);`.trim(),
|
||||
name: 'Enable update for users based on email',
|
||||
@@ -69,13 +69,14 @@ FOR UPDATE USING (
|
||||
},
|
||||
{
|
||||
id: 'policy-4',
|
||||
preview: false,
|
||||
templateName: 'Enable delete access for users based on their user ID *',
|
||||
description:
|
||||
'This policy assumes that your table has a column "user_id", and allows users to delete rows which the "user_id" column matches their ID',
|
||||
statement: `
|
||||
CREATE POLICY "policy_name"
|
||||
ON ${schema}.${table}
|
||||
FOR DELETE USING (
|
||||
create policy "Enable delete for users based on user_id"
|
||||
on ${schema}.${table}
|
||||
for delete using (
|
||||
auth.uid() = user_id
|
||||
);`.trim(),
|
||||
name: 'Enable delete for users based on user_id',
|
||||
@@ -86,15 +87,16 @@ FOR DELETE USING (
|
||||
},
|
||||
{
|
||||
id: 'policy-5',
|
||||
preview: false,
|
||||
templateName: 'Enable insert access for users based on their user ID *',
|
||||
description:
|
||||
'This policy assumes that your table has a column "user_id", and allows users to insert rows which the "user_id" column matches their ID',
|
||||
statement: `
|
||||
CREATE POLICY "policy_name"
|
||||
ON ${schema}.${table}
|
||||
FOR INSERT USING (
|
||||
create policy "Enable insert for users based on user_id"
|
||||
on ${schema}.${table}
|
||||
for insert using (
|
||||
auth.uid() = user_id
|
||||
) WITH CHECK (
|
||||
) with check (
|
||||
auth.uid() = user_id
|
||||
);`.trim(),
|
||||
name: 'Enable insert for users based on user_id',
|
||||
@@ -103,4 +105,73 @@ FOR INSERT USING (
|
||||
command: 'INSERT',
|
||||
roles: [],
|
||||
},
|
||||
{
|
||||
id: 'policy-6',
|
||||
preview: true,
|
||||
name: 'Policy with table joins',
|
||||
templateName: 'Policy with table joins',
|
||||
description: `
|
||||
Query across tables to build more advanced RLS rules
|
||||
|
||||
Assuming 2 tables called \`teams\` and \`members\`, you can query both tables in the policy to control access to the members table.`,
|
||||
statement: `
|
||||
create policy "Team members can update team details if they belong to the team"
|
||||
on teams for update using (
|
||||
auth.uid() in (
|
||||
select user_id from members where team_id = id
|
||||
)
|
||||
);
|
||||
`.trim(),
|
||||
definition: `auth.uid() in (select user_id from members where team_id = id)`,
|
||||
check: '',
|
||||
command: 'UPDATE',
|
||||
roles: [],
|
||||
},
|
||||
{
|
||||
id: 'policy-7',
|
||||
preview: true,
|
||||
templateName: 'Policy with security definer functions',
|
||||
description: `
|
||||
Useful in a many-to-many relationship where you want to restrict access to the linking table.
|
||||
|
||||
Assuming 2 tables called \`teams\` and \`members\`, you can use a security definer function in combination with a policy to control access to the members table.`.trim(),
|
||||
statement: `
|
||||
create or replace function get_teams_for_user(user_id uuid)
|
||||
returns setof bigint as $$
|
||||
select team_id from members where user_id = $1
|
||||
$$ stable language sql security definer;
|
||||
|
||||
create policy "Team members can update team members if they belong to the team"
|
||||
on members
|
||||
for all using (
|
||||
team_id in (select get_teams_for_user(auth.uid()))
|
||||
);
|
||||
`.trim(),
|
||||
name: 'Policy with security definer functions',
|
||||
definition: 'team_id in (select get_teams_for_user(auth.uid()))',
|
||||
check: '',
|
||||
command: 'ALL',
|
||||
roles: [],
|
||||
},
|
||||
{
|
||||
id: 'policy-8',
|
||||
preview: true,
|
||||
name: 'Policy to implement Time To Live (TTL)',
|
||||
templateName: 'Policy to implement Time To Live (TTL)',
|
||||
description: `
|
||||
Implement a TTL-like feature that you see in Instagram stories or Snapchat where messages expire after a day.
|
||||
|
||||
Assuming a \`stories\` table, rows under the table are available only if they have been created within the last 24 hours.`,
|
||||
statement: `
|
||||
create policy "Stories are live for a day"
|
||||
on stories
|
||||
for select using (
|
||||
created_at > (current_timestamp - interval '1 day')
|
||||
);
|
||||
`.trim(),
|
||||
definition: `created_at > (current_timestamp - interval '1 day')`,
|
||||
check: '',
|
||||
command: 'SELECT',
|
||||
roles: [],
|
||||
},
|
||||
]
|
||||
@@ -237,7 +237,7 @@ const PolicyEditorModal = ({
|
||||
/>
|
||||
) : view === POLICY_MODAL_VIEWS.TEMPLATES ? (
|
||||
<PolicyTemplates
|
||||
templates={getGeneralPolicyTemplates(schema, table)}
|
||||
templates={getGeneralPolicyTemplates(schema, table).filter((policy) => !policy.preview)}
|
||||
templatesNote="* References a specific column in the table"
|
||||
onUseTemplate={onUseTemplate}
|
||||
/>
|
||||
|
||||
+1
@@ -1,5 +1,6 @@
|
||||
export interface PolicyTemplate {
|
||||
id: string
|
||||
preview: boolean
|
||||
templateName: string
|
||||
description: string
|
||||
name: string
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useParams } from 'common'
|
||||
import { partition } from 'lodash'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useMemo, useState } from 'react'
|
||||
@@ -34,7 +35,6 @@ import { uuidv4 } from 'lib/helpers'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import { useSnippets, useSqlEditorStateSnapshot } from 'state/sql-editor'
|
||||
import QueryItem from './QueryItem'
|
||||
import { partition, uniq, uniqBy } from 'lodash'
|
||||
import { selectItemsInRange } from './SQLEditorLayout.utils'
|
||||
|
||||
const SideBarContent = observer(() => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Link from 'next/link'
|
||||
import React, { PropsWithChildren } from 'react'
|
||||
import { IconChevronRight, IconLoader } from 'ui'
|
||||
import { IconChevronRight, IconLoader, cn } from 'ui'
|
||||
|
||||
interface CardButtonProps {
|
||||
title: string | React.ReactNode
|
||||
title?: string | React.ReactNode
|
||||
description?: string
|
||||
footer?: React.ReactNode
|
||||
url?: string
|
||||
@@ -14,8 +14,32 @@ interface CardButtonProps {
|
||||
icon?: React.ReactNode
|
||||
loading?: boolean
|
||||
className?: string
|
||||
fixedHeight?: boolean
|
||||
hideChevron?: boolean
|
||||
titleClass?: string
|
||||
}
|
||||
|
||||
// Define separate interfaces for each type of container
|
||||
interface LinkContainerProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'title'> {
|
||||
href: string
|
||||
}
|
||||
|
||||
interface UrlContainerProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'title'> {
|
||||
href: string
|
||||
}
|
||||
|
||||
interface NonLinkContainerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {}
|
||||
|
||||
interface ButtonContainerProps
|
||||
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'title'> {}
|
||||
|
||||
// Union of all container props
|
||||
type ContainerProps =
|
||||
| LinkContainerProps
|
||||
| UrlContainerProps
|
||||
| NonLinkContainerProps
|
||||
| ButtonContainerProps
|
||||
|
||||
const CardButton = ({
|
||||
title,
|
||||
description,
|
||||
@@ -25,28 +49,44 @@ const CardButton = ({
|
||||
linkHref = '',
|
||||
imgUrl,
|
||||
imgAlt,
|
||||
onClick,
|
||||
icon,
|
||||
className,
|
||||
loading = false,
|
||||
}: PropsWithChildren<CardButtonProps>) => {
|
||||
const LinkContainer = ({ children }: { children: React.ReactNode }) => (
|
||||
<Link href={linkHref}>{children}</Link>
|
||||
)
|
||||
const UrlContainer = ({ children }: { children: React.ReactNode }) => <a href={url}>{children}</a>
|
||||
const NonLinkContainer = ({ children }: { children: React.ReactNode }) => <div>{children}</div>
|
||||
const ButtonContainer = ({ children }: { children: React.ReactNode }) => (
|
||||
<button onClick={onClick}>{children}</button>
|
||||
)
|
||||
fixedHeight = true,
|
||||
hideChevron = false,
|
||||
titleClass = '',
|
||||
...props
|
||||
}: PropsWithChildren<CardButtonProps & ContainerProps>) => {
|
||||
const isLink = url || linkHref || props.onClick
|
||||
|
||||
const isLink = url || linkHref || onClick
|
||||
let Container: React.ElementType
|
||||
let containerProps: ContainerProps = {}
|
||||
|
||||
if (props.onClick) {
|
||||
Container = 'button'
|
||||
containerProps = props
|
||||
} else if (linkHref) {
|
||||
Container = Link
|
||||
containerProps = {
|
||||
href: linkHref,
|
||||
...props,
|
||||
}
|
||||
} else if (url) {
|
||||
Container = 'a'
|
||||
containerProps = {
|
||||
href: url,
|
||||
...props,
|
||||
}
|
||||
} else {
|
||||
Container = 'div'
|
||||
containerProps = props
|
||||
}
|
||||
|
||||
let containerClasses = [
|
||||
className,
|
||||
'group relative text-left',
|
||||
'bg-surface-100',
|
||||
'border border-surface',
|
||||
'rounded-md p-5 flex flex-row h-32',
|
||||
'rounded-md p-5 flex flex-row',
|
||||
'transition ease-in-out duration-150',
|
||||
]
|
||||
|
||||
@@ -59,12 +99,16 @@ const CardButton = ({
|
||||
]
|
||||
}
|
||||
|
||||
if (fixedHeight) {
|
||||
containerClasses = [...containerClasses, 'h-32']
|
||||
}
|
||||
|
||||
const ImageContainer = ({ children }: { children: React.ReactNode }) => {
|
||||
return <div className="mr-4 flex flex-col">{children}</div>
|
||||
}
|
||||
|
||||
const contents = (
|
||||
<div className={containerClasses.join(' ')}>
|
||||
<>
|
||||
{imgUrl && (
|
||||
<ImageContainer>
|
||||
<img
|
||||
@@ -80,7 +124,11 @@ const CardButton = ({
|
||||
)}
|
||||
{icon && <ImageContainer>{icon}</ImageContainer>}
|
||||
<div className="flex h-full w-full flex-col space-y-2">
|
||||
{typeof title === 'string' ? <h5 className="text-foreground">{title}</h5> : title}
|
||||
{typeof title === 'string' ? (
|
||||
<h5 className={`text-foreground ${titleClass}`}>{title}</h5>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
{(children || description) && (
|
||||
<div className="flex w-full flex-1 flex-col">
|
||||
<p className="text-sm text-foreground-light">{description}</p>
|
||||
@@ -102,21 +150,23 @@ const CardButton = ({
|
||||
group-hover:text-foreground
|
||||
"
|
||||
>
|
||||
{loading ? <IconLoader className="animate-spin" /> : <IconChevronRight />}
|
||||
{loading ? (
|
||||
<IconLoader className="animate-spin" />
|
||||
) : !hideChevron ? (
|
||||
<IconChevronRight />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
if (onClick) {
|
||||
return <ButtonContainer>{contents}</ButtonContainer>
|
||||
} else if (linkHref) {
|
||||
return <LinkContainer>{contents}</LinkContainer>
|
||||
} else if (url) {
|
||||
return <UrlContainer>{contents}</UrlContainer>
|
||||
} else {
|
||||
return <NonLinkContainer>{contents}</NonLinkContainer>
|
||||
}
|
||||
return (
|
||||
<Container {...containerProps} className={cn(containerClasses, className)}>
|
||||
{contents}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default CardButton
|
||||
@@ -3,6 +3,17 @@
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
.monaco-editor,
|
||||
.monaco-diff-editor {
|
||||
--vscode-editor-background: hsl(var(--background-surface-200)) !important;
|
||||
--vscode-editorGutter-background: hsl(var(--background-surface-200)) !important;
|
||||
}
|
||||
|
||||
.gutter {
|
||||
@apply bg-border-control;
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.grid-monaco-editor,
|
||||
.monaco-editor p,
|
||||
label,
|
||||
|
||||
@@ -87,7 +87,7 @@ function kebabToNested(obj) {
|
||||
*/
|
||||
const uiConfig = ui({
|
||||
mode: 'JIT',
|
||||
darkMode: [ 'class', '[data-theme*="dark"]' ],
|
||||
darkMode: ['class', '[data-theme*="dark"]'],
|
||||
theme: {
|
||||
/**
|
||||
* Spread all theme colors and custom generated colors into theme
|
||||
@@ -102,7 +102,7 @@ const uiConfig = ui({
|
||||
/*
|
||||
* custom background re-maps
|
||||
*/
|
||||
"studio": `hsl(var(--background-200)/ <alpha-value>)`
|
||||
studio: `hsl(var(--background-200)/ <alpha-value>)`,
|
||||
}),
|
||||
borderColor: (theme) => ({
|
||||
...theme('colors'),
|
||||
@@ -361,8 +361,8 @@ const uiConfig = ui({
|
||||
xs: '480px',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: [ 'Circular', 'custom-font', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif' ],
|
||||
mono: [ 'Office Code Pro', 'Source Code Pro', 'Menlo', 'monospace' ],
|
||||
sans: ['Circular', 'custom-font', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'],
|
||||
mono: ['Office Code Pro', 'Source Code Pro', 'Menlo', 'monospace'],
|
||||
},
|
||||
|
||||
// shadcn defaults START
|
||||
@@ -376,6 +376,15 @@ const uiConfig = ui({
|
||||
'flash-code': 'flash-code 1s forwards',
|
||||
'flash-code-slow': 'flash-code 2s forwards',
|
||||
},
|
||||
borderRadius: {
|
||||
// lg: `var(--radius)`,
|
||||
// md: `calc(var(--radius) - 2px)`,
|
||||
// sm: 'calc(var(--radius) - 4px)',
|
||||
panel: '6px',
|
||||
},
|
||||
padding: {
|
||||
content: '21px',
|
||||
},
|
||||
// borderRadius: {
|
||||
// lg: `var(--radius)`,
|
||||
// md: `calc(var(--radius) - 2px)`,
|
||||
@@ -397,8 +406,8 @@ const uiConfig = ui({
|
||||
// shadcn defaults END
|
||||
},
|
||||
},
|
||||
plugins: [ require('@tailwindcss/typography'), require('tailwindcss-animate') ],
|
||||
});
|
||||
plugins: [require('@tailwindcss/typography'), require('tailwindcss-animate')],
|
||||
})
|
||||
|
||||
function arrayMergeFn(destinationArray, sourceArray) {
|
||||
return destinationArray.concat(sourceArray).reduce((acc, cur) => {
|
||||
|
||||
@@ -174,6 +174,8 @@ export { Checkbox as Checkbox_Shadcn_ } from './src/components/shadcn/ui/checkbo
|
||||
|
||||
export * from './src/components/shadcn/ui/scroll-area'
|
||||
|
||||
export * from './src/components/shadcn/ui/hover-card'
|
||||
|
||||
export {
|
||||
Collapsible as Collapsible_Shadcn_,
|
||||
CollapsibleTrigger as CollapsibleTrigger_Shadcn_,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Children } from 'react'
|
||||
import * as CopyToClipboard from 'react-copy-to-clipboard'
|
||||
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import { monokaiCustomTheme } from './CodeBlock.utils'
|
||||
import { Button, IconCheck, IconCopy } from 'ui'
|
||||
import { Button, IconCheck, IconCopy, cn } from 'ui'
|
||||
|
||||
import js from 'react-syntax-highlighter/dist/cjs/languages/hljs/javascript'
|
||||
import ts from 'react-syntax-highlighter/dist/cjs/languages/hljs/typescript'
|
||||
@@ -97,12 +97,12 @@ export const CodeBlock = ({
|
||||
wrapLines={true}
|
||||
// @ts-ignore
|
||||
style={monokaiTheme}
|
||||
className={[
|
||||
'code-block border p-4 w-full !my-0 !bg-surface-100',
|
||||
className={cn(
|
||||
'code-block border border-surface p-4 w-full !my-0 bg-surface-100',
|
||||
`${!title ? '!rounded-md' : '!rounded-t-none !rounded-b-md'}`,
|
||||
`${!showLineNumbers ? 'pl-6' : ''}`,
|
||||
className,
|
||||
].join(' ')}
|
||||
className
|
||||
)}
|
||||
customStyle={{
|
||||
fontSize: large ? 18 : 13,
|
||||
lineHeight: large ? 1.5 : 1.4,
|
||||
|
||||
@@ -3,7 +3,6 @@ export const monokaiCustomTheme = (isDarkMode: boolean) => {
|
||||
hljs: {
|
||||
display: 'block',
|
||||
overflowX: 'auto',
|
||||
background: '#272822',
|
||||
color: isDarkMode ? '#ddd' : '#888',
|
||||
},
|
||||
'hljs-tag': {
|
||||
|
||||
@@ -11,14 +11,19 @@ const HoverCardTrigger = HoverCardPrimitive.Trigger
|
||||
|
||||
const HoverCardContent = React.forwardRef<
|
||||
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
||||
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
|
||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content> & {
|
||||
animate?: 'zoom-in' | 'slide-in'
|
||||
}
|
||||
>(({ className, align = 'center', animate = 'zoom-in', sideOffset = 4, ...props }, ref) => (
|
||||
<HoverCardPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 w-64 rounded-md border bg-overlay p-4 text-popover-foreground shadow-md outline-none animate-in zoom-in-90',
|
||||
'z-50 w-64 rounded-md border bg-overlay p-4 text-popover-foreground shadow-md outline-none',
|
||||
animate === 'zoom-in'
|
||||
? 'animate-in zoom-in-[99%]'
|
||||
: 'animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -14,7 +14,7 @@ const ScrollArea = React.forwardRef<
|
||||
className={cn('relative overflow-hidden', className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit] shadow-[0_2px_10px]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
|
||||
@@ -13,7 +13,7 @@ const TabsList = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn('flex items-center border-b border-secondary', className)}
|
||||
className={cn('flex items-center border-b', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
@@ -7,7 +7,9 @@ import { cn } from '../../../lib/utils/cn'
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
const Tooltip = (props: React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Root>) => (
|
||||
<TooltipPrimitive.Root delayDuration={180} {...props} />
|
||||
)
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
@@ -18,11 +20,11 @@ const TooltipContent = React.forwardRef<
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
{...props}
|
||||
className={cn(
|
||||
'z-50 overflow-hidden rounded-md border bg-overlay px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1',
|
||||
'z-50 overflow-hidden rounded-md border bg-alternative px-3 py-1.5 text-xs text-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
Reference in new issue
Block a user