From ac95a40defcf9d676a37197cf19120751af3d790 Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Mon, 6 Oct 2025 09:41:28 +1000 Subject: [PATCH 1/4] chore: change terminology of prior post (#39068) --- apps/www/components/Blog/BlogPostRenderer.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/www/components/Blog/BlogPostRenderer.tsx b/apps/www/components/Blog/BlogPostRenderer.tsx index 95cb9021e8ad8..aa9864665f0c2 100644 --- a/apps/www/components/Blog/BlogPostRenderer.tsx +++ b/apps/www/components/Blog/BlogPostRenderer.tsx @@ -127,9 +127,11 @@ const BlogPostRenderer = ({

{label}

-
+
{'title' in post && ( -

{(post as { title?: string }).title}

+

+ {(post as { title?: string }).title} +

)} {'formattedDate' in post && (

{(post as { formattedDate?: string }).formattedDate}

@@ -306,7 +308,7 @@ const BlogPostRenderer = ({ formattedDate: string } } - label="Last post" + label="Previous post" /> )}
From 311cc5611a4b55ad09fed8a9e769fe044e56d7f1 Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Mon, 6 Oct 2025 09:52:00 +1000 Subject: [PATCH 2/4] chore(studio): improve Postgres upgrade experience (#39189) * chore: clearer error messages * chore: clearer language around upgrade destination * Update apps/studio/components/interfaces/Settings/Infrastructure/UpgradeWarnings.tsx Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> --------- Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> --- .../Infrastructure/InfrastructureInfo.tsx | 2 +- .../Infrastructure/UpgradeWarnings.tsx | 55 +++++++++---------- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx index 1ff808b861436..343fbc73e6af0 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx @@ -95,7 +95,7 @@ const InfrastructureInfo = () => {

Service Versions

- Information on your provisioned instance. + Service versions and upgrade eligibility for your provisioned instance.

diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/UpgradeWarnings.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/UpgradeWarnings.tsx index 1e147771370a1..81874dbe478e9 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/UpgradeWarnings.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/UpgradeWarnings.tsx @@ -24,36 +24,37 @@ export const ObjectsToBeDroppedWarning = ({ }: { objectsToBeDropped: string[] }) => { + const { ref } = useParams() return ( - - A new version of Postgres is available + + A newer version of Postgres is available -
-

The following objects have to be removed before upgrading:

+ <> +

+ The following objects are not supported and must be removed before upgrading.{' '} + + Learn more + +

-
    + {/* Old */} +
      {objectsToBeDropped.map((obj) => (
    • {obj}
    • ))}
    -
-

Check the docs for which objects need to be removed.

-
- -
+
) @@ -73,15 +74,15 @@ export const UnsupportedExtensionsWarning = ({ <>

The following extensions are not supported in newer versions of Postgres and must be - removed before you can upgrade.{' '} + removed before upgrading.{' '} Learn more - .

    @@ -107,15 +108,13 @@ export const UnsupportedExtensionsWarning = ({ export const UserDefinedObjectsInInternalSchemasWarning = ({ objects }: { objects: string[] }) => { return ( - - A new version of Postgres is available + + A newer version of Postgres is available

    - You'll need to move these objects out of auth/realtime/storage schemas before upgrading: + The following objects must be removed from the auth/realtime/storage schemas before + upgrading:

      From 54ad0b7495e655d8301eb6d174a76bfe713d5469 Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Mon, 6 Oct 2025 10:31:40 +1000 Subject: [PATCH 3/4] Chore/ai realtime (#39145) * update onboarding * update model and fix part issue * action orientated assistant * fix tool * lock * remove unused filter * fix tests * fix again * update package * update container * fix tests * ai realtime * ai realtime * refactor(ai assistant): break out message markdown and profile picture * wip * refactor(ai assistant): break up message component * refactor: break ai assistant message down into multiple files * add limitations prompt * limitations prompt * link prompt * refactor: simplify ReportBlock state * fix: styling of draggable report block header When the drag handle is showing, it overlaps with the block header. Decrease the opacity of the header so the handle can be seen and the two can be distinguished. * fix: minor tweaks to tool ui * refactor: simplify DisplayBlockRenderer state * fix: remove double deploy button in edge function block When the confirm footer is shown, the deploy button on the top right should be hidden (not just disabled) to avoid confusion. * refactor, test: message sanitization by opt-in level Refactor the message sanitization to have more type safety and be more testable. Add tests to ensure: - Message sanitization always runs on generate-v4 - Message sanitization correctly works by opt-in level * Fix conflicts in pnpm lock * Couple of nits and refactors * Revert casing for report block snippet * adjust sanitised prompt * Fix tests * empty states * prompt otpimise * refine prompt * prompt optimizer * remove realtime for now * Update apps/studio/lib/ai/prompts.ts Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * Update apps/studio/lib/ai/prompts.ts Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * updates * feature flag * use flag and additional check * remove sort * messages copy --------- Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com> Co-authored-by: Joshen Lim --- .../Triggers/TriggersList/TriggersList.tsx | 281 ++++++++++-------- .../Realtime/Inspector/AnimatedCursors.tsx | 61 ++++ .../Realtime/Inspector/EmptyRealtime.tsx | 101 +++++++ .../interfaces/Realtime/Inspector/index.tsx | 21 +- .../TableGridEditor/GridHeaderActions.tsx | 97 ++++-- apps/studio/lib/ai/prompts.ts | 275 +++++++++++++++++ apps/studio/pages/api/ai/sql/generate-v4.ts | 2 + apps/studio/styles/typography.scss | 2 +- 8 files changed, 673 insertions(+), 167 deletions(-) create mode 100644 apps/studio/components/interfaces/Realtime/Inspector/AnimatedCursors.tsx create mode 100644 apps/studio/components/interfaces/Realtime/Inspector/EmptyRealtime.tsx diff --git a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx index 3d6f2287f0a1b..9160a211ca5d4 100644 --- a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx +++ b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx @@ -1,7 +1,7 @@ import { PostgresTrigger } from '@supabase/postgres-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' import { noop } from 'lodash' -import { Plus, Search } from 'lucide-react' +import { DatabaseZap, FunctionSquare, Plus, Search, Shield } from 'lucide-react' import { useState } from 'react' import AlphaPreview from 'components/to-be-cleaned/AlphaPreview' @@ -19,7 +19,10 @@ import { useIsProtectedSchema, useProtectedSchemas } from 'hooks/useProtectedSch import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' import { AiIconAnimation, + Button, Card, + CardContent, + cn, Input, Table, TableBody, @@ -29,6 +32,7 @@ import { } from 'ui' import { ProtectedSchemaWarning } from '../../ProtectedSchemaWarning' import TriggerList from './TriggerList' +import Link from 'next/link' interface TriggersListProps { createTrigger: () => void @@ -79,146 +83,159 @@ const TriggersList = ({ return } + const schemaTriggers = triggers.filter((x) => x.schema == selectedSchema) + return ( - <> - {(triggers ?? []).length === 0 ? ( -
      - createTrigger()} - > - -

      - A PostgreSQL trigger is a function invoked automatically whenever an event associated - with a table occurs. -

      -

      - An event could be any of the following: INSERT, UPDATE, DELETE. A trigger is a special - user-defined function associated with a table. -

      -
      +
      +
      +
      + + } + value={filterString} + className="w-full lg:w-52" + onChange={(e) => setFilterString(e.target.value)} + />
      - ) : ( -
      -
      -
      - - } - value={filterString} - className="w-full lg:w-52" - onChange={(e) => setFilterString(e.target.value)} - /> -
      - {!isSchemaLocked && ( -
      - } - onClick={() => createTrigger()} - className="flex-grow" - tooltip={{ - content: { - side: 'bottom', - text: !hasTables - ? 'Create a table first before creating triggers' - : !canCreateTriggers - ? 'You need additional permissions to create triggers' - : undefined, - }, - }} - > - New trigger - - - {hasTables && ( - } - onClick={() => - aiSnap.newChat({ - name: 'Create new trigger', - open: true, - initialInput: `Create a new trigger for the schema ${selectedSchema} that does ...`, - suggestions: { - title: - 'I can help you create a new trigger, here are a few example prompts to get you started:', - prompts: [ - { - label: 'Log Changes', - description: 'Create a trigger that logs changes to the users table', - }, - { - label: 'Update Timestamp', - description: 'Create a trigger that updates updated_at timestamp', - }, - { - label: 'Validate Email', - description: - 'Create a trigger that validates email format before insert', - }, - ], + {!isSchemaLocked && ( +
      + } + onClick={() => createTrigger()} + className="flex-grow" + tooltip={{ + content: { + side: 'bottom', + text: !hasTables + ? 'Create a table first before creating triggers' + : !canCreateTriggers + ? 'You need additional permissions to create triggers' + : undefined, + }, + }} + > + New trigger + + + {hasTables && ( + } + onClick={() => + aiSnap.newChat({ + name: 'Create new trigger', + open: true, + initialInput: `Create a new trigger for the schema ${selectedSchema} that does ...`, + suggestions: { + title: + 'I can help you create a new trigger, here are a few example prompts to get you started:', + prompts: [ + { + label: 'Log Changes', + description: 'Create a trigger that logs changes to the users table', + }, + { + label: 'Update Timestamp', + description: 'Create a trigger that updates updated_at timestamp', }, - }) - } - tooltip={{ - content: { - side: 'bottom', - text: !canCreateTriggers - ? 'You need additional permissions to create triggers' - : 'Create with Supabase Assistant', - }, - }} - /> - )} -
      + { + label: 'Validate Email', + description: 'Create a trigger that validates email format before insert', + }, + ], + }, + }) + } + tooltip={{ + content: { + side: 'bottom', + text: !canCreateTriggers + ? 'You need additional permissions to create triggers' + : 'Create with Supabase Assistant', + }, + }} + /> )}
      + )} +
      - {isSchemaLocked && } - -
      - - - - - Name - Table - Function - Events - Orientation - - Enabled - - - - - - - -
      -
      + {isSchemaLocked && } + + {!isSchemaLocked && (schemaTriggers ?? []).length === 0 ? ( + +
      +
      + +

      Create realtime experiences

      +
      +

      + Keep your application in sync by automatically updating when data changes +

      +
      + +
      +
      + +

      Trigger an edge function

      +
      +

      + Automatically invoke edge functions when database events occur +

      +
      + +
      +
      + +

      Validate data

      +
      +

      + Ensure data meets your requirements before it is inserted into the database +

      +
      + ) : ( +
      + + + + + Name + Table + Function + Events + Orientation + + Enabled + + + + + + + +
      +
      )} - +
      ) } diff --git a/apps/studio/components/interfaces/Realtime/Inspector/AnimatedCursors.tsx b/apps/studio/components/interfaces/Realtime/Inspector/AnimatedCursors.tsx new file mode 100644 index 0000000000000..de4bd3abaed2a --- /dev/null +++ b/apps/studio/components/interfaces/Realtime/Inspector/AnimatedCursors.tsx @@ -0,0 +1,61 @@ +import { useState, useEffect } from 'react' +import { motion } from 'framer-motion' +import { MousePointer2 } from 'lucide-react' + +export const AnimatedCursors = () => { + const [cursor1Position, setCursor1Position] = useState({ x: 20, y: 20 }) + const [cursor2Position, setCursor2Position] = useState({ x: 180, y: 80 }) + + useEffect(() => { + const animateCursors = () => { + const newCursor1Position = { + x: Math.random() * 160 + 20, + y: Math.random() * 80 + 20, + } + const newCursor2Position = { + x: Math.random() * 160 + 20, + y: Math.random() * 80 + 20, + } + + setCursor1Position(newCursor1Position) + setCursor2Position(newCursor2Position) + } + + const initialTimer = setTimeout(animateCursors, 1000) + + const interval = setInterval(animateCursors, 3000) + + return () => { + clearTimeout(initialTimer) + clearInterval(interval) + } + }, []) + + return ( +
      + + + + + + +
      + ) +} diff --git a/apps/studio/components/interfaces/Realtime/Inspector/EmptyRealtime.tsx b/apps/studio/components/interfaces/Realtime/Inspector/EmptyRealtime.tsx new file mode 100644 index 0000000000000..8bed6a1596cf6 --- /dev/null +++ b/apps/studio/components/interfaces/Realtime/Inspector/EmptyRealtime.tsx @@ -0,0 +1,101 @@ +import { AiIconAnimation, Button, Card, cn } from 'ui' +import Link from 'next/link' +import { AnimatedCursors } from './AnimatedCursors' +import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' + +/** + * Acts as a container component for the entire log display + */ +export const EmptyRealtime = ({ projectRef }: { projectRef: string }) => { + const aiSnap = useAiAssistantStateSnapshot() + + const handleCreateTriggerWithAssistant = () => { + aiSnap.newChat({ + name: `Realtime`, + open: true, + initialInput: `Help me set up a realtime experience for my project`, + }) + } + + return ( +
      +
      +
      + +

      Create realtime experiences

      +

      + Send your first realtime message from your database, application code or edge function +

      + +
      + + +
      +
      + + 1 + +

      Broadcast messages

      +
      +

      + Send messages to a channel from your client application or database via triggers. +

      + +
      + +
      +
      + + 2 + +

      Write policies

      +
      +

      + Set up Row Level Security policies to control who can see messages within a channel +

      + +
      + +
      +
      + + 3 + +

      Subscribe to a channel

      +
      +

      + Receive realtime messages in your application by listening to a channel +

      + +
      +
      +
      +
      + ) +} diff --git a/apps/studio/components/interfaces/Realtime/Inspector/index.tsx b/apps/studio/components/interfaces/Realtime/Inspector/index.tsx index 28b616b773593..116ce210093a3 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/index.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/index.tsx @@ -1,5 +1,7 @@ import { useParams } from 'common' -import { useState } from 'react' +import { useState, useEffect } from 'react' +import { motion } from 'framer-motion' +import { MousePointer2 } from 'lucide-react' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' @@ -7,6 +9,7 @@ import { Header } from './Header' import MessagesTable from './MessagesTable' import { SendMessageModal } from './SendMessageModal' import { RealtimeConfig, useRealtimeMessages } from './useRealtimeMessages' +import { EmptyRealtime } from './EmptyRealtime' /** * Acts as a container component for the entire log display @@ -40,12 +43,16 @@ export const RealtimeInspector = () => {
      - 0} - enabled={realtimeConfig.enabled} - data={logData} - showSendMessage={() => setSendMessageShown(true)} - /> + {(logData ?? []).length > 0 ? ( + 0} + enabled={realtimeConfig.enabled} + data={logData} + showSendMessage={() => setSendMessageShown(true)} + /> + ) : ( + + )}
      ('triggersInsteadOfRealtime') const { realtimeAll: realtimeEnabled } = useIsFeatureEnabled(['realtime:all']) const { isSchemaLocked } = useIsProtectedSchema({ schema: table.schema }) @@ -116,6 +118,21 @@ export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProp }, }) + const { data: triggersData } = useDatabaseTriggersQuery( + { + projectRef: project?.ref, + connectionString: project?.connectionString, + }, + { + enabled: isTable, + } + ) + const tableTriggers = (triggersData ?? []).filter( + (trigger) => trigger.schema === table.schema && trigger.table === table.name + ) + + const tableTriggersCount = tableTriggers.length + const { can: canSqlWriteTables, isLoading: isLoadingPermissions } = useAsyncCheckPermissions( PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables' @@ -147,6 +164,8 @@ export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProp const { mutate: sendEvent } = useSendEventMutation() + const manageTriggersHref = `/project/${ref}/database/triggers?schema=${table.schema}` + const toggleRealtime = async () => { if (!project) return console.error('Project is required') if (!realtimePublication) return console.error('Unable to find realtime publication') @@ -325,6 +344,55 @@ export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProp ) ) : null} + {isTable && triggersInsteadOfRealtime ? ( +
      + } + > + + {tableTriggersCount === 1 ? 'Trigger' : 'Triggers'} + + + ) : ( + realtimeEnabled && ( + + } + onClick={() => setShowEnableRealtime(true)} + className={cn(isRealtimeEnabled && 'w-7 h-7 p-0 text-brand hover:text-brand-hover')} + tooltip={{ + content: { + side: 'bottom', + text: isRealtimeEnabled + ? 'Click to disable realtime for this table' + : 'Click to enable realtime for this table', + }, + }} + > + {!isRealtimeEnabled && 'Enable Realtime'} + + ) + )} + {isView && viewHasLints && ( @@ -459,31 +527,6 @@ export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProp - {isTable && realtimeEnabled && ( - - } - onClick={() => setShowEnableRealtime(true)} - className={cn(isRealtimeEnabled && 'w-7 h-7 p-0 text-brand hover:text-brand-hover')} - tooltip={{ - content: { - side: 'bottom', - text: isRealtimeEnabled - ? 'Click to disable realtime for this table' - : 'Click to enable realtime for this table', - }, - }} - > - {!isRealtimeEnabled && 'Enable Realtime'} - - )} - {doesHaveAutoGeneratedAPIDocs && } diff --git a/apps/studio/lib/ai/prompts.ts b/apps/studio/lib/ai/prompts.ts index 83b00581dc0d1..887832b66a3c7 100644 --- a/apps/studio/lib/ai/prompts.ts +++ b/apps/studio/lib/ai/prompts.ts @@ -287,6 +287,281 @@ export const PG_BEST_PRACTICES = ` - Use \`create or replace function\` whenever possible. ` +export const REALTIME_PROMPT = ` +# Supabase Realtime Implementation Guide + +## Core Rules + +### Do +- Use \`broadcast\` for all realtime events (database changes via triggers, messaging, notifications, game state) +- Use \`presence\` sparingly for user state tracking (online status, user counters) +- Create indexes for all columns used in RLS policies +- Use topic names that correlate with concepts and tables: \`scope:entity\` (e.g., \`room:123:messages\`) +- Use snake_case for event names: \`entity_action\` (e.g., \`message_created\`) +- Include unsubscribe/cleanup logic in all implementations +- Set \`private: true\` for channels using database triggers or RLS policies +- Prefer private channels over public channels for better security and control +- Implement proper error handling and reconnection logic + +### Don't +- Use \`postgres_changes\` for new applications (single-threaded, doesn't scale well) +- Create multiple subscriptions without proper cleanup +- Write complex RLS queries without proper indexing +- Use generic event names like "update" or "change" +- Subscribe directly in render functions without state management +- Use database functions (\`realtime.send\`, \`realtime.broadcast_changes\`) in client code + +## Function Selection +- **Custom payloads with business logic:** Use \`broadcast\` +- **Database change notifications:** Use \`broadcast\` via database triggers +- **High-frequency updates:** Use \`broadcast\` with minimal payload +- **User presence/status tracking:** Use \`presence\` (sparingly) +- **Client to client communication:** Use \`broadcast\` without triggers + +**Note:** Avoid \`postgres_changes\` due to scalability limitations. Use \`broadcast\` with database triggers for all database change notifications. + +## Naming Conventions + +### Topics (Channels) +- **Pattern:** \`scope:entity\` or \`scope:entity:id\` +- **Examples:** \`room:123:messages\`, \`game:456:moves\`, \`user:789:notifications\` +- **One topic per room/user/organization for better performance and scalability** + +### Events +- **Pattern:** \`entity_action\` (snake_case) +- **Examples:** \`message_created\`, \`user_joined\`, \`game_ended\`, \`status_changed\` + +## Database Triggers + +### Using realtime.broadcast_changes (Recommended for database changes) +\`\`\`sql +CREATE OR REPLACE FUNCTION room_messages_broadcast_trigger() +RETURNS TRIGGER AS $$ +SECURITY DEFINER +LANGUAGE plpgsql +AS $$ +BEGIN + PERFORM realtime.broadcast_changes( + 'room:' || COALESCE(NEW.room_id, OLD.room_id)::text, + TG_OP, + TG_OP, + TG_TABLE_NAME, + TG_TABLE_SCHEMA, + NEW, + OLD + ); + RETURN COALESCE(NEW, OLD); +END; +$$; + +CREATE TRIGGER messages_broadcast_trigger + AFTER INSERT OR UPDATE OR DELETE ON messages + FOR EACH ROW EXECUTE FUNCTION room_messages_broadcast_trigger(); +\`\`\` + +**Note:** \`realtime.broadcast_changes\` requires private channels by default. + +### Using realtime.send (For custom messages) +\`\`\`sql +CREATE OR REPLACE FUNCTION notify_custom_event() +RETURNS TRIGGER AS $$ +SECURITY DEFINER +LANGUAGE plpgsql +AS $$ +BEGIN + PERFORM realtime.send( + 'room:' || NEW.room_id::text, + 'status_changed', + jsonb_build_object('id', NEW.id, 'status', NEW.status), + false -- set to true for private channels + ); + RETURN NEW; +END; +$$; +\`\`\` + +### Conditional Broadcasting +\`\`\`sql +-- Only broadcast significant changes +IF TG_OP = 'UPDATE' AND OLD.status IS DISTINCT FROM NEW.status THEN + PERFORM realtime.broadcast_changes( + 'room:' || NEW.room_id::text, + TG_OP, + TG_OP, + TG_TABLE_NAME, + TG_TABLE_SCHEMA, + NEW, + OLD + ); +END IF; +\`\`\` + +## Authorization Setup + +### RLS Policies on realtime.messages + +#### Allow Users to Receive Broadcasts (SELECT) +\`\`\`sql +CREATE POLICY "room_members_can_read" ON realtime.messages +FOR SELECT TO authenticated +USING ( + topic LIKE 'room:%' AND + EXISTS ( + SELECT 1 FROM room_members + WHERE user_id = auth.uid() + AND room_id = SPLIT_PART(topic, ':', 2)::uuid + ) +); + +-- Required index for performance +CREATE INDEX idx_room_members_user_room ON room_members(user_id, room_id); +\`\`\` + +#### Allow Users to Send Broadcasts (INSERT) +\`\`\`sql +CREATE POLICY "room_members_can_write" ON realtime.messages +FOR INSERT TO authenticated +WITH CHECK ( + topic LIKE 'room:%' AND + EXISTS ( + SELECT 1 FROM room_members + WHERE user_id = auth.uid() + AND room_id = SPLIT_PART(topic, ':', 2)::uuid + ) +); +\`\`\` + +## Client Implementation + +### Broadcasting from Client +You can send broadcast messages using the Supabase client libraries: + +\`\`\`javascript +const myChannel = supabase.channel('room:123:messages', { + config: { private: true } +}) + +// Sending before subscribing uses HTTP +myChannel.send({ + type: 'broadcast', + event: 'message_created', + payload: { message: 'Hello', user_id: 123 }, +}) + +// Sending after subscribing uses WebSockets (recommended) +myChannel.subscribe((status) => { + if (status !== 'SUBSCRIBED') return + + myChannel.send({ + type: 'broadcast', + event: 'message_created', + payload: { message: 'Hello', user_id: 123 }, + }) +}) +\`\`\` + +**Note:** Sending messages after subscribing uses WebSockets and is more efficient than HTTP for real-time communication. + +### React Pattern +\`\`\`javascript +const channelRef = useRef(null) + +useEffect(() => { + // Check if already subscribed to prevent multiple subscriptions + if (channelRef.current?.state === 'subscribed') return + + const channel = supabase.channel('room:123:messages', { + config: { private: true } + }) + channelRef.current = channel + + // Set auth before subscribing + await supabase.realtime.setAuth() + + channel + .on('broadcast', { event: 'message_created' }, handleMessage) + .subscribe() + + return () => { + if (channelRef.current) { + supabase.removeChannel(channelRef.current) + channelRef.current = null + } + } +}, [roomId]) +\`\`\` + +### Channel Configuration +\`\`\`javascript +const channel = supabase.channel('room:123:messages', { + config: { + broadcast: { self: true, ack: true }, + presence: { key: 'user-session-id' }, + private: true // Required for RLS authorization + } +}) +\`\`\` + +## Best Practices + +### Scalability +- **Use dedicated, granular topics** - Messages only reach interested clients +- **One topic per room:** \`room:123:messages\` +- **One topic per user:** \`user:456:notifications\` +- **Avoid broad topics** that broadcast to all users + +### Security +- **Enable private-only channels** in Realtime Settings for production +- **Always use \`private: true\`** for database-triggered channels +- **Create separate RLS policies** for SELECT (receive) and INSERT (send) operations +- **Index columns used in RLS policies** for performance + +### Performance +- **Check channel state before subscribing** to prevent duplicate subscriptions +- **Include cleanup logic** - Always unsubscribe when component unmounts +- **Use \`SECURITY DEFINER\`** for trigger functions +- **Add conditional logic** to broadcast only significant changes + +## Migration from postgres_changes + +### Replace Client Code +\`\`\`javascript +// ❌ Old: postgres_changes +const oldChannel = supabase + .channel('changes') + .on('postgres_changes', { event: '*', schema: 'public', table: 'messages' }, callback) + +// ✅ New: broadcast +const newChannel = supabase + .channel(\`messages:\${room_id}:changes\`, { config: { private: true } }) + .on('broadcast', { event: 'INSERT' }, callback) + .on('broadcast', { event: 'UPDATE' }, callback) + .on('broadcast', { event: 'DELETE' }, callback) +\`\`\` + +### Add Database Trigger +\`\`\`sql +CREATE TRIGGER messages_broadcast_trigger + AFTER INSERT OR UPDATE OR DELETE ON messages + FOR EACH ROW EXECUTE FUNCTION room_messages_broadcast_trigger(); +\`\`\` + +### Setup Authorization +\`\`\`sql +CREATE POLICY "users_can_receive_broadcasts" ON realtime.messages + FOR SELECT TO authenticated USING (true); +\`\`\` + +## Implementation Workflow +1. Understand the use case (messaging, notifications, game state, etc.) +2. Determine if database triggers are needed or client-only messaging +3. Create RLS policies on \`realtime.messages\` for SELECT and INSERT +4. If using database triggers, create trigger functions using \`realtime.broadcast_changes\` or \`realtime.send\` +5. Add indexes for columns used in RLS policies +6. Implement client code with proper cleanup and state management +7. Enable private-only channels in Realtime Settings for production +` + export const GENERAL_PROMPT = ` # Role and Objective Act as a Supabase Postgres expert to assist users in efficiently managing their Supabase projects. diff --git a/apps/studio/pages/api/ai/sql/generate-v4.ts b/apps/studio/pages/api/ai/sql/generate-v4.ts index e67c7d2ffc33b..006da3a2ccdef 100644 --- a/apps/studio/pages/api/ai/sql/generate-v4.ts +++ b/apps/studio/pages/api/ai/sql/generate-v4.ts @@ -15,6 +15,7 @@ import { GENERAL_PROMPT, PG_BEST_PRACTICES, RLS_PROMPT, + REALTIME_PROMPT, SECURITY_PROMPT, LIMITATIONS_PROMPT, } from 'lib/ai/prompts' @@ -177,6 +178,7 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) { ${PG_BEST_PRACTICES} ${RLS_PROMPT} ${EDGE_FUNCTION_PROMPT} + ${REALTIME_PROMPT} ${SECURITY_PROMPT} ${LIMITATIONS_PROMPT} ` diff --git a/apps/studio/styles/typography.scss b/apps/studio/styles/typography.scss index 55a5ca29acef7..ab28b715c7131 100644 --- a/apps/studio/styles/typography.scss +++ b/apps/studio/styles/typography.scss @@ -72,6 +72,6 @@ /* Link */ .text-link { - @apply text-foreground-light underline underline-offset-4 decoration-border hover:decoration-foreground transition-colors hover:text-foreground; + @apply text-foreground-light underline underline-offset-4 decoration-inherit hover:decoration-foreground transition-colors hover:text-foreground; } } From 426fda2ebca9622bbff80be944bb9882d705b669 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 6 Oct 2025 11:01:56 +0800 Subject: [PATCH 4/4] Address circular dependencies across multiple files (#39231) * Address circular dependencies across multiple files * Fix TS --- .../grid/components/grid/RowRenderer.tsx | 2 +- .../grid/components/menu/RowContextMenu.tsx | 2 +- .../components/grid/components/menu/index.ts | 2 - apps/studio/components/grid/constants.ts | 2 + .../Database/Replication/DestinationRow.tsx | 4 +- .../Database/Replication/Pipeline.utils.ts | 2 +- .../Database/Replication/PipelineStatus.tsx | 9 +- .../Replication/Replication.constants.ts | 8 + .../ReplicationPipelineStatus.tsx | 4 +- .../Database/Replication/RowMenu.tsx | 4 +- .../Replication/UpdateVersionModal.tsx | 5 +- .../CreateCronJobSheet.constants.ts | 122 ++++++++++++++++ .../CreateCronJobSheet.tsx | 137 ++---------------- .../CronJobScheduleSection.tsx | 6 +- .../Integrations/CronJobs/CronJobPage.tsx | 2 +- .../CronJobs/CronJobTableCell.tsx | 30 +++- .../CronJobs/CronJobs.constants.tsx | 6 + .../CronJobs/CronJobs.utils.test.ts | 3 +- .../Integrations/CronJobs/CronJobs.utils.tsx | 50 +------ .../Integrations/CronJobs/CronJobsTab.tsx | 2 +- .../CronJobs/EdgeFunctionSection.tsx | 2 +- .../CronJobs/HttpBodyFieldSection.tsx | 3 +- .../CronJobs/HttpHeaderFieldsSection.tsx | 2 +- .../CronJobs/HttpRequestSection.tsx | 2 +- .../CronJobs/SqlFunctionSection.tsx | 2 +- .../CronJobs/SqlSnippetSection.tsx | 2 +- .../Integrations/Vault/Secrets/SecretRow.tsx | 2 +- .../Vault/Secrets/Secrets.types.ts | 7 + .../Vault/Secrets/Secrets.utils.tsx | 11 +- .../Wrappers/CreateWrapperSheet.tsx | 4 +- .../Integrations/Wrappers/OverviewTab.tsx | 9 +- .../Wrappers/Wrappers.constants.ts | 3 +- .../Integrations/Wrappers/Wrappers.types.ts | 4 +- .../interfaces/Reports/Reports.types.ts | 5 +- .../UnifiedLogs/UnifiedLogs.constants.tsx | 17 --- .../interfaces/UnifiedLogs/UnifiedLogs.tsx | 18 ++- apps/studio/data/content/keys.ts | 11 +- apps/studio/data/table-rows/keys.ts | 11 +- 38 files changed, 253 insertions(+), 264 deletions(-) create mode 100644 apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.constants.ts rename apps/studio/components/interfaces/Integrations/CronJobs/{ => CreateCronJobSheet}/CreateCronJobSheet.tsx (79%) rename apps/studio/components/interfaces/Integrations/CronJobs/{ => CreateCronJobSheet}/CronJobScheduleSection.tsx (97%) create mode 100644 apps/studio/components/interfaces/Integrations/Vault/Secrets/Secrets.types.ts diff --git a/apps/studio/components/grid/components/grid/RowRenderer.tsx b/apps/studio/components/grid/components/grid/RowRenderer.tsx index c814617905742..05e082f725451 100644 --- a/apps/studio/components/grid/components/grid/RowRenderer.tsx +++ b/apps/studio/components/grid/components/grid/RowRenderer.tsx @@ -2,8 +2,8 @@ import type { Key } from 'react' import { TriggerEvent, useContextMenu } from 'react-contexify' import { RenderRowProps, Row } from 'react-data-grid' +import { ROW_CONTEXT_MENU_ID } from 'components/grid/constants' import { SupaRow } from 'components/grid/types' -import { ROW_CONTEXT_MENU_ID } from '../menu' export default function RowRenderer(key: Key, props: RenderRowProps) { const { show: showContextMenu } = useContextMenu() diff --git a/apps/studio/components/grid/components/menu/RowContextMenu.tsx b/apps/studio/components/grid/components/menu/RowContextMenu.tsx index 1e90055e4e2ac..4460cc0314019 100644 --- a/apps/studio/components/grid/components/menu/RowContextMenu.tsx +++ b/apps/studio/components/grid/components/menu/RowContextMenu.tsx @@ -3,11 +3,11 @@ import { useCallback } from 'react' import { Item, ItemParams, Menu } from 'react-contexify' import { toast } from 'sonner' +import { ROW_CONTEXT_MENU_ID } from 'components/grid/constants' import type { SupaRow } from 'components/grid/types' import { useTableEditorStateSnapshot } from 'state/table-editor' import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' import { copyToClipboard, DialogSectionSeparator } from 'ui' -import { ROW_CONTEXT_MENU_ID } from '.' import { formatClipboardValue } from '../../utils/common' export type RowContextMenuProps = { diff --git a/apps/studio/components/grid/components/menu/index.ts b/apps/studio/components/grid/components/menu/index.ts index d205906f8112f..cd53af00a3334 100644 --- a/apps/studio/components/grid/components/menu/index.ts +++ b/apps/studio/components/grid/components/menu/index.ts @@ -1,4 +1,2 @@ export { default as ColumnMenu } from './ColumnMenu' export { default as RowContextMenu } from './RowContextMenu' - -export const ROW_CONTEXT_MENU_ID = 'row-context-menu-id' diff --git a/apps/studio/components/grid/constants.ts b/apps/studio/components/grid/constants.ts index cc99ab0013761..4494d5bd573dd 100644 --- a/apps/studio/components/grid/constants.ts +++ b/apps/studio/components/grid/constants.ts @@ -12,3 +12,5 @@ const RLS_ACKNOWLEDGED_KEY = 'supabase-acknowledge-rls-warning' export const rlsAcknowledgedKey = (tableID?: string | number) => `${RLS_ACKNOWLEDGED_KEY}-${String(tableID)}` + +export const ROW_CONTEXT_MENU_ID = 'row-context-menu-id' diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx index 90a76a0c5f279..05e57b5345b80 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx @@ -22,8 +22,8 @@ import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' import { DeleteDestination } from './DeleteDestination' import { DestinationPanel } from './DestinationPanel' import { getStatusName, PIPELINE_ERROR_MESSAGES } from './Pipeline.utils' -import { PipelineStatus, PipelineStatusName } from './PipelineStatus' -import { STATUS_REFRESH_FREQUENCY_MS } from './Replication.constants' +import { PipelineStatus } from './PipelineStatus' +import { PipelineStatusName, STATUS_REFRESH_FREQUENCY_MS } from './Replication.constants' import { RowMenu } from './RowMenu' import { UpdateVersionModal } from './UpdateVersionModal' diff --git a/apps/studio/components/interfaces/Database/Replication/Pipeline.utils.ts b/apps/studio/components/interfaces/Database/Replication/Pipeline.utils.ts index 8a4733175b727..1dcefc55ca656 100644 --- a/apps/studio/components/interfaces/Database/Replication/Pipeline.utils.ts +++ b/apps/studio/components/interfaces/Database/Replication/Pipeline.utils.ts @@ -1,6 +1,6 @@ import { ReplicationPipelineStatusData } from 'data/replication/pipeline-status-query' import { PipelineStatusRequestStatus } from 'state/replication-pipeline-request-status' -import { PipelineStatusName } from './PipelineStatus' +import { PipelineStatusName } from './Replication.constants' export const PIPELINE_ERROR_MESSAGES = { RETRIEVE_PIPELINE: 'Failed to retrieve pipeline information', diff --git a/apps/studio/components/interfaces/Database/Replication/PipelineStatus.tsx b/apps/studio/components/interfaces/Database/Replication/PipelineStatus.tsx index 16c666ba212fc..5e7bec9d0e20d 100644 --- a/apps/studio/components/interfaces/Database/Replication/PipelineStatus.tsx +++ b/apps/studio/components/interfaces/Database/Replication/PipelineStatus.tsx @@ -8,14 +8,7 @@ import { ResponseError } from 'types' import { cn, Tooltip, TooltipContent, TooltipTrigger, WarningIcon } from 'ui' import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' import { getPipelineStateMessages } from './Pipeline.utils' - -export enum PipelineStatusName { - FAILED = 'failed', - STARTING = 'starting', - STARTED = 'started', - STOPPED = 'stopped', - UNKNOWN = 'unknown', -} +import { PipelineStatusName } from './Replication.constants' interface PipelineStatusProps { pipelineStatus: ReplicationPipelineStatusData['status'] | undefined diff --git a/apps/studio/components/interfaces/Database/Replication/Replication.constants.ts b/apps/studio/components/interfaces/Database/Replication/Replication.constants.ts index 273adac9f5bf6..687e7307483d6 100644 --- a/apps/studio/components/interfaces/Database/Replication/Replication.constants.ts +++ b/apps/studio/components/interfaces/Database/Replication/Replication.constants.ts @@ -1 +1,9 @@ export const STATUS_REFRESH_FREQUENCY_MS: number = 5000 + +export enum PipelineStatusName { + FAILED = 'failed', + STARTING = 'starting', + STARTED = 'started', + STOPPED = 'stopped', + UNKNOWN = 'unknown', +} diff --git a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.tsx b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.tsx index ec36254a3e7db..1095fd08a770e 100644 --- a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.tsx +++ b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.tsx @@ -39,8 +39,8 @@ import { PIPELINE_ERROR_MESSAGES, getStatusName, } from '../Pipeline.utils' -import { PipelineStatus, PipelineStatusName } from '../PipelineStatus' -import { STATUS_REFRESH_FREQUENCY_MS } from '../Replication.constants' +import { PipelineStatus } from '../PipelineStatus' +import { PipelineStatusName, STATUS_REFRESH_FREQUENCY_MS } from '../Replication.constants' import { UpdateVersionModal } from '../UpdateVersionModal' import { SlotLagMetrics, TableState } from './ReplicationPipelineStatus.types' import { getDisabledStateConfig, getStatusConfig } from './ReplicationPipelineStatus.utils' diff --git a/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx b/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx index 81f82343cc941..5974fe6f98ae8 100644 --- a/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx +++ b/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx @@ -1,4 +1,4 @@ -import { Edit, MoreVertical, Pause, Play, RotateCcw, Trash, ArrowUpCircle } from 'lucide-react' +import { ArrowUpCircle, Edit, MoreVertical, Pause, Play, RotateCcw, Trash } from 'lucide-react' import { toast } from 'sonner' import { useParams } from 'common' @@ -28,7 +28,7 @@ import { PIPELINE_ERROR_MESSAGES, getStatusName, } from './Pipeline.utils' -import { PipelineStatusName } from './PipelineStatus' +import { PipelineStatusName } from './Replication.constants' interface RowMenuProps { pipeline: Pipeline | undefined diff --git a/apps/studio/components/interfaces/Database/Replication/UpdateVersionModal.tsx b/apps/studio/components/interfaces/Database/Replication/UpdateVersionModal.tsx index 8bcdcf0c2a599..bcf0a86e7896e 100644 --- a/apps/studio/components/interfaces/Database/Replication/UpdateVersionModal.tsx +++ b/apps/studio/components/interfaces/Database/Replication/UpdateVersionModal.tsx @@ -12,8 +12,7 @@ import { } from 'state/replication-pipeline-request-status' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { getStatusName } from './Pipeline.utils' -import { PipelineStatusName } from './PipelineStatus' -import { STATUS_REFRESH_FREQUENCY_MS } from './Replication.constants' +import { PipelineStatusName, STATUS_REFRESH_FREQUENCY_MS } from './Replication.constants' interface UpdateVersionModalProps { visible: boolean @@ -26,8 +25,6 @@ interface UpdateVersionModalProps { export const UpdateVersionModal = ({ visible, pipeline, - // currentVersionName, - // newVersionName, confirmLabel = 'Update and restart', confirmLabelLoading = 'Updating', onClose, diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.constants.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.constants.ts new file mode 100644 index 0000000000000..a9f70ce8c2a92 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.constants.ts @@ -0,0 +1,122 @@ +import { toString as CronToString } from 'cronstrue' +import z from 'zod' + +import { urlRegex } from 'components/interfaces/Auth/Auth.constants' +import { cronPattern, secondsPattern } from '../CronJobs.constants' + +const convertCronToString = (schedule: string) => { + // pg_cron can also use "30 seconds" format for schedule. Cronstrue doesn't understand that format so just use the + // original schedule when cronstrue throws + try { + return CronToString(schedule) + } catch (error) { + return schedule + } +} + +const edgeFunctionSchema = z.object({ + type: z.literal('edge_function'), + method: z.enum(['GET', 'POST']), + edgeFunctionName: z.string().trim().min(1, 'Please select one of the listed Edge Functions'), + timeoutMs: z.coerce.number().int().gte(1000).lte(5000).default(1000), + httpHeaders: z.array(z.object({ name: z.string(), value: z.string() })), + httpBody: z + .string() + .trim() + .optional() + .refine((value) => { + if (!value) return true + try { + JSON.parse(value) + return true + } catch { + return false + } + }, 'Input must be valid JSON'), + // When editing a cron job, we want to keep the original command as a snippet in case the user wants to manually edit it + snippet: z.string().trim(), +}) + +const httpRequestSchema = z.object({ + type: z.literal('http_request'), + method: z.enum(['GET', 'POST']), + endpoint: z + .string() + .trim() + .min(1, 'Please provide a URL') + .regex(urlRegex(), 'Please provide a valid URL') + .refine((value) => value.startsWith('http'), 'Please include HTTP/HTTPs to your URL'), + timeoutMs: z.coerce.number().int().gte(1000).lte(5000).default(1000), + httpHeaders: z.array(z.object({ name: z.string(), value: z.string() })), + httpBody: z + .string() + .trim() + .optional() + .refine((value) => { + if (!value) return true + try { + JSON.parse(value) + return true + } catch { + return false + } + }, 'Input must be valid JSON'), + // When editing a cron job, we want to keep the original command as a snippet in case the user wants to manually edit it + snippet: z.string().trim(), +}) + +const sqlFunctionSchema = z.object({ + type: z.literal('sql_function'), + schema: z.string().trim().min(1, 'Please select one of the listed database schemas'), + functionName: z.string().trim().min(1, 'Please select one of the listed database functions'), + // When editing a cron job, we want to keep the original command as a snippet in case the user wants to manually edit it + snippet: z.string().trim(), +}) + +const sqlSnippetSchema = z.object({ + type: z.literal('sql_snippet'), + snippet: z.string().trim().min(1), +}) + +export 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 { + convertCronToString(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'], + }) + } + } + }) + +export type CreateCronJobForm = z.infer +export type CronJobType = CreateCronJobForm['values'] diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.tsx similarity index 79% rename from apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx rename to apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.tsx index e7a42f67760fd..057ba40cb9c49 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.tsx @@ -1,15 +1,12 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { toString as CronToString } from 'cronstrue' import { parseAsString, useQueryState } from 'nuqs' import { useEffect, useState } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' -import z from 'zod' import { useWatch } from '@ui/components/shadcn/ui/form' import { useParams } from 'common' -import { urlRegex } from 'components/interfaces/Auth/Auth.constants' import EnableExtensionModal from 'components/interfaces/Database/Extensions/EnableExtensionModal' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { getDatabaseCronJob } from 'data/database-cron-jobs/database-cron-job-query' @@ -38,23 +35,22 @@ import { import { Admonition } from 'ui-patterns/admonition' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -import { CRONJOB_DEFINITIONS } from './CronJobs.constants' +import { CRONJOB_DEFINITIONS } from '../CronJobs.constants' +import { buildCronQuery, buildHttpRequestCommand, parseCronJobCommand } from '../CronJobs.utils' +import { EdgeFunctionSection } from '../EdgeFunctionSection' +import { HttpBodyFieldSection } from '../HttpBodyFieldSection' +import { HTTPHeaderFieldsSection } from '../HttpHeaderFieldsSection' +import { HttpRequestSection } from '../HttpRequestSection' +import { SqlFunctionSection } from '../SqlFunctionSection' +import { SqlSnippetSection } from '../SqlSnippetSection' import { - buildCronQuery, - buildHttpRequestCommand, - cronPattern, - parseCronJobCommand, - secondsPattern, -} from './CronJobs.utils' + FormSchema, + type CreateCronJobForm, + type CronJobType, +} from './CreateCronJobSheet.constants' import { CronJobScheduleSection } from './CronJobScheduleSection' -import { EdgeFunctionSection } from './EdgeFunctionSection' -import { HttpBodyFieldSection } from './HttpBodyFieldSection' -import { HTTPHeaderFieldsSection } from './HttpHeaderFieldsSection' -import { HttpRequestSection } from './HttpRequestSection' -import { SqlFunctionSection } from './SqlFunctionSection' -import { SqlSnippetSection } from './SqlSnippetSection' -export interface CreateCronJobSheetProps { +interface CreateCronJobSheetProps { selectedCronJob?: Pick supportsSeconds: boolean isClosing: boolean @@ -62,113 +58,6 @@ export interface CreateCronJobSheetProps { onClose: () => void } -const edgeFunctionSchema = z.object({ - type: z.literal('edge_function'), - method: z.enum(['GET', 'POST']), - edgeFunctionName: z.string().trim().min(1, 'Please select one of the listed Edge Functions'), - timeoutMs: z.coerce.number().int().gte(1000).lte(5000).default(1000), - httpHeaders: z.array(z.object({ name: z.string(), value: z.string() })), - httpBody: z - .string() - .trim() - .optional() - .refine((value) => { - if (!value) return true - try { - JSON.parse(value) - return true - } catch { - return false - } - }, 'Input must be valid JSON'), - // When editing a cron job, we want to keep the original command as a snippet in case the user wants to manually edit it - snippet: z.string().trim(), -}) - -const httpRequestSchema = z.object({ - type: z.literal('http_request'), - method: z.enum(['GET', 'POST']), - endpoint: z - .string() - .trim() - .min(1, 'Please provide a URL') - .regex(urlRegex(), 'Please provide a valid URL') - .refine((value) => value.startsWith('http'), 'Please include HTTP/HTTPs to your URL'), - timeoutMs: z.coerce.number().int().gte(1000).lte(5000).default(1000), - httpHeaders: z.array(z.object({ name: z.string(), value: z.string() })), - httpBody: z - .string() - .trim() - .optional() - .refine((value) => { - if (!value) return true - try { - JSON.parse(value) - return true - } catch { - return false - } - }, 'Input must be valid JSON'), - // When editing a cron job, we want to keep the original command as a snippet in case the user wants to manually edit it - snippet: z.string().trim(), -}) - -const sqlFunctionSchema = z.object({ - type: z.literal('sql_function'), - schema: z.string().trim().min(1, 'Please select one of the listed database schemas'), - functionName: z.string().trim().min(1, 'Please select one of the listed database functions'), - // When editing a cron job, we want to keep the original command as a snippet in case the user wants to manually edit it - snippet: z.string().trim(), -}) - -const sqlSnippetSchema = z.object({ - type: z.literal('sql_snippet'), - snippet: z.string().trim().min(1), -}) - -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 - } - 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'], - }) - } - } - }) - -export type CreateCronJobForm = z.infer -export type CronJobType = CreateCronJobForm['values'] - const FORM_ID = 'create-cron-job-sidepanel' const buildCommand = (values: CronJobType) => { diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobScheduleSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CronJobScheduleSection.tsx similarity index 97% rename from apps/studio/components/interfaces/Integrations/CronJobs/CronJobScheduleSection.tsx rename to apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CronJobScheduleSection.tsx index eb5ba94810480..f911e90b625aa 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobScheduleSection.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CronJobScheduleSection.tsx @@ -23,9 +23,9 @@ import { Switch, } from 'ui' import { Input } from 'ui-patterns/DataInputs/Input' -import { CreateCronJobForm } from './CreateCronJobSheet' -import { formatScheduleString, getScheduleMessage } from './CronJobs.utils' -import CronSyntaxChart from './CronSyntaxChart' +import { formatScheduleString, getScheduleMessage } from '../CronJobs.utils' +import CronSyntaxChart from '../CronSyntaxChart' +import { type CreateCronJobForm } from './CreateCronJobSheet.constants' interface CronJobScheduleSectionProps { form: UseFormReturn diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobPage.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobPage.tsx index 02de19a27d94a..eb0622f44301c 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobPage.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobPage.tsx @@ -20,7 +20,7 @@ import { TooltipTrigger, } from 'ui' import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' -import { CreateCronJobSheet } from './CreateCronJobSheet' +import { CreateCronJobSheet } from './CreateCronJobSheet/CreateCronJobSheet' import { isSecondsFormat, parseCronJobCommand } from './CronJobs.utils' import { PreviousRunsTab } from './PreviousRunsTab' diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx index ea33c32f9285e..b7017e8684a20 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx @@ -1,3 +1,4 @@ +import parser from 'cron-parser' import dayjs from 'dayjs' import { Clipboard, Edit, MoreVertical, Play, Trash } from 'lucide-react' import { parseAsString, useQueryState } from 'nuqs' @@ -41,7 +42,34 @@ import { TooltipTrigger, } from 'ui' import { TimestampInfo } from 'ui-patterns' -import { getNextRun } from './CronJobs.utils' + +const getNextRun = (schedule: string, lastRun?: string) => { + // cron-parser can only deal with the traditional cron syntax but technically users can also + // use strings like "30 seconds" now, For the latter case, we try our best to parse the next run + // (can't guarantee as scope is quite big) + if (schedule.includes('*')) { + try { + const interval = parser.parseExpression(schedule, { tz: 'UTC' }) + return interval.next().getTime() + } catch (error) { + return undefined + } + } else { + // [Joshen] Only going to attempt to parse if the schedule is as simple as "n second" or "n seconds" + // Returned undefined otherwise - we can revisit this perhaps if we get feedback about this + const [value, unit] = schedule.toLocaleLowerCase().split(' ') + if ( + ['second', 'seconds'].includes(unit) && + !Number.isNaN(Number(value)) && + lastRun !== undefined + ) { + const parsedLastRun = dayjs(lastRun).add(Number(value), unit as dayjs.ManipulateType) + return parsedLastRun.valueOf() + } else { + return undefined + } + } +} interface CronJobTableCellProps { col: any diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.constants.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.constants.tsx index 3399942fd4573..c830c8afc9e09 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.constants.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.constants.tsx @@ -1,6 +1,12 @@ import { EdgeFunctions, RESTApi, SqlEditor } from 'icons' import { ScrollText } from 'lucide-react' +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 const CRONJOB_TYPES = [ 'http_request', 'edge_function', diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts index 08827113b6076..75a87094ea69f 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import { cronPattern, parseCronJobCommand, secondsPattern } from './CronJobs.utils' +import { cronPattern, secondsPattern } from './CronJobs.constants' +import { parseCronJobCommand } from './CronJobs.utils' describe('parseCronJobCommand', () => { it('should return a default object when the command is null', () => { diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.tsx index c17360b7a0f2b..926d9990349df 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.tsx @@ -1,12 +1,10 @@ -import parser from 'cron-parser' import { toString as CronToString } from 'cronstrue' -import dayjs from 'dayjs' import { Column } from 'react-data-grid' import { CronJob } from 'data/database-cron-jobs/database-cron-jobs-infinite-query' import { cn } from 'ui' -import { CronJobType } from './CreateCronJobSheet' -import { CRON_TABLE_COLUMNS, HTTPHeader } from './CronJobs.constants' +import { CronJobType } from './CreateCronJobSheet/CreateCronJobSheet.constants' +import { CRON_TABLE_COLUMNS, HTTPHeader, secondsPattern } from './CronJobs.constants' import { CronJobTableCell } from './CronJobTableCell' export function buildCronQuery(name: string, schedule: string, command: string) { @@ -186,12 +184,6 @@ export function formatDate(dateString: string): string { return date.toLocaleString(undefined, options) } -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().toLocaleLowerCase()) } @@ -230,44 +222,6 @@ export const formatScheduleString = (value: string) => { } } -export const convertCronToString = (schedule: string) => { - // pg_cron can also use "30 seconds" format for schedule. Cronstrue doesn't understand that format so just use the - // original schedule when cronstrue throws - try { - return CronToString(schedule) - } catch (error) { - return schedule - } -} - -export const getNextRun = (schedule: string, lastRun?: string) => { - // cron-parser can only deal with the traditional cron syntax but technically users can also - // use strings like "30 seconds" now, For the latter case, we try our best to parse the next run - // (can't guarantee as scope is quite big) - if (schedule.includes('*')) { - try { - const interval = parser.parseExpression(schedule, { tz: 'UTC' }) - return interval.next().getTime() - } catch (error) { - return undefined - } - } else { - // [Joshen] Only going to attempt to parse if the schedule is as simple as "n second" or "n seconds" - // Returned undefined otherwise - we can revisit this perhaps if we get feedback about this - const [value, unit] = schedule.toLocaleLowerCase().split(' ') - if ( - ['second', 'seconds'].includes(unit) && - !Number.isNaN(Number(value)) && - lastRun !== undefined - ) { - const parsedLastRun = dayjs(lastRun).add(Number(value), unit as dayjs.ManipulateType) - return parsedLastRun.valueOf() - } else { - return undefined - } - } -} - export const formatCronJobColumns = ({ onSelectEdit, onSelectDelete, diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx index 65fb83d93a4ab..8a55a9b29e525 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx @@ -5,7 +5,7 @@ import { UIEvent, useMemo, useRef, useState } from 'react' import DataGrid, { DataGridHandle, Row } from 'react-data-grid' import { useParams } from 'common' -import { CreateCronJobSheet } from 'components/interfaces/Integrations/CronJobs/CreateCronJobSheet' +import { CreateCronJobSheet } from 'components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet' import AlertError from 'components/ui/AlertError' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useCronJobsCountQuery } from 'data/database-cron-jobs/database-cron-jobs-count-query' diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/EdgeFunctionSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/EdgeFunctionSection.tsx index 6883555c086fa..5a23112951e96 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/EdgeFunctionSection.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/EdgeFunctionSection.tsx @@ -21,7 +21,7 @@ import { SheetSection, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -import { CreateCronJobForm } from './CreateCronJobSheet' +import { CreateCronJobForm } from './CreateCronJobSheet/CreateCronJobSheet.constants' interface HTTPRequestFieldsProps { form: UseFormReturn diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/HttpBodyFieldSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/HttpBodyFieldSection.tsx index efda8511f72bd..4ba3b23f081fc 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/HttpBodyFieldSection.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/HttpBodyFieldSection.tsx @@ -2,7 +2,6 @@ import { UseFormReturn } from 'react-hook-form' import { FormControl_Shadcn_, - FormDescription_Shadcn_, FormField_Shadcn_, FormItem_Shadcn_, FormLabel_Shadcn_, @@ -10,7 +9,7 @@ import { SheetSection, TextArea_Shadcn_, } from 'ui' -import { CreateCronJobForm } from './CreateCronJobSheet' +import { CreateCronJobForm } from './CreateCronJobSheet/CreateCronJobSheet.constants' interface HttpBodyFieldSectionProps { form: UseFormReturn diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/HttpHeaderFieldsSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/HttpHeaderFieldsSection.tsx index 536fd8905013a..fb4cb9998131c 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/HttpHeaderFieldsSection.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/HttpHeaderFieldsSection.tsx @@ -19,7 +19,7 @@ import { Input_Shadcn_, SheetSection, } from 'ui' -import { CreateCronJobForm } from './CreateCronJobSheet' +import { CreateCronJobForm } from './CreateCronJobSheet/CreateCronJobSheet.constants' interface HTTPHeaderFieldsSectionProps { variant: 'edge_function' | 'http_request' diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/HttpRequestSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/HttpRequestSection.tsx index 03d522eb6263a..3107f0b1258ac 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/HttpRequestSection.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/HttpRequestSection.tsx @@ -15,7 +15,7 @@ import { SheetSection, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -import { CreateCronJobForm } from './CreateCronJobSheet' +import { CreateCronJobForm } from './CreateCronJobSheet/CreateCronJobSheet.constants' interface HttpRequestSectionProps { form: UseFormReturn diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/SqlFunctionSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/SqlFunctionSection.tsx index ffbbc7d720bbd..3d25f88e61e05 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/SqlFunctionSection.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/SqlFunctionSection.tsx @@ -4,7 +4,7 @@ import FunctionSelector from 'components/ui/FunctionSelector' import SchemaSelector from 'components/ui/SchemaSelector' import { FormField_Shadcn_, SheetSection } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -import { CreateCronJobForm } from './CreateCronJobSheet' +import { CreateCronJobForm } from './CreateCronJobSheet/CreateCronJobSheet.constants' interface SqlFunctionSectionProps { form: UseFormReturn diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/SqlSnippetSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/SqlSnippetSection.tsx index 8d3bb465d783a..25f14eb141b78 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/SqlSnippetSection.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/SqlSnippetSection.tsx @@ -3,7 +3,7 @@ import { UseFormReturn } from 'react-hook-form' import CodeEditor from 'components/ui/CodeEditor/CodeEditor' import { FormField_Shadcn_, SheetSection } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -import { CreateCronJobForm } from './CreateCronJobSheet' +import { CreateCronJobForm } from './CreateCronJobSheet/CreateCronJobSheet.constants' interface SqlSnippetSectionProps { form: UseFormReturn diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx b/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx index b0b738b858e6d..133bbe9ef6679 100644 --- a/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx +++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx @@ -18,7 +18,7 @@ import { Edit3, Eye, EyeOff, Key, Loader, MoreVertical, Trash } from 'lucide-rea import type { VaultSecret } from 'types' import { Input } from 'ui-patterns/DataInputs/Input' import EditSecretModal from './EditSecretModal' -import type { SecretTableColumn } from './Secrets.utils' +import { SecretTableColumn } from './Secrets.types' interface SecretRowProps { row: VaultSecret diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/Secrets.types.ts b/apps/studio/components/interfaces/Integrations/Vault/Secrets/Secrets.types.ts new file mode 100644 index 0000000000000..6cddb9c8291ff --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/Secrets.types.ts @@ -0,0 +1,7 @@ +export interface SecretTableColumn { + id: 'secret' | 'id' | 'secret_value' | 'updated_at' | 'actions' + name: string + minWidth?: number + width?: number + maxWidth?: number +} diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/Secrets.utils.tsx b/apps/studio/components/interfaces/Integrations/Vault/Secrets/Secrets.utils.tsx index a1bec3a55dbc1..ccb7f07405e3a 100644 --- a/apps/studio/components/interfaces/Integrations/Vault/Secrets/Secrets.utils.tsx +++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/Secrets.utils.tsx @@ -3,16 +3,7 @@ import type { Column } from 'react-data-grid' import type { VaultSecret } from 'types' import { cn } from 'ui' import SecretRow from './SecretRow' - -export type SecretColumnId = 'secret' | 'id' | 'secret_value' | 'updated_at' | 'actions' - -export interface SecretTableColumn { - id: SecretColumnId - name: string - minWidth?: number - width?: number - maxWidth?: number -} +import { SecretTableColumn } from './Secrets.types' export const SECRET_TABLE_COLUMNS: SecretTableColumn[] = [ { id: 'secret', name: 'Secret', minWidth: 300, width: 360 }, diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/CreateWrapperSheet.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/CreateWrapperSheet.tsx index a2c7e0de25e8c..e3dcb01cd3e39 100644 --- a/apps/studio/components/interfaces/Integrations/Wrappers/CreateWrapperSheet.tsx +++ b/apps/studio/components/interfaces/Integrations/Wrappers/CreateWrapperSheet.tsx @@ -30,6 +30,8 @@ import { WrapperMeta } from './Wrappers.types' import { makeValidateRequired } from './Wrappers.utils' import WrapperTableEditor from './WrapperTableEditor' +const FORM_ID = 'create-wrapper-form' + export interface CreateWrapperSheetProps { isClosing: boolean wrapperMeta: WrapperMeta @@ -37,8 +39,6 @@ export interface CreateWrapperSheetProps { onClose: () => void } -const FORM_ID = 'create-wrapper-form' - export const CreateWrapperSheet = ({ wrapperMeta, isClosing, diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/OverviewTab.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/OverviewTab.tsx index 6d422b8dfe8aa..14ccc25b99df8 100644 --- a/apps/studio/components/interfaces/Integrations/Wrappers/OverviewTab.tsx +++ b/apps/studio/components/interfaces/Integrations/Wrappers/OverviewTab.tsx @@ -18,6 +18,7 @@ import { WarningIcon, } from 'ui' import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab' +import { CreateIcebergWrapperSheet } from './CreateIcebergWrapperSheet' import { CreateWrapperSheet } from './CreateWrapperSheet' import { WRAPPERS } from './Wrappers.constants' import { WrapperTable } from './WrapperTable' @@ -53,7 +54,13 @@ export const WrapperOverviewTab = () => { const databaseNeedsUpgrading = wrappersExtension?.installed_version === wrappersExtension?.default_version - const CreateWrapperSheetComponent = wrapperMeta.createComponent || CreateWrapperSheet + // [Joshen] Opting to declare custom wrapper sheets here instead of within Wrappers.constants.ts + // as we'll easily run into circular dependencies doing so unfortunately + const CreateWrapperSheetComponent = wrapperMeta.customComponent + ? wrapperMeta.name === 'iceberg_wrapper' + ? CreateIcebergWrapperSheet + : ({}) => null + : CreateWrapperSheet return ( + customComponent?: boolean // If true, the wrapper can target a schema which will be populated with tables specified by the wrapper.. canTargetSchema?: boolean sourceSchemaOption?: ServerOption diff --git a/apps/studio/components/interfaces/Reports/Reports.types.ts b/apps/studio/components/interfaces/Reports/Reports.types.ts index 0283558801eee..bc304f2d21a3d 100644 --- a/apps/studio/components/interfaces/Reports/Reports.types.ts +++ b/apps/studio/components/interfaces/Reports/Reports.types.ts @@ -1,5 +1,4 @@ import type { ResponseError } from 'types' -import { DEFAULT_QUERY_PARAMS } from './Reports.constants' export enum Presets { API = 'api', @@ -11,7 +10,9 @@ export enum Presets { export type MetaQueryResponse = any & { error: ResponseError } -export type BaseReportParams = typeof DEFAULT_QUERY_PARAMS & { sql?: string } & unknown +export type BaseReportParams = { iso_timestamp_start: string; iso_timestamp_end: string } & { + sql?: string +} & unknown export interface PresetConfig { title: string queries: BaseQueries diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.constants.tsx b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.constants.tsx index d320ad58872cd..8ad58dc0c7f8d 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.constants.tsx +++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.constants.tsx @@ -15,23 +15,6 @@ import { SLIDER_DELIMITER, SORT_DELIMITER, } from 'components/ui/DataTable/DataTable.constants' -import { ChartConfig } from 'ui' -import { TooltipLabel } from './components/TooltipLabel' - -export const CHART_CONFIG = { - success: { - label: , - color: 'hsl(var(--foreground-muted))', - }, - warning: { - label: , - color: 'hsl(var(--warning-default))', - }, - error: { - label: , - color: 'hsl(var(--destructive-default))', - }, -} satisfies ChartConfig export const REGIONS = ['ams', 'fra', 'gru', 'hkg', 'iad', 'syd'] as const export const METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] as const diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx index 20050a0693e87..d2250d0576ab9 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx +++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx @@ -44,13 +44,29 @@ import { RefreshButton } from '../../ui/DataTable/RefreshButton' import { generateDynamicColumns, UNIFIED_LOGS_COLUMNS } from './components/Columns' import { DownloadLogsButton } from './components/DownloadLogsButton' import { LogsListPanel } from './components/LogsListPanel' +import { TooltipLabel } from './components/TooltipLabel' import { ServiceFlowPanel } from './ServiceFlowPanel' -import { CHART_CONFIG, SEARCH_PARAMS_PARSER } from './UnifiedLogs.constants' +import { SEARCH_PARAMS_PARSER } from './UnifiedLogs.constants' import { filterFields as defaultFilterFields } from './UnifiedLogs.fields' import { useLiveMode, useResetFocus } from './UnifiedLogs.hooks' import { QuerySearchParamsType } from './UnifiedLogs.types' import { getFacetedUniqueValues, getLevelRowClassName } from './UnifiedLogs.utils' +export const CHART_CONFIG = { + success: { + label: , + color: 'hsl(var(--foreground-muted))', + }, + warning: { + label: , + color: 'hsl(var(--warning-default))', + }, + error: { + label: , + color: 'hsl(var(--destructive-default))', + }, +} satisfies ChartConfig + export const UnifiedLogs = () => { useResetFocus() diff --git a/apps/studio/data/content/keys.ts b/apps/studio/data/content/keys.ts index 2c803f01a3fa1..5becc1afcbcca 100644 --- a/apps/studio/data/content/keys.ts +++ b/apps/studio/data/content/keys.ts @@ -1,12 +1,9 @@ -import type { ContentType } from './content-query' -import type { SqlSnippet } from './sql-snippets-query' - export const contentKeys = { allContentLists: (projectRef: string | undefined) => ['projects', projectRef, 'content'] as const, infiniteList: ( projectRef: string | undefined, options?: { - type: ContentType | undefined + type: string name: string | undefined limit?: number sort?: string @@ -14,14 +11,14 @@ export const contentKeys = { ) => ['projects', projectRef, 'content-infinite', options].filter(Boolean), list: ( projectRef: string | undefined, - options: { type?: ContentType; name?: string; limit?: number } + options: { type?: string; name?: string; limit?: number } ) => ['projects', projectRef, 'content', options] as const, sqlSnippets: ( projectRef: string | undefined, options?: { sort?: 'inserted_at' | 'name' name?: string - visibility?: SqlSnippet['visibility'] + visibility?: string favorite?: boolean } ) => ['projects', projectRef, 'content', 'sql', options].filter(Boolean), @@ -41,7 +38,7 @@ export const contentKeys = { type?: string, options?: { cumulative?: boolean - visibility?: SqlSnippet['visibility'] + visibility?: string favorite?: boolean name?: string } diff --git a/apps/studio/data/table-rows/keys.ts b/apps/studio/data/table-rows/keys.ts index ba972f722ee64..27163fb5c7e75 100644 --- a/apps/studio/data/table-rows/keys.ts +++ b/apps/studio/data/table-rows/keys.ts @@ -1,12 +1,5 @@ -import type { GetTableRowsArgs } from './table-rows-query' - -type TableRowKeyArgs = Omit & { table?: { id?: number } } - export const tableRowKeys = { - tableRows: ( - projectRef?: string, - { table, roleImpersonationState, ...args }: TableRowKeyArgs = {} - ) => + tableRows: (projectRef?: string, { table, roleImpersonationState, ...args }: any = {}) => [ 'projects', projectRef, @@ -15,7 +8,7 @@ export const tableRowKeys = { 'rows', { roleImpersonation: roleImpersonationState?.role, ...args }, ] as const, - tableRowsCount: (projectRef?: string, { table, ...args }: TableRowKeyArgs = {}) => + tableRowsCount: (projectRef?: string, { table, ...args }: any = {}) => ['projects', projectRef, 'table-rows', table?.id, 'count', args] as const, tableRowsAndCount: (projectRef?: string, tableId?: number) => ['projects', projectRef, 'table-rows', tableId] as const,