Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<Date | undefined>(search ? new Date(search) : undefined)
const parsed = search ? new Date(search) : undefined
const [date, setDate] = useState<Date | undefined>(
parsed && !isNaN(parsed.getTime()) ? parsed : undefined
)

return (
<div className="w-[300px] space-y-4">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export const CronJobPage = () => {
<span className="cursor-pointer underline decoration-dotted lowercase">
{isSecondsFormat(job.schedule)
? job.schedule.toLowerCase()
: CronToString(job.schedule.toLowerCase())}
: CronToString(job.schedule.toLowerCase().replace(/\$/g, 'L'))}
</span>
</TooltipTrigger>
<TooltipContent side="bottom" align="center">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 =
Expand All @@ -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`)
Expand Down
Original file line number Diff line number Diff line change
@@ -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*$/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}"`, () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,12 @@ export const IntegrationOverviewTab = ({

{!!actions && (
<div
aria-disabled={hasToInstallExtensions}
aria-disabled={hasToInstallExtensions && !hideRequiredExtensionsSection}
className={cn(
'px-10 max-w-4xl',
hasToInstallExtensions && 'opacity-25 [&_button]:pointer-events-none'
hasToInstallExtensions &&
!hideRequiredExtensionsSection &&
'opacity-25 [&_button]:pointer-events-none'
)}
>
{actions}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ export const InstallIntegrationSheet = ({ integration }: InstallIntegrationSheet
</SheetClose>
<Button
type="primary"
disabled={hasMissingExtensions}
disabled={hasMissingExtensions && !installationCommand}
loading={isInstalling}
onClick={onInstallIntegration}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const ReadReplicasWarning = ({ latestPgVersion }: { latestPgVersion: stri
const getValidationErrorTitle = (error: ProjectUpgradeEligibilityValidationError): string => {
switch (error.type) {
case 'objects_depending_on_pg_cron':
return error.dependents.join(', ')
return (error.dependents ?? []).join(', ') || 'Objects depending on pg_cron'
case 'indexes_referencing_ll_to_earth':
return `${error.schema_name}.${error.index_name}`
case 'function_using_obsolete_lang':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export const JsonEditor = ({

useEffect(() => {
if (visible) {
const temp = prettifyJSON(jsonString)
const temp = prettifyJSON(jsonString ?? '')
setJsonStr(temp)
}
}, [visible])
Expand Down Expand Up @@ -174,7 +174,7 @@ export const JsonEditor = ({
key={jsonString}
readOnly={readOnly}
onInputChange={(val) => setJsonStr(val ?? '')}
value={jsonStr.toString()}
value={(jsonStr ?? '').toString()}
/>
</div>
) : (
Expand Down
7 changes: 7 additions & 0 deletions apps/studio/instrumentation-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand All @@ -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.',
Expand All @@ -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
Expand Down
Loading