diff --git a/apps/studio/components/grid/components/grid/RowRenderer.tsx b/apps/studio/components/grid/components/grid/RowRenderer.tsx index c8146179057..05e082f7254 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 1e90055e4e2..4460cc03140 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 d205906f811..cd53af00a33 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 cc99ab00137..4494d5bd573 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 90a76a0c5f2..05e57b5345b 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 8a4733175b7..1dcefc55ca6 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 16c666ba212..5e7bec9d0e2 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 273adac9f5b..687e7307483 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 ec36254a3e7..1095fd08a77 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 81f82343cc9..5974fe6f98a 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 8bcdcf0c2a5..bcf0a86e789 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 00000000000..a9f70ce8c2a --- /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 e7a42f67760..057ba40cb9c 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 eb5ba948104..f911e90b625 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 02de19a27d9..eb0622f4430 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 ea33c32f928..b7017e8684a 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 3399942fd45..c830c8afc9e 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 08827113b60..75a87094ea6 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 c17360b7a0f..926d9990349 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 65fb83d93a4..8a55a9b29e5 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 6883555c086..5a23112951e 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 efda8511f72..4ba3b23f081 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 536fd890501..fb4cb999813 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 03d522eb626..3107f0b1258 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 ffbbc7d720b..3d25f88e61e 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 8d3bb465d78..25f14eb141b 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 b0b738b858e..133bbe9ef66 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 00000000000..6cddb9c8291 --- /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 a1bec3a55db..ccb7f07405e 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 a2c7e0de25e..e3dcb01cd3e 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 6d422b8dfe8..14ccc25b99d 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 0283558801e..bc304f2d21a 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 d320ad58872..8ad58dc0c7f 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 20050a0693e..d2250d0576a 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 2c803f01a3f..5becc1afcbc 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 ba972f722ee..27163fb5c7e 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,