mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Address circular dependencies across multiple files (#39231)
* Address circular dependencies across multiple files * Fix TS
This commit is contained in:
1 parent
54ad0b7495
commit
426fda2ebc
38 files changed
+253
-264
No files matched your search
@@ -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<SupaRow>) {
|
||||
const { show: showContextMenu } = useContextMenu()
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1,9 @@
|
||||
export const STATUS_REFRESH_FREQUENCY_MS: number = 5000
|
||||
|
||||
export enum PipelineStatusName {
|
||||
FAILED = 'failed',
|
||||
STARTING = 'starting',
|
||||
STARTED = 'started',
|
||||
STOPPED = 'stopped',
|
||||
UNKNOWN = 'unknown',
|
||||
}
|
||||
+2
-2
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
+122
@@ -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<typeof FormSchema>
|
||||
export type CronJobType = CreateCronJobForm['values']
|
||||
+13
-124
@@ -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<CronJob, 'jobname' | 'schedule' | 'active' | 'command'>
|
||||
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<typeof FormSchema>
|
||||
export type CronJobType = CreateCronJobForm['values']
|
||||
|
||||
const FORM_ID = 'create-cron-job-sidepanel'
|
||||
|
||||
const buildCommand = (values: CronJobType) => {
|
||||
+3
-3
@@ -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<CreateCronJobForm>
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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<CreateCronJobForm>
|
||||
|
||||
@@ -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<CreateCronJobForm>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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<CreateCronJobForm>
|
||||
|
||||
@@ -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<CreateCronJobForm>
|
||||
|
||||
@@ -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<CreateCronJobForm>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface SecretTableColumn {
|
||||
id: 'secret' | 'id' | 'secret_value' | 'updated_at' | 'actions'
|
||||
name: string
|
||||
minWidth?: number
|
||||
width?: number
|
||||
maxWidth?: number
|
||||
}
|
||||
@@ -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 },
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
<IntegrationOverviewTab
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { BASE_PATH, DOCS_URL } from 'lib/constants'
|
||||
import { CreateIcebergWrapperSheet } from './CreateIcebergWrapperSheet'
|
||||
import type { ServerOption, WrapperMeta } from './Wrappers.types'
|
||||
|
||||
export const WRAPPER_HANDLERS = {
|
||||
@@ -2175,7 +2174,7 @@ export const WRAPPERS: WrapperMeta[] = [
|
||||
label: 'Iceberg',
|
||||
docsUrl: `${DOCS_URL}/guides/database/extensions/wrappers/iceberg`,
|
||||
minimumExtensionVersion: '0.5.3',
|
||||
createComponent: CreateIcebergWrapperSheet,
|
||||
customComponent: true,
|
||||
server: {
|
||||
// The fields are intentionally not required. The required flag is enforced in the create iceberg wrapper sheet.
|
||||
// In the edit wrapper sheet, all fields are shown and not required.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { CreateWrapperSheetProps } from './CreateWrapperSheet'
|
||||
|
||||
export type WrapperMeta = {
|
||||
name: string
|
||||
handlerName: string
|
||||
@@ -12,7 +10,7 @@ export type WrapperMeta = {
|
||||
server: Server
|
||||
docsUrl: string
|
||||
minimumExtensionVersion?: string
|
||||
createComponent?: React.ComponentType<CreateWrapperSheetProps>
|
||||
customComponent?: boolean
|
||||
// If true, the wrapper can target a schema which will be populated with tables specified by the wrapper..
|
||||
canTargetSchema?: boolean
|
||||
sourceSchemaOption?: ServerOption
|
||||
|
||||
@@ -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<string>
|
||||
|
||||
@@ -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: <TooltipLabel level="success" />,
|
||||
color: 'hsl(var(--foreground-muted))',
|
||||
},
|
||||
warning: {
|
||||
label: <TooltipLabel level="warning" />,
|
||||
color: 'hsl(var(--warning-default))',
|
||||
},
|
||||
error: {
|
||||
label: <TooltipLabel level="error" />,
|
||||
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
|
||||
|
||||
@@ -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: <TooltipLabel level="success" />,
|
||||
color: 'hsl(var(--foreground-muted))',
|
||||
},
|
||||
warning: {
|
||||
label: <TooltipLabel level="warning" />,
|
||||
color: 'hsl(var(--warning-default))',
|
||||
},
|
||||
error: {
|
||||
label: <TooltipLabel level="error" />,
|
||||
color: 'hsl(var(--destructive-default))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export const UnifiedLogs = () => {
|
||||
useResetFocus()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import type { GetTableRowsArgs } from './table-rows-query'
|
||||
|
||||
type TableRowKeyArgs = Omit<GetTableRowsArgs, 'table'> & { 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,
|
||||
|
||||
Reference in new issue
Block a user