fix: dashboard improvements and query speed improvements
This commit is contained in:
@@ -4,160 +4,322 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { useNumber } from '@/hooks/use-numer-formatter';
|
||||
import { useVisibleSeries } from '@/hooks/use-visible-series';
|
||||
import type { IChartData } from '@/trpc/client';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { getChartColor } from '@/utils/theme';
|
||||
import { DropdownMenuPortal } from '@radix-ui/react-dropdown-menu';
|
||||
import { SearchIcon } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { round } from '@openpanel/common';
|
||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
|
||||
import { OverviewWidgetTable } from '../../overview/overview-widget-table';
|
||||
import { DeltaChip } from '@/components/delta-chip';
|
||||
import { PreviousDiffIndicator } from '../common/previous-diff-indicator';
|
||||
import { SerieIcon } from '../common/serie-icon';
|
||||
import { SerieName } from '../common/serie-name';
|
||||
import { useReportChartContext } from '../context';
|
||||
|
||||
type SortOption =
|
||||
| 'count-desc'
|
||||
| 'count-asc'
|
||||
| 'name-asc'
|
||||
| 'name-desc'
|
||||
| 'percent-desc'
|
||||
| 'percent-asc';
|
||||
|
||||
interface Props {
|
||||
data: IChartData;
|
||||
}
|
||||
|
||||
export function Chart({ data }: Props) {
|
||||
const [isOpen, setOpen] = useState<string | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [sortBy, setSortBy] = useState<SortOption>('count-desc');
|
||||
const {
|
||||
isEditMode,
|
||||
report: { metric, limit, previous },
|
||||
options: { onClick, dropdownMenuContent, columns },
|
||||
options: { onClick, dropdownMenuContent },
|
||||
} = useReportChartContext();
|
||||
const number = useNumber();
|
||||
const series = useMemo(
|
||||
() => (isEditMode ? data.series : data.series.slice(0, limit || 10)),
|
||||
[data, isEditMode, limit],
|
||||
);
|
||||
const maxCount = Math.max(
|
||||
...series.map((serie) => serie.metrics[metric] ?? 0),
|
||||
);
|
||||
|
||||
const tableColumns = [
|
||||
{
|
||||
name: columns?.[0] || 'Name',
|
||||
width: 'w-full',
|
||||
render: (serie: (typeof series)[0]) => {
|
||||
const isClickable = !serie.names.includes(NOT_SET_VALUE) && onClick;
|
||||
const isDropDownEnabled =
|
||||
!serie.names.includes(NOT_SET_VALUE) &&
|
||||
(dropdownMenuContent?.(serie) || []).length > 0;
|
||||
// Use useVisibleSeries to add index property for colors
|
||||
const { series: allSeriesWithIndex } = useVisibleSeries(data, 500);
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
onOpenChange={() =>
|
||||
setOpen((p) => (p === serie.id ? null : serie.id))
|
||||
}
|
||||
open={isOpen === serie.id}
|
||||
>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
disabled={!isDropDownEnabled}
|
||||
{...(isDropDownEnabled
|
||||
? {
|
||||
onPointerDown: (e) => e.preventDefault(),
|
||||
onClick: () => setOpen(serie.id),
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-2 break-all font-medium',
|
||||
(isClickable || isDropDownEnabled) && 'cursor-pointer',
|
||||
)}
|
||||
{...(isClickable && !isDropDownEnabled
|
||||
? {
|
||||
onClick: () => onClick(serie),
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
<SerieIcon name={serie.names[0]} />
|
||||
<SerieName name={serie.names} />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent>
|
||||
{dropdownMenuContent?.(serie).map((item) => (
|
||||
<DropdownMenuItem key={item.title} onClick={item.onClick}>
|
||||
{item.icon && <item.icon size={16} className="mr-2" />}
|
||||
{item.title}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenu>
|
||||
);
|
||||
},
|
||||
},
|
||||
// Percentage column
|
||||
{
|
||||
name: '%',
|
||||
width: '70px',
|
||||
render: (serie: (typeof series)[0]) => (
|
||||
<div className="text-muted-foreground font-mono">
|
||||
{number.format(
|
||||
round((serie.metrics.sum / data.metrics.sum) * 100, 2),
|
||||
)}
|
||||
%
|
||||
</div>
|
||||
),
|
||||
},
|
||||
const totalSum = data.metrics.sum || 1;
|
||||
|
||||
// Previous value column
|
||||
{
|
||||
name: 'Previous',
|
||||
width: '130px',
|
||||
render: (serie: (typeof series)[0]) => (
|
||||
<div className="flex items-center gap-2 font-mono justify-end">
|
||||
<div className="font-bold">
|
||||
{number.format(serie.metrics.previous?.[metric]?.value)}
|
||||
</div>
|
||||
<PreviousDiffIndicator
|
||||
{...serie.metrics.previous?.[metric]}
|
||||
size="xs"
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
// Calculate original ranks (based on count descending - default sort)
|
||||
const seriesWithOriginalRank = useMemo(() => {
|
||||
const sortedByCount = [...allSeriesWithIndex].sort(
|
||||
(a, b) => b.metrics.sum - a.metrics.sum,
|
||||
);
|
||||
const rankMap = new Map<string, number>();
|
||||
sortedByCount.forEach((serie, idx) => {
|
||||
rankMap.set(serie.id, idx + 1);
|
||||
});
|
||||
return allSeriesWithIndex.map((serie) => ({
|
||||
...serie,
|
||||
originalRank: rankMap.get(serie.id) ?? 0,
|
||||
}));
|
||||
}, [allSeriesWithIndex]);
|
||||
|
||||
// Main count column (always last)
|
||||
{
|
||||
name: 'Count',
|
||||
width: '80px',
|
||||
render: (serie: (typeof series)[0]) => (
|
||||
<div className="font-bold font-mono">
|
||||
{number.format(serie.metrics.sum)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
// Filter and sort series
|
||||
const series = useMemo(() => {
|
||||
let filtered = seriesWithOriginalRank;
|
||||
|
||||
// Filter by search query
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
filtered = filtered.filter((serie) =>
|
||||
serie.names.some((name) => name.toLowerCase().includes(query)),
|
||||
);
|
||||
}
|
||||
|
||||
// Sort
|
||||
const sorted = [...filtered].sort((a, b) => {
|
||||
switch (sortBy) {
|
||||
case 'count-desc':
|
||||
return b.metrics.sum - a.metrics.sum;
|
||||
case 'count-asc':
|
||||
return a.metrics.sum - b.metrics.sum;
|
||||
case 'name-asc':
|
||||
return a.names.join(' > ').localeCompare(b.names.join(' > '));
|
||||
case 'name-desc':
|
||||
return b.names.join(' > ').localeCompare(a.names.join(' > '));
|
||||
case 'percent-desc':
|
||||
return b.metrics.sum / totalSum - a.metrics.sum / totalSum;
|
||||
case 'percent-asc':
|
||||
return a.metrics.sum / totalSum - b.metrics.sum / totalSum;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
// Apply limit if not in edit mode
|
||||
return isEditMode ? sorted : sorted.slice(0, limit || 10);
|
||||
}, [
|
||||
seriesWithOriginalRank,
|
||||
searchQuery,
|
||||
sortBy,
|
||||
totalSum,
|
||||
isEditMode,
|
||||
limit,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'text-sm',
|
||||
isEditMode ? 'card gap-2 p-4 text-base' : '-m-3',
|
||||
<div className={cn('w-full', isEditMode && 'card')}>
|
||||
{isEditMode && (
|
||||
<div className="flex items-center gap-3 p-4 border-b border-def-200 dark:border-def-800">
|
||||
<div className="relative flex-1">
|
||||
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground pointer-events-none" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Filter by name"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
value={sortBy}
|
||||
onValueChange={(value) => setSortBy(value as SortOption)}
|
||||
>
|
||||
<SelectTrigger size="sm" className="w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="count-desc">Count (High → Low)</SelectItem>
|
||||
<SelectItem value="count-asc">Count (Low → High)</SelectItem>
|
||||
<SelectItem value="name-asc">Name (A → Z)</SelectItem>
|
||||
<SelectItem value="name-desc">Name (Z → A)</SelectItem>
|
||||
<SelectItem value="percent-desc">
|
||||
Percentage (High → Low)
|
||||
</SelectItem>
|
||||
<SelectItem value="percent-asc">
|
||||
Percentage (Low → High)
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<OverviewWidgetTable
|
||||
data={series}
|
||||
keyExtractor={(serie) => serie.id}
|
||||
columns={tableColumns.filter((column) => {
|
||||
if (!previous && column.name === 'Previous') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})}
|
||||
getColumnPercentage={(serie) => serie.metrics.sum / maxCount}
|
||||
className={cn(isEditMode ? 'min-h-[358px]' : 'min-h-0')}
|
||||
/>
|
||||
<div className="overflow-hidden">
|
||||
<div className="divide-y divide-def-200 dark:divide-def-800">
|
||||
{series.map((serie, idx) => {
|
||||
const isClickable =
|
||||
!serie.names.includes(NOT_SET_VALUE) && !!onClick;
|
||||
const isDropDownEnabled =
|
||||
!serie.names.includes(NOT_SET_VALUE) &&
|
||||
(dropdownMenuContent?.(serie) || []).length > 0;
|
||||
|
||||
const color = getChartColor(serie.index);
|
||||
const percentOfTotal = round(
|
||||
(serie.metrics.sum / totalSum) * 100,
|
||||
1,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={serie.id}
|
||||
className={cn(
|
||||
'group relative px-4 py-3 transition-colors overflow-hidden',
|
||||
isClickable && 'cursor-pointer',
|
||||
)}
|
||||
role={isClickable ? 'button' : undefined}
|
||||
tabIndex={isClickable ? 0 : undefined}
|
||||
onClick={() => {
|
||||
if (isClickable && !isDropDownEnabled) {
|
||||
onClick?.(serie);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (!isClickable || isDropDownEnabled) return;
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onClick?.(serie);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Subtle accent glow */}
|
||||
<div
|
||||
className="pointer-events-none absolute -left-10 -top-10 h-40 w-96 rounded-full opacity-0 blur-3xl transition-opacity duration-500 group-hover:opacity-10"
|
||||
style={{
|
||||
background: `radial-gradient(circle, ${color} 0%, transparent 70%)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div
|
||||
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl border bg-def-50 dark:border-def-800 dark:bg-def-900"
|
||||
style={{ borderColor: `${color}22` }}
|
||||
>
|
||||
<SerieIcon name={serie.names[0]} />
|
||||
</div>
|
||||
|
||||
<div className="min-w-0">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<div
|
||||
className="h-2 w-2 rounded-full"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
<span className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
||||
Rank {serie.originalRank}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<DropdownMenu
|
||||
onOpenChange={() =>
|
||||
setOpen((p) => (p === serie.id ? null : serie.id))
|
||||
}
|
||||
open={isOpen === serie.id}
|
||||
>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
disabled={!isDropDownEnabled}
|
||||
{...(isDropDownEnabled
|
||||
? {
|
||||
onPointerDown: (e) => e.preventDefault(),
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
setOpen(serie.id);
|
||||
},
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'min-w-0',
|
||||
isDropDownEnabled && 'cursor-pointer',
|
||||
)}
|
||||
{...(isClickable && !isDropDownEnabled
|
||||
? {
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
onClick?.(serie);
|
||||
},
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
<SerieName
|
||||
name={serie.names}
|
||||
className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-sm font-semibold tracking-tight"
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent>
|
||||
{dropdownMenuContent?.(serie).map((item) => (
|
||||
<DropdownMenuItem
|
||||
key={item.title}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
item.onClick();
|
||||
}}
|
||||
>
|
||||
{item.icon && (
|
||||
<item.icon size={16} className="mr-2" />
|
||||
)}
|
||||
{item.title}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 flex-col items-end gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-base font-semibold font-mono tracking-tight">
|
||||
{number.format(serie.metrics.sum)}
|
||||
</div>
|
||||
{previous && serie.metrics.previous?.[metric] && (
|
||||
<DeltaChip
|
||||
variant={
|
||||
serie.metrics.previous[metric].state ===
|
||||
'positive'
|
||||
? 'inc'
|
||||
: 'dec'
|
||||
}
|
||||
size="sm"
|
||||
>
|
||||
{serie.metrics.previous[metric].diff?.toFixed(1)}%
|
||||
</DeltaChip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bar */}
|
||||
<div className="flex items-center">
|
||||
<div className="h-1.5 flex-1 overflow-hidden rounded-full bg-def-100 dark:bg-def-900">
|
||||
<div
|
||||
className="h-full rounded-full transition-[width] duration-700 ease-out"
|
||||
style={{
|
||||
width: `${percentOfTotal}%`,
|
||||
background: `linear-gradient(90deg, ${color}aa, ${color})`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTRPC } from '@/integrations/trpc/react';
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { cn } from '@/utils/cn';
|
||||
import { AspectContainer } from '../aspect-container';
|
||||
import { ReportChartEmpty } from '../common/empty';
|
||||
import { ReportChartError } from '../common/error';
|
||||
@@ -12,7 +13,7 @@ export function ReportBarChart() {
|
||||
const trpc = useTRPC();
|
||||
|
||||
const res = useQuery(
|
||||
trpc.chart.chart.queryOptions(report, {
|
||||
trpc.chart.aggregate.queryOptions(report, {
|
||||
placeholderData: keepPreviousData,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
enabled: !isLazyLoading,
|
||||
@@ -26,7 +27,6 @@ export function ReportBarChart() {
|
||||
) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (res.isError) {
|
||||
return <Error />;
|
||||
}
|
||||
@@ -39,22 +39,62 @@ export function ReportBarChart() {
|
||||
}
|
||||
|
||||
function Loading() {
|
||||
const { isEditMode } = useReportChartContext();
|
||||
return (
|
||||
<AspectContainer className="col gap-4 overflow-hidden">
|
||||
{Array.from({ length: 10 }).map((_, index) => (
|
||||
<div
|
||||
key={index as number}
|
||||
className="row animate-pulse justify-between"
|
||||
>
|
||||
<div className="h-4 w-2/5 rounded bg-def-200" />
|
||||
<div className="row w-1/5 gap-2">
|
||||
<div className="h-4 w-full rounded bg-def-200" />
|
||||
<div className="h-4 w-full rounded bg-def-200" />
|
||||
<div className="h-4 w-full rounded bg-def-200" />
|
||||
</div>
|
||||
<div className={cn('w-full', isEditMode && 'card')}>
|
||||
<div className="overflow-hidden">
|
||||
<div className="divide-y divide-def-200 dark:divide-def-800">
|
||||
{Array.from({ length: 10 }).map((_, index) => (
|
||||
<div
|
||||
key={index as number}
|
||||
className="relative px-4 py-3 animate-pulse"
|
||||
>
|
||||
<div className="relative z-10 flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
{/* Icon skeleton */}
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl border bg-def-100 dark:border-def-800 dark:bg-def-900" />
|
||||
|
||||
<div className="min-w-0">
|
||||
{/* Rank badge skeleton */}
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full bg-def-200 dark:bg-def-700" />
|
||||
<div className="h-2 w-12 rounded bg-def-200 dark:bg-def-700" />
|
||||
</div>
|
||||
|
||||
{/* Name skeleton */}
|
||||
<div
|
||||
className="h-4 rounded bg-def-200 dark:bg-def-700"
|
||||
style={{
|
||||
width: `${Math.random() * 100 + 100}px`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Count skeleton */}
|
||||
<div className="flex shrink-0 flex-col items-end gap-1">
|
||||
<div className="h-5 w-16 rounded bg-def-200 dark:bg-def-700" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bar skeleton */}
|
||||
<div className="flex items-center">
|
||||
<div className="h-1.5 flex-1 overflow-hidden rounded-full bg-def-100 dark:bg-def-900">
|
||||
<div
|
||||
className="h-full rounded-full bg-def-200 dark:bg-def-700"
|
||||
style={{
|
||||
width: `${Math.random() * 60 + 20}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</AspectContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export function ReportChartEmpty({
|
||||
</div>
|
||||
<ForkliftIcon
|
||||
strokeWidth={1.2}
|
||||
className="mb-4 size-1/3 animate-pulse text-muted-foreground"
|
||||
className="mb-4 size-1/3 max-w-40 animate-pulse text-muted-foreground"
|
||||
/>
|
||||
<div className="font-medium text-muted-foreground">
|
||||
Ready when you're
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useNumber } from '@/hooks/use-numer-formatter';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { ArrowDownIcon, ArrowUpIcon } from 'lucide-react';
|
||||
|
||||
import { DeltaChip } from '@/components/delta-chip';
|
||||
import { useReportChartContext } from '../context';
|
||||
|
||||
export function getDiffIndicator<A, B, C>(
|
||||
@@ -29,7 +30,7 @@ interface PreviousDiffIndicatorProps {
|
||||
children?: React.ReactNode;
|
||||
inverted?: boolean;
|
||||
className?: string;
|
||||
size?: 'sm' | 'lg' | 'md' | 'xs';
|
||||
size?: 'sm' | 'lg' | 'md';
|
||||
}
|
||||
|
||||
export function PreviousDiffIndicator({
|
||||
@@ -81,7 +82,6 @@ export function PreviousDiffIndicator({
|
||||
variant,
|
||||
size === 'lg' && 'size-8',
|
||||
size === 'md' && 'size-6',
|
||||
size === 'xs' && 'size-3',
|
||||
)}
|
||||
>
|
||||
{renderIcon()}
|
||||
@@ -97,7 +97,7 @@ interface PreviousDiffIndicatorPureProps {
|
||||
diff?: number | null | undefined;
|
||||
state?: string | null | undefined;
|
||||
inverted?: boolean;
|
||||
size?: 'sm' | 'lg' | 'md' | 'xs';
|
||||
size?: 'sm' | 'lg' | 'md';
|
||||
className?: string;
|
||||
showPrevious?: boolean;
|
||||
}
|
||||
@@ -133,25 +133,35 @@ export function PreviousDiffIndicatorPure({
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-1 font-mono font-medium',
|
||||
size === 'lg' && 'gap-2',
|
||||
className,
|
||||
)}
|
||||
<DeltaChip
|
||||
variant={state === 'positive' ? 'inc' : 'dec'}
|
||||
size={size}
|
||||
inverted={inverted}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex size-2.5 items-center justify-center rounded-full',
|
||||
variant,
|
||||
size === 'lg' && 'size-8',
|
||||
size === 'md' && 'size-6',
|
||||
size === 'xs' && 'size-3',
|
||||
)}
|
||||
>
|
||||
{renderIcon()}
|
||||
</div>
|
||||
{diff.toFixed(1)}%
|
||||
</div>
|
||||
</DeltaChip>
|
||||
);
|
||||
|
||||
// return (
|
||||
// <div
|
||||
// className={cn(
|
||||
// 'flex items-center gap-1 font-mono font-medium',
|
||||
// size === 'lg' && 'gap-2',
|
||||
// className,
|
||||
// )}
|
||||
// >
|
||||
// <div
|
||||
// className={cn(
|
||||
// 'flex size-2.5 items-center justify-center rounded-full',
|
||||
// variant,
|
||||
// size === 'lg' && 'size-8',
|
||||
// size === 'md' && 'size-6',
|
||||
// size === 'xs' && 'size-3',
|
||||
// )}
|
||||
// >
|
||||
// {renderIcon()}
|
||||
// </div>
|
||||
// {diff.toFixed(1)}%
|
||||
// </div>
|
||||
// );
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ const data = {
|
||||
whale: 'https://whale.naver.com',
|
||||
wechat: 'https://wechat.com',
|
||||
chrome: 'https://upload.wikimedia.org/wikipedia/commons/e/e1/Google_Chrome_icon_%28February_2022%29.svg',
|
||||
'mobile chrome': 'https://upload.wikimedia.org/wikipedia/commons/e/e1/Google_Chrome_icon_%28February_2022%29.svg',
|
||||
'chrome webview': 'https://upload.wikimedia.org/wikipedia/commons/e/e1/Google_Chrome_icon_%28February_2022%29.svg',
|
||||
'chrome headless': 'https://upload.wikimedia.org/wikipedia/commons/e/e1/Google_Chrome_icon_%28February_2022%29.svg',
|
||||
chromecast: 'https://upload.wikimedia.org/wikipedia/commons/archive/2/26/20161130022732%21Chromecast_cast_button_icon.svg',
|
||||
@@ -39,6 +40,7 @@ const data = {
|
||||
edge: 'https://upload.wikimedia.org/wikipedia/commons/7/7e/Microsoft_Edge_logo_%282019%29.png',
|
||||
facebook: 'https://facebook.com',
|
||||
firefox: 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a0/Firefox_logo%2C_2019.svg/1920px-Firefox_logo%2C_2019.svg.png',
|
||||
'mobile firefox': 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a0/Firefox_logo%2C_2019.svg/1920px-Firefox_logo%2C_2019.svg.png',
|
||||
github: 'https://github.com',
|
||||
gmail: 'https://mail.google.com',
|
||||
google: 'https://google.com',
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useTRPC } from '@/integrations/trpc/react';
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { AspectContainer } from '../aspect-container';
|
||||
import { ReportChartEmpty } from '../common/empty';
|
||||
import { ReportChartError } from '../common/error';
|
||||
import { useReportChartContext } from '../context';
|
||||
import { Chart } from './chart';
|
||||
|
||||
@@ -47,28 +50,18 @@ export function Loading() {
|
||||
);
|
||||
}
|
||||
|
||||
export function Error() {
|
||||
function Error() {
|
||||
return (
|
||||
<div className="relative h-[70px]">
|
||||
<div className="opacity-50">
|
||||
<Loading />
|
||||
</div>
|
||||
<div className="center-center absolute inset-0 text-muted-foreground">
|
||||
<div className="text-sm font-medium">Error fetching data</div>
|
||||
</div>
|
||||
</div>
|
||||
<AspectContainer>
|
||||
<ReportChartError />
|
||||
</AspectContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function Empty() {
|
||||
function Empty() {
|
||||
return (
|
||||
<div className="relative h-[70px]">
|
||||
<div className="opacity-50">
|
||||
<Loading />
|
||||
</div>
|
||||
<div className="center-center absolute inset-0 text-muted-foreground">
|
||||
<div className="text-sm font-medium">No data</div>
|
||||
</div>
|
||||
</div>
|
||||
<AspectContainer>
|
||||
<ReportChartEmpty />
|
||||
</AspectContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export function ReportPieChart() {
|
||||
const trpc = useTRPC();
|
||||
|
||||
const res = useQuery(
|
||||
trpc.chart.chart.queryOptions(report, {
|
||||
trpc.chart.aggregate.queryOptions(report, {
|
||||
placeholderData: keepPreviousData,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
enabled: !isLazyLoading,
|
||||
|
||||
Reference in New Issue
Block a user