diff --git a/apps/studio/components/grid/components/header/filter/FilterPopoverNew.tsx b/apps/studio/components/grid/components/header/filter/FilterPopoverNew.tsx index 8a661a21b4f7f..4cfb450546abc 100644 --- a/apps/studio/components/grid/components/header/filter/FilterPopoverNew.tsx +++ b/apps/studio/components/grid/components/header/filter/FilterPopoverNew.tsx @@ -59,7 +59,10 @@ function filterGroupToFilters(group: FilterGroup): Filter[] { // Custom date picker component for the FilterBar function DatePickerOption({ onChange, onCancel, search }: CustomOptionProps) { - const [date, setDate] = useState(search ? new Date(search) : undefined) + const parsed = search ? new Date(search) : undefined + const [date, setDate] = useState( + parsed && !isNaN(parsed.getTime()) ? parsed : undefined + ) return (
diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.constants.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.constants.ts index ddbdf55928b4b..c187b76162ea1 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.constants.ts +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.constants.ts @@ -6,9 +6,10 @@ import { httpEndpointUrlSchema } from '@/lib/validation/http-url' 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 + // original schedule when cronstrue throws. + // pg_cron uses '$' for "last day of month"; cronstrue uses 'L' — normalize before parsing. try { - return CronToString(schedule) + return CronToString(schedule.replace(/\$/g, 'L')) } catch (error) { return schedule } diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobPage.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobPage.tsx index 6e8cc1d0d0634..2875af6a5cadc 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobPage.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobPage.tsx @@ -70,7 +70,7 @@ export const CronJobPage = () => { {isSecondsFormat(job.schedule) ? job.schedule.toLowerCase() - : CronToString(job.schedule.toLowerCase())} + : CronToString(job.schedule.toLowerCase().replace(/\$/g, 'L'))} diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx index 82e0d02d711f8..0fbfcd134d3e8 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx @@ -46,9 +46,12 @@ 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('*')) { + if (schedule.includes('*') || schedule.includes('$')) { try { - const interval = parser.parseExpression(schedule, { tz: 'UTC' }) + // pg_cron uses '$' for "last day of month", but cron-parser uses 'L' + // Convert pg_cron syntax to cron-parser syntax before parsing + const normalizedSchedule = schedule.replace(/\$/g, 'L') + const interval = parser.parseExpression(normalizedSchedule, { tz: 'UTC' }) return interval.next().getTime() } catch (error) { return undefined @@ -89,7 +92,6 @@ export const CronJobTableCell = ({ const [showToggleModal, setShowToggleModal] = useState(false) const value = row?.[col.id] - const hasValue = col.id in row const { jobid, schedule, latest_run, status, active, jobname } = row const formattedValue = @@ -103,6 +105,8 @@ export const CronJobTableCell = ({ ? getNextRun(schedule, latest_run) : value + const hasValue = col.id === 'next_run' ? !!formattedValue : col.id in row + const { mutate: runCronJob, isPending: isRunning } = useDatabaseCronJobRunCommandMutation({ onSuccess: () => { toast.success(`Command from "${jobname}" ran successfully`) diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.constants.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.constants.tsx index 9def10fbcf75d..05a304ba900c1 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.constants.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.constants.tsx @@ -1,8 +1,11 @@ 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}$/ +const cronField = /(\*|(\d+|\*\/\d+)|\d+\/\d+|\d+-\d+|\d+(,\d+)*)/ +const cronDayOfMonth = /(\*|\$|(\d+|\*\/\d+)|\d+\/\d+|\d+-\d+|\d+(,\d+)*)/ +export const cronPattern = new RegExp( + `^${cronField.source}\\s+${cronField.source}\\s+${cronDayOfMonth.source}\\s+${cronField.source}\\s+${cronField.source}$` +) // detect seconds like "10 seconds" or normal cron syntax like "*/5 * * * *" export const secondsPattern = /^\d+\s+seconds*$/ 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 143b76de9de28..48d58231924a9 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts @@ -270,8 +270,23 @@ describe('parseCronJobCommand', () => { { description: 'every weekend at 10 AM', command: '0 10 * * 0,6' }, { description: 'every quarter hour', command: '*/15 * * * *' }, { description: 'twice daily at 8 AM and 8 PM', command: '0 8,20 * * *' }, + { description: 'last day of every month at midnight (pg_cron $ syntax)', command: '0 0 $ * *' }, + { description: 'last day of every month at noon (pg_cron $ syntax)', command: '0 12 $ * *' }, ] + const cronPatternRejectTests = [ + { description: '$ in minute field', command: '$ * * * *' }, + { description: '$ in hour field', command: '* $ * * *' }, + { description: '$ in month field', command: '* * * $ *' }, + { description: '$ in day-of-week field', command: '* * * * $' }, + ] + + cronPatternRejectTests.forEach(({ description, command }) => { + it(`should not match the regex for a cronPattern with "${description}"`, () => { + expect(command).not.toMatch(cronPattern) + }) + }) + // Replace the single cronPattern test with forEach cronPatternTests.forEach(({ description, command }) => { it(`should match the regex for a cronPattern with "${description}"`, () => { diff --git a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx index 580482a2ea6a0..9bd5ec0e4e903 100644 --- a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx +++ b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx @@ -105,10 +105,12 @@ export const IntegrationOverviewTab = ({ {!!actions && (
{actions} diff --git a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/InstallIntegrationSheet.tsx b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/InstallIntegrationSheet.tsx index 3569954bcbf0e..36a0897c2091b 100644 --- a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/InstallIntegrationSheet.tsx +++ b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/InstallIntegrationSheet.tsx @@ -398,7 +398,7 @@ export const InstallIntegrationSheet = ({ integration }: InstallIntegrationSheet
) : ( diff --git a/apps/studio/instrumentation-client.ts b/apps/studio/instrumentation-client.ts index eda73e22c598a..61de60dd54412 100644 --- a/apps/studio/instrumentation-client.ts +++ b/apps/studio/instrumentation-client.ts @@ -268,6 +268,7 @@ Sentry.init({ // === Browser extensions & Google Translate DOM manipulation === 'Node.insertBefore: Child to insert before is not a child of this node', + 'Node.removeChild: The node to be removed is not a child of this node', "NotFoundError: Failed to execute 'removeChild' on 'Node'", "NotFoundError: Failed to execute 'insertBefore' on 'Node'", 'NotFoundError: The object can not be found here.', @@ -279,6 +280,7 @@ Sentry.init({ // === Non-Error throws (extensions, third-party libs throwing strings/objects) === 'Non-Error exception captured', 'Non-Error promise rejection captured', + /^Object captured as exception with keys:/, // === Cross-origin script errors (no useful info) === 'Script error.', @@ -293,6 +295,11 @@ Sentry.init({ // === Web crawler / bot errors === 'instantSearchSDKJSBridgeClearHighlight', + // === Third-party library race conditions === + // cmdk: useSyncExternalStore subscribe called before store context is available + "Cannot read properties of undefined (reading 'subscribe')", + "undefined is not an object (evaluating 't.subscribe')", + // === Misc known noise === 'r.default.setDefaultLevel is not a function', // Clipboard permission denied