diff --git a/apps/docs/content/guides/auth/auth-anonymous.mdx b/apps/docs/content/guides/auth/auth-anonymous.mdx index cf0ee68c4c79b..4ccb77a318177 100644 --- a/apps/docs/content/guides/auth/auth-anonymous.mdx +++ b/apps/docs/content/guides/auth/auth-anonymous.mdx @@ -148,10 +148,10 @@ try await supabase.auth.updateUser( -You can use the [`modifyUser()`](/docs/reference/kotlin/auth-updateuser) method to link an email or phone identity to the anonymous user. +You can use the [`updateUser()`](/docs/reference/kotlin/auth-updateuser) method to link an email or phone identity to the anonymous user. ```kotlin -supabase.auth.modifyUser { +supabase.auth.updateUser { email = "example@email.com" } ``` diff --git a/apps/docs/content/guides/auth/debugging/error-codes.mdx b/apps/docs/content/guides/auth/debugging/error-codes.mdx index 92392b22fc0e7..1116fbd4778fb 100644 --- a/apps/docs/content/guides/auth/debugging/error-codes.mdx +++ b/apps/docs/content/guides/auth/debugging/error-codes.mdx @@ -59,16 +59,15 @@ Errors originating from the server API classed as `AuthApiError` always have a ` -All errors originating from the `supabase.auth` namespace of the JavaScript client library will be wrapped by the `AuthError` class. +All exceptions originating from the `supabase.auth` namespace of the Kotlin client library will be a subclass of `RestException`. -Error objects are split in a few classes: +Rest exceptions are split into a few classes: -- `AuthApiError` -- errors which originate from the Supabase Auth API. - - Use `isAuthApiError` instead of `instanceof` checks to see if an error you caught is of this type. -- `CustomAuthError` -- errors which generally originate from state in the client library. - - Use the `name` property on the error to identify the class of error received. +- `AuthRestException` -- exceptions which originate from the Supabase Auth API and have a `errorCode` property that can be used to identify the error returned by the server. +- `AuthWeakPasswordException` -- an `AuthRestException` which indicates that the password is too weak. +- `AuthSessionMissingException` -- an `AuthRestException` which indicates that the session is missing, if the user was logged out or deleted. -Errors originating from the server API classed as `AuthApiError` always have a `code` property that can be used to identify the error returned by the server. The `status` property is also present, encoding the HTTP status code received in the response. +All instances and subclasses of a `AuthRestException` have a `errorCode` property that can be used to identify the error returned by the server. diff --git a/apps/docs/content/guides/auth/phone-login.mdx b/apps/docs/content/guides/auth/phone-login.mdx index b42005f47c106..aa006d73d048a 100644 --- a/apps/docs/content/guides/auth/phone-login.mdx +++ b/apps/docs/content/guides/auth/phone-login.mdx @@ -215,6 +215,15 @@ try await supabase.auth.updateUser( ) ``` + + + +```kotlin +supabase.auth.updateUser { + phone = "123456789" +} +``` + diff --git a/apps/docs/docs/ref/kotlin/installing.mdx b/apps/docs/docs/ref/kotlin/installing.mdx index c3eb71b2374a6..93a918c49ffc1 100644 --- a/apps/docs/docs/ref/kotlin/installing.mdx +++ b/apps/docs/docs/ref/kotlin/installing.mdx @@ -30,6 +30,8 @@ custom_edit_url: https://github.com/supabase/supabase/edit/master/web/spec/supab Checkout the different READMEs for information about supported Kotlin targets. + *Note that the minimum Android SDK version is 26. For lower versions, you need to enable [core library desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring).* + void } -const generateJobDetailsSQL = (jobId: number) => { - return `select * from cron.job_run_details where jobid = '${jobId}' order by start_time desc limit 10` -} - export const CronJobCard = ({ job, onEditCronJob, onDeleteCronJob }: CronJobCardProps) => { const [toggleConfirmationModalShown, showToggleConfirmationModal] = useState(false) const { ref } = useParams() @@ -81,9 +77,7 @@ export const CronJobCard = ({ job, onEditCronJob, onDeleteCronJob }: CronJobCard Edit cron job - + View previous runs diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.constants.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.constants.tsx index ec4039916f021..470a2c97d4a32 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.constants.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.constants.tsx @@ -18,8 +18,8 @@ export const CRONJOB_DEFINITIONS = [ { value: 'sql_function', icon: , - label: 'Postgres SQL Function', - description: 'Choose a Postgres SQL functions to run.', + label: 'Database function', + description: 'Choose a database function to run.', }, { diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts index af9f02fa5285d..8ad110fa62cfc 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts @@ -111,7 +111,36 @@ export const parseCronJobCommand = (originalCommand: string): CronJobType => { return DEFAULT_CRONJOB_COMMAND } +export function calculateDuration(start: string, end: string): string { + const startTime = new Date(start).getTime() + const endTime = new Date(end).getTime() + const duration = endTime - startTime + return isNaN(duration) ? 'Invalid Date' : `${duration} ms` +} + +export function formatDate(dateString: string): string { + const date = new Date(dateString) + if (isNaN(date.getTime())) { + return 'Invalid Date' + } + const options: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: 'short', // Use 'long' for full month name + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, // Use 12-hour format if preferred + timeZoneName: 'short', // Optional: to include timezone + } + return date.toLocaleString(undefined, options) +} + // detect seconds like "10 seconds" or normal cron syntax like "*/5 * * * *" export const secondsPattern = /^\d+\s+seconds$/ export const cronPattern = /^(\*|(\d+|\*\/\d+)|\d+\/\d+|\d+-\d+|\d+(,\d+)*)(\s+(\*|(\d+|\*\/\d+)|\d+\/\d+|\d+-\d+|\d+(,\d+)*)){4}$/ + +export function isSecondsFormat(schedule: string): boolean { + return secondsPattern.test(schedule.trim()) +} diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsEmptyState.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsEmptyState.tsx new file mode 100644 index 0000000000000..9abe797ab7d5e --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsEmptyState.tsx @@ -0,0 +1,14 @@ +export default function CronJobsEmptyState({ page }: { page: string }) { + return ( +
+

+ {page === 'jobs' ? 'No cron jobs created yet' : 'No runs for this cron job yet'} +

+

+ {page === 'jobs' + ? 'Create one by clicking "Create a new cron job"' + : 'Check the schedule of your cron jobs to see when they run'} +

+
+ ) +} diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx index 8f89f3668b6a6..8136e14a377ea 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx @@ -26,7 +26,6 @@ export const CronjobsTab = () => { projectRef: project?.ref, connectionString: project?.connectionString, }) - if (isLoading) return (
@@ -95,9 +94,14 @@ export const CronjobsTab = () => { Your search for "{searchQuery}" did not return any results

+ ) : isLoading ? ( +
+ +
) : ( filteredCronJobs.map((job) => ( setCreateCronJobSheetShown(job)} onDeleteCronJob={(job) => setCronJobForDeletion(job)} diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/PreviousRunsTab.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/PreviousRunsTab.tsx new file mode 100644 index 0000000000000..1beebad7e4d6c --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/CronJobs/PreviousRunsTab.tsx @@ -0,0 +1,283 @@ +import { toString as CronToString } from 'cronstrue' +import { List } from 'lucide-react' +import Link from 'next/link' +import { UIEvent, useCallback, useMemo } from 'react' +import DataGrid, { Column, Row } from 'react-data-grid' + +import { useParams } from 'common' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { useCronJobsQuery } from 'data/database-cron-jobs/database-cron-jobs-query' +import { + CronJobRun, + useCronJobRunsInfiniteQuery, +} from 'data/database-cron-jobs/database-cron-jobs-runs-infinite-query' +import { + Badge, + Button, + cn, + LoadingLine, + SimpleCodeBlock, + Tooltip_Shadcn_, + TooltipContent_Shadcn_, + TooltipTrigger_Shadcn_, +} from 'ui' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' +import { calculateDuration, formatDate, isSecondsFormat } from './CronJobs.utils' +import CronJobsEmptyState from './CronJobsEmptyState' + +const cronJobColumns = [ + { + id: 'runid', + name: 'RunID', + minWidth: 60, + value: (row: CronJobRun) => ( +
+

{row.runid}

+
+ ), + }, + { + id: 'message', + name: 'Message', + minWidth: 200, + value: (row: CronJobRun) => ( +
+ + + + {row.return_message} + + + + + {row.return_message} + + + +
+ ), + }, + + { + id: 'status', + name: 'Status', + minWidth: 75, + value: (row: CronJobRun) => ( + {row.status} + ), + }, + { + id: 'start_time', + name: 'Start Time', + minWidth: 120, + value: (row: CronJobRun) =>
{formatDate(row.start_time)}
, + }, + { + id: 'end_time', + name: 'End Time', + minWidth: 120, + value: (row: CronJobRun) =>
{formatDate(row.end_time)}
, + }, + + { + id: 'duration', + name: 'Duration', + minWidth: 100, + value: (row: CronJobRun) => ( +
{calculateDuration(row.start_time, row.end_time)}
+ ), + }, +] + +const columns = cronJobColumns.map((col) => { + const result: Column = { + key: col.id, + name: col.name, + resizable: true, + minWidth: col.minWidth ?? 120, + headerCellClass: 'first:pl-6 cursor-pointer', + renderHeaderCell: () => { + return ( +
+
+

{col.name}

+
+
+ ) + }, + renderCell: (props) => { + const value = col.value(props.row) + + return ( +
+ {value} +
+ ) + }, + } + return result +}) + +function isAtBottom({ currentTarget }: UIEvent): boolean { + return currentTarget.scrollTop + 10 >= currentTarget.scrollHeight - currentTarget.clientHeight +} + +export const PreviousRunsTab = () => { + const { childId: jobName } = useParams() + const { project } = useProjectContext() + + const { data: cronJobs, isLoading: isLoadingCronJobs } = useCronJobsQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + + const currentJobState = cronJobs?.find((job) => job.jobname === jobName) + + const { + data, + isLoading: isLoadingCronJobRuns, + fetchNextPage, + isFetching, + } = useCronJobRunsInfiniteQuery( + { + projectRef: project?.ref, + connectionString: project?.connectionString, + jobId: Number(currentJobState?.jobid), + }, + { enabled: !!currentJobState?.jobid, staleTime: 30 } + ) + + const handleScroll = useCallback( + (event: UIEvent) => { + if (isLoadingCronJobRuns || !isAtBottom(event)) return + // the cancelRefetch is to prevent the query from being refetched when the user scrolls back up and down again, + // resulting in multiple fetchNextPage calls + fetchNextPage({ cancelRefetch: false }) + }, + [fetchNextPage, isLoadingCronJobRuns] + ) + + const cronJobRuns = useMemo(() => data?.pages.flatMap((p) => p) || [], [data?.pages]) + + return ( +
+
+ + { + const isSelected = false + return cn([ + `${isSelected ? 'bg-surface-300 dark:bg-surface-300' : 'bg-200'} `, + `${isSelected ? '[&>div:first-child]:border-l-4 border-l-secondary [&>div]:border-l-foreground' : ''}`, + '[&>.rdg-cell]:border-box [&>.rdg-cell]:outline-none [&>.rdg-cell]:shadow-none', + '[&>.rdg-cell:first-child>div]:ml-4', + ]) + }} + renderers={{ + renderRow(_idx, props) { + return + }, + noRowsFallback: isLoadingCronJobRuns ? ( +
+ +
+ ) : ( +
+ +
+ ), + }} + /> +
+ +
+ {isLoadingCronJobs ? ( + + ) : ( + <> +
+

Schedule

+

+ {currentJobState?.schedule ? ( + <> + {currentJobState.schedule} +

+ {isSecondsFormat(currentJobState.schedule) + ? '' + : CronToString(currentJobState.schedule.toLowerCase())} +

+ + ) : ( + Loading schedule... + )} +

+
+ +
+

Command

+ + + + {currentJobState?.command} + +
+ + + + {currentJobState?.command} + + + + {/*
+ + {currentJobState?.command} + +
*/} +
+ +
+

Explore

+ +
+ + )} +
+
+ ) +} diff --git a/apps/studio/components/interfaces/Integrations/Landing/AvailableIntegrations.tsx b/apps/studio/components/interfaces/Integrations/Landing/AvailableIntegrations.tsx index 8a391518a38f9..c0a2d721bcd33 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/AvailableIntegrations.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/AvailableIntegrations.tsx @@ -40,7 +40,7 @@ export const AvailableIntegrations = () => { value={selectedCategory} onValueChange={(value) => setSelectedCategory(value as IntegrationCategory)} > - + setSearch(e.target.value)} @@ -55,15 +55,9 @@ export const AvailableIntegrations = () => { className="pl-7 rounded-none !border-0 border-transparent bg-transparent !shadow-none !ring-0 !ring-offset-0" placeholder="Search..." /> - - All Integrations - - - Foreign Data Wrappers - - - Postgres Extensions - + All Integrations + Foreign Data Wrappers + Postgres Extensions
diff --git a/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx b/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx index 1bcaf7b433112..cad9403f5ea95 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx @@ -1,4 +1,4 @@ -import { Clock5, Layers, Vault, Webhook } from 'lucide-react' +import { Clock5, Layers, Timer, Vault, Webhook } from 'lucide-react' import Image from 'next/image' import { ComponentType, ReactNode } from 'react' @@ -131,9 +131,21 @@ const supabaseIntegrations: IntegrationDefinition[] = [ { route: 'cron-jobs', label: 'Cron Jobs', + hasChild: true, + childIcon: ( + + ), }, ], navigate: (id: string, pageId: string = 'overview', childId: string | undefined) => { + if (childId) { + return dynamic( + () => import('../CronJobs/PreviousRunsTab').then((mod) => mod.PreviousRunsTab), + { + loading: Loading, + } + ) + } switch (pageId) { case 'overview': return dynamic( diff --git a/apps/studio/components/interfaces/Settings/Logs/LogSelection.tsx b/apps/studio/components/interfaces/Settings/Logs/LogSelection.tsx index fb62e0e536040..1913b4ba78ae5 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogSelection.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogSelection.tsx @@ -42,6 +42,8 @@ const LogSelection = ({ log, onClose, queryType, isLoading, error }: LogSelectio case 'database': return + case 'pg_cron': + return case 'fn_edge': return diff --git a/apps/studio/components/interfaces/Settings/Logs/LogTable.tsx b/apps/studio/components/interfaces/Settings/Logs/LogTable.tsx index ba438fac150cd..f83f79c0f8d6e 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogTable.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogTable.tsx @@ -173,6 +173,9 @@ const LogTable = ({ case 'auth': columns = AuthColumnRenderer break + case 'pg_cron': + columns = DatabasePostgresColumnRender + break default: if (firstRow && isDefaultLogPreviewFormat(firstRow)) { diff --git a/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts b/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts index 2da4d33575241..4af99e34e515c 100644 --- a/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts +++ b/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts @@ -260,7 +260,7 @@ from edge_logs f cross join unnest(m.request) as r cross join unnest(m.response) as res cross join unnest(res.headers) as h -where starts_with(r.path, '/storage/v1/object') +where starts_with(r.path, '/storage/v1/object') and r.method = 'GET' and h.cf_cache_status in ('MISS', 'NONE/UNKNOWN', 'EXPIRED', 'BYPASS', 'DYNAMIC') group by path, search @@ -363,7 +363,7 @@ export enum LogsTableName { POSTGREST = 'postgrest_logs', SUPAVISOR = 'supavisor_logs', WAREHOUSE = 'warehouse_logs', - CRON_JOBS = 'cron_job_run_details', + PG_CRON = 'pg_cron_logs', } export const LOGS_TABLES = { @@ -377,7 +377,7 @@ export const LOGS_TABLES = { postgrest: LogsTableName.POSTGREST, supavisor: LogsTableName.SUPAVISOR, warehouse: LogsTableName.WAREHOUSE, - cron: LogsTableName.CRON_JOBS, + pg_cron: LogsTableName.POSTGRES, } export const LOGS_SOURCE_DESCRIPTION = { @@ -391,7 +391,7 @@ export const LOGS_SOURCE_DESCRIPTION = { [LogsTableName.POSTGREST]: 'RESTful API web server logs', [LogsTableName.SUPAVISOR]: 'Cloud-native Postgres connection pooler logs', [LogsTableName.WAREHOUSE]: 'Logs obtained from a data warehouse collection', - [LogsTableName.CRON_JOBS]: 'Logs obtained from cron job runs', + [LogsTableName.PG_CRON]: 'Postgres logs from pg_cron cron jobs', } export const genQueryParams = (params: { [k: string]: string }) => { diff --git a/apps/studio/components/interfaces/Settings/Logs/Logs.types.ts b/apps/studio/components/interfaces/Settings/Logs/Logs.types.ts index 3ba1eef4fb849..565c00e41b1a2 100644 --- a/apps/studio/components/interfaces/Settings/Logs/Logs.types.ts +++ b/apps/studio/components/interfaces/Settings/Logs/Logs.types.ts @@ -88,7 +88,7 @@ export type QueryType = | 'supavisor' | 'postgrest' | 'warehouse' - | 'cron' + | 'pg_cron' export type Mode = 'simple' | 'custom' diff --git a/apps/studio/components/interfaces/Settings/Logs/Logs.utils.ts b/apps/studio/components/interfaces/Settings/Logs/Logs.utils.ts index 6a4c84ec55c89..7367bf296ee6b 100644 --- a/apps/studio/components/interfaces/Settings/Logs/Logs.utils.ts +++ b/apps/studio/components/interfaces/Settings/Logs/Logs.utils.ts @@ -145,8 +145,8 @@ export const genDefaultQuery = (table: LogsTableName, filters: Filters, limit: n if (IS_PLATFORM === false) { return ` -- local dev edge_logs query -select id, edge_logs.timestamp, event_message, request.method, request.path, response.status_code -from edge_logs +select id, edge_logs.timestamp, event_message, request.method, request.path, response.status_code +from edge_logs ${joins} ${where} ${orderBy} @@ -212,8 +212,19 @@ limit ${limit} limit ${limit} ` - case 'cron_job_run_details': - return `select status, start_time, end_time, jobid from ${table} ${where} ${orderBy} limit ${limit}` + case 'pg_cron_logs': + const baseWhere = `where (parsed.application_name = 'pg_cron' OR event_message LIKE '%cron job%')` + + const pgCronWhere = where ? `${baseWhere} AND ${where.substring(6)}` : baseWhere + + return `select identifier, postgres_logs.timestamp, id, event_message, parsed.error_severity, parsed.query +from postgres_logs + cross join unnest(metadata) as m + cross join unnest(m.parsed) as parsed +${pgCronWhere} +${orderBy} +limit ${limit} +` } } diff --git a/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx b/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx index 0eda209a2300f..d24b38bfa3f98 100644 --- a/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx +++ b/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx @@ -166,8 +166,8 @@ export function LogsSidebarMenuV2() { }, { name: 'Cron Jobs', - key: 'cron-logs', - url: `/project/${ref}/logs/cron-logs`, + key: 'pg_cron', + url: `/project/${ref}/logs/pgcron-logs`, items: [], }, ] diff --git a/apps/studio/data/database-cron-jobs/database-cron-jobs-runs-infinite-query.ts b/apps/studio/data/database-cron-jobs/database-cron-jobs-runs-infinite-query.ts new file mode 100644 index 0000000000000..d52865cf607d5 --- /dev/null +++ b/apps/studio/data/database-cron-jobs/database-cron-jobs-runs-infinite-query.ts @@ -0,0 +1,84 @@ +import { UseInfiniteQueryOptions, useInfiniteQuery } from '@tanstack/react-query' +import { last } from 'lodash' + +import { executeSql } from 'data/sql/execute-sql-query' +import { ResponseError } from 'types' +import { databaseCronJobsKeys } from './keys' + +export type DatabaseCronJobRunsVariables = { + projectRef?: string + connectionString?: string + jobId: number +} + +export type CronJobRun = { + jobid: number + runid: number + job_pid: number + database: string + username: string + command: string + status: 'succeeded' | 'failed' + return_message: string + start_time: string + end_time: string +} + +export const CRON_JOB_RUNS_PAGE_SIZE = 30 + +export async function getDatabaseCronJobRuns({ + projectRef, + connectionString, + jobId, + afterTimestamp, +}: DatabaseCronJobRunsVariables & { afterTimestamp: string }) { + if (!projectRef) throw new Error('Project ref is required') + + let query = ` + SELECT * FROM cron.job_run_details + WHERE + jobid = '${jobId}' + ${afterTimestamp ? `AND start_time < '${afterTimestamp}'` : ''} + ORDER BY start_time DESC + LIMIT ${CRON_JOB_RUNS_PAGE_SIZE}` + + const { result } = await executeSql({ + projectRef, + connectionString, + sql: query, + }) + return result +} + +export type DatabaseCronJobData = CronJobRun[] +export type DatabaseCronJobError = ResponseError + +export const useCronJobRunsInfiniteQuery = ( + { projectRef, connectionString, jobId }: DatabaseCronJobRunsVariables, + { + enabled = true, + ...options + }: UseInfiniteQueryOptions = {} +) => + useInfiniteQuery( + databaseCronJobsKeys.runsInfinite(projectRef, jobId, { status }), + ({ pageParam }) => { + return getDatabaseCronJobRuns({ + projectRef, + connectionString, + jobId, + afterTimestamp: pageParam, + }) + }, + { + staleTime: 0, + enabled: enabled && typeof projectRef !== 'undefined', + + getNextPageParam(lastPage) { + const hasNextPage = lastPage.length <= CRON_JOB_RUNS_PAGE_SIZE + if (!hasNextPage) return undefined + return last(lastPage)?.start_time + }, + ...options, + } + ) diff --git a/apps/studio/data/database-cron-jobs/keys.ts b/apps/studio/data/database-cron-jobs/keys.ts index 5df1b7ec631d1..f132226a3d81e 100644 --- a/apps/studio/data/database-cron-jobs/keys.ts +++ b/apps/studio/data/database-cron-jobs/keys.ts @@ -3,5 +3,12 @@ export const databaseCronJobsKeys = { delete: () => ['cron-jobs', 'delete'] as const, alter: () => ['cronjobs', 'alter'] as const, list: (projectRef: string | undefined) => ['projects', projectRef, 'cron-jobs'] as const, + runsInfinite: (projectRef: string | undefined, jobId: number, options?: object) => [ + 'projects', + projectRef, + 'cron-jobs', + jobId, + options, + ], timezone: (projectRef: string | undefined) => ['database-cron-timezone', projectRef] as const, } diff --git a/apps/studio/data/database-queues/database-queues-toggle-postgrest-mutation.ts b/apps/studio/data/database-queues/database-queues-toggle-postgrest-mutation.ts index 5265c2527abd8..6db06636e5d54 100644 --- a/apps/studio/data/database-queues/database-queues-toggle-postgrest-mutation.ts +++ b/apps/studio/data/database-queues/database-queues-toggle-postgrest-mutation.ts @@ -19,7 +19,7 @@ const EXPOSE_QUEUES_TO_POSTGREST_SQL = minify(/* SQL */ ` create schema if not exists ${QUEUES_SCHEMA}; grant usage on schema ${QUEUES_SCHEMA} to postgres, anon, authenticated, service_role; -create or replace function ${QUEUES_SCHEMA}.queue_pop( +create or replace function ${QUEUES_SCHEMA}.pop( queue_name text ) returns setof pgmq.message_record @@ -35,10 +35,10 @@ begin end; $$; -comment on function ${QUEUES_SCHEMA}.queue_pop(queue_name text) is 'Retrieves and locks the next message from the specified queue.'; +comment on function ${QUEUES_SCHEMA}.pop(queue_name text) is 'Retrieves and locks the next message from the specified queue.'; -create or replace function ${QUEUES_SCHEMA}.queue_send( +create or replace function ${QUEUES_SCHEMA}.send( queue_name text, message jsonb, sleep_seconds integer default 0 -- renamed from 'delay' @@ -58,10 +58,10 @@ begin end; $$; -comment on function ${QUEUES_SCHEMA}.queue_send(queue_name text, message jsonb, sleep_seconds integer) is 'Sends a message to the specified queue, optionally delaying its availability by a number of seconds.'; +comment on function ${QUEUES_SCHEMA}.send(queue_name text, message jsonb, sleep_seconds integer) is 'Sends a message to the specified queue, optionally delaying its availability by a number of seconds.'; -create or replace function ${QUEUES_SCHEMA}.queue_send_batch( +create or replace function ${QUEUES_SCHEMA}.send_batch( queue_name text, messages jsonb[], sleep_seconds integer default 0 -- renamed from 'delay' @@ -81,10 +81,10 @@ begin end; $$; -comment on function ${QUEUES_SCHEMA}.queue_send_batch(queue_name text, messages jsonb[], sleep_seconds integer) is 'Sends a batch of messages to the specified queue, optionally delaying their availability by a number of seconds.'; +comment on function ${QUEUES_SCHEMA}.send_batch(queue_name text, messages jsonb[], sleep_seconds integer) is 'Sends a batch of messages to the specified queue, optionally delaying their availability by a number of seconds.'; -create or replace function ${QUEUES_SCHEMA}.queue_archive( +create or replace function ${QUEUES_SCHEMA}.archive( queue_name text, message_id bigint ) @@ -101,10 +101,10 @@ begin end; $$; -comment on function ${QUEUES_SCHEMA}.queue_archive(queue_name text, message_id bigint) is 'Archives a message by moving it from the queue to a permanent archive.'; +comment on function ${QUEUES_SCHEMA}.archive(queue_name text, message_id bigint) is 'Archives a message by moving it from the queue to a permanent archive.'; -create or replace function ${QUEUES_SCHEMA}.queue_archive( +create or replace function ${QUEUES_SCHEMA}.archive( queue_name text, message_id bigint ) @@ -121,10 +121,10 @@ begin end; $$; -comment on function ${QUEUES_SCHEMA}.queue_archive(queue_name text, message_id bigint) is 'Archives a message by moving it from the queue to a permanent archive.'; +comment on function ${QUEUES_SCHEMA}.archive(queue_name text, message_id bigint) is 'Archives a message by moving it from the queue to a permanent archive.'; -create or replace function ${QUEUES_SCHEMA}.queue_delete( +create or replace function ${QUEUES_SCHEMA}.delete( queue_name text, message_id bigint ) @@ -141,9 +141,9 @@ begin end; $$; -comment on function ${QUEUES_SCHEMA}.queue_delete(queue_name text, message_id bigint) is 'Permanently deletes a message from the specified queue.'; +comment on function ${QUEUES_SCHEMA}.delete(queue_name text, message_id bigint) is 'Permanently deletes a message from the specified queue.'; -create or replace function ${QUEUES_SCHEMA}.queue_read( +create or replace function ${QUEUES_SCHEMA}.read( queue_name text, sleep_seconds integer, n integer @@ -163,25 +163,25 @@ begin end; $$; -comment on function ${QUEUES_SCHEMA}.queue_read(queue_name text, sleep_seconds integer, n integer) is 'Reads up to "n" messages from the specified queue with an optional "sleep_seconds" (visibility timeout).'; +comment on function ${QUEUES_SCHEMA}.read(queue_name text, sleep_seconds integer, n integer) is 'Reads up to "n" messages from the specified queue with an optional "sleep_seconds" (visibility timeout).'; -- Grant execute permissions on wrapper functions to roles -grant execute on function ${QUEUES_SCHEMA}.queue_pop(text) to postgres, service_role, anon, authenticated; +grant execute on function ${QUEUES_SCHEMA}.pop(text) to postgres, service_role, anon, authenticated; grant execute on function pgmq.pop(text) to postgres, service_role, anon, authenticated; -grant execute on function ${QUEUES_SCHEMA}.queue_send(text, jsonb, integer) to postgres, service_role, anon, authenticated; +grant execute on function ${QUEUES_SCHEMA}.send(text, jsonb, integer) to postgres, service_role, anon, authenticated; grant execute on function pgmq.send(text, jsonb, integer) to postgres, service_role, anon, authenticated; -grant execute on function ${QUEUES_SCHEMA}.queue_send_batch(text, jsonb[], integer) to postgres, service_role, anon, authenticated; +grant execute on function ${QUEUES_SCHEMA}.send_batch(text, jsonb[], integer) to postgres, service_role, anon, authenticated; grant execute on function pgmq.send_batch(text, jsonb[], integer) to postgres, service_role, anon, authenticated; -grant execute on function ${QUEUES_SCHEMA}.queue_archive(text, bigint) to postgres, service_role, anon, authenticated; +grant execute on function ${QUEUES_SCHEMA}.archive(text, bigint) to postgres, service_role, anon, authenticated; grant execute on function pgmq.archive(text, bigint) to postgres, service_role, anon, authenticated; -grant execute on function ${QUEUES_SCHEMA}.queue_delete(text, bigint) to postgres, service_role, anon, authenticated; +grant execute on function ${QUEUES_SCHEMA}.delete(text, bigint) to postgres, service_role, anon, authenticated; grant execute on function pgmq.delete(text, bigint) to postgres, service_role, anon, authenticated; -grant execute on function ${QUEUES_SCHEMA}.queue_read(text, integer, integer) to postgres, service_role, anon, authenticated; +grant execute on function ${QUEUES_SCHEMA}.read(text, integer, integer) to postgres, service_role, anon, authenticated; grant execute on function pgmq.read(text, integer, integer) to postgres, service_role, anon, authenticated; -- For the service role, we want full access @@ -196,12 +196,12 @@ grant usage on schema pgmq to postgres, anon, authenticated, service_role; const HIDE_QUEUES_FROM_POSTGREST_SQL = minify(/* SQL */ ` drop function if exists - ${QUEUES_SCHEMA}.queue_pop(queue_name text), - ${QUEUES_SCHEMA}.queue_send(queue_name text, message jsonb, sleep_seconds integer), - ${QUEUES_SCHEMA}.queue_send_batch(queue_name text, message jsonb[], sleep_seconds integer), - ${QUEUES_SCHEMA}.queue_archive(queue_name text, message_id bigint), - ${QUEUES_SCHEMA}.queue_delete(queue_name text, message_id bigint), - ${QUEUES_SCHEMA}.queue_read(queue_name text, sleep integer, n integer) + ${QUEUES_SCHEMA}.pop(queue_name text), + ${QUEUES_SCHEMA}.send(queue_name text, message jsonb, sleep_seconds integer), + ${QUEUES_SCHEMA}.send_batch(queue_name text, message jsonb[], sleep_seconds integer), + ${QUEUES_SCHEMA}.archive(queue_name text, message_id bigint), + ${QUEUES_SCHEMA}.delete(queue_name text, message_id bigint), + ${QUEUES_SCHEMA}.read(queue_name text, sleep integer, n integer) ; -- Revoke execute permissions on inner pgmq functions to roles (inverse of enabling) diff --git a/apps/studio/hooks/analytics/useLogsQuery.tsx b/apps/studio/hooks/analytics/useLogsQuery.tsx index 9f3bf306dbefc..0452722bb385a 100644 --- a/apps/studio/hooks/analytics/useLogsQuery.tsx +++ b/apps/studio/hooks/analytics/useLogsQuery.tsx @@ -55,7 +55,6 @@ const useLogsQuery = ( const usesWith = checkForWithClause(params.sql || '') const usesILIKE = checkForILIKEClause(params.sql || '') - const usesWildcard = checkForWildcard(params.sql || '') const { data, @@ -94,13 +93,6 @@ const useLogsQuery = ( docs: 'https://supabase.com/docs/guides/platform/advanced-log-filtering#the-ilike-and-similar-to-keywords-are-not-supported', } } - if (usesWildcard) { - error = { - message: - 'Wildcard (*) queries are not supported. Please remove the wildcard and try again.', - docs: 'https://supabase.com/docs/guides/platform/advanced-log-filtering#the-wildcard-operator--to-select-columns-is-not-supported', - } - } } const changeQuery = (newQuery = '') => { setParams((prev) => ({ ...prev, sql: newQuery })) diff --git a/apps/studio/pages/project/[ref]/logs/cron-logs.tsx b/apps/studio/pages/project/[ref]/logs/cron-logs.tsx index cb80975342b26..d6a549a139455 100644 --- a/apps/studio/pages/project/[ref]/logs/cron-logs.tsx +++ b/apps/studio/pages/project/[ref]/logs/cron-logs.tsx @@ -15,8 +15,8 @@ export const LogPage: NextPageWithLayout = () => { ) } diff --git a/apps/studio/pages/project/[ref]/logs/pgcron-logs.tsx b/apps/studio/pages/project/[ref]/logs/pgcron-logs.tsx new file mode 100644 index 0000000000000..25ba5c01e3561 --- /dev/null +++ b/apps/studio/pages/project/[ref]/logs/pgcron-logs.tsx @@ -0,0 +1,24 @@ +import { useRouter } from 'next/router' + +import { LogsTableName } from 'components/interfaces/Settings/Logs/Logs.constants' +import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer' +import LogsLayout from 'components/layouts/LogsLayout/LogsLayout' +import type { NextPageWithLayout } from 'types' + +export const LogPage: NextPageWithLayout = () => { + const router = useRouter() + const { ref } = router.query + + return ( + + ) +} + +LogPage.getLayout = (page) => {page} + +export default LogPage diff --git a/apps/www/data/Footer.ts b/apps/www/data/Footer.ts index 960c62865e64f..088cb91b8a9d5 100644 --- a/apps/www/data/Footer.ts +++ b/apps/www/data/Footer.ts @@ -57,10 +57,6 @@ const footerData = [ text: 'Integrations', url: '/partners/integrations', }, - { - text: 'Experts', - url: '/partners/experts', - }, { text: 'Brand Assets / Logos', url: '/brand-assets', diff --git a/apps/www/lib/redirects.js b/apps/www/lib/redirects.js index 178f7945320b0..7fe17daad1e4f 100644 --- a/apps/www/lib/redirects.js +++ b/apps/www/lib/redirects.js @@ -2834,6 +2834,16 @@ module.exports = [ source: '/docs/guides/database/connecting-to-postgres/serverless-drivers', destination: '/docs/guides/database/connecting-to-postgres', }, + { + permanent: true, + source: '/partners/experts', + destination: '/partners', + }, + { + permanent: true, + source: '/partners/experts/:path*', + destination: '/partners', + }, // marketing diff --git a/apps/www/pages/partners/[slug].tsx b/apps/www/pages/partners/[slug].tsx index f1ae62b2a49c2..60e532b0ab15b 100644 --- a/apps/www/pages/partners/[slug].tsx +++ b/apps/www/pages/partners/[slug].tsx @@ -36,26 +36,16 @@ export const getStaticProps: GetStaticProps = async ({ params }) => { .eq('slug', params!.slug as string) .single() - if (!partner || process.env.npm_lifecycle_event === 'build') { + if (!partner || partner.type === 'expert' || process.env.npm_lifecycle_event === 'build') { return { notFound: true, } } - let redirectUrl: string - switch (partner.type) { - case 'technology': - redirectUrl = `/partners/integrations/${partner.slug}` - break - case 'expert': - redirectUrl = `/partners/experts/${partner.slug}` - break - } - return { redirect: { permanent: false, - destination: redirectUrl, + destination: `/partners/integrations/${partner.slug}`, }, } } diff --git a/apps/www/pages/partners/experts/[slug].tsx b/apps/www/pages/partners/experts/[slug].tsx deleted file mode 100644 index c8f3eb41644db..0000000000000 --- a/apps/www/pages/partners/experts/[slug].tsx +++ /dev/null @@ -1,257 +0,0 @@ -import { ChevronLeft, ExternalLink } from 'lucide-react' -import { GetStaticPaths, GetStaticProps } from 'next' -import { MDXRemote, MDXRemoteSerializeResult } from 'next-mdx-remote' -import { serialize } from 'next-mdx-remote/serialize' -import { NextSeo } from 'next-seo' -import Image from 'next/image' -import Link from 'next/link' -import 'swiper/css' -import { Swiper, SwiperSlide } from 'swiper/react' -import DefaultLayout from '~/components/Layouts/Default' -import SectionContainer from '~/components/Layouts/SectionContainer' -import supabase from '~/lib/supabaseMisc' -import type { Partner } from '~/types/partners' -import Error404 from '../../404' - -function Partner({ - partner, - overview, -}: { - partner: Partner - overview: MDXRemoteSerializeResult, Record> -}) { - if (!partner) return - return ( - <> - - - - -
- {/* Back button */} - - - Back - - -
- {partner.title} -

- {partner.title} -

-
- -
- - {partner.images?.map((image: any, i: number) => { - return ( - -
- {partner.title} -
-
- ) - })} -
-
- -
-
-

- Overview -

- - {partner.video && ( -
-