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 (
-
-
+
+
-
-
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 (
-
-
- }
+
+
+
- ) : (
-
- )
+
+ {services.map((service, index) => (
+
+ ))}
+
}
- >
- {overallStatusLabel}
-
+ 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 = (
+