diff --git a/apps/studio/components/interfaces/SQLEditor/OngoingQueriesPanel.tsx b/apps/studio/components/interfaces/SQLEditor/OngoingQueriesPanel.tsx new file mode 100644 index 00000000000..4f4db881c9c --- /dev/null +++ b/apps/studio/components/interfaces/SQLEditor/OngoingQueriesPanel.tsx @@ -0,0 +1,168 @@ +import dayjs from 'dayjs' +import { RefreshCcw, RefreshCw, StopCircle } from 'lucide-react' +import { useState } from 'react' +import toast from 'react-hot-toast' + +import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' +import { useQueryAbortMutation } from 'data/sql/abort-query-mutation' +import { useOngoingQueriesQuery } from 'data/sql/ongoing-queries-query' +import { useSelectedProject } from 'hooks' +import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' +import { + Button, + CodeBlock, + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetSection, + SheetTitle, + TooltipContent_Shadcn_, + TooltipTrigger_Shadcn_, + Tooltip_Shadcn_, + cn, +} from 'ui' +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import AlertError from 'components/ui/AlertError' +import { ResponseError } from 'types' + +interface OngoingQueriesPanel { + visible: boolean + onClose: () => void +} + +export const OngoingQueriesPanel = ({ visible, onClose }: OngoingQueriesPanel) => { + const project = useSelectedProject() + const state = useDatabaseSelectorStateSnapshot() + const [selectedId, setSelectedId] = useState() + + const { data: databases } = useReadReplicasQuery({ projectRef: project?.ref }) + const database = (databases ?? []).find((db) => db.identifier === state.selectedDatabaseId) + + const { + data, + error, + isError, + isLoading: isLoadingOngoingQueries, + isFetching: isFetchingOngoingQueries, + refetch, + } = useOngoingQueriesQuery( + { + projectRef: project?.ref, + connectionString: database?.connectionString, + }, + { staleTime: 5000 } + ) + const queries = data ?? [] + + const { mutate: abortQuery, isLoading } = useQueryAbortMutation({ + onSuccess: () => { + toast.success(`Successfully aborted query (ID: ${selectedId})`) + setSelectedId(undefined) + }, + }) + + return ( + <> + onClose()}> + + + + Running queries on{' '} + {database?.identifier === project?.ref ? 'primary database' : 'read replica'} + + + )} + {queries.map((query) => ( + +
+ code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap' + )} + /> +
+

PID: {query.pid}

+

•

+

+ Started since: {dayjs(query.query_start).format('DD MMM YYYY HH:mm (ZZ)')} +

+
+
+ + + + +
+
+
+ +
{ )}
+ +
+ +
{ ) } - -export default SideBarContent diff --git a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx index 93837e4b59a..7576fc81c9b 100644 --- a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx +++ b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx @@ -1,5 +1,10 @@ import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' +import { partition } from 'lodash' +import { Plus } from 'lucide-react' +import { useRouter } from 'next/router' +import { useMemo, useState } from 'react' + import { useParams } from 'common' import { ProtectedSchemaModal } from 'components/interfaces/Database/ProtectedSchemaWarning' import AlertError from 'components/ui/AlertError' @@ -9,10 +14,6 @@ import { useSchemasQuery } from 'data/database/schemas-query' import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query' import { useCheckPermissions, useLocalStorage } from 'hooks' import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' -import { partition } from 'lodash' -import { Plus } from 'lucide-react' -import { useRouter } from 'next/router' -import { useMemo, useState } from 'react' import { useTableEditorStateSnapshot } from 'state/table-editor' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui' import { @@ -92,9 +93,9 @@ const TableEditorMenu = () => { className="pt-5 flex flex-col flex-grow gap-5 h-full" style={{ maxHeight: 'calc(100vh - 48px)' }} > -
+
{ setSearchText('') diff --git a/apps/studio/components/ui/SchemaSelector.tsx b/apps/studio/components/ui/SchemaSelector.tsx index 737b95efd80..d0ea91d41fd 100644 --- a/apps/studio/components/ui/SchemaSelector.tsx +++ b/apps/studio/components/ui/SchemaSelector.tsx @@ -92,7 +92,6 @@ const SchemaSelector = ({ size={size} disabled={disabled} type="default" - // className="w-full justify-start" className={`w-full [&>span]:w-full`} iconRight={ diff --git a/apps/studio/data/sql/abort-query-mutation.ts b/apps/studio/data/sql/abort-query-mutation.ts new file mode 100644 index 00000000000..866e183db26 --- /dev/null +++ b/apps/studio/data/sql/abort-query-mutation.ts @@ -0,0 +1,49 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { toast } from 'react-hot-toast' + +import { executeSql } from 'data/sql/execute-sql-query' +import type { ResponseError } from 'types' +import { sqlKeys } from './keys' + +export type QueryAbortVariables = { + pid: number + projectRef?: string + connectionString?: string +} + +export async function abortQuery({ pid, projectRef, connectionString }: QueryAbortVariables) { + const sql = /* SQL */ `select pg_terminate_backend(${pid})` + const { result } = await executeSql({ projectRef, connectionString, sql }) + return result +} + +type QueryAbortData = Awaited> + +export const useQueryAbortMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + return useMutation( + (vars) => abortQuery(vars), + { + async onSuccess(data, variables, context) { + const { projectRef } = variables + await queryClient.invalidateQueries(sqlKeys.query(projectRef, ['ongoing-queries'])) + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to abort query: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) +} diff --git a/apps/studio/data/sql/ongoing-queries-query.ts b/apps/studio/data/sql/ongoing-queries-query.ts new file mode 100644 index 00000000000..bbc1b989dab --- /dev/null +++ b/apps/studio/data/sql/ongoing-queries-query.ts @@ -0,0 +1,46 @@ +import { UseQueryOptions } from '@tanstack/react-query' +import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query' + +type OngoingQuery = { + pid: number + query: string + query_start: string +} + +export const getOngoingQueries = () => { + const sql = /* SQL */ ` +select pid, query, query_start from pg_stat_activity where state = 'active' and datname = 'postgres'; +`.trim() + + return sql +} + +export type OngoingQueriesVariables = { + projectRef?: string + connectionString?: string +} + +export type OngoingQueriesData = OngoingQuery[] +export type OngoingQueriesError = ExecuteSqlError + +export const useOngoingQueriesQuery = ( + { projectRef, connectionString }: OngoingQueriesVariables, + options: UseQueryOptions = {} +) => { + return useExecuteSqlQuery( + { + projectRef, + connectionString, + sql: getOngoingQueries(), + queryKey: ['ongoing-queries'], + }, + { + ...options, + select(data) { + return (data?.result ?? []).filter( + (x: OngoingQuery) => !x.query.startsWith(getOngoingQueries()) + ) + }, + } + ) +}