diff --git a/apps/studio/components/interfaces/Billing/Payment/AddNewPaymentMethodModal.tsx b/apps/studio/components/interfaces/Billing/Payment/AddNewPaymentMethodModal.tsx index 8d4f50aa036ab..7c75bfae4e76b 100644 --- a/apps/studio/components/interfaces/Billing/Payment/AddNewPaymentMethodModal.tsx +++ b/apps/studio/components/interfaces/Billing/Payment/AddNewPaymentMethodModal.tsx @@ -18,6 +18,7 @@ interface AddNewPaymentMethodModalProps { onCancel: () => void onConfirm: () => void showSetDefaultCheckbox?: boolean + autoMarkAsDefaultPaymentMethod?: boolean } const stripePromise = loadStripe(STRIPE_PUBLIC_KEY) @@ -28,6 +29,7 @@ const AddNewPaymentMethodModal = ({ onCancel, onConfirm, showSetDefaultCheckbox, + autoMarkAsDefaultPaymentMethod, }: AddNewPaymentMethodModalProps) => { const { resolvedTheme } = useTheme() const [intent, setIntent] = useState() @@ -140,6 +142,7 @@ const AddNewPaymentMethodModal = ({ onCancel={onLocalCancel} onConfirm={onLocalConfirm} showSetDefaultCheckbox={showSetDefaultCheckbox} + autoMarkAsDefaultPaymentMethod={autoMarkAsDefaultPaymentMethod} /> diff --git a/apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx b/apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx index 770d8ebbc103a..cc4f888b7604e 100644 --- a/apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx +++ b/apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx @@ -12,6 +12,7 @@ interface AddPaymentMethodFormProps { onCancel: () => void onConfirm: () => void showSetDefaultCheckbox?: boolean + autoMarkAsDefaultPaymentMethod?: boolean } // Stripe docs recommend to use the new SetupIntent flow over @@ -23,6 +24,7 @@ const AddPaymentMethodForm = ({ onCancel, onConfirm, showSetDefaultCheckbox = false, + autoMarkAsDefaultPaymentMethod = false, }: AddPaymentMethodFormProps) => { const stripe = useStripe() const elements = useElements() @@ -59,7 +61,11 @@ const AddPaymentMethodForm = ({ setIsSaving(false) toast.error(error?.message ?? ' Failed to save card details') } else { - if (isDefault && selectedOrganization && typeof setupIntent?.payment_method === 'string') { + if ( + (isDefault || autoMarkAsDefaultPaymentMethod) && + selectedOrganization && + typeof setupIntent?.payment_method === 'string' + ) { try { await markAsDefault({ slug: selectedOrganization.slug, diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx index c3130c51d6ffb..e477d0c146c5a 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx @@ -18,7 +18,6 @@ import { Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, - FormLabel_Shadcn_, Input_Shadcn_, RadioGroupStacked, RadioGroupStackedItem, @@ -33,13 +32,14 @@ import { Admonition } from 'ui-patterns' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import EnableExtensionModal from 'components/interfaces/Database/Extensions/EnableExtensionModal' import { CRONJOB_DEFINITIONS } from './CronJobs.constants' import { buildCronQuery, buildHttpRequestCommand, cronPattern, - secondsPattern, parseCronJobCommand, + secondsPattern, } from './CronJobs.utils' import { CronJobScheduleSection } from './CronJobScheduleSection' import { EdgeFunctionSection } from './EdgeFunctionSection' @@ -48,10 +48,10 @@ import { HTTPParameterFieldsSection } from './HttpParameterFieldsSection' import { HttpRequestSection } from './HttpRequestSection' import { SqlFunctionSection } from './SqlFunctionSection' import { SqlSnippetSection } from './SqlSnippetSection' -import EnableExtensionModal from 'components/interfaces/Database/Extensions/EnableExtensionModal' export interface CreateCronJobSheetProps { selectedCronJob?: Pick + supportsSeconds: boolean isClosing: boolean setIsClosing: (v: boolean) => void onClose: () => void @@ -90,32 +90,45 @@ const sqlSnippetSchema = z.object({ snippet: z.string().trim().min(1), }) -const FormSchema = z.object({ - name: z.string().trim().min(1, 'Please provide a name for your cron job'), - schedule: z - .string() - .trim() - .min(1) - .refine((value) => { - if (cronPattern.test(value)) { - try { - CronToString(value) +const FormSchema = z + .object({ + name: z.string().trim().min(1, 'Please provide a name for your cron job'), + supportsSeconds: z.boolean(), + schedule: z + .string() + .trim() + .min(1) + .refine((value) => { + if (cronPattern.test(value)) { + try { + CronToString(value) + return true + } catch { + return false + } + } else if (secondsPattern.test(value)) { return true - } catch { - return false } - } else if (secondsPattern.test(value)) { - return true + return false + }, 'Invalid Cron format'), + values: z.discriminatedUnion('type', [ + edgeFunctionSchema, + httpRequestSchema, + sqlFunctionSchema, + sqlSnippetSchema, + ]), + }) + .superRefine((data, ctx) => { + if (!cronPattern.test(data.schedule)) { + if (!(data.supportsSeconds && secondsPattern.test(data.schedule))) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Seconds are supported only in pg_cron v1.5.0+. Please use a valid Cron format.', + path: ['schedule'], + }) } - return false - }, 'The schedule needs to be in a valid Cron format or specify seconds like "x seconds".'), - values: z.discriminatedUnion('type', [ - edgeFunctionSchema, - httpRequestSchema, - sqlFunctionSchema, - sqlSnippetSchema, - ]), -}) + } + }) export type CreateCronJobForm = z.infer export type CronJobType = CreateCronJobForm['values'] @@ -124,11 +137,14 @@ const FORM_ID = 'create-cron-job-sidepanel' export const CreateCronJobSheet = ({ selectedCronJob, + supportsSeconds, isClosing, setIsClosing, onClose, }: CreateCronJobSheetProps) => { + const { project } = useProjectContext() const isEditing = !!selectedCronJob?.jobname + const [showEnableExtensionModal, setShowEnableExtensionModal] = useState(false) const { mutate: upsertCronJob, isLoading } = useDatabaseCronJobCreateMutation() @@ -144,11 +160,11 @@ export const CreateCronJobSheet = ({ defaultValues: { name: selectedCronJob?.jobname || '', schedule: selectedCronJob?.schedule || '*/5 * * * *', + supportsSeconds, values: cronJobValues, }, }) - const { project } = useProjectContext() const isEdited = form.formState.isDirty // if the form hasn't been touched and the user clicked esc or the backdrop, close the sheet @@ -244,16 +260,15 @@ export const CreateCronJobSheet = ({ - - + Cron jobs cannot be renamed once created - + )} /> - + + supportsSeconds: boolean } -const PRESETS = [ - { name: 'Every minute', expression: '* * * * *' }, - { name: 'Every 5 minutes', expression: '*/5 * * * *' }, - { name: 'Every first of the month, at 00:00', expression: '0 0 1 * *' }, - { name: 'Every night at midnight', expression: '0 0 * * *' }, - { name: 'Every Monday at 2 AM', expression: '0 2 * * 1' }, - { name: 'Every 30 seconds', expression: '30 seconds' }, -] as const - -export const CronJobScheduleSection = ({ form }: CronJobScheduleSectionProps) => { +export const CronJobScheduleSection = ({ form, supportsSeconds }: CronJobScheduleSectionProps) => { const { project } = useProjectContext() const initialValue = form.getValues('schedule') - const { schedule } = form.watch() + const schedule = form.watch('schedule') const [presetValue, setPresetValue] = useState(initialValue) const [inputValue, setInputValue] = useState(initialValue) @@ -52,6 +45,15 @@ export const CronJobScheduleSection = ({ form }: CronJobScheduleSectionProps) => const [useNaturalLanguage, setUseNaturalLanguage] = useState(false) const [scheduleString, setScheduleString] = useState('') + const PRESETS = [ + ...(supportsSeconds ? [{ name: 'Every 30 seconds', expression: '30 seconds' }] : []), + { name: 'Every minute', expression: '* * * * *' }, + { name: 'Every 5 minutes', expression: '*/5 * * * *' }, + { name: 'Every first of the month, at 00:00', expression: '0 0 1 * *' }, + { name: 'Every night at midnight', expression: '0 0 * * *' }, + { name: 'Every Monday at 2 AM', expression: '0 2 * * 1' }, + ] as const + const { complete: generateCronSyntax, isLoading: isGeneratingCron } = useCompletion({ api: `${BASE_PATH}/api/ai/sql/cron`, onResponse: async (response) => { @@ -102,10 +104,18 @@ export const CronJobScheduleSection = ({ form }: CronJobScheduleSectionProps) => } try { + // Don't allow seconds-based schedules if seconds aren't supported + if (!supportsSeconds && secondsPattern.test(schedule)) { + setScheduleString('Invalid cron expression') + return + } + setScheduleString(CronToString(schedule)) } catch (error) { + setScheduleString('Invalid cron expression') console.error('Error converting cron expression to string:', error) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [schedule]) return ( @@ -116,10 +126,15 @@ export const CronJobScheduleSection = ({ form }: CronJobScheduleSectionProps) => render={({ field }) => { return ( - Schedule - - {useNaturalLanguage ? 'Describe your schedule in words' : 'Enter a cron expression'} - +
+ Schedule + + {useNaturalLanguage + ? 'Describe your schedule in words' + : 'Enter a cron expression'} + +
+
{useNaturalLanguage ? ( @@ -146,6 +161,7 @@ export const CronJobScheduleSection = ({ form }: CronJobScheduleSectionProps) => }} /> )} +
onClick={() => { setUseNaturalLanguage(false) form.setValue('schedule', preset.expression) + form.trigger('schedule') setPresetValue(preset.expression) }} > @@ -218,20 +235,8 @@ export const CronJobScheduleSection = ({ form }: CronJobScheduleSectionProps) => {isGeneratingCron ? ( - ) : scheduleString === '' ? ( // set a min length before showing invalid message - 'Enter a valid cron expression above' - ) : scheduleString.includes('Invalid cron expression') ? ( - 'Invalid cron expression' ) : ( - <> - The cron will be run{' '} - {secondsPattern.test(schedule) - ? 'every ' + schedule - : scheduleString - .split(' ') - .map((s, i) => (i === 0 ? s.toLocaleLowerCase() : s)) - .join(' ') + '.'} - + getScheduleMessage(scheduleString, schedule) )} )} diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts index 8ad110fa62cfc..4ebe44ebe89bf 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts @@ -29,7 +29,7 @@ export const buildHttpRequestCommand = ( $$` } -export const DEFAULT_CRONJOB_COMMAND = { +const DEFAULT_CRONJOB_COMMAND = { type: 'sql_snippet', snippet: '', } as const @@ -136,11 +136,33 @@ export function formatDate(dateString: string): string { return date.toLocaleString(undefined, options) } -// detect seconds like "10 seconds" or normal cron syntax like "*/5 * * * *" -export const secondsPattern = /^\d+\s+seconds$/ export const cronPattern = /^(\*|(\d+|\*\/\d+)|\d+\/\d+|\d+-\d+|\d+(,\d+)*)(\s+(\*|(\d+|\*\/\d+)|\d+\/\d+|\d+-\d+|\d+(,\d+)*)){4}$/ +// detect seconds like "10 seconds" or normal cron syntax like "*/5 * * * *" +export const secondsPattern = /^\d+\s+seconds$/ + export function isSecondsFormat(schedule: string): boolean { return secondsPattern.test(schedule.trim()) } + +export function getScheduleMessage(scheduleString: string, schedule: string) { + if (!scheduleString) { + return 'Enter a valid cron expression above' + } + + if (secondsPattern.test(schedule)) { + return `The cron will be run every ${schedule}` + } + + if (scheduleString.includes('Invalid cron expression')) { + return scheduleString + } + + const readableSchedule = scheduleString + .split(' ') + .map((s, i) => (i === 0 ? s.toLowerCase() : s)) + .join(' ') + + return `The cron will be run ${readableSchedule}.` +} diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx index 8136e14a377ea..2c103c430251e 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx @@ -9,6 +9,7 @@ import { parseAsString, useQueryState } from 'nuqs' import { Button, Input, Sheet, SheetContent } from 'ui' import { CronJobCard } from '../CronJobs/CronJobCard' import DeleteCronJob from '../CronJobs/DeleteCronJob' +import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' export const CronjobsTab = () => { const { project } = useProjectContext() @@ -26,6 +27,17 @@ export const CronjobsTab = () => { projectRef: project?.ref, connectionString: project?.connectionString, }) + + const { data: extensions } = useDatabaseExtensionsQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + + // check pg_cron version to see if it supports seconds + const pgCronExtension = (extensions ?? []).find((ext) => ext.name === 'pg_cron') + const installedVersion = pgCronExtension?.installed_version + const supportsSeconds = installedVersion ? parseFloat(installedVersion) >= 1.5 : false + if (isLoading) return (
@@ -125,6 +137,7 @@ export const CronjobsTab = () => { { setIsClosingCreateCronJobSheet(false) setCreateCronJobSheetShown(undefined) diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx index 710c98f85232e..f5ba6e1ec0673 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx @@ -150,6 +150,7 @@ const PaymentMethodSelection = ({ visible={showAddNewPaymentMethodModal} returnUrl={`${getURL()}/org/${selectedOrganization?.slug}/billing?panel=subscriptionPlan`} onCancel={() => setShowAddNewPaymentMethodModal(false)} + autoMarkAsDefaultPaymentMethod={true} onConfirm={async () => { setShowAddNewPaymentMethodModal(false) toast.success('Successfully added new payment method') diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx index 57d3b730ba17f..8c779f5579f39 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx @@ -147,9 +147,9 @@ const Subscription = () => { title="This organization is limited by the included usage" >
- Projects may become unresponsive when this organization exceeds its + Projects may become unresponsive when this organization exceeds its{' '} included usage quota. To scale - seamlessly and pay for over-usage, $ + seamlessly and pay for over-usage,{' '} {currentPlan?.id === 'free' ? 'upgrade to a paid plan.' : 'you can disable Spend Cap under the Cost Control settings.'} diff --git a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx index acfd0705ee736..601fd07558a88 100644 --- a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx +++ b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx @@ -114,7 +114,11 @@ const ProjectLayout = forwardRef { const handler = (e: KeyboardEvent) => { - if (e.metaKey && e.code === 'KeyI') setAiAssistantPanel({ open: !open }) + if (e.metaKey && e.code === 'KeyI') { + setAiAssistantPanel({ open: !open }) + e.preventDefault() + e.stopPropagation() + } } if (isAssistantV2Enabled) window.addEventListener('keydown', handler) return () => window.removeEventListener('keydown', handler) diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx index 050323869e2b1..81f71e9bd35ad 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx @@ -1,5 +1,5 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { motion } from 'framer-motion' +import { AnimatePresence, motion } from 'framer-motion' import { last } from 'lodash' import { FileText } from 'lucide-react' import { memo, useEffect, useMemo, useRef, useState } from 'react' @@ -7,6 +7,7 @@ import { toast } from 'sonner' import type { Message as MessageType } from 'ai/react' import { useChat } from 'ai/react' +import { useParams, useSearchParamsShallow } from 'common/hooks' import { subscriptionHasHipaaAddon } from 'components/interfaces/Billing/Subscription/Subscription.utils' import { Markdown } from 'components/interfaces/Markdown' import OptInToOpenAIToggle from 'components/interfaces/Organization/GeneralSettings/OptInToOpenAIToggle' @@ -25,7 +26,9 @@ import { useFlag } from 'hooks/ui/useFlag' import { BASE_PATH, IS_PLATFORM, OPT_IN_TAGS } from 'lib/constants' import { TELEMETRY_EVENTS, TELEMETRY_VALUES } from 'lib/constants/telemetry' import uuidv4 from 'lib/uuid' +import { useRouter } from 'next/router' import { useAppStateSnapshot } from 'state/app-state' +import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' import { AiIconAnimation, Button, @@ -40,10 +43,6 @@ import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import AIOnboarding from './AIOnboarding' import CollapsibleCodeBlock from './CollapsibleCodeBlock' import { Message } from './Message' -import { useParams } from 'common/hooks' -import { useSearchParamsShallow } from 'common/hooks' -import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' -import { useRouter } from 'next/router' const MemoizedMessage = memo( ({ message, isLoading }: { message: MessageType; isLoading: boolean }) => { @@ -235,14 +234,18 @@ export const AIAssistant = ({ // Add useEffect to set up scroll listener useEffect(() => { - const container = scrollContainerRef.current - if (container) { - container.addEventListener('scroll', handleScroll) - // Initial check - handleScroll() - } + // Use a small delay to ensure container is mounted and has content + const timeoutId = setTimeout(() => { + const container = scrollContainerRef.current + if (container) { + container.addEventListener('scroll', handleScroll) + handleScroll() + } + }, 100) return () => { + clearTimeout(timeoutId) + const container = scrollContainerRef.current if (container) { container.removeEventListener('scroll', handleScroll) } @@ -507,12 +510,18 @@ export const AIAssistant = ({
)}
- - {showFade && ( -
-
-
- )} + + {showFade && ( + +
+ + )} +
{sqlSnippets && sqlSnippets.length > 0 && ( @@ -565,7 +574,7 @@ export const AIAssistant = ({ ? 'Reply to the assistant...' : (sqlSnippets ?? [])?.length > 0 ? 'Ask a question or make a change...' - : 'How can we help you today?' + : 'Chat to Postgres...' } value={value} onValueChange={(e) => setValue(e.target.value)} diff --git a/apps/studio/components/ui/AIAssistantPanel/AIOnboarding.tsx b/apps/studio/components/ui/AIAssistantPanel/AIOnboarding.tsx index 5f505e24f9b6b..0d330085b60fe 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AIOnboarding.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AIOnboarding.tsx @@ -1,6 +1,13 @@ import { motion } from 'framer-motion' +import { FileText, MessageCircleMore, WandSparkles } from 'lucide-react' + +import DotGrid from 'components/ui/DotGrid' import { Button } from 'ui' -import { WandSparkles, FileText, MessageCircle, MessageCircleMore } from 'lucide-react' +import { + InnerSideMenuCollapsible, + InnerSideMenuCollapsibleContent, + InnerSideMenuCollapsibleTrigger, +} from 'ui-patterns/InnerSideMenu' interface AIOnboardingProps { setMessages: (messages: any[]) => void @@ -13,233 +20,226 @@ export default function AIOnboarding({ setMessages, onSendMessage }: AIOnboardin } return ( -
-
- -
- - - -
+
+
+
- -

How can I help you today?

-
- - + -

Tables

-
- +

How can I assist you?

+

+ I can help you build and manage your database by writing SQL or supabase-js, set up + policies, functions or triggers, and query your data - ask me anything. +

+ + + + + + +
+ - + - -
- + +
+ + +
- -

RLS Policies

-
- + + + + +
+ - + - -
- + +
+ + +
- -

Functions

-
- + + + + +
+ - + - -
- + +
+ + + - -

Triggers

-
- + + + + +
+ - + - -
- -
+ +
+ + + + +
) } diff --git a/apps/studio/components/ui/AIAssistantPanel/SqlSnippet.tsx b/apps/studio/components/ui/AIAssistantPanel/SqlSnippet.tsx index de91f7eab3072..5cbde110e8bb2 100644 --- a/apps/studio/components/ui/AIAssistantPanel/SqlSnippet.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/SqlSnippet.tsx @@ -1,4 +1,4 @@ -import { Code, DatabaseIcon, Edit, Play } from 'lucide-react' +import { Code, Edit, Play } from 'lucide-react' import { useRouter } from 'next/router' import { useCallback, useEffect, useState } from 'react' import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts' @@ -21,6 +21,7 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, + SQL_ICON, cn, } from 'ui' import { Admonition } from 'ui-patterns' @@ -211,7 +212,17 @@ export const SqlCard = ({ ) : ( <> - +

{title}

{!readOnly && ( @@ -313,11 +324,12 @@ export const SqlCard = ({ {showCode && ( code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap [&>code]:block [&>code>span]:text-foreground' + '[&>code]:m-0 [&>code>span]:text-foreground' )} /> )} diff --git a/apps/studio/components/ui/DotGrid.tsx b/apps/studio/components/ui/DotGrid.tsx new file mode 100644 index 0000000000000..906206b9e414e --- /dev/null +++ b/apps/studio/components/ui/DotGrid.tsx @@ -0,0 +1,73 @@ +import { motion } from 'framer-motion' + +interface DotGridProps { + rows: number + columns: number + count: number +} + +const DotGrid = ({ rows, columns, count }: DotGridProps) => { + const container = { + hidden: { opacity: 1 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.05, + delayChildren: 0.05, + }, + }, + } + + const item = { + hidden: { opacity: 0 }, + visible: { opacity: 0.5 }, + } + + const highlightedVariants = { + visible: { + opacity: [1, 0.5, 1], + transition: { + repeat: Infinity, + duration: 0.5, + repeatDelay: 1.5, + ease: 'easeInOut', + }, + }, + } + + return ( +
+ + {Array.from({ length: rows * columns }).map((_, index) => { + const isHighlighted = index < count + return ( + + ) + })} + +
+ ) +} + +export default DotGrid diff --git a/apps/studio/package.json b/apps/studio/package.json index 44208f8399ff1..62e41e283474e 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -59,6 +59,7 @@ "config": "*", "configcat-js": "^7.0.0", "cronstrue": "^2.50.0", + "cron-parser": "^4.9.0", "dayjs": "^1.11.10", "dnd-core": "^16.0.1", "file-saver": "^2.0.5", diff --git a/apps/studio/pages/api/ai/sql/generate-v3.ts b/apps/studio/pages/api/ai/sql/generate-v3.ts index 343cde4a462da..917c3d07bf51e 100644 --- a/apps/studio/pages/api/ai/sql/generate-v3.ts +++ b/apps/studio/pages/api/ai/sql/generate-v3.ts @@ -77,6 +77,7 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) { - Output as markdown - Always include code snippets if available - If a code snippet is SQL, the first line of the snippet should always be -- props: {"title": "Query title", "isChart": "true", "xAxis": "columnName", "yAxis": "columnName"} + - Only set chart to true if the query makes sense as a chart - Explain what the snippet does in a sentence or two before showing it - Use vector(384) data type for any embedding/vector related query - When debugging, retrieve sql schema details to ensure sql is correct @@ -106,7 +107,7 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) { Please make sure that all queries are valid Postgres SQL queries # You convert sql to supabase-js client code - Use the convertSqlToSupabaseJs tool to convert select sql to supabase-js client code. If conversion isn't supported, build a postgres function instead and suggest using supabase-js to call it via "const { data, error } = await supabase.rpc('echo', { say: '👋'})" + Use the convertSqlToSupabaseJs tool to convert select sql to supabase-js client code. Only provide js code snippets if explicitly asked. If conversion isn't supported, build a postgres function instead and suggest using supabase-js to call it via "const { data, error } = await supabase.rpc('echo', { say: '👋'})" Follow these instructions: - First look at the list of provided schemas and if needed, get more information about a schema. You will almost always need to retrieve information about the public schema before answering a question. If the question is about users, also retrieve the auth schema. diff --git a/package-lock.json b/package-lock.json index 05a306abd373b..0dfcff6546960 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1818,6 +1818,7 @@ "common-tags": "^1.8.2", "config": "*", "configcat-js": "^7.0.0", + "cron-parser": "^4.9.0", "cronstrue": "^2.50.0", "dayjs": "^1.11.10", "dnd-core": "^16.0.1", @@ -20618,6 +20619,17 @@ "optional": true, "peer": true }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/cronstrue": { "version": "2.50.0", "resolved": "https://registry.npmjs.org/cronstrue/-/cronstrue-2.50.0.tgz", @@ -29146,6 +29158,14 @@ "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", "dev": true }, + "node_modules/luxon": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/lz-string": { "version": "1.5.0", "dev": true, diff --git a/packages/ai-commands/src/sql/cron.ts b/packages/ai-commands/src/sql/cron.ts index 27df01ec0fd47..26f756e8c1dc6 100644 --- a/packages/ai-commands/src/sql/cron.ts +++ b/packages/ai-commands/src/sql/cron.ts @@ -12,22 +12,22 @@ export async function generateCron(openai: OpenAI, prompt: string) { You are a cron syntax expert. Your purpose is to convert natural language time descriptions into valid cron expressions for pg_cron. Rules for responses: - - Output cron expressions in the 5-field format supported by pg_cron + - For standard intervals (minutes and above), output cron expressions in the 5-field format supported by pg_cron + - For second-based intervals, use the special pg_cron "x seconds" syntax - Do not provide any explanation of what the cron expression does - Format output as markdown with the cron expression in a code block - Do not ask for clarification if you need it. Just output the cron expression. Example input: "Every Monday at 3am" Example output: - This cron expression runs every Monday at 3:00:00 AM: \`\`\` 0 3 * * 1 \`\`\` - - This cron expression runs every minute: + Example input: "Every 30 seconds" + Example output: \`\`\` - * * * * * + 30 seconds \`\`\` Additional examples: @@ -36,14 +36,19 @@ export async function generateCron(openai: OpenAI, prompt: string) { - Every first of the month, at 00:00: \`0 0 1 * *\` - Every night at midnight: \`0 0 * * *\` - Every Monday at 2am: \`0 2 * * 1\` + - Every 15 seconds: \`15 seconds\` + - Every 45 seconds: \`45 seconds\` - Field order: + Field order for standard cron: - minute (0-59) - hour (0-23) - day (1-31) - month (1-12) - weekday (0-6, Sunday=0) + Important: pg_cron uses "x seconds" for second-based intervals, not "x * * * *". + If the user asks for seconds, do not use the 5-field format, instead use "x seconds". + Here is the user's prompt: ${prompt} `, diff --git a/packages/ui/src/components/CodeBlock/CodeBlock.tsx b/packages/ui/src/components/CodeBlock/CodeBlock.tsx index 0aeaedc241061..2013320d5bafc 100644 --- a/packages/ui/src/components/CodeBlock/CodeBlock.tsx +++ b/packages/ui/src/components/CodeBlock/CodeBlock.tsx @@ -52,6 +52,7 @@ export interface CodeBlockProps { children?: string renderer?: SyntaxHighlighterProps['renderer'] focusable?: boolean + wrapLines?: boolean } /** @@ -84,6 +85,7 @@ export const CodeBlock = ({ children, hideCopy = false, hideLineNumbers = false, + wrapLines = true, renderer, focusable = true, }: CodeBlockProps) => { @@ -150,7 +152,7 @@ export const CodeBlock = ({ {/* @ts-ignore */}