diff --git a/apps/studio/components/interfaces/Linter/LinterFilters.tsx b/apps/studio/components/interfaces/Linter/LinterFilters.tsx
index 1fcab85e326..ff93e170674 100644
--- a/apps/studio/components/interfaces/Linter/LinterFilters.tsx
+++ b/apps/studio/components/interfaces/Linter/LinterFilters.tsx
@@ -1,6 +1,10 @@
+import { useParams } from 'common'
import { LINTER_LEVELS, LINT_TABS } from 'components/interfaces/Linter/Linter.constants'
+import { DownloadResultsButton } from 'components/ui/DownloadResultsButton'
import { FilterPopover } from 'components/ui/FilterPopover'
import { Lint } from 'data/lint/lint-query'
+import { RefreshCw } from 'lucide-react'
+import { Button } from 'ui'
interface LinterFiltersProps {
filterOptions: {
@@ -8,18 +12,26 @@ interface LinterFiltersProps {
value: string
}[]
activeLints: Lint[]
+ filteredLints: Lint[]
currentTab: LINTER_LEVELS
filters: { level: LINTER_LEVELS; filters: string[] }[]
+ isLoading: boolean
setFilters: (value: { level: LINTER_LEVELS; filters: string[] }[]) => void
+ onClickRefresh: () => void
}
const LinterFilters = ({
filterOptions,
activeLints,
+ filteredLints,
currentTab,
filters,
+ isLoading,
setFilters,
+ onClickRefresh,
}: LinterFiltersProps) => {
+ const { ref } = useParams()
+
const updateFilters = (level: LINTER_LEVELS, newFilters: string[]) => {
const updatedFilters = [...filters]
@@ -31,11 +43,12 @@ const LinterFilters = ({
}
return (
-
+
{LINT_TABS.map((tab) => (
x.level === tab.id).length === 0}
labelKey="name"
@@ -45,6 +58,24 @@ const LinterFilters = ({
/>
))}
+
+
+ }
+ >
+ Refresh
+
+
+
)
}
diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.tsx b/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.tsx
index c19a6bae911..c9fdfb6778a 100644
--- a/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.tsx
+++ b/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.tsx
@@ -13,6 +13,7 @@ import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import {
Button,
+ LoadingLine,
TabsList_Shadcn_,
TabsTrigger_Shadcn_,
Tabs_Shadcn_,
@@ -44,6 +45,8 @@ export const QueryPerformance = ({
const { project } = useProjectContext()
const state = useDatabaseSelectorStateSnapshot()
+ const { isLoading, isRefetching } = queryPerformanceQuery
+
const [page, setPage] = useState
(
(preset as QUERY_PERFORMANCE_REPORT_TYPES) ?? QUERY_PERFORMANCE_REPORT_TYPES.MOST_TIME_CONSUMING
)
@@ -190,14 +193,11 @@ export const QueryPerformance = ({
-
- {
- setShowResetgPgStatStatements(true)
- }}
- />
-
+ setShowResetgPgStatStatements(true)}
+ />
+
@@ -210,9 +210,7 @@ export const QueryPerformance = ({
className="absolute top-1.5 right-3 px-1.5"
type="text"
size="tiny"
- onClick={() => {
- setShowBottomSection(false)
- }}
+ onClick={() => setShowBottomSection(false)}
>
diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceFilterBar.tsx b/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceFilterBar.tsx
index debc3a41c48..f346b74cfc7 100644
--- a/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceFilterBar.tsx
+++ b/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceFilterBar.tsx
@@ -2,7 +2,9 @@ import { ArrowDown, ArrowUp, RefreshCw } from 'lucide-react'
import { useRouter } from 'next/router'
import { useState } from 'react'
+import { useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
+import { DownloadResultsButton } from 'components/ui/DownloadResultsButton'
import { FilterPopover } from 'components/ui/FilterPopover'
import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query'
import { DbQueryHook } from 'hooks/analytics/useDbQuery'
@@ -27,6 +29,7 @@ export const QueryPerformanceFilterBar = ({
onResetReportClick?: () => void
}) => {
const router = useRouter()
+ const { ref } = useParams()
const { project } = useProjectContext()
const [showBottomSection] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.QUERY_PERF_SHOW_BOTTOM_SECTION,
@@ -86,7 +89,7 @@ export const QueryPerformanceFilterBar = ({
}
return (
-
+
Filter by
@@ -126,29 +129,27 @@ export const QueryPerformanceFilterBar = ({
{!showBottomSection && onResetReportClick && (
-
)
diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/ResultsDropdown.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/ResultsDropdown.tsx
deleted file mode 100644
index 0d6bd02e820..00000000000
--- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/ResultsDropdown.tsx
+++ /dev/null
@@ -1,155 +0,0 @@
-import { compact, isObject, isString, map } from 'lodash'
-import { ChevronDownIcon, Clipboard, Download } from 'lucide-react'
-import { markdownTable } from 'markdown-table'
-import { useMemo, useRef } from 'react'
-import { CSVLink } from 'react-csv'
-import { toast } from 'sonner'
-
-import { useParams } from 'common'
-import { TelemetryActions } from 'common/telemetry-constants'
-import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
-import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
-import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
-import { copyToClipboard } from 'lib/helpers'
-import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
-import {
- Button,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from 'ui'
-
-export type ResultsDropdownProps = {
- id: string
-}
-
-const ResultsDropdown = ({ id }: ResultsDropdownProps) => {
- const { project } = useProjectContext()
- const snapV2 = useSqlEditorV2StateSnapshot()
-
- const result = snapV2.results?.[id]?.[0] ?? undefined
- const csvRef = useRef
(null)
-
- const { ref } = useParams()
- const org = useSelectedOrganization()
- const { mutate: sendEvent } = useSendEventMutation()
-
- const csvData = useMemo(() => {
- if (result?.rows) {
- const rows = Array.from(result.rows || []).map((row) => {
- return map(row, (v, k) => {
- if (isString(v)) {
- // replace all newlines with the character \n
- // escape all quotation marks
- return v.replaceAll(/\n/g, '\\n').replaceAll(/"/g, '""')
- }
- if (isObject(v)) {
- // replace all quotation marks with two quotation marks to escape them.
- return JSON.stringify(v).replaceAll(/\"/g, '""')
- }
- return v
- })
- })
-
- return compact(rows)
- }
- return ''
- }, [result])
-
- const headers = useMemo(() => {
- if (result?.rows) {
- const firstRow = Array.from(result.rows || [])[0]
- if (firstRow) {
- return Object.keys(firstRow)
- }
- }
- // if undefined is returned no headers will be set. In this case, no headers would be better
- // than malformed headers.
- return undefined
- }, [result])
-
- function onDownloadCSV() {
- csvRef.current?.link.click()
- sendEvent({
- action: TelemetryActions.SQL_EDITOR_RESULT_DOWNLOAD_CSV_CLICKED,
- groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
- })
- }
-
- function onCopyAsMarkdown() {
- if (navigator) {
- if (!result || !result.rows) return 'results is empty'
- if (result.rows.constructor !== Array && !!result.error) return result.error
- if (result.rows.length == 0) return 'results is empty'
-
- const columns = Object.keys(result.rows[0])
- const rows = result.rows.map((x) => {
- let temp: any[] = []
- columns.forEach((col) => temp.push(x[col]))
- return temp
- })
- const table = [columns].concat(rows)
- const markdownData = markdownTable(table)
-
- copyToClipboard(markdownData, () => {
- toast.success('Copied results to clipboard')
- sendEvent({
- action: TelemetryActions.SQL_EDITOR_RESULT_COPY_MARKDOWN_CLICKED,
- groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
- })
- })
- }
- }
-
- function onCopyAsJSON() {
- if (navigator) {
- if (!result || !result.rows) return 'results is empty'
- if (result.rows.constructor !== Array && !!result.error) return result.error
- if (result.rows.length == 0) return 'results is empty'
-
- copyToClipboard(JSON.stringify(result.rows, null, 2), () => {
- toast.success('Copied results to clipboard')
- sendEvent({
- action: TelemetryActions.SQL_EDITOR_RESULT_COPY_JSON_CLICKED,
- groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
- })
- })
- }
- }
-
- return (
-
-
- }>
- Export
-
-
-
-
-
-
-
-
- Download CSV
-
-
-
- Copy as markdown
-
-
-
- Copy as JSON
-
-
-
- )
-}
-
-export default ResultsDropdown
diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx
index eef6544215a..954059ffb99 100644
--- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx
+++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx
@@ -1,12 +1,12 @@
import { toast } from 'sonner'
import { useParams } from 'common'
+import { DownloadResultsButton } from 'components/ui/DownloadResultsButton'
import { useContentUpsertMutation } from 'data/content/content-upsert-mutation'
import { Snippet } from 'data/content/sql-folders-query'
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
import { TabsContent_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_, Tabs_Shadcn_ } from 'ui'
import { ChartConfig } from './ChartConfig'
-import ResultsDropdown from './ResultsDropdown'
import UtilityActions from './UtilityActions'
import UtilityTabResults from './UtilityTabResults'
@@ -114,7 +114,13 @@ const UtilityPanel = ({
Chart
- {result?.rows && }
+ {result?.rows && (
+
+ )}
void
+ onCopyAsJSON?: () => void
+}
+
+export const DownloadResultsButton = ({
+ type = 'default',
+ align = 'start',
+ results,
+ fileName,
+ onCopyAsMarkdown,
+ onCopyAsJSON,
+}: DownloadResultsButtonProps) => {
+ const csvRef = useRef(null)
+
+ const headers = useMemo(() => {
+ if (results) {
+ const firstRow = Array.from(results)[0]
+ if (firstRow) return Object.keys(firstRow)
+ }
+ return undefined
+ }, [results])
+
+ const copyAsMarkdown = () => {
+ if (navigator) {
+ if (results.length == 0) toast('Results are empty')
+
+ const columns = Object.keys(results[0])
+ const rows = results.map((x) => {
+ let temp: any[] = []
+ columns.forEach((col) => temp.push(x[col]))
+ return temp
+ })
+ const table = [columns].concat(rows)
+ const markdownData = markdownTable(table)
+
+ copyToClipboard(markdownData, () => {
+ toast.success('Copied results to clipboard')
+ onCopyAsMarkdown?.()
+ })
+ }
+ }
+
+ const copyAsJSON = () => {
+ if (navigator) {
+ if (results.length === 0) return toast('Results are empty')
+ copyToClipboard(JSON.stringify(results, null, 2), () => {
+ toast.success('Copied results to clipboard')
+ onCopyAsJSON?.()
+ })
+ }
+ }
+
+ return (
+ <>
+
+
+ } disabled={results.length === 0}>
+ Export
+
+
+
+ csvRef.current?.link.click()}>
+
+ Download CSV
+
+
+
+ Copy as markdown
+
+
+
+ Copy as JSON
+
+
+
+
+ >
+ )
+}
diff --git a/apps/studio/components/ui/FilterPopover.tsx b/apps/studio/components/ui/FilterPopover.tsx
index f3448d2880d..d3eed31644a 100644
--- a/apps/studio/components/ui/FilterPopover.tsx
+++ b/apps/studio/components/ui/FilterPopover.tsx
@@ -24,6 +24,7 @@ interface FilterPopoverProps {
labelClass?: string
maxHeightClass?: string
clearButtonText?: string
+ className?: string
onSaveFilters: (options: string[]) => void
}
@@ -39,6 +40,7 @@ export const FilterPopover = >({
buttonType,
disabled,
labelClass,
+ className,
maxHeightClass = 'h-[205px]',
clearButtonText = 'Clear',
onSaveFilters,
@@ -81,7 +83,7 @@ export const FilterPopover = >({
-
+
{title ?? `Select ${name.toLowerCase()}`}
@@ -108,7 +110,7 @@ export const FilterPopover = >({
/>
{icon && (
{
const project = useSelectedProject()
- const { preset, id } = useParams()
+ const { ref, preset, id } = useParams()
// need to maintain a list of filters for each tab
const [filters, setFilters] = useState<{ level: LINTER_LEVELS; filters: string[] }[]>([
@@ -24,7 +24,6 @@ const ProjectLints: NextPageWithLayout = () => {
{ level: LINTER_LEVELS.WARN, filters: [] },
{ level: LINTER_LEVELS.INFO, filters: [] },
])
-
const [currentTab, setCurrentTab] = useState(
(preset as LINTER_LEVELS) ?? LINTER_LEVELS.ERROR
)
@@ -34,25 +33,15 @@ const ProjectLints: NextPageWithLayout = () => {
projectRef: project?.ref,
})
- let clientLints: Lint[] = []
-
const activeLints = useMemo(() => {
- return [...(data ?? []), ...clientLints]?.filter((x) => x.categories.includes('PERFORMANCE'))
+ return [...(data ?? [])]?.filter((x) => x.categories.includes('PERFORMANCE'))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data])
-
- useEffect(() => {
- // check the URL for an ID and set the selected lint
- if (id) setSelectedLint(activeLints.find((lint) => lint.cache_key === id) ?? null)
- }, [id, activeLints])
-
const currentTabFilters = (filters.find((filter) => filter.level === currentTab)?.filters ||
[]) as string[]
-
const filteredLints = activeLints
.filter((x) => x.level === currentTab)
.filter((x) => (currentTabFilters.length > 0 ? currentTabFilters.includes(x.name) : x))
-
const filterOptions = lintInfoMap
// only show filters for lint types which are present in the results and not ignored
.filter((item) =>
@@ -63,6 +52,11 @@ const ProjectLints: NextPageWithLayout = () => {
value: type.name,
}))
+ useEffect(() => {
+ // check the URL for an ID and set the selected lint
+ if (id) setSelectedLint(activeLints.find((lint) => lint.cache_key === id) ?? null)
+ }, [id, activeLints])
+
return (
{
{
const project = useSelectedProject()
- const { preset, id } = useParams()
+ const { ref, preset, id } = useParams()
// need to maintain a list of filters for each tab
const [filters, setFilters] = useState<{ level: LINTER_LEVELS; filters: string[] }[]>([
@@ -24,42 +24,21 @@ const ProjectLints: NextPageWithLayout = () => {
{ level: LINTER_LEVELS.WARN, filters: [] },
{ level: LINTER_LEVELS.INFO, filters: [] },
])
-
const [currentTab, setCurrentTab] = useState(
(preset as LINTER_LEVELS) ?? LINTER_LEVELS.ERROR
)
-
const [selectedLint, setSelectedLint] = useState(null)
- const {
- data,
- isLoading: areLintsLoading,
- isRefetching,
- refetch: refetchLintsQuery,
- } = useProjectLintsQuery({
+ const { data, isLoading, isRefetching, refetch } = useProjectLintsQuery({
projectRef: project?.ref,
})
- const isLoading = areLintsLoading
-
- const refetch = () => {
- refetchLintsQuery()
- }
-
const activeLints = (data ?? []).filter((lint) => lint.categories.includes('SECURITY'))
-
- useEffect(() => {
- // check the URL for an ID and set the selected lint
- if (id) setSelectedLint(activeLints.find((lint) => lint.cache_key === id) ?? null)
- }, [id, activeLints])
-
const currentTabFilters = (filters.find((filter) => filter.level === currentTab)?.filters ||
[]) as string[]
-
const filteredLints = activeLints
.filter((x) => x.level === currentTab)
.filter((x) => (currentTabFilters.length > 0 ? currentTabFilters.includes(x.name) : x))
-
const filterOptions = lintInfoMap
// only show filters for lint types which are present in the results and not ignored
.filter((item) =>
@@ -70,6 +49,11 @@ const ProjectLints: NextPageWithLayout = () => {
value: type.name,
}))
+ useEffect(() => {
+ // check the URL for an ID and set the selected lint
+ if (id) setSelectedLint(activeLints.find((lint) => lint.cache_key === id) ?? null)
+ }, [id, activeLints])
+
return (
{
/>