diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyChat.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyChat.tsx index cb730a81271..51541a729d4 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyChat.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyChat.tsx @@ -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 (
-
+
@@ -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 })} /> ))} diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyHeader.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyHeader.tsx index 5fdd04a10b7..26ff309e444 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyHeader.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyHeader.tsx @@ -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 (
Close -
+
{selectedPolicy !== undefined ? `Update policy: ${selectedPolicy.name}` : 'Create a new Row Level Security policy'}
- {!hasHipaaAddon && ( - - )} + + + + + + {assistantVisible ? 'Hide' : 'Show'} tools + + ) } diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyPre.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyPre.tsx index 0d0700bd74d..4d12138b9f2 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyPre.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyPre.tsx @@ -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 /> -
+
@@ -93,6 +106,7 @@ export const AIPolicyPre = ({ onDiff, children, className }: AAIPolicyPreProps) diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/Message.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/Message.tsx index 50e19c6ad65..2a2e8c9c79e 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/Message.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/Message.tsx @@ -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) { @@ -31,12 +33,21 @@ const Message = memo(function Message({ const icon = useMemo(() => { return role === 'assistant' ? ( - +
+ div>div]:border-background')} + /> +
) : (
+ {/* // TODO: this only works for GitHub profiles */} +
{icon} - {role === 'assistant' ? 'Assistant' : name ? name : 'You'} + + {role === 'assistant' ? 'Assistant' : name ? name : 'You'} + {createdAt && ( {dayjs(createdAt).fromNow()} )} @@ -67,7 +80,12 @@ const Message = memo(function Message({ ...markdownComponents, pre: (props: any) => { return ( - + div>pre]:!border-stronger [&>div>pre]:!bg-surface-200' : '' + } + > {props.children[0].props.children} ) diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/PolicyTemplates.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/PolicyTemplates.tsx new file mode 100644 index 00000000000..8e3a65e5ff0 --- /dev/null +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/PolicyTemplates.tsx @@ -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 ( +
+ + } + placeholder="Search templates" + value={search} + onChange={(event) => setSearch(event.target.value)} + /> + + {search.length > 0 && filteredTemplates.length === 0 && ( + + )} + +
+ {filteredTemplates.map((template) => { + return ( + + + onSelectTemplate({ id: template.id, content: template.statement })} + hideChevron + fixedHeight={false} + icon={ +
+ + {template.command} + +
+ } + > + +
+
+ + + {template.statement} + + + +
+ ) + })} +
+
+ ) +} diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/RLSCodeEditor.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/RLSCodeEditor.tsx index ff22b95e239..6ce0ff4cc0a 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/RLSCodeEditor.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/RLSCodeEditor.tsx @@ -81,7 +81,6 @@ const RLSCodeEditor = ({ readOnly: false, minimap: { enabled: false }, wordWrap: 'on' as const, - fixedOverflowWidgets: true, contextmenu: true, lineNumbers: undefined, glyphMargin: undefined, diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx index f42e025bc44..992d0dce05f 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx @@ -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() - const [errorPanelOpen, setErrorPanelOpen] = useState(true) - const [showDetails, setShowDetails] = useState(false) + const [errorPanelOpen, setErrorPanelOpen] = useState(true) + const [showDetails, setShowDetails] = useState(false) + const [selectedDiff, setSelectedDiff] = useState() // [Joshen] Separate state here as there's a delay between submitting and the API updating the loading status const [debugThread, setDebugThread] = useState([]) - const [assistantVisible, setAssistantPanel] = useState(false) - const [isAssistantChatInputEmpty, setIsAssistantChatInputEmpty] = useState(true) + const [assistantVisible, setAssistantPanel] = useState(false) + const [isAssistantChatInputEmpty, setIsAssistantChatInputEmpty] = useState(true) const [incomingChange, setIncomingChange] = useState(undefined) // used for confirmation when closing the panel with unsaved changes - const [isClosingPolicyEditorPanel, setIsClosingPolicyEditorPanel] = useState(false) + const [isClosingPolicyEditorPanel, setIsClosingPolicyEditorPanel] = useState(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 ({
@@ -261,16 +306,14 @@ export const AIPolicyEditorPanel = memo(function ({ assistantVisible={assistantVisible} setAssistantVisible={setAssistantPanel} /> - setShowDetails(!showDetails)} /> -
{incomingChange ? ( -
+
Accept changes from assistant @@ -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 ({
{assistantVisible && ( -
- - append({ - content: message, - role: 'user', - createdAt: new Date(), - }) - } - onDiff={setIncomingChange} - onChange={setIsAssistantChatInputEmpty} - loading={isLoading || isDebugSqlLoading} - /> +
+ + + + Templates + + {!hasHipaaAddon && ( + + Assistant + + )} + + + + + + + + + append({ + content: message, + role: 'user', + createdAt: new Date(), + }) + } + onDiff={updateEditorWithCheckForDiff} + onChange={setIsAssistantChatInputEmpty} + loading={isLoading || isDebugSqlLoading} + /> + +
)} diff --git a/apps/studio/components/interfaces/Auth/Policies/Policies.tsx b/apps/studio/components/interfaces/Auth/Policies/Policies.tsx index 05f090c3fa1..64f9b90b13a 100644 --- a/apps/studio/components/interfaces/Auth/Policies/Policies.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/Policies.tsx @@ -170,6 +170,43 @@ const Policies = ({ }) } + if (tables.length === 0) { + 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. +

+
+
+
+ ) + } + return ( <>
@@ -189,40 +226,7 @@ const Policies = ({ )) ) : hasTables ? ( - ) : ( -
- 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. -

-
-
-
- )} + ) : null}
[ { 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: [], + }, ] diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx index 36121b21ecb..76bc7892f26 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx @@ -237,7 +237,7 @@ const PolicyEditorModal = ({ /> ) : view === POLICY_MODAL_VIEWS.TEMPLATES ? ( !policy.preview)} templatesNote="* References a specific column in the table" onUseTemplate={onUseTemplate} /> diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTemplates/PolicyTemplates.constants.ts b/apps/studio/components/interfaces/Auth/Policies/PolicyTemplates/PolicyTemplates.constants.ts index f020e3ffcfc..eba327053d1 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTemplates/PolicyTemplates.constants.ts +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTemplates/PolicyTemplates.constants.ts @@ -1,5 +1,6 @@ export interface PolicyTemplate { id: string + preview: boolean templateName: string description: string name: string diff --git a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx index 4ceef5b5b4f..d8575006979 100644 --- a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx +++ b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx @@ -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(() => { diff --git a/apps/studio/components/ui/CardButton.tsx b/apps/studio/components/ui/CardButton.tsx index e3c5abc14cb..ae04e19e16c 100644 --- a/apps/studio/components/ui/CardButton.tsx +++ b/apps/studio/components/ui/CardButton.tsx @@ -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, 'title'> { + href: string +} + +interface UrlContainerProps extends Omit, 'title'> { + href: string +} + +interface NonLinkContainerProps extends Omit, 'title'> {} + +interface ButtonContainerProps + extends Omit, '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) => { - const LinkContainer = ({ children }: { children: React.ReactNode }) => ( - {children} - ) - const UrlContainer = ({ children }: { children: React.ReactNode }) => {children} - const NonLinkContainer = ({ children }: { children: React.ReactNode }) =>
{children}
- const ButtonContainer = ({ children }: { children: React.ReactNode }) => ( - - ) + fixedHeight = true, + hideChevron = false, + titleClass = '', + ...props +}: PropsWithChildren) => { + 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
{children}
} const contents = ( -
+ <> {imgUrl && ( {icon}}
- {typeof title === 'string' ?
{title}
: title} + {typeof title === 'string' ? ( +
{title}
+ ) : ( + title + )} {(children || description) && (

{description}

@@ -102,21 +150,23 @@ const CardButton = ({ group-hover:text-foreground " > - {loading ? : } + {loading ? ( + + ) : !hideChevron ? ( + + ) : ( + <> + )}
)} -
+ ) - if (onClick) { - return {contents} - } else if (linkHref) { - return {contents} - } else if (url) { - return {contents} - } else { - return {contents} - } + return ( + + {contents} + + ) } export default CardButton diff --git a/apps/studio/styles/monaco.scss b/apps/studio/styles/monaco.scss index 642bc197a1d..a1a4d279935 100644 --- a/apps/studio/styles/monaco.scss +++ b/apps/studio/styles/monaco.scss @@ -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, diff --git a/packages/config/tailwind.config.js b/packages/config/tailwind.config.js index 8216cb3a840..2b22ef02a20 100644 --- a/packages/config/tailwind.config.js +++ b/packages/config/tailwind.config.js @@ -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)/ )` + studio: `hsl(var(--background-200)/ )`, }), 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) => { diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index 77b83b88a65..0c5d85b1f4f 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -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_, diff --git a/packages/ui/src/components/CodeBlock/CodeBlock.tsx b/packages/ui/src/components/CodeBlock/CodeBlock.tsx index 4147132f810..f94469634e1 100644 --- a/packages/ui/src/components/CodeBlock/CodeBlock.tsx +++ b/packages/ui/src/components/CodeBlock/CodeBlock.tsx @@ -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, diff --git a/packages/ui/src/components/CodeBlock/CodeBlock.utils.ts b/packages/ui/src/components/CodeBlock/CodeBlock.utils.ts index 2699a1793dd..d3d5f7a0e4b 100644 --- a/packages/ui/src/components/CodeBlock/CodeBlock.utils.ts +++ b/packages/ui/src/components/CodeBlock/CodeBlock.utils.ts @@ -3,7 +3,6 @@ export const monokaiCustomTheme = (isDarkMode: boolean) => { hljs: { display: 'block', overflowX: 'auto', - background: '#272822', color: isDarkMode ? '#ddd' : '#888', }, 'hljs-tag': { diff --git a/packages/ui/src/components/shadcn/ui/hover-card.tsx b/packages/ui/src/components/shadcn/ui/hover-card.tsx index 264e2f1d3ad..450c73581ca 100644 --- a/packages/ui/src/components/shadcn/ui/hover-card.tsx +++ b/packages/ui/src/components/shadcn/ui/hover-card.tsx @@ -11,14 +11,19 @@ const HoverCardTrigger = HoverCardPrimitive.Trigger const HoverCardContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( + React.ComponentPropsWithoutRef & { + animate?: 'zoom-in' | 'slide-in' + } +>(({ className, align = 'center', animate = 'zoom-in', sideOffset = 4, ...props }, ref) => ( - + {children} diff --git a/packages/ui/src/components/shadcn/ui/tabs/tabs.tsx b/packages/ui/src/components/shadcn/ui/tabs/tabs.tsx index e03fe897c81..1835b67428c 100644 --- a/packages/ui/src/components/shadcn/ui/tabs/tabs.tsx +++ b/packages/ui/src/components/shadcn/ui/tabs/tabs.tsx @@ -13,7 +13,7 @@ const TabsList = React.forwardRef< >(({ className, ...props }, ref) => ( )) diff --git a/packages/ui/src/components/shadcn/ui/tooltip.tsx b/packages/ui/src/components/shadcn/ui/tooltip.tsx index c3e1d788696..064661c5c08 100644 --- a/packages/ui/src/components/shadcn/ui/tooltip.tsx +++ b/packages/ui/src/components/shadcn/ui/tooltip.tsx @@ -7,7 +7,9 @@ import { cn } from '../../../lib/utils/cn' const TooltipProvider = TooltipPrimitive.Provider -const Tooltip = TooltipPrimitive.Root +const Tooltip = (props: React.ComponentPropsWithoutRef) => ( + +) const TooltipTrigger = TooltipPrimitive.Trigger @@ -18,11 +20,11 @@ const TooltipContent = React.forwardRef< )) TooltipContent.displayName = TooltipPrimitive.Content.displayName