feat(dashboard): added new Pages
This commit is contained in:
@@ -3,8 +3,10 @@ import { DataTable } from '@/components/data-table';
|
||||
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||
import { Pagination } from '@/components/pagination';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TableSkeleton } from '@/components/ui/table';
|
||||
import type { UseQueryResult } from '@tanstack/react-query';
|
||||
import { GanttChartIcon } from 'lucide-react';
|
||||
import { column } from 'mathjs';
|
||||
|
||||
import type { IServiceEvent } from '@openpanel/db';
|
||||
|
||||
@@ -25,15 +27,7 @@ export const EventsTable = ({ query, ...props }: Props) => {
|
||||
const { data, isFetching, isLoading } = query;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
||||
</div>
|
||||
);
|
||||
return <TableSkeleton cols={columns.length} />;
|
||||
}
|
||||
|
||||
if (data?.length === 0) {
|
||||
|
||||
@@ -60,7 +60,7 @@ export const GridCell: React.FC<
|
||||
}) => (
|
||||
<Component
|
||||
className={cn(
|
||||
'flex h-12 items-center whitespace-nowrap px-4 align-middle shadow-[0_0_0_0.5px] shadow-border',
|
||||
'flex min-h-12 items-center whitespace-nowrap px-4 align-middle shadow-[0_0_0_0.5px] shadow-border',
|
||||
isHeader && 'h-10 bg-def-100 font-semibold text-muted-foreground',
|
||||
colSpan && `col-span-${colSpan}`,
|
||||
className
|
||||
|
||||
@@ -9,7 +9,7 @@ export function PageTabs({
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn('overflow-x-auto', className)}>
|
||||
<div className={cn('h-7 overflow-x-auto', className)}>
|
||||
<div className="flex gap-4 whitespace-nowrap text-3xl font-semibold">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { DataTable } from '@/components/data-table';
|
||||
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||
import { Pagination } from '@/components/pagination';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TableSkeleton } from '@/components/ui/table';
|
||||
import type { UseQueryResult } from '@tanstack/react-query';
|
||||
import { GanttChartIcon } from 'lucide-react';
|
||||
|
||||
@@ -26,15 +27,7 @@ export const ProfilesTable = ({ type, query, ...props }: Props) => {
|
||||
const { data, isFetching, isLoading } = query;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
||||
</div>
|
||||
);
|
||||
return <TableSkeleton cols={columns.length} />;
|
||||
}
|
||||
|
||||
if (data?.length === 0) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { useChartContext } from './ChartProvider';
|
||||
import { MetricCardEmpty } from './MetricCard';
|
||||
import { ResponsiveContainer } from './ResponsiveContainer';
|
||||
|
||||
export function ChartEmpty() {
|
||||
const { editMode, chartType } = useChartContext();
|
||||
@@ -20,12 +20,10 @@ export function ChartEmpty() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'flex aspect-video max-h-[300px] min-h-[200px] w-full items-center justify-center'
|
||||
}
|
||||
>
|
||||
No data
|
||||
</div>
|
||||
<ResponsiveContainer>
|
||||
<div className={'flex h-full w-full items-center justify-center'}>
|
||||
No data
|
||||
</div>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { ResponsiveContainer } from './ResponsiveContainer';
|
||||
|
||||
interface ChartLoadingProps {
|
||||
className?: string;
|
||||
aspectRatio?: number;
|
||||
}
|
||||
export function ChartLoading({ className }: ChartLoadingProps) {
|
||||
export function ChartLoading({ className, aspectRatio }: ChartLoadingProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'bg-def-200 aspect-video max-h-[300px] min-h-[200px] w-full animate-pulse rounded',
|
||||
className
|
||||
)}
|
||||
/>
|
||||
<ResponsiveContainer aspectRatio={aspectRatio}>
|
||||
<div
|
||||
className={cn(
|
||||
'h-full w-full animate-pulse rounded bg-def-200',
|
||||
className
|
||||
)}
|
||||
/>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ import type { LucideIcon } from 'lucide-react';
|
||||
import type { IChartProps, IChartSerie } from '@openpanel/validation';
|
||||
|
||||
export interface IChartContextType extends IChartProps {
|
||||
hideXAxis?: boolean;
|
||||
hideYAxis?: boolean;
|
||||
aspectRatio?: number;
|
||||
editMode?: boolean;
|
||||
hideID?: boolean;
|
||||
onClick?: (item: IChartSerie) => void;
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useInViewport } from 'react-in-viewport';
|
||||
|
||||
import type { IChartRoot } from '.';
|
||||
import { ChartRoot } from '.';
|
||||
import { ChartLoading } from './ChartLoading';
|
||||
|
||||
export function LazyChart(props: IChartRoot) {
|
||||
export function LazyChart({
|
||||
className,
|
||||
...props
|
||||
}: IChartRoot & { className?: string }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const once = useRef(false);
|
||||
const { inViewport } = useInViewport(ref, undefined, {
|
||||
@@ -21,11 +25,11 @@ export function LazyChart(props: IChartRoot) {
|
||||
}, [inViewport]);
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<div ref={ref} className={cn('w-full', className)}>
|
||||
{once.current || inViewport ? (
|
||||
<ChartRoot {...props} editMode={false} />
|
||||
) : (
|
||||
<ChartLoading />
|
||||
<ChartLoading aspectRatio={props.aspectRatio} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||
import { useMappings } from '@/hooks/useMappings';
|
||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||
import type { IRechartPayloadItem } from '@/hooks/useRechartDataModel';
|
||||
import type { IToolTipProps } from '@/types';
|
||||
import * as Portal from '@radix-ui/react-portal';
|
||||
import { bind } from 'bind-event-listener';
|
||||
import throttle from 'lodash.throttle';
|
||||
|
||||
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
||||
import { useChartContext } from './ChartProvider';
|
||||
@@ -24,11 +26,35 @@ export function ReportChartTooltip({
|
||||
const { unit, interval } = useChartContext();
|
||||
const formatDate = useFormatDateInterval(interval);
|
||||
const number = useNumber();
|
||||
if (!active || !payload) {
|
||||
return null;
|
||||
}
|
||||
const [position, setPosition] = useState<{ x: number; y: number } | null>(
|
||||
null
|
||||
);
|
||||
|
||||
if (!payload.length) {
|
||||
const inactive = !active || !payload?.length;
|
||||
useEffect(() => {
|
||||
const setPositionThrottled = throttle(setPosition, 50);
|
||||
const unsubMouseMove = bind(window, {
|
||||
type: 'mousemove',
|
||||
listener(event) {
|
||||
if (!inactive) {
|
||||
setPositionThrottled({ x: event.clientX, y: event.clientY + 20 });
|
||||
}
|
||||
},
|
||||
});
|
||||
const unsubDragEnter = bind(window, {
|
||||
type: 'pointerdown',
|
||||
listener() {
|
||||
setPosition(null);
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubMouseMove();
|
||||
unsubDragEnter();
|
||||
};
|
||||
}, [inactive]);
|
||||
|
||||
if (inactive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -41,55 +67,81 @@ export function ReportChartTooltip({
|
||||
const visible = sorted.slice(0, limit);
|
||||
const hidden = sorted.slice(limit);
|
||||
|
||||
const correctXPosition = (x: number | undefined) => {
|
||||
if (!x) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const tooltipWidth = 300;
|
||||
const screenWidth = window.innerWidth;
|
||||
const newX = x;
|
||||
|
||||
if (newX + tooltipWidth > screenWidth) {
|
||||
return screenWidth - tooltipWidth;
|
||||
}
|
||||
return newX;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-w-[180px] flex-col gap-2 rounded-xl border bg-card p-3 shadow-xl">
|
||||
{visible.map((item, index) => {
|
||||
// If we have a <Cell /> component, payload can be nested
|
||||
const payload = item.payload.payload ?? item.payload;
|
||||
const data = (
|
||||
item.dataKey.includes(':')
|
||||
? // @ts-expect-error
|
||||
payload[`${item.dataKey.split(':')[0]}:payload`]
|
||||
: payload
|
||||
) as IRechartPayloadItem;
|
||||
<Portal.Portal
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: position?.y,
|
||||
left: correctXPosition(position?.x),
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
<div className="flex min-w-[180px] flex-col gap-2 rounded-xl border bg-card p-3 shadow-xl">
|
||||
{visible.map((item, index) => {
|
||||
// If we have a <Cell /> component, payload can be nested
|
||||
const payload = item.payload.payload ?? item.payload;
|
||||
const data = (
|
||||
item.dataKey.includes(':')
|
||||
? // @ts-expect-error
|
||||
payload[`${item.dataKey.split(':')[0]}:payload`]
|
||||
: payload
|
||||
) as IRechartPayloadItem;
|
||||
|
||||
return (
|
||||
<React.Fragment key={data.id}>
|
||||
{index === 0 && data.date && (
|
||||
<div className="flex justify-between gap-8">
|
||||
<div>{formatDate(new Date(data.date))}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<div
|
||||
className="w-[3px] rounded-full"
|
||||
style={{ background: data.color }}
|
||||
/>
|
||||
<div className="col flex-1 gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<SerieIcon name={data.names} />
|
||||
<SerieName name={data.names} />
|
||||
return (
|
||||
<React.Fragment key={data.id}>
|
||||
{index === 0 && data.date && (
|
||||
<div className="flex justify-between gap-8">
|
||||
<div>{formatDate(new Date(data.date))}</div>
|
||||
</div>
|
||||
<div className="font-mono flex justify-between gap-8 font-medium">
|
||||
<div className="row gap-1">
|
||||
{number.formatWithUnit(data.count, unit)}
|
||||
{!!data.previous && (
|
||||
<span className="text-muted-foreground">
|
||||
({number.formatWithUnit(data.previous.value, unit)})
|
||||
</span>
|
||||
)}
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<div
|
||||
className="w-[3px] rounded-full"
|
||||
style={{ background: data.color }}
|
||||
/>
|
||||
<div className="col flex-1 gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<SerieIcon name={data.names} />
|
||||
<SerieName name={data.names} />
|
||||
</div>
|
||||
<div className="flex justify-between gap-8 font-mono font-medium">
|
||||
<div className="row gap-1">
|
||||
{number.formatWithUnit(data.count, unit)}
|
||||
{!!data.previous && (
|
||||
<span className="text-muted-foreground">
|
||||
({number.formatWithUnit(data.previous.value, unit)})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<PreviousDiffIndicator {...data.previous} />
|
||||
<PreviousDiffIndicator {...data.previous} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
{hidden.length > 0 && (
|
||||
<div className="text-muted-foreground">and {hidden.length} more...</div>
|
||||
)}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
{hidden.length > 0 && (
|
||||
<div className="text-muted-foreground">
|
||||
and {hidden.length} more...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Portal.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,10 +6,9 @@ import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
||||
import type { IChartData } from '@/trpc/client';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { getChartColor, theme } from '@/utils/theme';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
import type { IInterval } from '@openpanel/validation';
|
||||
|
||||
import { getYAxisWidth } from './chart-utils';
|
||||
import { useChartContext } from './ChartProvider';
|
||||
import { ReportChartTooltip } from './ReportChartTooltip';
|
||||
@@ -21,7 +20,11 @@ interface ReportHistogramChartProps {
|
||||
}
|
||||
|
||||
function BarHover({ x, y, width, height, top, left, right, bottom }: any) {
|
||||
const bg = theme?.colors?.slate?.['200'] as string;
|
||||
const themeMode = useTheme();
|
||||
const bg =
|
||||
themeMode?.theme === 'dark'
|
||||
? theme.colors['def-100']
|
||||
: theme.colors['def-300'];
|
||||
return (
|
||||
<rect
|
||||
{...{ x, y, width, height, top, left, right, bottom }}
|
||||
@@ -33,7 +36,7 @@ function BarHover({ x, y, width, height, top, left, right, bottom }: any) {
|
||||
}
|
||||
|
||||
export function ReportHistogramChart({ data }: ReportHistogramChartProps) {
|
||||
const { editMode, previous, interval } = useChartContext();
|
||||
const { editMode, previous, interval, aspectRatio } = useChartContext();
|
||||
const formatDate = useFormatDateInterval(interval);
|
||||
const { series, setVisibleSeries } = useVisibleSeries(data);
|
||||
const rechartData = useRechartDataModel(series);
|
||||
@@ -41,10 +44,15 @@ export function ReportHistogramChart({ data }: ReportHistogramChartProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn(editMode && 'card p-4')}>
|
||||
<ResponsiveContainer>
|
||||
<div className={cn('w-full', editMode && 'card p-4')}>
|
||||
<ResponsiveContainer aspectRatio={aspectRatio}>
|
||||
{({ width, height }) => (
|
||||
<BarChart width={width} height={height} data={rechartData}>
|
||||
<BarChart
|
||||
width={width}
|
||||
height={height}
|
||||
data={rechartData}
|
||||
barCategoryGap={10}
|
||||
>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
vertical={false}
|
||||
@@ -70,22 +78,45 @@ export function ReportHistogramChart({ data }: ReportHistogramChartProps) {
|
||||
{series.map((serie) => {
|
||||
return (
|
||||
<React.Fragment key={serie.id}>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="colorGradient"
|
||||
x1="0"
|
||||
y1="1"
|
||||
x2="0"
|
||||
y2="0"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={getChartColor(serie.index)}
|
||||
stopOpacity={0.7}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={getChartColor(serie.index)}
|
||||
stopOpacity={1}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{previous && (
|
||||
<Bar
|
||||
key={`${serie.id}:prev`}
|
||||
name={`${serie.id}:prev`}
|
||||
dataKey={`${serie.id}:prev:count`}
|
||||
fill={getChartColor(serie.index)}
|
||||
fillOpacity={0.2}
|
||||
fillOpacity={0.1}
|
||||
radius={3}
|
||||
barSize={20} // Adjust the bar width here
|
||||
/>
|
||||
)}
|
||||
<Bar
|
||||
key={serie.id}
|
||||
name={serie.id}
|
||||
dataKey={`${serie.id}:count`}
|
||||
fill={getChartColor(serie.index)}
|
||||
fill="url(#colorGradient)"
|
||||
radius={3}
|
||||
fillOpacity={1}
|
||||
barSize={20} // Adjust the bar width here
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
@@ -45,7 +45,11 @@ export function ReportLineChart({ data }: ReportLineChartProps) {
|
||||
endDate,
|
||||
range,
|
||||
lineType,
|
||||
aspectRatio,
|
||||
hideXAxis,
|
||||
hideYAxis,
|
||||
} = useChartContext();
|
||||
const dataLength = data.series[0]?.data?.length || 0;
|
||||
const references = api.reference.getChartReferences.useQuery(
|
||||
{
|
||||
projectId,
|
||||
@@ -120,11 +124,10 @@ export function ReportLineChart({ data }: ReportLineChartProps) {
|
||||
}, [series]);
|
||||
|
||||
const isAreaStyle = series.length === 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn(editMode && 'card p-4')}>
|
||||
<ResponsiveContainer>
|
||||
<div className={cn('w-full', editMode && 'card p-4')}>
|
||||
<ResponsiveContainer aspectRatio={aspectRatio}>
|
||||
{({ width, height }) => (
|
||||
<ComposedChart width={width} height={height} data={rechartData}>
|
||||
<CartesianGrid
|
||||
@@ -149,7 +152,7 @@ export function ReportLineChart({ data }: ReportLineChartProps) {
|
||||
/>
|
||||
))}
|
||||
<YAxis
|
||||
width={getYAxisWidth(data.metrics.max)}
|
||||
width={hideYAxis ? 0 : getYAxisWidth(data.metrics.max)}
|
||||
fontSize={12}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
@@ -164,6 +167,7 @@ export function ReportLineChart({ data }: ReportLineChartProps) {
|
||||
)}
|
||||
<Tooltip content={<ReportChartTooltip />} />
|
||||
<XAxis
|
||||
height={hideXAxis ? 0 : undefined}
|
||||
axisLine={false}
|
||||
fontSize={12}
|
||||
dataKey="timestamp"
|
||||
@@ -212,7 +216,7 @@ export function ReportLineChart({ data }: ReportLineChartProps) {
|
||||
)}
|
||||
</defs>
|
||||
<Line
|
||||
dot={isAreaStyle}
|
||||
dot={isAreaStyle && dataLength <= 8}
|
||||
type={lineType}
|
||||
name={serie.id}
|
||||
isAnimationActive={false}
|
||||
|
||||
@@ -1,28 +1,45 @@
|
||||
'use client';
|
||||
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import { DEFAULT_ASPECT_RATIO } from '@openpanel/constants';
|
||||
|
||||
interface ResponsiveContainerProps {
|
||||
children: (props: { width: number; height: number }) => React.ReactNode;
|
||||
aspectRatio?: number;
|
||||
children:
|
||||
| ((props: { width: number; height: number }) => React.ReactNode)
|
||||
| React.ReactNode;
|
||||
}
|
||||
|
||||
export function ResponsiveContainer({ children }: ResponsiveContainerProps) {
|
||||
export function ResponsiveContainer({
|
||||
children,
|
||||
aspectRatio = 0.5625,
|
||||
}: ResponsiveContainerProps) {
|
||||
const maxHeight = 300;
|
||||
const minHeight = 200;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full"
|
||||
style={{
|
||||
aspectRatio: 1 / (aspectRatio || DEFAULT_ASPECT_RATIO),
|
||||
maxHeight,
|
||||
minHeight,
|
||||
}}
|
||||
className={'aspect-video w-full max-sm:-mx-3'}
|
||||
>
|
||||
<AutoSizer disableHeight>
|
||||
{({ width }) =>
|
||||
children({
|
||||
width,
|
||||
height: Math.min(maxHeight, width * 0.5625),
|
||||
})
|
||||
}
|
||||
</AutoSizer>
|
||||
{typeof children === 'function' ? (
|
||||
<AutoSizer disableHeight>
|
||||
{({ width }) =>
|
||||
children({
|
||||
width,
|
||||
height: Math.min(
|
||||
maxHeight,
|
||||
width * aspectRatio || DEFAULT_ASPECT_RATIO
|
||||
),
|
||||
})
|
||||
}
|
||||
</AutoSizer>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Suspense, useEffect, useState } from 'react';
|
||||
import * as Portal from '@radix-ui/react-portal';
|
||||
|
||||
import type { IChartProps } from '@openpanel/validation';
|
||||
|
||||
@@ -24,14 +25,18 @@ export function ChartRoot(props: IChartContextType) {
|
||||
return props.chartType === 'metric' ? (
|
||||
<MetricCardLoading />
|
||||
) : (
|
||||
<ChartLoading />
|
||||
<ChartLoading aspectRatio={props.aspectRatio} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
props.chartType === 'metric' ? <MetricCardLoading /> : <ChartLoading />
|
||||
props.chartType === 'metric' ? (
|
||||
<MetricCardLoading />
|
||||
) : (
|
||||
<ChartLoading aspectRatio={props.aspectRatio} />
|
||||
)
|
||||
}
|
||||
>
|
||||
<ChartProvider {...props}>
|
||||
@@ -49,9 +54,13 @@ interface ChartRootShortcutProps {
|
||||
interval?: IChartProps['interval'];
|
||||
events: IChartProps['events'];
|
||||
breakdowns?: IChartProps['breakdowns'];
|
||||
lineType?: IChartProps['lineType'];
|
||||
hideXAxis?: boolean;
|
||||
aspectRatio?: number;
|
||||
}
|
||||
|
||||
export const ChartRootShortcut = ({
|
||||
hideXAxis,
|
||||
projectId,
|
||||
range = '7d',
|
||||
previous = false,
|
||||
@@ -59,19 +68,25 @@ export const ChartRootShortcut = ({
|
||||
interval = 'day',
|
||||
events,
|
||||
breakdowns,
|
||||
aspectRatio,
|
||||
lineType = 'monotone',
|
||||
}: ChartRootShortcutProps) => {
|
||||
return (
|
||||
<ChartRoot
|
||||
projectId={projectId}
|
||||
range={range}
|
||||
breakdowns={breakdowns ?? []}
|
||||
previous={previous}
|
||||
chartType={chartType}
|
||||
interval={interval}
|
||||
name="Random"
|
||||
lineType="bump"
|
||||
metric="sum"
|
||||
events={events}
|
||||
/>
|
||||
<Portal.Root>
|
||||
<ChartRoot
|
||||
projectId={projectId}
|
||||
range={range}
|
||||
breakdowns={breakdowns ?? []}
|
||||
previous={previous}
|
||||
chartType={chartType}
|
||||
interval={interval}
|
||||
name="Random"
|
||||
lineType={lineType}
|
||||
metric="sum"
|
||||
events={events}
|
||||
aspectRatio={aspectRatio}
|
||||
hideXAxis={hideXAxis}
|
||||
/>
|
||||
</Portal.Root>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -110,6 +110,39 @@ const TableCaption = React.forwardRef<
|
||||
));
|
||||
TableCaption.displayName = 'TableCaption';
|
||||
|
||||
export function TableSkeleton({
|
||||
rows = 10,
|
||||
cols = 2,
|
||||
}: {
|
||||
rows?: number;
|
||||
cols?: number;
|
||||
}) {
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{Array.from({ length: cols }).map((_, j) => (
|
||||
<TableHead key={j}>
|
||||
<div className="h-4 w-1/4 animate-pulse rounded-full bg-def-300" />
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{Array.from({ length: rows }).map((_, i) => (
|
||||
<TableRow key={i}>
|
||||
{Array.from({ length: cols }).map((_, j) => (
|
||||
<TableCell key={j}>
|
||||
<div className="h-4 w-1/4 min-w-20 animate-pulse rounded-full bg-def-300" />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
|
||||
@@ -37,7 +37,7 @@ export function WidgetTitle({
|
||||
)}
|
||||
>
|
||||
{Icon && (
|
||||
<div className="bg-def-200 absolute left-0 rounded-lg p-2">
|
||||
<div className="absolute left-0 rounded-lg bg-def-200 p-2">
|
||||
<Icon size={18} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user