diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.tsx b/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.tsx index 1c5d4d2893b..5cb52f3bef7 100644 --- a/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.tsx +++ b/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.tsx @@ -1,8 +1,8 @@ import { InformationCircleIcon } from '@heroicons/react/16/solid' +import { X } from 'lucide-react' import { useRouter } from 'next/router' import { useEffect, useMemo, useState } from 'react' import toast from 'react-hot-toast' -import { X } from 'lucide-react' import { useParams } from 'common' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' @@ -10,6 +10,7 @@ import { executeSql } from 'data/sql/execute-sql-query' import { useLocalStorageQuery } from 'hooks' import { DbQueryHook } from 'hooks/analytics/useDbQuery' import { LOCAL_STORAGE_KEYS } from 'lib/constants' +import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' import { Button, TabsList_Shadcn_, @@ -28,7 +29,6 @@ import { PresetHookResult } from '../Reports/Reports.utils' import { QUERY_PERFORMANCE_REPORT_TYPES } from './QueryPerformance.constants' import { QueryPerformanceFilterBar } from './QueryPerformanceFilterBar' import { QueryPerformanceGrid } from './QueryPerformanceGrid' -import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' interface QueryPerformanceProps { queryHitRate: PresetHookResult diff --git a/apps/studio/components/interfaces/Reports/GridResize.tsx b/apps/studio/components/interfaces/Reports/GridResize.tsx index 6e17cd4e33a..3c7af8abd36 100644 --- a/apps/studio/components/interfaces/Reports/GridResize.tsx +++ b/apps/studio/components/interfaces/Reports/GridResize.tsx @@ -1,11 +1,33 @@ +import { X } from 'lucide-react' import RGL, { WidthProvider } from 'react-grid-layout' -import { LAYOUT_COLUMN_COUNT } from './Reports.constants' + +import { useParams } from 'common' import ChartHandler from 'components/to-be-cleaned/Charts/ChartHandler' +import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' +import { Button, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_, Tooltip_Shadcn_ } from 'ui' +import { LAYOUT_COLUMN_COUNT } from './Reports.constants' const ReactGridLayout = WidthProvider(RGL) -const GridResize = ({ startDate, endDate, interval, editableReport, setEditableReport }: any) => { - if (!editableReport) return null +interface GridResizeProps { + startDate: string + endDate: string + interval: string + editableReport: any + onRemoveChart: ({ metric }: { metric: { key: string } }) => void + setEditableReport: (payload: any) => void +} + +const GridResize = ({ + startDate, + endDate, + interval, + editableReport, + onRemoveChart, + setEditableReport, +}: GridResizeProps) => { + const { ref } = useParams() + const state = useDatabaseSelectorStateSnapshot() function onLayoutChange(layout: any) { let updatedLayout = editableReport.layout @@ -23,6 +45,8 @@ const GridResize = ({ startDate, endDate, interval, editableReport, setEditableR setEditableReport(payload) } + if (!editableReport) return null + return ( <> + > + + + @@ -285,12 +298,13 @@ const ReportFilterBar = ({ asChild type="default" size="tiny" - icon={} + icon={} > Add filter + {showReadReplicasUI && } ) } diff --git a/apps/studio/components/interfaces/Reports/ReportWidget.tsx b/apps/studio/components/interfaces/Reports/ReportWidget.tsx index f8bf9fa3524..f745036efb4 100644 --- a/apps/studio/components/interfaces/Reports/ReportWidget.tsx +++ b/apps/studio/components/interfaces/Reports/ReportWidget.tsx @@ -1,7 +1,10 @@ import * as Tooltip from '@radix-ui/react-tooltip' -import Panel from 'components/ui/Panel' +import { ExternalLink, HelpCircle } from 'lucide-react' import { NextRouter, useRouter } from 'next/router' -import { Button, IconExternalLink, IconHelpCircle, Loading } from 'ui' + +import { useParams } from 'common' +import Panel from 'components/ui/Panel' +import { Button, Loading, cn } from 'ui' import { LogsEndpointParams } from '../Settings/Logs' import type { BaseReportParams, ReportQueryType } from './Reports.types' @@ -27,15 +30,16 @@ export interface ReportWidgetRendererProps extends ReportWidgetProps projectRef: string } -const ReportWidget: React.FC = (props) => { +const ReportWidget = (props: ReportWidgetProps) => { const router = useRouter() - const { ref } = router.query + const { ref } = useParams() const projectRef = ref as string + return ( @@ -47,11 +51,7 @@ const ReportWidget: React.FC = (props) => { {props?.tooltip && ( - + @@ -76,7 +76,7 @@ const ReportWidget: React.FC = (props) => { + + + )}
@@ -330,7 +241,7 @@ const Reports = () => { onChange={handleDateRangePicker} value="7d" options={TIME_PERIODS_REPORTS} - loading={loading} + loading={isLoading} /> {startDate && endDate && ( @@ -339,7 +250,7 @@ const Reports = () => { {dayjs(startDate).format('MMM D, YYYY')} - + {dayjs(endDate).format('MMM D, YYYY')} @@ -348,75 +259,48 @@ const Reports = () => { )}
-
- {hasEdits && ( -
- There are unsaved changes -
- )} - {hasEdits && ( - - )} - +
{canUpdateReport ? ( - - + ) : ( - - - - - - - -
- - You need additional permissions to update this project's report - -
-
-
-
+ + + You need additional permissions to update this project's report + + )} + + {hasReadReplicasEnabled && }
- {config.layout.length <= 0 ? ( + {config?.layout !== undefined && config.layout.length <= 0 ? (
{canUpdateReport ? ( - - + ) : ( @@ -431,6 +315,7 @@ const Reports = () => { endDate={endDate} interval={config.interval} editableReport={config} + onRemoveChart={popChart} setEditableReport={setConfig} /> )} diff --git a/apps/studio/components/interfaces/Reports/renderers/ApiRenderers.tsx b/apps/studio/components/interfaces/Reports/renderers/ApiRenderers.tsx index e9f47b7d94f..aa96863c082 100644 --- a/apps/studio/components/interfaces/Reports/renderers/ApiRenderers.tsx +++ b/apps/studio/components/interfaces/Reports/renderers/ApiRenderers.tsx @@ -1,3 +1,8 @@ +import sumBy from 'lodash/sumBy' +import { Fragment, useState } from 'react' +import { ChevronRight } from 'lucide-react' + +import { useParams } from 'common' import { jsonSyntaxHighlight, TextFormatter, @@ -5,12 +10,9 @@ import { import Table from 'components/to-be-cleaned/Table' import BarChart from 'components/ui/Charts/BarChart' import useFillTimeseriesSorted from 'hooks/analytics/useFillTimeseriesSorted' -import sumBy from 'lodash/sumBy' -import { Fragment, useState } from 'react' -import { Button, Collapsible, IconChevronRight } from 'ui' +import { Button, Collapsible } from 'ui' import { queryParamsToObject } from '../Reports.utils' import { ReportWidgetProps, ReportWidgetRendererProps } from '../ReportWidget' -import { useParams } from 'common/hooks' export const NetworkTrafficRenderer = ( props: ReportWidgetProps<{ @@ -63,6 +65,7 @@ export const NetworkTrafficRenderer = (
) } + export const TotalRequestsChartRenderer = ( props: ReportWidgetProps<{ timestamp: string @@ -107,15 +110,17 @@ export const TopApiRoutesRenderer = ( }> ) => { const { ref: projectRef } = useParams() - const [showMore, setShowMore] = useState(false) + + const headerClasses = '!text-xs !py-2 p-0 font-bold !bg-surface-200 !border-x-0 !rounded-none' + const cellClasses = '!text-xs !py-2 !border-x-0 !rounded-none align-middle' + if (props.data.length === 0) return null - const headerClasses = '!text-xs !py-2 p-0 font-bold !bg-surface-200' - const cellClasses = '!text-xs !py-2' return ( Request @@ -128,10 +133,10 @@ export const TopApiRoutesRenderer = ( body={ <> {props.data.map((datum, index) => ( - + = 3 ? 'w-full h-full opacity-100' : '', !showMore && index >= 3 ? ' w-0 h-0 translate-y-10 opacity-0' : '', ].join(' ')} @@ -270,10 +275,10 @@ interface RouteTdContentProps { const RouteTdContent = (datum: RouteTdContentProps) => ( -
+
)} + {children} + {chartStyle === 'bar' ? ( ) : ( diff --git a/apps/studio/components/to-be-cleaned/Reports/EditReportModal.tsx b/apps/studio/components/to-be-cleaned/Reports/EditReportModal.tsx deleted file mode 100644 index f94eb710f4c..00000000000 --- a/apps/studio/components/to-be-cleaned/Reports/EditReportModal.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { toJS } from 'mobx' -import { observer } from 'mobx-react-lite' -import { useRouter } from 'next/router' -import { useEffect, useState } from 'react' -import toast from 'react-hot-toast' - -import { useProjectContentStore } from 'stores/projectContentStore' -import { Input, Modal } from 'ui' - -const EditModal = () => { - const router = useRouter() - const { id, ref } = router.query - const [visible, setVisible] = useState(false) - const [loading, setLoading] = useState(false) - const [name, setName] = useState() - const [description, setDescription] = useState() - - const contentStore = useProjectContentStore(ref) - - /* - * fetchReport() - * - * Fetches the report and sets the Name and description - * - * toJS() is used to deepcopy mobx to js - */ - const fetchReport = async () => { - setLoading(true) - await contentStore.load() - let reportData = contentStore.byId(id) - - setName(toJS(reportData?.name)) - setDescription(toJS(reportData?.description)) - - setLoading(false) - return reportData - } - - useEffect(() => { - fetchReport() - setVisible(false) - }, []) - - async function handleSubmit() { - try { - setLoading(true) - const { data, error } = await contentStore.update(id, { - name, - description, - }) - if (error) throw error - setLoading(false) - await contentStore.load() - setVisible(!visible) - return data - } catch (error: any) { - toast.error(error.message) - setLoading(false) - } - } - - return ( - setVisible(false)} - size="medium" - onConfirm={handleSubmit} - loading={loading} - > -
- setName(e.target.value)} - > - Name - - setDescription(e.target.value)} - > - Description - -
-
- ) -} - -export default observer(EditModal) diff --git a/apps/studio/components/ui/Charts/EmptyState.tsx b/apps/studio/components/ui/Charts/EmptyState.tsx index b7f791c7bd8..39888e06666 100644 --- a/apps/studio/components/ui/Charts/EmptyState.tsx +++ b/apps/studio/components/ui/Charts/EmptyState.tsx @@ -1,4 +1,4 @@ -import { IconBarChart2 } from 'ui' +import { BarChart2 } from 'lucide-react' interface EmptyStateProps { title?: string @@ -17,7 +17,7 @@ const EmptyState = ({ border-dashed border-control py-4 text-center " > - +

{title}

{message}

diff --git a/apps/studio/components/ui/Charts/NoDataPlaceholder.tsx b/apps/studio/components/ui/Charts/NoDataPlaceholder.tsx index ec8599b9907..84c0d6c23bd 100644 --- a/apps/studio/components/ui/Charts/NoDataPlaceholder.tsx +++ b/apps/studio/components/ui/Charts/NoDataPlaceholder.tsx @@ -1,4 +1,4 @@ -import { IconBarChart2 } from 'ui' +import { BarChart2 } from 'lucide-react' import { useChartSize } from './Charts.utils' interface Props { @@ -23,7 +23,7 @@ const NoDataPlaceholder: React.FC = ({ // extra 20 px for the x ticks style={{ minHeight: minHeight + 20 }} > - +

{title}

{message &&

{message}

} diff --git a/apps/studio/data/analytics/constants.ts b/apps/studio/data/analytics/constants.ts index 556dc996957..225ac288287 100644 --- a/apps/studio/data/analytics/constants.ts +++ b/apps/studio/data/analytics/constants.ts @@ -15,3 +15,5 @@ export interface AnalyticsData { yAxisLimit: number hasNoData?: boolean } + +export type AnalyticsInterval = '1m' | '5m' | '10m' | '30m' | '1h' | '1d' diff --git a/apps/studio/data/analytics/infra-monitoring-query.ts b/apps/studio/data/analytics/infra-monitoring-query.ts index ce73629d8b4..1c3362c5aef 100644 --- a/apps/studio/data/analytics/infra-monitoring-query.ts +++ b/apps/studio/data/analytics/infra-monitoring-query.ts @@ -1,28 +1,38 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query' import dayjs from 'dayjs' -import { get } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' -import type { AnalyticsData } from './constants' + +import { get, handleError } from 'data/fetchers' +import type { AnalyticsData, AnalyticsInterval } from './constants' import { analyticsKeys } from './keys' +export type InfraMonitoringAttribute = + | 'max_cpu_usage' + | 'avg_cpu_usage' + | 'disk_io_budget' + | 'ram_usage' + | 'disk_io_consumption' + | 'swap_usage' + export type InfraMonitoringVariables = { projectRef?: string - attribute: - | 'max_cpu_usage' - | 'avg_cpu_usage' - | 'disk_io_budget' - | 'ram_usage' - | 'disk_io_consumption' - | 'swap_usage' + attribute: InfraMonitoringAttribute startDate?: string endDate?: string - interval?: '1m' | '5m' | '10m' | '30m' | '1h' | '1d' + interval?: AnalyticsInterval dateFormat?: string + databaseIdentifier?: string modifier?: (x: number) => number } export async function getInfraMonitoring( - { projectRef, attribute, startDate, endDate, interval = '1d' }: InfraMonitoringVariables, + { + projectRef, + attribute, + startDate, + endDate, + interval = '1d', + databaseIdentifier, + }: InfraMonitoringVariables, signal?: AbortSignal ) { if (!projectRef) throw new Error('Project ref is required') @@ -30,15 +40,22 @@ export async function getInfraMonitoring( if (!startDate) throw new Error('Start date is required') if (!endDate) throw new Error('End date is required') - const data = await get( - `${API_URL}/projects/${projectRef}/infra-monitoring?attribute=${attribute}&startDate=${encodeURIComponent( - startDate - )}&endDate=${encodeURIComponent(endDate)}&interval=${interval}`, - { signal } - ) + const { data, error } = await get('/platform/projects/{ref}/infra-monitoring', { + params: { + path: { ref: projectRef }, + query: { + attribute, + startDate, + endDate, + interval, + databaseIdentifier, + }, + }, + signal, + }) - if (data.error) throw data.error - return data as AnalyticsData + if (error) throw handleError(error) + return data as unknown as AnalyticsData } export type InfraMonitoringData = Awaited> @@ -52,6 +69,7 @@ export const useInfraMonitoringQuery = ( endDate, interval = '1d', dateFormat = 'DD MMM', + databaseIdentifier, modifier, }: InfraMonitoringVariables, { @@ -60,9 +78,18 @@ export const useInfraMonitoringQuery = ( }: UseQueryOptions = {} ) => useQuery( - analyticsKeys.infraMonitoring(projectRef, { attribute, startDate, endDate, interval }), + analyticsKeys.infraMonitoring(projectRef, { + attribute, + startDate, + endDate, + interval, + databaseIdentifier, + }), ({ signal }) => - getInfraMonitoring({ projectRef, attribute, startDate, endDate, interval }, signal), + getInfraMonitoring( + { projectRef, attribute, startDate, endDate, interval, databaseIdentifier }, + signal + ), { enabled: enabled && diff --git a/apps/studio/data/analytics/keys.ts b/apps/studio/data/analytics/keys.ts index 9f1cee14020..a40481331fe 100644 --- a/apps/studio/data/analytics/keys.ts +++ b/apps/studio/data/analytics/keys.ts @@ -115,18 +115,20 @@ export const analyticsKeys = { startDate, endDate, interval, + databaseIdentifier, }: { attribute?: string startDate?: string endDate?: string interval?: string + databaseIdentifier?: string } ) => [ 'projects', projectRef, 'infra-monitoring', - { attribute, startDate, endDate, interval }, + { attribute, startDate, endDate, interval, databaseIdentifier }, ] as const, usageApiCounts: (projectRef: string | undefined, interval: string | undefined) => ['projects', projectRef, 'usage.api-counts', interval] as const, diff --git a/apps/studio/data/analytics/project-daily-stats-query.ts b/apps/studio/data/analytics/project-daily-stats-query.ts new file mode 100644 index 00000000000..593ff9225e2 --- /dev/null +++ b/apps/studio/data/analytics/project-daily-stats-query.ts @@ -0,0 +1,119 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import dayjs from 'dayjs' + +import { get, handleError } from 'data/fetchers' +import type { AnalyticsData } from './constants' +import { analyticsKeys } from './keys' + +export type ProjectDailyStatsAttribute = + | 'max_cpu_usage' + | 'avg_cpu_usage' + | 'disk_io_budget' + | 'ram_usage' + | 'disk_io_consumption' + | 'swap_usage' + +export type ProjectDailyStatsInterval = '1m' | '5m' | '10m' | '30m' | '1h' | '1d' + +export type ProjectDailyStatsVariables = { + projectRef?: string + attribute: ProjectDailyStatsAttribute + startDate?: string + endDate?: string + interval?: ProjectDailyStatsInterval + dateFormat?: string + databaseIdentifier?: string + modifier?: (x: number) => number +} + +export async function getProjectDailyStats( + { + projectRef, + attribute, + startDate, + endDate, + interval = '1d', + databaseIdentifier, + }: ProjectDailyStatsVariables, + signal?: AbortSignal +) { + if (!projectRef) throw new Error('Project ref is required') + if (!attribute) throw new Error('Attribute is required') + if (!startDate) throw new Error('Start date is required') + if (!endDate) throw new Error('End date is required') + + const { data, error } = await get('/platform/projects/{ref}/daily-stats', { + params: { + path: { ref: projectRef }, + query: { + attribute, + startDate, + endDate, + interval, + // [Joshen] TODO: Once API support is ready + // databaseIdentifier, + }, + }, + signal, + }) + + if (error) throw handleError(error) + return data as unknown as AnalyticsData +} + +export type ProjectDailyStatsData = Awaited> +export type ProjectDailyStatsError = unknown + +export const useProjectDailyStatsQuery = ( + { + projectRef, + attribute, + startDate, + endDate, + interval = '1d', + dateFormat = 'DD MMM', + databaseIdentifier, + modifier, + }: ProjectDailyStatsVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => + useQuery( + analyticsKeys.infraMonitoring(projectRef, { + attribute, + startDate, + endDate, + interval, + databaseIdentifier, + }), + ({ signal }) => + getProjectDailyStats( + { projectRef, attribute, startDate, endDate, interval, databaseIdentifier }, + signal + ), + { + enabled: + enabled && + typeof projectRef !== 'undefined' && + typeof attribute !== 'undefined' && + typeof startDate !== 'undefined' && + typeof endDate !== 'undefined', + select(data) { + return { + ...data, + data: data.data.map((x) => { + return { + ...x, + [attribute]: + modifier !== undefined ? modifier(Number(x[attribute])) : Number(x[attribute]), + periodStartFormatted: dayjs(x.period_start).format(dateFormat), + } + }), + } as TData + }, + staleTime: 1000 * 60, // default good for a minute + ...options, + } + ) diff --git a/apps/studio/data/content/content-query.ts b/apps/studio/data/content/content-query.ts index 578b7bf674e..d5129ace1c5 100644 --- a/apps/studio/data/content/content-query.ts +++ b/apps/studio/data/content/content-query.ts @@ -72,15 +72,3 @@ export const useContentQuery = ( ({ signal }) => getContent(projectRef, signal), { enabled: enabled && typeof projectRef !== 'undefined', ...options } ) - -export const useContentPrefetch = (projectRef: string | undefined) => { - const client = useQueryClient() - - return useCallback(() => { - if (projectRef) { - client.prefetchQuery(contentKeys.list(projectRef), ({ signal }) => - getContent(projectRef, signal) - ) - } - }, [projectRef]) -} diff --git a/apps/studio/data/content/content-update-mutation.ts b/apps/studio/data/content/content-update-mutation.ts new file mode 100644 index 00000000000..c3faf2908c7 --- /dev/null +++ b/apps/studio/data/content/content-update-mutation.ts @@ -0,0 +1,58 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import toast from 'react-hot-toast' + +import { patch } from 'data/fetchers' +import type { ResponseError } from 'types' +import { contentKeys } from './keys' + +type UpdateContentVariables = { projectRef: string; id: string; type: 'report'; content: any } + +export async function updateContent( + { projectRef, id, type, content }: UpdateContentVariables, + signal?: AbortSignal +) { + const { data, error } = await patch('/platform/projects/{ref}/content', { + params: { + // @ts-ignore API codegen issue + path: { ref: projectRef }, + query: { id }, + }, + body: { id, type, content }, + signal, + }) + + if (error) throw error + return data +} + +type UpdateContentData = Awaited> + +export const useContentUpdateMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation( + (args) => updateContent(args), + { + async onSuccess(data, variables, context) { + const { projectRef } = variables + await queryClient.invalidateQueries(contentKeys.list(projectRef)) + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to update content: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) +} diff --git a/apps/studio/hooks/analytics/useDbQuery.tsx b/apps/studio/hooks/analytics/useDbQuery.tsx index 583f3a09249..0bf949a1d73 100644 --- a/apps/studio/hooks/analytics/useDbQuery.tsx +++ b/apps/studio/hooks/analytics/useDbQuery.tsx @@ -36,8 +36,8 @@ const useDbQuery = ({ orderBy?: string }): DbQueryHook => { const { project } = useProjectContext() - const state = useDatabaseSelectorStateSnapshot() + const { data: databases } = useReadReplicasQuery({ projectRef: project?.ref }) const connectionString = project?.is_read_replicas_enabled ? (databases || []).find((db) => db.identifier === state.selectedDatabaseId)?.connectionString diff --git a/apps/studio/hooks/analytics/useLogsQuery.tsx b/apps/studio/hooks/analytics/useLogsQuery.tsx index 2460f8d2c2e..eb18f77bfa3 100644 --- a/apps/studio/hooks/analytics/useLogsQuery.tsx +++ b/apps/studio/hooks/analytics/useLogsQuery.tsx @@ -1,17 +1,21 @@ import { useQuery } from '@tanstack/react-query' +import { Dispatch, SetStateAction, useState } from 'react' + import { EXPLORER_DATEPICKER_HELPERS, genQueryParams, getDefaultHelper, } from 'components/interfaces/Settings/Logs' -import { Dispatch, SetStateAction, useState } from 'react' import type { - LogsEndpointParams, - Logs, LogData, + Logs, + LogsEndpointParams, } from 'components/interfaces/Settings/Logs/Logs.types' -import { API_URL } from 'lib/constants' +import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' import { get, isResponseOk } from 'lib/common/fetch' +import { API_URL } from 'lib/constants' +import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' + export interface LogsQueryHook { params: LogsEndpointParams isLoading: boolean @@ -27,6 +31,10 @@ const useLogsQuery = ( projectRef: string, initialParams: Partial = {} ): LogsQueryHook => { + // [Joshen] TODO once API is unblocked + const state = useDatabaseSelectorStateSnapshot() + const { data: databases } = useReadReplicasQuery({ projectRef }) + const defaultHelper = getDefaultHelper(EXPLORER_DATEPICKER_HELPERS) const [params, setParams] = useState({ sql: initialParams?.sql || '', diff --git a/apps/studio/pages/project/[ref]/reports/[id].tsx b/apps/studio/pages/project/[ref]/reports/[id].tsx index fd048568f65..9ac7c0cccc5 100644 --- a/apps/studio/pages/project/[ref]/reports/[id].tsx +++ b/apps/studio/pages/project/[ref]/reports/[id].tsx @@ -1,15 +1,11 @@ import Reports from 'components/interfaces/Reports/Reports' import { ReportsLayout } from 'components/layouts' -import EditReportModal from 'components/to-be-cleaned/Reports/EditReportModal' import type { NextPageWithLayout } from 'types' const PageLayout: NextPageWithLayout = () => ( - <> -
- -
- - +
+ +
) PageLayout.getLayout = (page) => {page} diff --git a/apps/studio/pages/project/[ref]/reports/api-overview.tsx b/apps/studio/pages/project/[ref]/reports/api-overview.tsx index 6774133f4ea..508f71c38d0 100644 --- a/apps/studio/pages/project/[ref]/reports/api-overview.tsx +++ b/apps/studio/pages/project/[ref]/reports/api-overview.tsx @@ -25,6 +25,8 @@ import { ReportsLayout } from 'components/layouts' import ShimmerLine from 'components/ui/ShimmerLine' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useSelectedOrganization } from 'hooks' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' export const ApiReport: NextPageWithLayout = () => { const report = useApiReport() @@ -108,7 +110,12 @@ export const ApiReport: NextPageWithLayout = () => { // hook to fetch data const useApiReport = () => { + const { project } = useProjectContext() const { ref: projectRef } = useParams() + const state = useDatabaseSelectorStateSnapshot() + + // [Joshen] TODO: Once API support is out + const showReadReplicasUI = false // project?.is_read_replicas_enabled const queryHooks = queriesFactory( PRESET_CONFIG.api.queries, diff --git a/apps/studio/pages/project/[ref]/reports/database.tsx b/apps/studio/pages/project/[ref]/reports/database.tsx index e55d4e0983b..0f9f6b4f70b 100644 --- a/apps/studio/pages/project/[ref]/reports/database.tsx +++ b/apps/studio/pages/project/[ref]/reports/database.tsx @@ -1,14 +1,9 @@ import { useParams } from 'common/hooks' import dayjs from 'dayjs' +import { ArrowRight } from 'lucide-react' import Link from 'next/link' import { useState } from 'react' -import { - AlertDescription_Shadcn_, - Alert_Shadcn_, - Button, - IconArrowRight, - IconExternalLink, -} from 'ui' +import { AlertDescription_Shadcn_, Alert_Shadcn_, Button, IconExternalLink } from 'ui' import ReportWidget from 'components/interfaces/Reports/ReportWidget' import { PRESET_CONFIG } from 'components/interfaces/Reports/Reports.constants' @@ -18,6 +13,7 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectConte import ChartHandler from 'components/to-be-cleaned/Charts/ChartHandler' import DateRangePicker from 'components/to-be-cleaned/DateRangePicker' import Table from 'components/to-be-cleaned/Table' +import DatabaseSelector from 'components/ui/DatabaseSelector' import Panel from 'components/ui/Panel' import { useDatabaseSizeQuery } from 'data/database/database-size-query' import type { DbQueryHook } from 'hooks/analytics/useDbQuery' @@ -45,19 +41,27 @@ const DatabaseUsage = () => { const { project } = useProjectContext() const [dateRange, setDateRange] = useState(undefined) + const showReadReplicasUI = project?.is_read_replicas_enabled + + const report = useDatabaseReport() const { data } = useDatabaseSizeQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const databaseSizeBytes = data?.result[0].db_size ?? 0 - const report = useDatabaseReport() - return ( <>
- Database health}> + +

Database health

+ {showReadReplicasUI && } +
+ } + >
{ onChange={setDateRange} /> {dateRange && ( -
+

{dayjs(dateRange.period_start.date).format('MMMM D, hh:mma')}

- +

{dayjs(dateRange.period_end.date).format('MMMM D, hh:mma')} diff --git a/apps/studio/pages/project/[ref]/reports/index.tsx b/apps/studio/pages/project/[ref]/reports/index.tsx index 3972cdc5b62..72d4a30cff9 100644 --- a/apps/studio/pages/project/[ref]/reports/index.tsx +++ b/apps/studio/pages/project/[ref]/reports/index.tsx @@ -1,50 +1,41 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useRouter } from 'next/router' -import { useEffect, useState } from 'react' +import { useState } from 'react' -import { useParams } from 'common/hooks' +import { useParams } from 'common' import { CreateReportModal } from 'components/interfaces/Reports/Reports.CreateReportModal' import { ReportsLayout } from 'components/layouts' import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' import { Loading } from 'components/ui/Loading' +import { useContentQuery } from 'data/content/content-query' import { useCheckPermissions } from 'hooks' import { useProfile } from 'lib/profile' -import { useProjectContentStore } from 'stores/projectContentStore' import type { NextPageWithLayout } from 'types' export const UserReportPage: NextPageWithLayout = () => { - const [loading, setLoading] = useState(true) - const router = useRouter() const { ref } = useParams() const { profile } = useProfile() const [showCreateReportModal, setShowCreateReportModal] = useState(false) - const contentStore = useProjectContentStore(ref) + const { isLoading } = useContentQuery(ref, { + onSuccess: (data) => { + const reports = data.content + .filter((x) => x.type === 'report') + .sort((a, b) => a.name.localeCompare(b.name)) + if (reports.length > 1) router.push(`/project/${ref}/reports/${reports[0].id}`) + }, + }) + const canCreateReport = useCheckPermissions(PermissionAction.CREATE, 'user_content', { resource: { type: 'report', owner_id: profile?.id }, subject: { id: profile?.id }, }) - async function loadReports() { - await contentStore.load() - const reports = contentStore.reports() - - if (reports.length >= 1) { - router.push(`/project/${ref}/reports/${reports[0].id}`) - } else { - setLoading(false) - } - } - - useEffect(() => { - loadReports() - }, [ref]) - return (

- {loading ? ( + {isLoading ? ( ) : ( <> @@ -67,12 +58,8 @@ export const UserReportPage: NextPageWithLayout = () => { { - setShowCreateReportModal(false) - }} - afterSubmit={() => { - setShowCreateReportModal(false) - }} + onCancel={() => setShowCreateReportModal(false)} + afterSubmit={() => setShowCreateReportModal(false)} /> )} diff --git a/apps/studio/stores/projectContentStore.ts b/apps/studio/stores/projectContentStore.ts deleted file mode 100644 index cac7ff84f35..00000000000 --- a/apps/studio/stores/projectContentStore.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { makeObservable, observable, computed } from 'mobx' -import { get, patch, post, delete_ } from 'lib/common/fetch' -import { keyBy } from 'lodash' -import type { UserContent } from 'types' -import { API_URL } from 'lib/constants' - -/** - * Singleton - * Usage: - * import { useProjectContentStore } from 'stores/userContent/projectContentStore' - * const contentStore = useProjectContentStore(projectRef) - * contentStore.load() // inside usEffect() - * @deprecated use content store on RootStore instead. - */ -let store: any = null -let projectRef: any = null -export function useProjectContentStore(ref: any) { - if (store === null || ref !== projectRef) { - // set the current ref - projectRef = ref - const baseUrl = `${API_URL}/projects/${ref}/content` - store = new ProjectContent(baseUrl) - } - return store -} - -/** - * Local type declarations - */ -interface UserContentMap { - [key: string]: UserContent -} - -/** - * MobX Store - * @deprecated - */ -export default class ProjectContent { - STATES = { - INITIAL: 'initial', - LOADING: 'loading', - ERROR: 'error', - LOADED: 'loaded', - } - rootStore = null - url = '' - state = this.STATES.INITIAL - data: UserContentMap = {} - error = null - - /** - * @param connectionString And encrypted database connection string to pass to the API - */ - constructor(dataUrl: any) { - this.url = dataUrl - makeObservable(this, { - state: observable, - data: observable, - list: observable, - hasError: computed, - isLoading: computed, - }) - } - - async fetchData() { - const headers = { - 'Content-Type': 'application/json', - } - const res = await get(this.url, { headers }) - if (res.error) { - throw res.error - } - this.data = res.data.length > 0 ? keyBy(res.data, 'id') : res.data - return res - } - - async load() { - let { LOADING, ERROR, LOADED } = this.STATES - try { - this.error = null - this.state = LOADING - await this.fetchData() - this.state = LOADED - return this.data - } catch (e: any) { - console.error('e.message', e.message) - this.error = e - this.state = ERROR - } - } - - get hasError() { - return this.state === this.STATES.ERROR - } - - get isLoading() { - return this.state === this.STATES.INITIAL || this.state === this.STATES.LOADING - } - - list(filter: any) { - let arr = Object.values(this.data) - if (!!filter) { - return arr.filter(filter).sort((a, b) => a.name.localeCompare(b.name)) - } else { - return arr.sort((a, b) => a.name.localeCompare(b.name)) - } - } - - reports(filter: any) { - let arr = Object.values(this.data) - if (!!filter) { - return arr - .filter((x) => x.type == 'report') - .filter(filter) - .sort((a, b) => a.name.localeCompare(b.name)) - } else { - return arr.filter((x) => x.type == 'report').sort((a, b) => a.name.localeCompare(b.name)) - } - } - - sqlSnippets(filter: any) { - let arr = Object.values(this.data) - if (!!filter) { - const arr_filtered = arr - .filter((x) => x.type == 'sql') - .filter(filter) - .sort((a, b) => a.name.localeCompare(b.name)) - - return arr_filtered - } else { - const arr_filtered = arr - .filter((x) => x.type == 'sql') - .sort((a, b) => a.name.localeCompare(b.name)) - - return arr_filtered - } - } - - byId(id: any) { - return this.data[id] - } - - async create(payload: any) { - try { - const headers = { - 'Content-Type': 'application/json', - } - const created = await post(this.url, payload, { headers }) - if (created.error) throw created.error - this.data[created['id']] = created - return { data: created, error: null } - } catch (error) { - return { data: null, error } - } - } - - async update(id: any, updates: any, type: any) { - try { - const headers = { - 'Content-Type': 'application/json', - } - let payload = { - ...updates, - type, - id, - } - const url = `${this.url}?id=${id}` - const updated = await patch(url, payload, { headers }) - if (updated.error) throw updated.error - this.data[id] = updated - return { data: updated, error: null } - } catch (error) { - return { data: null, error } - } - } - - async updateSql(id: any, updates: any) { - return this.update(id, updates, 'sql') - } - - async updateReport(id: any, updates: any) { - return this.update(id, updates, 'report') - } - - async del(id: any) { - try { - const headers = { - 'Content-Type': 'application/json', - } - const url = `${this.url}?id=${id}` - const deleted = await delete_(url, {}, { headers }) - if (deleted.error) throw deleted.error - delete this.data[id] - return { data: true, error: null } - } catch (error) { - return { data: false, error } - } - } -} diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts index 4181f308eb8..c88eacdb5b9 100644 --- a/packages/api-types/types/api.d.ts +++ b/packages/api-types/types/api.d.ts @@ -796,11 +796,11 @@ export interface paths { post: operations['TelemetryPageController_sendServerPage'] } '/platform/telemetry/activity': { - /** Sends mixpanel server activity */ + /** Sends server activity */ post: operations['TelemetryActivityController_sendServerActivity'] } '/platform/telemetry/pageview': { - /** Send mixpanel page event */ + /** Send pageview event */ post: operations['TelemetryPageviewController_sendServerPageViewed'] } '/platform/vercel/token': { @@ -11404,7 +11404,7 @@ export interface operations { } } } - /** Sends mixpanel server activity */ + /** Sends server activity */ TelemetryActivityController_sendServerActivity: { requestBody: { content: { @@ -11415,13 +11415,13 @@ export interface operations { 201: { content: never } - /** @description Failed to send mixpanel server activity */ + /** @description Failed to send server activity */ 500: { content: never } } } - /** Send mixpanel page event */ + /** Send pageview event */ TelemetryPageviewController_sendServerPageViewed: { requestBody: { content: { @@ -11432,7 +11432,7 @@ export interface operations { 201: { content: never } - /** @description Failed to send mixpanel page event */ + /** @description Failed to send pageview event */ 500: { content: never }