diff --git a/apps/studio/components/interfaces/HomeNew/ActivityStats.tsx b/apps/studio/components/interfaces/HomeNew/ActivityStats.tsx index 22464a7a5fbc3..4612f6c4b7606 100644 --- a/apps/studio/components/interfaces/HomeNew/ActivityStats.tsx +++ b/apps/studio/components/interfaces/HomeNew/ActivityStats.tsx @@ -1,9 +1,9 @@ import dayjs from 'dayjs' -import { GitBranch } from 'lucide-react' -import Link from 'next/link' +import { Archive, Database, GitBranch } from 'lucide-react' import { useMemo } from 'react' import { useParams } from 'common' +import { SingleStat } from 'components/ui/SingleStat' import { useBranchesQuery } from 'data/branches/branches-query' import { useBackupsQuery } from 'data/database/backups-query' import { useMigrationsQuery } from 'data/database/migrations-query' @@ -55,17 +55,15 @@ export const ActivityStats = () => { return (
-
-
-

Status

- -
+
+ - -

Last migration

- -
- {isLoadingMigrations ? ( + } + label={Last migration} + value={ + isLoadingMigrations ? ( ) : latestMigration ? ( { /> ) : (

No migrations

- )} -
- - - -

Last backup

+ ) + } + /> -
- {isLoadingBackups ? ( + } + label={Last backup} + value={ + isLoadingBackups ? ( ) : backupsData?.pitr_enabled ? (

PITR enabled

@@ -95,25 +94,26 @@ export const ActivityStats = () => { /> ) : (

No backups

- )} -
- - - -

- {isDefaultProject ? 'Recent branch' : 'Branch Created'} -

+ ) + } + /> -
- {isLoadingBranches ? ( + } + label={{isDefaultProject ? 'Recent branch' : 'Branch Created'}} + value={ + isLoadingBranches ? ( ) : isDefaultProject ? ( -
- -

- {latestNonDefaultBranch?.name ?? 'No branches'} -

-
+

+ {latestNonDefaultBranch?.name ?? 'No branches'} +

) : currentBranch?.created_at ? ( { /> ) : (

Unknown

- )} -
- + ) + } + />
) diff --git a/apps/studio/components/interfaces/HomeNew/ServiceStatus.tsx b/apps/studio/components/interfaces/HomeNew/ServiceStatus.tsx index 768c0de480a36..0b1539a933e11 100644 --- a/apps/studio/components/interfaces/HomeNew/ServiceStatus.tsx +++ b/apps/studio/components/interfaces/HomeNew/ServiceStatus.tsx @@ -1,9 +1,9 @@ -import { AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, Loader2 } from 'lucide-react' +import { AlertTriangle, CheckCircle2, ChevronRight, Loader2 } from 'lucide-react' import Link from 'next/link' -import { useState } from 'react' import { PopoverSeparator } from '@ui/components/shadcn/ui/popover' import { useParams } from 'common' +import { SingleStat } from 'components/ui/SingleStat' import { useBranchesQuery } from 'data/branches/branches-query' import { useEdgeFunctionServiceStatusQuery } from 'data/service-status/edge-functions-status-query' import { @@ -12,14 +12,7 @@ import { } from 'data/service-status/service-status-query' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { - Button, - InfoIcon, - PopoverContent_Shadcn_, - PopoverTrigger_Shadcn_, - Popover_Shadcn_, - cn, -} from 'ui' +import { InfoIcon, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, Popover_Shadcn_, cn } from 'ui' /** * [Joshen] JFYI before we go live with this, we need to revisit the migrations section @@ -85,7 +78,6 @@ const StatusIcon = ({ export const ServiceStatus = () => { const { ref } = useParams() const { data: project } = useSelectedProjectQuery() - const [open, setOpen] = useState(false) const { projectAuthAll: authEnabled, @@ -262,27 +254,29 @@ export const ServiceStatus = () => { : 'Healthy' return ( - - - + label={Health} + value={{overallStatusLabel}} + /> {services.map((service) => ( diff --git a/apps/studio/components/interfaces/Reports/ReportWidget.tsx b/apps/studio/components/interfaces/Reports/ReportWidget.tsx index 01f96d9939ca4..4d3c6857e7bc3 100644 --- a/apps/studio/components/interfaces/Reports/ReportWidget.tsx +++ b/apps/studio/components/interfaces/Reports/ReportWidget.tsx @@ -74,8 +74,8 @@ const ReportWidget = (props: ReportWidgetProps) => { query.content = props.resolvedSql } else { query.q = props.params?.sql - query.its = props.params!.iso_timestamp_start - query.ite = props.params!.iso_timestamp_end + query.its = props.params?.iso_timestamp_start || '' + query.ite = props.params?.iso_timestamp_end || '' } router.push({ pathname, query }) diff --git a/apps/studio/components/interfaces/Reports/SharedAPIReport/SharedAPIReport.constants.ts b/apps/studio/components/interfaces/Reports/SharedAPIReport/SharedAPIReport.constants.ts index e9d97dbea325f..768925edf862e 100644 --- a/apps/studio/components/interfaces/Reports/SharedAPIReport/SharedAPIReport.constants.ts +++ b/apps/studio/components/interfaces/Reports/SharedAPIReport/SharedAPIReport.constants.ts @@ -219,8 +219,15 @@ const fetchLogs = async ({ const DEFAULT_KEYS = ['shared-api-report'] +export type SharedAPIReportFilterBy = + | 'auth' + | 'realtime' + | 'storage' + | 'graphql' + | 'functions' + | 'postgrest' type SharedAPIReportParams = { - filterBy: 'auth' | 'realtime' | 'storage' | 'graphql' | 'functions' | 'postgrest' + filterBy: SharedAPIReportFilterBy start: string end: string projectRef: string @@ -263,7 +270,16 @@ export const useSharedAPIReport = ({ const queries = useQueries({ queries: Object.entries(SHARED_API_REPORT_SQL).map(([key, value]) => ({ - queryKey: [...DEFAULT_KEYS, key, filterByMapSource[filterBy], filters, start, end, ref], + queryKey: [ + ...DEFAULT_KEYS, + filterBy, + key, + filterByMapSource[filterBy], + filters, + start, + end, + ref, + ], enabled: enabled && !!ref && !!filterBy, queryFn: () => fetchLogs({ @@ -324,6 +340,22 @@ export const useSharedAPIReport = ({ const isLoadingData = Object.values(isLoading).some(Boolean) + const SQLMap: Record = { + totalRequests: SHARED_API_REPORT_SQL.totalRequests.sql(allFilters, filterByMapSource[filterBy]), + topRoutes: SHARED_API_REPORT_SQL.topRoutes.sql(allFilters, filterByMapSource[filterBy]), + errorCounts: SHARED_API_REPORT_SQL.errorCounts.sql(allFilters, filterByMapSource[filterBy]), + topErrorRoutes: SHARED_API_REPORT_SQL.topErrorRoutes.sql( + allFilters, + filterByMapSource[filterBy] + ), + responseSpeed: SHARED_API_REPORT_SQL.responseSpeed.sql(allFilters, filterByMapSource[filterBy]), + topSlowRoutes: SHARED_API_REPORT_SQL.topSlowRoutes.sql(allFilters, filterByMapSource[filterBy]), + networkTraffic: SHARED_API_REPORT_SQL.networkTraffic.sql( + allFilters, + filterByMapSource[filterBy] + ), + } + return { data, error, @@ -334,5 +366,9 @@ export const useSharedAPIReport = ({ filters, addFilter, removeFilters, + /** + * The SQL queries used to fetch each metric + */ + sql: SQLMap, } } diff --git a/apps/studio/components/interfaces/Reports/SharedAPIReport/SharedAPIReport.tsx b/apps/studio/components/interfaces/Reports/SharedAPIReport/SharedAPIReport.tsx index 8a60490cf8ab9..3adccf664820a 100644 --- a/apps/studio/components/interfaces/Reports/SharedAPIReport/SharedAPIReport.tsx +++ b/apps/studio/components/interfaces/Reports/SharedAPIReport/SharedAPIReport.tsx @@ -14,6 +14,7 @@ type SharedAPIReportWidgetsProps = { isLoading: any isRefetching: boolean hiddenReports?: SharedAPIReportKey[] + sql: Record } export function SharedAPIReport({ @@ -22,6 +23,7 @@ export function SharedAPIReport({ isLoading, isRefetching, hiddenReports = [], + sql, }: SharedAPIReportWidgetsProps) { return (
@@ -34,6 +36,10 @@ export function SharedAPIReport({ renderer={TotalRequestsChartRenderer} append={TopApiRoutesRenderer} appendProps={{ data: data.topRoutes }} + queryType="logs" + params={{ + sql: sql.totalRequests, + }} /> )} {!hiddenReports.includes('errorCounts') && ( @@ -48,6 +54,10 @@ export function SharedAPIReport({ data: data.topErrorRoutes || [], }} append={TopApiRoutesRenderer} + queryType="logs" + params={{ + sql: sql.errorCounts, + }} /> )} {!hiddenReports.includes('responseSpeed') && ( @@ -60,6 +70,10 @@ export function SharedAPIReport({ renderer={ResponseSpeedChartRenderer} appendProps={{ data: data.topSlowRoutes || [] }} append={TopApiRoutesRenderer} + queryType="logs" + params={{ + sql: sql.responseSpeed, + }} /> )} {!hiddenReports.includes('networkTraffic') && ( @@ -70,6 +84,10 @@ export function SharedAPIReport({ tooltip="Ingress and egress of requests and responses respectively" data={data.networkTraffic || []} renderer={NetworkTrafficRenderer} + queryType="logs" + params={{ + sql: sql.networkTraffic, + }} /> )}
diff --git a/apps/studio/components/interfaces/Reports/renderers/ApiRenderers.tsx b/apps/studio/components/interfaces/Reports/renderers/ApiRenderers.tsx index 397b383d98e21..f6a0884353aa9 100644 --- a/apps/studio/components/interfaces/Reports/renderers/ApiRenderers.tsx +++ b/apps/studio/components/interfaces/Reports/renderers/ApiRenderers.tsx @@ -220,22 +220,6 @@ export const TopApiRoutesRenderer = ( > {!showMore ? 'Show more' : 'Show less'} -
diff --git a/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx b/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx index 1cdd6e97b2496..39dcd1c83d1c7 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx @@ -30,6 +30,54 @@ import { maybeShowUpgradePrompt } from './Logs.utils' import { PreviewFilterPanelWithUniversal } from './PreviewFilterPanelWithUniversal' import UpgradePrompt from './UpgradePrompt' +/** + * Calculates the appropriate time range for bar click filtering based on the current time range duration. + * + * @param currentRangeStart - The start timestamp of the current time range + * @param currentRangeEnd - The end timestamp of the current time range + * @param clickedTimestamp - The timestamp of the clicked bar + * @returns Object containing the new start and end timestamps for filtering + */ +export const calculateBarClickTimeRange = ( + currentRangeStart: string, + currentRangeEnd: string | undefined, + clickedTimestamp: string +) => { + const datumTimestamp = dayjs(clickedTimestamp).toISOString() + + // Calculate the current time range duration in hours + // If currentRangeEnd is not provided, use current time as the end + const endTime = currentRangeEnd ? dayjs(currentRangeEnd) : dayjs() + const currentRangeDuration = endTime.diff(dayjs(currentRangeStart), 'hour', true) + + let rangeOffset: number + let rangeUnit: dayjs.ManipulateType + + if (currentRangeDuration >= 12) { + // For ranges >= 12h, use 1h range + rangeOffset = 0.5 + rangeUnit = 'hour' + } else if (currentRangeDuration >= 1) { + // For ranges >= 1h but < 12h, use 5min range + rangeOffset = 2.5 + rangeUnit = 'minute' + } else if (currentRangeDuration >= 1 / 30) { + // 2 minutes = 1/30 hour + // For ranges >= 2min but < 1h, use 2min range + rangeOffset = 1 + rangeUnit = 'minute' + } else { + // For ranges < 2min, use 15sec range + rangeOffset = 7.5 + rangeUnit = 'second' + } + + return { + start: dayjs(datumTimestamp).subtract(rangeOffset, rangeUnit).toISOString(), + end: dayjs(datumTimestamp).add(rangeOffset, rangeUnit).toISOString(), + } +} + /** * Acts as a container component for the entire log display * @@ -257,10 +305,11 @@ export const LogsPreviewer = ({ onBarClick={(datum) => { if (!datum?.timestamp) return - const datumTimestamp = dayjs(datum.timestamp).toISOString() - - const start = dayjs(datumTimestamp).subtract(1, 'minute').toISOString() - const end = dayjs(datumTimestamp).add(1, 'minute').toISOString() + const { start, end } = calculateBarClickTimeRange( + timestampStart, + timestampEnd, + datum.timestamp + ) handleSearch('event-chart-bar-click', { query: filters.search_query?.toString(), @@ -321,5 +370,3 @@ export const LogsPreviewer = ({ ) } - -export default LogsPreviewer diff --git a/apps/studio/components/ui/SingleStat.tsx b/apps/studio/components/ui/SingleStat.tsx new file mode 100644 index 0000000000000..e34ea09773cde --- /dev/null +++ b/apps/studio/components/ui/SingleStat.tsx @@ -0,0 +1,45 @@ +import Link from 'next/link' +import type { ReactNode } from 'react' + +type SingleStatProps = { + icon: ReactNode + label: ReactNode + value: ReactNode + className?: string + href?: string + onClick?: () => void +} + +export const SingleStat = ({ icon, label, value, className, href, onClick }: SingleStatProps) => { + const content = ( +
+
+ {icon} +
+
+
{label}
+
+ {value} +
+
+
+ ) + + if (href) { + return ( + + {content} + + ) + } + + if (onClick) { + return ( + + ) + } + + return content +} diff --git a/apps/studio/hooks/misc/useCheckPermissions.ts b/apps/studio/hooks/misc/useCheckPermissions.ts index 8b2106c3d58d6..96f3667c0523b 100644 --- a/apps/studio/hooks/misc/useCheckPermissions.ts +++ b/apps/studio/hooks/misc/useCheckPermissions.ts @@ -6,6 +6,7 @@ import { IS_PLATFORM } from 'lib/constants' import type { Permission } from 'types' import { useSelectedOrganizationQuery } from './useSelectedOrganization' import { useSelectedProjectQuery } from './useSelectedProject' +import { useMemo } from 'react' const toRegexpString = (actionOrResource: string) => `^${actionOrResource.replace('.', '\\.').replace('%', '.*')}$` @@ -156,33 +157,34 @@ export function useAsyncCheckPermissions( isSuccess: isPermissionsSuccess, } = useGetProjectPermissions(permissions, organizationSlug, projectRef, isLoggedIn) - if (!isLoggedIn) { - return { - isLoading: true, - isSuccess: false, - can: false, - } - } - if (!IS_PLATFORM) { - return { - isLoading: false, - isSuccess: true, - can: true, - } - } - - const can = doPermissionsCheck( + const can = useMemo(() => { + if (!IS_PLATFORM) return true + if (!isLoggedIn) return false + if (!isPermissionsSuccess || !allPermissions) return false + + return doPermissionsCheck( + allPermissions, + action, + resource, + data, + _organizationSlug, + _projectRef + ) + }, [ + isLoggedIn, + isPermissionsSuccess, allPermissions, action, resource, data, _organizationSlug, - _projectRef - ) + _projectRef, + ]) - return { - isLoading: isPermissionsLoading, - isSuccess: isPermissionsSuccess, - can, - } + // Derive loading/success consistently from the same branches + const isLoading = !IS_PLATFORM ? false : !isLoggedIn ? true : isPermissionsLoading + + const isSuccess = !IS_PLATFORM ? true : !isLoggedIn ? false : isPermissionsSuccess + + return { isLoading, isSuccess, can } } diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/invocations.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/invocations.tsx index 6925123da7276..cd89b526d141a 100644 --- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/invocations.tsx +++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/invocations.tsx @@ -1,5 +1,5 @@ import { useParams } from 'common' -import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer' +import { LogsPreviewer } from 'components/interfaces/Settings/Logs/LogsPreviewer' import DefaultLayout from 'components/layouts/DefaultLayout' import EdgeFunctionDetailsLayout from 'components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout' import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query' diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/logs.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/logs.tsx index ff82efab095a5..90264bf866b83 100644 --- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/logs.tsx +++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/logs.tsx @@ -1,5 +1,5 @@ import { useParams } from 'common' -import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer' +import { LogsPreviewer } from 'components/interfaces/Settings/Logs/LogsPreviewer' import DefaultLayout from 'components/layouts/DefaultLayout' import EdgeFunctionDetailsLayout from 'components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout' import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query' diff --git a/apps/studio/pages/project/[ref]/logs/auth-logs.tsx b/apps/studio/pages/project/[ref]/logs/auth-logs.tsx index cf03e3b2bd37f..6503270a7c186 100644 --- a/apps/studio/pages/project/[ref]/logs/auth-logs.tsx +++ b/apps/studio/pages/project/[ref]/logs/auth-logs.tsx @@ -1,6 +1,6 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer' +import { LogsPreviewer } from 'components/interfaces/Settings/Logs/LogsPreviewer' import DefaultLayout from 'components/layouts/DefaultLayout' import LogsLayout from 'components/layouts/LogsLayout/LogsLayout' import NoPermission from 'components/ui/NoPermission' diff --git a/apps/studio/pages/project/[ref]/logs/cron-logs.tsx b/apps/studio/pages/project/[ref]/logs/cron-logs.tsx index 44bb6f108d48e..58bccd410de11 100644 --- a/apps/studio/pages/project/[ref]/logs/cron-logs.tsx +++ b/apps/studio/pages/project/[ref]/logs/cron-logs.tsx @@ -2,7 +2,7 @@ import { useRouter } from 'next/router' -import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer' +import { LogsPreviewer } from 'components/interfaces/Settings/Logs/LogsPreviewer' import LogsLayout from 'components/layouts/LogsLayout/LogsLayout' import type { NextPageWithLayout } from 'types' import { LogsTableName } from 'components/interfaces/Settings/Logs/Logs.constants' diff --git a/apps/studio/pages/project/[ref]/logs/dedicated-pooler-logs.tsx b/apps/studio/pages/project/[ref]/logs/dedicated-pooler-logs.tsx index 7527bc88a3f64..62f26ace50351 100644 --- a/apps/studio/pages/project/[ref]/logs/dedicated-pooler-logs.tsx +++ b/apps/studio/pages/project/[ref]/logs/dedicated-pooler-logs.tsx @@ -1,6 +1,6 @@ import { useParams } from 'common' import { LogsTableName } from 'components/interfaces/Settings/Logs/Logs.constants' -import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer' +import { LogsPreviewer } from 'components/interfaces/Settings/Logs/LogsPreviewer' import DefaultLayout from 'components/layouts/DefaultLayout' import LogsLayout from 'components/layouts/LogsLayout/LogsLayout' import { Loading } from 'components/ui/Loading' diff --git a/apps/studio/pages/project/[ref]/logs/edge-functions-logs.tsx b/apps/studio/pages/project/[ref]/logs/edge-functions-logs.tsx index 213544a74ded5..34ca6a8bf9057 100644 --- a/apps/studio/pages/project/[ref]/logs/edge-functions-logs.tsx +++ b/apps/studio/pages/project/[ref]/logs/edge-functions-logs.tsx @@ -1,7 +1,7 @@ import { useRouter } from 'next/router' import { LogsTableName } from 'components/interfaces/Settings/Logs/Logs.constants' -import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer' +import { LogsPreviewer } from 'components/interfaces/Settings/Logs/LogsPreviewer' import LogsLayout from 'components/layouts/LogsLayout/LogsLayout' import type { NextPageWithLayout } from 'types' import DefaultLayout from 'components/layouts/DefaultLayout' diff --git a/apps/studio/pages/project/[ref]/logs/edge-logs.tsx b/apps/studio/pages/project/[ref]/logs/edge-logs.tsx index 21cbc34cc8627..b97ef42a87540 100644 --- a/apps/studio/pages/project/[ref]/logs/edge-logs.tsx +++ b/apps/studio/pages/project/[ref]/logs/edge-logs.tsx @@ -1,7 +1,7 @@ import { useRouter } from 'next/router' import { LogsTableName } from 'components/interfaces/Settings/Logs/Logs.constants' -import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer' +import { LogsPreviewer } from 'components/interfaces/Settings/Logs/LogsPreviewer' import LogsLayout from 'components/layouts/LogsLayout/LogsLayout' import type { NextPageWithLayout } from 'types' import DefaultLayout from 'components/layouts/DefaultLayout' diff --git a/apps/studio/pages/project/[ref]/logs/etl-replication-logs.tsx b/apps/studio/pages/project/[ref]/logs/etl-replication-logs.tsx index e80111dc90eea..25171a4d1e275 100644 --- a/apps/studio/pages/project/[ref]/logs/etl-replication-logs.tsx +++ b/apps/studio/pages/project/[ref]/logs/etl-replication-logs.tsx @@ -1,5 +1,5 @@ import { LogsTableName } from 'components/interfaces/Settings/Logs/Logs.constants' -import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer' +import { LogsPreviewer } from 'components/interfaces/Settings/Logs/LogsPreviewer' import LogsLayout from 'components/layouts/LogsLayout/LogsLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import type { NextPageWithLayout } from 'types' diff --git a/apps/studio/pages/project/[ref]/logs/pg-upgrade-logs.tsx b/apps/studio/pages/project/[ref]/logs/pg-upgrade-logs.tsx index 0b9cc41594f1a..9661aa7bdd978 100644 --- a/apps/studio/pages/project/[ref]/logs/pg-upgrade-logs.tsx +++ b/apps/studio/pages/project/[ref]/logs/pg-upgrade-logs.tsx @@ -1,5 +1,5 @@ import { useParams } from 'common' -import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer' +import { LogsPreviewer } from 'components/interfaces/Settings/Logs/LogsPreviewer' import DefaultLayout from 'components/layouts/DefaultLayout' import LogsLayout from 'components/layouts/LogsLayout/LogsLayout' import type { NextPageWithLayout } from 'types' diff --git a/apps/studio/pages/project/[ref]/logs/pgcron-logs.tsx b/apps/studio/pages/project/[ref]/logs/pgcron-logs.tsx index fbb829360f758..e3a32652066cc 100644 --- a/apps/studio/pages/project/[ref]/logs/pgcron-logs.tsx +++ b/apps/studio/pages/project/[ref]/logs/pgcron-logs.tsx @@ -1,7 +1,7 @@ import { useRouter } from 'next/router' import { LogsTableName } from 'components/interfaces/Settings/Logs/Logs.constants' -import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer' +import { LogsPreviewer } from 'components/interfaces/Settings/Logs/LogsPreviewer' import LogsLayout from 'components/layouts/LogsLayout/LogsLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import type { NextPageWithLayout } from 'types' diff --git a/apps/studio/pages/project/[ref]/logs/pooler-logs.tsx b/apps/studio/pages/project/[ref]/logs/pooler-logs.tsx index 707286cee0b08..bee89264a26f0 100644 --- a/apps/studio/pages/project/[ref]/logs/pooler-logs.tsx +++ b/apps/studio/pages/project/[ref]/logs/pooler-logs.tsx @@ -1,6 +1,6 @@ import { useParams } from 'common' import { LogsTableName } from 'components/interfaces/Settings/Logs/Logs.constants' -import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer' +import { LogsPreviewer } from 'components/interfaces/Settings/Logs/LogsPreviewer' import DefaultLayout from 'components/layouts/DefaultLayout' import LogsLayout from 'components/layouts/LogsLayout/LogsLayout' import { Loading } from 'components/ui/Loading' diff --git a/apps/studio/pages/project/[ref]/logs/postgres-logs.tsx b/apps/studio/pages/project/[ref]/logs/postgres-logs.tsx index 3f93fb2598161..55cbed7ceeefe 100644 --- a/apps/studio/pages/project/[ref]/logs/postgres-logs.tsx +++ b/apps/studio/pages/project/[ref]/logs/postgres-logs.tsx @@ -1,6 +1,6 @@ import { useRouter } from 'next/router' -import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer' +import { LogsPreviewer } from 'components/interfaces/Settings/Logs/LogsPreviewer' import LogsLayout from 'components/layouts/LogsLayout/LogsLayout' import type { NextPageWithLayout } from 'types' import { LogsTableName } from 'components/interfaces/Settings/Logs/Logs.constants' diff --git a/apps/studio/pages/project/[ref]/logs/postgrest-logs.tsx b/apps/studio/pages/project/[ref]/logs/postgrest-logs.tsx index 09c25060447a3..66a5622f6e604 100644 --- a/apps/studio/pages/project/[ref]/logs/postgrest-logs.tsx +++ b/apps/studio/pages/project/[ref]/logs/postgrest-logs.tsx @@ -1,7 +1,7 @@ import { useRouter } from 'next/router' import { LogsTableName } from 'components/interfaces/Settings/Logs/Logs.constants' -import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer' +import { LogsPreviewer } from 'components/interfaces/Settings/Logs/LogsPreviewer' import LogsLayout from 'components/layouts/LogsLayout/LogsLayout' import type { NextPageWithLayout } from 'types' import { LogsTableEmptyState } from 'components/interfaces/Settings/Logs/LogsTableEmptyState' diff --git a/apps/studio/pages/project/[ref]/logs/realtime-logs.tsx b/apps/studio/pages/project/[ref]/logs/realtime-logs.tsx index ac99ca8a00017..f3233584c162c 100644 --- a/apps/studio/pages/project/[ref]/logs/realtime-logs.tsx +++ b/apps/studio/pages/project/[ref]/logs/realtime-logs.tsx @@ -1,6 +1,6 @@ import { useRouter } from 'next/router' -import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer' +import { LogsPreviewer } from 'components/interfaces/Settings/Logs/LogsPreviewer' import LogsLayout from 'components/layouts/LogsLayout/LogsLayout' import type { NextPageWithLayout } from 'types' import DefaultLayout from 'components/layouts/DefaultLayout' diff --git a/apps/studio/pages/project/[ref]/logs/storage-logs.tsx b/apps/studio/pages/project/[ref]/logs/storage-logs.tsx index 2be8683e54d15..1461ad5917c86 100644 --- a/apps/studio/pages/project/[ref]/logs/storage-logs.tsx +++ b/apps/studio/pages/project/[ref]/logs/storage-logs.tsx @@ -1,6 +1,6 @@ import { useRouter } from 'next/router' -import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer' +import { LogsPreviewer } from 'components/interfaces/Settings/Logs/LogsPreviewer' import LogsLayout from 'components/layouts/LogsLayout/LogsLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import type { NextPageWithLayout } from 'types' diff --git a/apps/studio/pages/project/[ref]/reports/auth.tsx b/apps/studio/pages/project/[ref]/reports/auth.tsx index c70822a3a0138..1a26d3fdee983 100644 --- a/apps/studio/pages/project/[ref]/reports/auth.tsx +++ b/apps/studio/pages/project/[ref]/reports/auth.tsx @@ -64,6 +64,7 @@ const AuthUsage = () => { addFilter, removeFilters, isLoadingData, + sql, } = useSharedAPIReport({ filterBy: 'auth', start: selectedDateRange?.period_start?.date, @@ -169,6 +170,7 @@ const AuthUsage = () => { error={error} isLoading={isLoading} isRefetching={isRefetching} + sql={sql} /> diff --git a/apps/studio/pages/project/[ref]/reports/postgrest.tsx b/apps/studio/pages/project/[ref]/reports/postgrest.tsx index 65f1c238a7d13..343e300e0f767 100644 --- a/apps/studio/pages/project/[ref]/reports/postgrest.tsx +++ b/apps/studio/pages/project/[ref]/reports/postgrest.tsx @@ -61,6 +61,7 @@ const PostgrestReport = () => { addFilter, removeFilters, isLoadingData, + sql, } = useSharedAPIReport({ filterBy: 'postgrest', start: selectedDateRange?.period_start?.date, @@ -151,6 +152,7 @@ const PostgrestReport = () => { error={error} isLoading={isLoading} isRefetching={isRefetching} + sql={sql} /> diff --git a/apps/studio/pages/project/[ref]/reports/realtime.tsx b/apps/studio/pages/project/[ref]/reports/realtime.tsx index 14ef2d580bdb7..ba982757c0d96 100644 --- a/apps/studio/pages/project/[ref]/reports/realtime.tsx +++ b/apps/studio/pages/project/[ref]/reports/realtime.tsx @@ -79,6 +79,7 @@ const RealtimeUsage = () => { addFilter, removeFilters, isLoadingData, + sql, } = useSharedAPIReport({ filterBy: 'realtime', start: selectedDateRange?.period_start?.date, @@ -218,6 +219,7 @@ const RealtimeUsage = () => { isLoading={isLoading} isRefetching={isRefetching} hiddenReports={['networkTraffic']} + sql={sql} /> diff --git a/apps/studio/tests/features/logs/LogsPreviewer.test.tsx b/apps/studio/tests/features/logs/LogsPreviewer.test.tsx index af44a42a9501b..9f11eaefeeade 100644 --- a/apps/studio/tests/features/logs/LogsPreviewer.test.tsx +++ b/apps/studio/tests/features/logs/LogsPreviewer.test.tsx @@ -1,9 +1,12 @@ import { screen, waitFor } from '@testing-library/react' import dayjs from 'dayjs' import utc from 'dayjs/plugin/utc' -import { beforeEach, expect, test, vi } from 'vitest' +import { beforeEach, describe, expect, test, vi } from 'vitest' import { LogsTableName } from 'components/interfaces/Settings/Logs/Logs.constants' -import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer' +import { + LogsPreviewer, + calculateBarClickTimeRange, +} from 'components/interfaces/Settings/Logs/LogsPreviewer' import { customRender, customRenderHook } from 'tests/lib/custom-render' import userEvent from '@testing-library/user-event' @@ -127,3 +130,88 @@ test('can click load older', async () => { expect(loadOlder.onclick).toHaveBeenCalled() }) + +describe('calculateBarClickTimeRange', () => { + const clickedTime = '2024-01-15T12:30:00.000Z' + + test('uses 15-second range for time ranges less than 2 minutes', () => { + const rangeStart = '2024-01-15T12:00:00.000Z' + const rangeEnd = '2024-01-15T12:01:30.000Z' // 1.5 minutes + + const result = calculateBarClickTimeRange(rangeStart, rangeEnd, clickedTime) + + expect(result.start).toBe('2024-01-15T12:29:52.500Z') // 7.5 seconds before + expect(result.end).toBe('2024-01-15T12:30:07.500Z') // 7.5 seconds after + }) + + test('uses 2-minute range for time ranges between 2 minutes and 1 hour', () => { + const rangeStart = '2024-01-15T12:00:00.000Z' + const rangeEnd = '2024-01-15T12:30:00.000Z' // 30 minutes + + const result = calculateBarClickTimeRange(rangeStart, rangeEnd, clickedTime) + + expect(result.start).toBe('2024-01-15T12:29:00.000Z') // 1 minute before + expect(result.end).toBe('2024-01-15T12:31:00.000Z') // 1 minute after + }) + + test('uses 5-minute range for time ranges between 1 and 12 hours', () => { + const rangeStart = '2024-01-15T10:00:00.000Z' + const rangeEnd = '2024-01-15T14:00:00.000Z' // 4 hours + + const result = calculateBarClickTimeRange(rangeStart, rangeEnd, clickedTime) + + expect(result.start).toBe('2024-01-15T12:27:30.000Z') // 2.5 minutes before + expect(result.end).toBe('2024-01-15T12:32:30.000Z') // 2.5 minutes after + }) + + test('uses 1-hour range for time ranges 12 hours or more', () => { + const rangeStart = '2024-01-15T00:00:00.000Z' + const rangeEnd = '2024-01-15T24:00:00.000Z' // 24 hours + + const result = calculateBarClickTimeRange(rangeStart, rangeEnd, clickedTime) + + expect(result.start).toBe('2024-01-15T12:00:00.000Z') // 30 minutes before + expect(result.end).toBe('2024-01-15T13:00:00.000Z') // 30 minutes after + }) + + test('handles edge case of exactly 2 minutes range', () => { + const rangeStart = '2024-01-15T12:00:00.000Z' + const rangeEnd = '2024-01-15T12:02:00.000Z' // exactly 2 minutes + + const result = calculateBarClickTimeRange(rangeStart, rangeEnd, clickedTime) + + expect(result.start).toBe('2024-01-15T12:29:00.000Z') // 1 minute before + expect(result.end).toBe('2024-01-15T12:31:00.000Z') // 1 minute after + }) + + test('handles edge case of exactly 1 hour range', () => { + const rangeStart = '2024-01-15T12:00:00.000Z' + const rangeEnd = '2024-01-15T13:00:00.000Z' // exactly 1 hour + + const result = calculateBarClickTimeRange(rangeStart, rangeEnd, clickedTime) + + expect(result.start).toBe('2024-01-15T12:27:30.000Z') // 2.5 minutes before + expect(result.end).toBe('2024-01-15T12:32:30.000Z') // 2.5 minutes after + }) + + test('handles edge case of exactly 12 hours range', () => { + const rangeStart = '2024-01-15T00:00:00.000Z' + const rangeEnd = '2024-01-15T12:00:00.000Z' // exactly 12 hours + + const result = calculateBarClickTimeRange(rangeStart, rangeEnd, clickedTime) + + expect(result.start).toBe('2024-01-15T12:00:00.000Z') // 30 minutes before + expect(result.end).toBe('2024-01-15T13:00:00.000Z') // 30 minutes after + }) + + test('handles different clicked timestamps correctly', () => { + const rangeStart = '2024-01-15T00:00:00.000Z' + const rangeEnd = '2024-01-15T24:00:00.000Z' // 24 hours + const differentClickedTime = '2024-01-15T06:15:30.000Z' + + const result = calculateBarClickTimeRange(rangeStart, rangeEnd, differentClickedTime) + + expect(result.start).toBe('2024-01-15T05:45:30.000Z') // 30 minutes before + expect(result.end).toBe('2024-01-15T06:45:30.000Z') // 30 minutes after + }) +})