feat: add sortable overview widgets
This commit is contained in:
@@ -4,7 +4,8 @@ import { cn } from '@/utils/cn';
|
|||||||
import { DialogTitle } from '@radix-ui/react-dialog';
|
import { DialogTitle } from '@radix-ui/react-dialog';
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import { SearchIcon } from 'lucide-react';
|
import { SearchIcon } from 'lucide-react';
|
||||||
import React, { useMemo, useRef, useState } from 'react';
|
import type React from 'react';
|
||||||
|
import { useMemo, useRef, useState } from 'react';
|
||||||
import { Input } from '../ui/input';
|
import { Input } from '../ui/input';
|
||||||
|
|
||||||
const ROW_HEIGHT = 36;
|
const ROW_HEIGHT = 36;
|
||||||
@@ -106,7 +107,9 @@ export function OverviewListModal<T extends OverviewListItem>({
|
|||||||
// Calculate totals and check for revenue
|
// Calculate totals and check for revenue
|
||||||
const { maxSessions, totalRevenue, hasRevenue, hasPageviews } =
|
const { maxSessions, totalRevenue, hasRevenue, hasPageviews } =
|
||||||
useMemo(() => {
|
useMemo(() => {
|
||||||
const maxSessions = Math.max(...filteredData.map((item) => item.sessions));
|
const maxSessions = Math.max(
|
||||||
|
...filteredData.map((item) => item.sessions),
|
||||||
|
);
|
||||||
const totalRevenue = filteredData.reduce(
|
const totalRevenue = filteredData.reduce(
|
||||||
(sum, item) => sum + (item.revenue ?? 0),
|
(sum, item) => sum + (item.revenue ?? 0),
|
||||||
0,
|
0,
|
||||||
@@ -152,7 +155,8 @@ export function OverviewListModal<T extends OverviewListItem>({
|
|||||||
<div
|
<div
|
||||||
className="grid px-4 py-2 text-sm font-medium text-muted-foreground bg-def-100"
|
className="grid px-4 py-2 text-sm font-medium text-muted-foreground bg-def-100"
|
||||||
style={{
|
style={{
|
||||||
gridTemplateColumns: `1fr ${hasRevenue ? '100px' : ''} ${hasPageviews ? '80px' : ''} ${showSessions ? '80px' : ''}`.trim(),
|
gridTemplateColumns:
|
||||||
|
`1fr ${hasRevenue ? '100px' : ''} ${hasPageviews ? '80px' : ''} ${showSessions ? '80px' : ''}`.trim(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="text-left truncate">{columnName}</div>
|
<div className="text-left truncate">{columnName}</div>
|
||||||
@@ -204,11 +208,14 @@ export function OverviewListModal<T extends OverviewListItem>({
|
|||||||
<div
|
<div
|
||||||
className="relative grid h-full items-center px-4 border-b border-border"
|
className="relative grid h-full items-center px-4 border-b border-border"
|
||||||
style={{
|
style={{
|
||||||
gridTemplateColumns: `1fr ${hasRevenue ? '100px' : ''} ${hasPageviews ? '80px' : ''} ${showSessions ? '80px' : ''}`.trim(),
|
gridTemplateColumns:
|
||||||
|
`1fr ${hasRevenue ? '100px' : ''} ${hasPageviews ? '80px' : ''} ${showSessions ? '80px' : ''}`.trim(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Main content cell */}
|
{/* Main content cell */}
|
||||||
<div className="min-w-0 truncate pr-2">{renderItem(item)}</div>
|
<div className="min-w-0 truncate pr-2">
|
||||||
|
{renderItem(item)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Revenue cell */}
|
{/* Revenue cell */}
|
||||||
{hasRevenue && (
|
{hasRevenue && (
|
||||||
@@ -261,4 +268,3 @@ export function OverviewListModal<T extends OverviewListItem>({
|
|||||||
</ModalContent>
|
</ModalContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -351,7 +351,7 @@ export default function OverviewTopDevices({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
const data = (query.data ?? []).slice(0, 15);
|
const data = query.data ?? [];
|
||||||
if (!searchQuery.trim()) {
|
if (!searchQuery.trim()) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,12 +118,12 @@ export default function OverviewTopEvents({
|
|||||||
|
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
if (!searchQuery.trim()) {
|
if (!searchQuery.trim()) {
|
||||||
return tableData.slice(0, 15);
|
return tableData;
|
||||||
}
|
}
|
||||||
const queryLower = searchQuery.toLowerCase();
|
const queryLower = searchQuery.toLowerCase();
|
||||||
return tableData
|
return tableData.filter((item) =>
|
||||||
.filter((item) => item.name?.toLowerCase().includes(queryLower))
|
item.name?.toLowerCase().includes(queryLower),
|
||||||
.slice(0, 15);
|
);
|
||||||
}, [tableData, searchQuery]);
|
}, [tableData, searchQuery]);
|
||||||
|
|
||||||
const tabs = useMemo(
|
const tabs = useMemo(
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
const data = (query.data ?? []).slice(0, 15);
|
const data = query.data ?? [];
|
||||||
if (!searchQuery.trim()) {
|
if (!searchQuery.trim()) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
const data = query.data?.slice(0, 15) ?? [];
|
const data = query.data ?? [];
|
||||||
if (!searchQuery.trim()) {
|
if (!searchQuery.trim()) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export default function OverviewTopSources({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
const data = (query.data ?? []).slice(0, 15);
|
const data = query.data ?? [];
|
||||||
if (!searchQuery.trim()) {
|
if (!searchQuery.trim()) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { useEventQueryFilters } from '@/hooks/use-event-query-filters';
|
|||||||
import { useNumber } from '@/hooks/use-numer-formatter';
|
import { useNumber } from '@/hooks/use-numer-formatter';
|
||||||
import type { RouterOutputs } from '@/trpc/client';
|
import type { RouterOutputs } from '@/trpc/client';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { ExternalLinkIcon } from 'lucide-react';
|
import { ChevronDown, ChevronUp, ExternalLinkIcon } from 'lucide-react';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
import { SerieIcon } from '../report-chart/common/serie-icon';
|
import { SerieIcon } from '../report-chart/common/serie-icon';
|
||||||
import { Skeleton } from '../skeleton';
|
import { Skeleton } from '../skeleton';
|
||||||
import { Tooltiper } from '../ui/tooltip';
|
import { Tooltiper } from '../ui/tooltip';
|
||||||
@@ -45,6 +46,42 @@ function RevenuePieChart({ percentage }: { percentage: number }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SortableHeader({
|
||||||
|
name,
|
||||||
|
isSorted,
|
||||||
|
sortDirection,
|
||||||
|
onClick,
|
||||||
|
isRightAligned,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
isSorted: boolean;
|
||||||
|
sortDirection: 'asc' | 'desc' | null;
|
||||||
|
onClick: () => void;
|
||||||
|
isRightAligned?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
className={cn(
|
||||||
|
'row items-center gap-1 hover:opacity-80 transition-opacity',
|
||||||
|
isRightAligned && 'justify-end ml-auto',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span>{name}</span>
|
||||||
|
{isSorted ? (
|
||||||
|
sortDirection === 'desc' ? (
|
||||||
|
<ChevronDown className="size-3" />
|
||||||
|
) : (
|
||||||
|
<ChevronUp className="size-3" />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="size-3 opacity-30" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type Props<T> = WidgetTableProps<T> & {
|
type Props<T> = WidgetTableProps<T> & {
|
||||||
getColumnPercentage: (item: T) => number;
|
getColumnPercentage: (item: T) => number;
|
||||||
};
|
};
|
||||||
@@ -56,10 +93,113 @@ export const OverviewWidgetTable = <T,>({
|
|||||||
getColumnPercentage,
|
getColumnPercentage,
|
||||||
className,
|
className,
|
||||||
}: Props<T>) => {
|
}: Props<T>) => {
|
||||||
|
const [sortColumn, setSortColumn] = useState<string | null>(null);
|
||||||
|
const [sortDirection, setSortDirection] = useState<'asc' | 'desc' | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle column header click for sorting
|
||||||
|
const handleSort = (columnName: string) => {
|
||||||
|
if (sortColumn === columnName) {
|
||||||
|
// Cycle through: desc -> asc -> null
|
||||||
|
if (sortDirection === 'desc') {
|
||||||
|
setSortDirection('asc');
|
||||||
|
} else if (sortDirection === 'asc') {
|
||||||
|
setSortColumn(null);
|
||||||
|
setSortDirection(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// First click on a column = descending (highest to lowest)
|
||||||
|
setSortColumn(columnName);
|
||||||
|
setSortDirection('desc');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sort data based on current sort state
|
||||||
|
// Sort all available items, then limit display to top 15
|
||||||
|
const sortedData = useMemo(() => {
|
||||||
|
const allData = data ?? [];
|
||||||
|
|
||||||
|
if (!sortColumn || !sortDirection) {
|
||||||
|
// When not sorting, return top 15 (maintain original behavior)
|
||||||
|
return allData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const column = columns.find((col) => {
|
||||||
|
if (typeof col.name === 'string') {
|
||||||
|
return col.name === sortColumn;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!column?.getSortValue) {
|
||||||
|
return allData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort all available items
|
||||||
|
const sorted = [...allData].sort((a, b) => {
|
||||||
|
const aValue = column.getSortValue!(a);
|
||||||
|
const bValue = column.getSortValue!(b);
|
||||||
|
|
||||||
|
// Handle null values
|
||||||
|
if (aValue === null && bValue === null) return 0;
|
||||||
|
if (aValue === null) return 1;
|
||||||
|
if (bValue === null) return -1;
|
||||||
|
|
||||||
|
// Compare values
|
||||||
|
let comparison = 0;
|
||||||
|
if (typeof aValue === 'number' && typeof bValue === 'number') {
|
||||||
|
comparison = aValue - bValue;
|
||||||
|
} else {
|
||||||
|
comparison = String(aValue).localeCompare(String(bValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortDirection === 'desc' ? -comparison : comparison;
|
||||||
|
});
|
||||||
|
|
||||||
|
return sorted;
|
||||||
|
}, [data, sortColumn, sortDirection, columns]).slice(0, 15);
|
||||||
|
|
||||||
|
// Create columns with sortable headers
|
||||||
|
const columnsWithSortableHeaders = useMemo(() => {
|
||||||
|
return columns.map((column, index) => {
|
||||||
|
const columnName =
|
||||||
|
typeof column.name === 'string' ? column.name : String(column.name);
|
||||||
|
const isSortable = !!column.getSortValue;
|
||||||
|
const isSorted = sortColumn === columnName;
|
||||||
|
const currentSortDirection = isSorted ? sortDirection : null;
|
||||||
|
const isRightAligned = index !== 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
// Add a key property for React keys (using the original column name string)
|
||||||
|
key: columnName,
|
||||||
|
name: isSortable ? (
|
||||||
|
<SortableHeader
|
||||||
|
name={columnName}
|
||||||
|
isSorted={isSorted}
|
||||||
|
sortDirection={currentSortDirection}
|
||||||
|
onClick={() => handleSort(columnName)}
|
||||||
|
isRightAligned={isRightAligned}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
column.name
|
||||||
|
),
|
||||||
|
className: cn(
|
||||||
|
index === 0
|
||||||
|
? 'text-left w-full font-medium min-w-0'
|
||||||
|
: 'text-right font-mono',
|
||||||
|
// Remove old responsive logic - now handled by responsive prop
|
||||||
|
column.className,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [columns, sortColumn, sortDirection]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(className)}>
|
<div className={cn(className)}>
|
||||||
<WidgetTable
|
<WidgetTable
|
||||||
data={data ?? []}
|
data={sortedData}
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
className={'text-sm min-h-[358px] @container'}
|
className={'text-sm min-h-[358px] @container'}
|
||||||
columnClassName="[&_.cell:first-child]:pl-4 [&_.cell:last-child]:pr-4"
|
columnClassName="[&_.cell:first-child]:pl-4 [&_.cell:last-child]:pr-4"
|
||||||
@@ -75,18 +215,7 @@ export const OverviewWidgetTable = <T,>({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
columns={columns.map((column, index) => {
|
columns={columnsWithSortableHeaders}
|
||||||
return {
|
|
||||||
...column,
|
|
||||||
className: cn(
|
|
||||||
index === 0
|
|
||||||
? 'text-left w-full font-medium min-w-0'
|
|
||||||
: 'text-right font-mono',
|
|
||||||
// Remove old responsive logic - now handled by responsive prop
|
|
||||||
column.className,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -208,6 +337,8 @@ export function OverviewWidgetTablePages({
|
|||||||
name: 'Revenue',
|
name: 'Revenue',
|
||||||
width: '100px',
|
width: '100px',
|
||||||
responsive: { priority: 3 }, // Always show if possible
|
responsive: { priority: 3 }, // Always show if possible
|
||||||
|
getSortValue: (item: (typeof data)[number]) =>
|
||||||
|
item.revenue ?? 0,
|
||||||
render(item: (typeof data)[number]) {
|
render(item: (typeof data)[number]) {
|
||||||
const revenue = item.revenue ?? 0;
|
const revenue = item.revenue ?? 0;
|
||||||
const revenuePercentage =
|
const revenuePercentage =
|
||||||
@@ -231,6 +362,7 @@ export function OverviewWidgetTablePages({
|
|||||||
name: 'Views',
|
name: 'Views',
|
||||||
width: '84px',
|
width: '84px',
|
||||||
responsive: { priority: 2 }, // Always show if possible
|
responsive: { priority: 2 }, // Always show if possible
|
||||||
|
getSortValue: (item: (typeof data)[number]) => item.pageviews,
|
||||||
render(item) {
|
render(item) {
|
||||||
return (
|
return (
|
||||||
<div className="row gap-2 justify-end">
|
<div className="row gap-2 justify-end">
|
||||||
@@ -245,6 +377,7 @@ export function OverviewWidgetTablePages({
|
|||||||
name: 'Sess.',
|
name: 'Sess.',
|
||||||
width: '84px',
|
width: '84px',
|
||||||
responsive: { priority: 2 }, // Always show if possible
|
responsive: { priority: 2 }, // Always show if possible
|
||||||
|
getSortValue: (item: (typeof data)[number]) => item.sessions,
|
||||||
render(item) {
|
render(item) {
|
||||||
return (
|
return (
|
||||||
<div className="row gap-2 justify-end">
|
<div className="row gap-2 justify-end">
|
||||||
@@ -339,6 +472,8 @@ export function OverviewWidgetTableEntries({
|
|||||||
name: 'Revenue',
|
name: 'Revenue',
|
||||||
width: '100px',
|
width: '100px',
|
||||||
responsive: { priority: 3 }, // Always show if possible
|
responsive: { priority: 3 }, // Always show if possible
|
||||||
|
getSortValue: (item: (typeof data)[number]) =>
|
||||||
|
item.revenue ?? 0,
|
||||||
render(item: (typeof data)[number]) {
|
render(item: (typeof data)[number]) {
|
||||||
const revenue = item.revenue ?? 0;
|
const revenue = item.revenue ?? 0;
|
||||||
const revenuePercentage =
|
const revenuePercentage =
|
||||||
@@ -362,6 +497,7 @@ export function OverviewWidgetTableEntries({
|
|||||||
name: lastColumnName,
|
name: lastColumnName,
|
||||||
width: '84px',
|
width: '84px',
|
||||||
responsive: { priority: 2 }, // Always show if possible
|
responsive: { priority: 2 }, // Always show if possible
|
||||||
|
getSortValue: (item: (typeof data)[number]) => item.sessions,
|
||||||
render(item) {
|
render(item) {
|
||||||
return (
|
return (
|
||||||
<div className="row gap-2 justify-end">
|
<div className="row gap-2 justify-end">
|
||||||
@@ -494,6 +630,9 @@ export function OverviewWidgetTableGeneric({
|
|||||||
name: 'Revenue',
|
name: 'Revenue',
|
||||||
width: '100px',
|
width: '100px',
|
||||||
responsive: { priority: 3 },
|
responsive: { priority: 3 },
|
||||||
|
getSortValue: (
|
||||||
|
item: RouterOutputs['overview']['topGeneric'][number],
|
||||||
|
) => item.revenue ?? 0,
|
||||||
render(item: RouterOutputs['overview']['topGeneric'][number]) {
|
render(item: RouterOutputs['overview']['topGeneric'][number]) {
|
||||||
const revenue = item.revenue ?? 0;
|
const revenue = item.revenue ?? 0;
|
||||||
const revenuePercentage =
|
const revenuePercentage =
|
||||||
@@ -521,6 +660,9 @@ export function OverviewWidgetTableGeneric({
|
|||||||
name: 'Views',
|
name: 'Views',
|
||||||
width: '84px',
|
width: '84px',
|
||||||
responsive: { priority: 2 },
|
responsive: { priority: 2 },
|
||||||
|
getSortValue: (
|
||||||
|
item: RouterOutputs['overview']['topGeneric'][number],
|
||||||
|
) => item.pageviews,
|
||||||
render(item: RouterOutputs['overview']['topGeneric'][number]) {
|
render(item: RouterOutputs['overview']['topGeneric'][number]) {
|
||||||
return (
|
return (
|
||||||
<div className="row gap-2 justify-end">
|
<div className="row gap-2 justify-end">
|
||||||
@@ -537,6 +679,9 @@ export function OverviewWidgetTableGeneric({
|
|||||||
name: 'Sess.',
|
name: 'Sess.',
|
||||||
width: '84px',
|
width: '84px',
|
||||||
responsive: { priority: 2 },
|
responsive: { priority: 2 },
|
||||||
|
getSortValue: (
|
||||||
|
item: RouterOutputs['overview']['topGeneric'][number],
|
||||||
|
) => item.sessions,
|
||||||
render(item) {
|
render(item) {
|
||||||
return (
|
return (
|
||||||
<div className="row gap-2 justify-end">
|
<div className="row gap-2 justify-end">
|
||||||
@@ -599,6 +744,7 @@ export function OverviewWidgetTableEvents({
|
|||||||
name: 'Count',
|
name: 'Count',
|
||||||
width: '84px',
|
width: '84px',
|
||||||
responsive: { priority: 2 },
|
responsive: { priority: 2 },
|
||||||
|
getSortValue: (item: EventTableItem) => item.count,
|
||||||
render(item) {
|
render(item) {
|
||||||
return (
|
return (
|
||||||
<div className="row gap-2 justify-end">
|
<div className="row gap-2 justify-end">
|
||||||
|
|||||||
@@ -29,6 +29,14 @@ export interface Props<T> {
|
|||||||
* If not provided, column is always visible.
|
* If not provided, column is always visible.
|
||||||
*/
|
*/
|
||||||
responsive?: ColumnResponsive;
|
responsive?: ColumnResponsive;
|
||||||
|
/**
|
||||||
|
* Function to extract sortable value. If provided, header becomes clickable.
|
||||||
|
*/
|
||||||
|
getSortValue?: (item: T) => number | string | null;
|
||||||
|
/**
|
||||||
|
* Optional key for React keys. If not provided, will try to extract from name or use index.
|
||||||
|
*/
|
||||||
|
key?: string;
|
||||||
}[];
|
}[];
|
||||||
keyExtractor: (item: T) => string;
|
keyExtractor: (item: T) => string;
|
||||||
data: T[];
|
data: T[];
|
||||||
@@ -177,9 +185,16 @@ export function WidgetTable<T>({
|
|||||||
dataAttrs['data-min-width'] = String(column.responsive.minWidth);
|
dataAttrs['data-min-width'] = String(column.responsive.minWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use column.key if available, otherwise try to extract string from name, fallback to index
|
||||||
|
const columnKey =
|
||||||
|
column.key ??
|
||||||
|
(typeof column.name === 'string'
|
||||||
|
? column.name
|
||||||
|
: `col-${colIndex}`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={column.name?.toString()}
|
key={columnKey}
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-2 font-medium font-sans text-sm whitespace-nowrap cell',
|
'p-2 font-medium font-sans text-sm whitespace-nowrap cell',
|
||||||
columns.length > 1 && column !== columns[0]
|
columns.length > 1 && column !== columns[0]
|
||||||
@@ -231,9 +246,16 @@ export function WidgetTable<T>({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use column.key if available, otherwise try to extract string from name, fallback to index
|
||||||
|
const columnKey =
|
||||||
|
column.key ??
|
||||||
|
(typeof column.name === 'string'
|
||||||
|
? column.name
|
||||||
|
: `col-${colIndex}`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={column.name?.toString()}
|
key={columnKey}
|
||||||
className={cn(
|
className={cn(
|
||||||
'px-2 relative cell',
|
'px-2 relative cell',
|
||||||
columns.length > 1 && column !== columns[0]
|
columns.length > 1 && column !== columns[0]
|
||||||
|
|||||||
@@ -109,7 +109,9 @@ export const zGetTopGenericSeriesInput = zGetTopGenericInput.extend({
|
|||||||
interval: zTimeInterval,
|
interval: zTimeInterval,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type IGetTopGenericSeriesInput = z.infer<typeof zGetTopGenericSeriesInput> & {
|
export type IGetTopGenericSeriesInput = z.infer<
|
||||||
|
typeof zGetTopGenericSeriesInput
|
||||||
|
> & {
|
||||||
timezone: string;
|
timezone: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -734,7 +736,7 @@ export class OverviewService {
|
|||||||
}>;
|
}>;
|
||||||
}> {
|
}> {
|
||||||
const prefixColumn = COLUMN_PREFIX_MAP[column] ?? null;
|
const prefixColumn = COLUMN_PREFIX_MAP[column] ?? null;
|
||||||
const TOP_LIMIT = 15;
|
const TOP_LIMIT = 500;
|
||||||
const fillConfig = this.getFillConfig(interval, startDate, endDate);
|
const fillConfig = this.getFillConfig(interval, startDate, endDate);
|
||||||
|
|
||||||
// Step 1: Get top 15 items
|
// Step 1: Get top 15 items
|
||||||
|
|||||||
Reference in New Issue
Block a user