mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-24 01:42:55 +08:00
feat: doc status & share status (#14426)
#### PR Dependency Tree * **PR #14426** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Admin dashboard: view workspace analytics (storage, sync activity, top shared links) with charts and configurable windows. * Document analytics tab: see total/unique/guest views and trends over selectable time windows. * Last-accessed members: view who last accessed a document, with pagination. * Shared links analytics: browse and paginate all shared links with view/unique/guest metrics and share URLs. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
173
packages/frontend/admin/src/components/ui/chart.tsx
Normal file
173
packages/frontend/admin/src/components/ui/chart.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import { cn } from '@affine/admin/utils';
|
||||
import * as React from 'react';
|
||||
import type { TooltipProps } from 'recharts';
|
||||
import { ResponsiveContainer, Tooltip as RechartsTooltip } from 'recharts';
|
||||
|
||||
const THEMES = { light: '', dark: '.dark' } as const;
|
||||
|
||||
export type ChartConfig = Record<
|
||||
string,
|
||||
{
|
||||
label?: React.ReactNode;
|
||||
color?: string;
|
||||
theme?: Partial<Record<keyof typeof THEMES, string>>;
|
||||
}
|
||||
>;
|
||||
|
||||
type ChartContextValue = {
|
||||
config: ChartConfig;
|
||||
};
|
||||
|
||||
const ChartContext = React.createContext<ChartContextValue | null>(null);
|
||||
|
||||
function useChart() {
|
||||
const value = React.useContext(ChartContext);
|
||||
if (!value) {
|
||||
throw new Error('useChart must be used within <ChartContainer />');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function ChartStyle({
|
||||
chartId,
|
||||
config,
|
||||
}: {
|
||||
chartId: string;
|
||||
config: ChartConfig;
|
||||
}) {
|
||||
const colorEntries = Object.entries(config).filter(
|
||||
([, item]) => item.color || item.theme
|
||||
);
|
||||
|
||||
if (!colorEntries.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const css = Object.entries(THEMES)
|
||||
.map(([themeKey, prefix]) => {
|
||||
const declarations = colorEntries
|
||||
.map(([key, item]) => {
|
||||
const color =
|
||||
item.theme?.[themeKey as keyof typeof THEMES] ?? item.color;
|
||||
return color ? ` --color-${key}: ${color};` : '';
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
|
||||
if (!declarations) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `${prefix} [data-chart="${chartId}"] {\n${declarations}\n}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
|
||||
if (!css) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <style dangerouslySetInnerHTML={{ __html: css }} />;
|
||||
}
|
||||
|
||||
type ChartContainerProps = React.ComponentProps<'div'> & {
|
||||
config: ChartConfig;
|
||||
children: React.ComponentProps<typeof ResponsiveContainer>['children'];
|
||||
};
|
||||
|
||||
const ChartContainer = React.forwardRef<HTMLDivElement, ChartContainerProps>(
|
||||
({ id, className, children, config, ...props }, ref) => {
|
||||
const uniqueId = React.useId();
|
||||
const chartId = `chart-${id ?? uniqueId.replace(/:/g, '')}`;
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
ref={ref}
|
||||
data-chart={chartId}
|
||||
className={cn(
|
||||
'flex min-h-0 w-full items-center justify-center text-xs',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle chartId={chartId} config={config} />
|
||||
<ResponsiveContainer>{children}</ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
);
|
||||
}
|
||||
);
|
||||
ChartContainer.displayName = 'ChartContainer';
|
||||
|
||||
const ChartTooltip = RechartsTooltip;
|
||||
|
||||
type TooltipContentProps = {
|
||||
active?: boolean;
|
||||
payload?: TooltipProps<number, string>['payload'];
|
||||
label?: string | number;
|
||||
labelFormatter?: (
|
||||
label: string | number,
|
||||
payload: TooltipProps<number, string>['payload']
|
||||
) => React.ReactNode;
|
||||
valueFormatter?: (value: number, key: string) => React.ReactNode;
|
||||
};
|
||||
|
||||
const ChartTooltipContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
TooltipContentProps
|
||||
>(({ active, payload, label, labelFormatter, valueFormatter }, ref) => {
|
||||
const { config } = useChart();
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const title = labelFormatter ? labelFormatter(label ?? '', payload) : label;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="min-w-44 rounded-md border bg-popover px-3 py-2 text-xs text-popover-foreground shadow-md"
|
||||
>
|
||||
{title ? (
|
||||
<div className="mb-2 font-medium text-foreground/90">{title}</div>
|
||||
) : null}
|
||||
<div className="space-y-1">
|
||||
{payload.map((item, index) => {
|
||||
const dataKey = String(item.dataKey ?? item.name ?? index);
|
||||
const itemConfig = config[dataKey];
|
||||
const labelText = itemConfig?.label ?? item.name ?? dataKey;
|
||||
const numericValue =
|
||||
typeof item.value === 'number'
|
||||
? item.value
|
||||
: Number(item.value ?? 0);
|
||||
const valueText = valueFormatter
|
||||
? valueFormatter(numericValue, dataKey)
|
||||
: numericValue;
|
||||
const color = item.color ?? `var(--color-${dataKey})`;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${dataKey}-${index}`}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<span
|
||||
className="h-2 w-2 rounded-full"
|
||||
style={{ backgroundColor: color }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="text-muted-foreground">{labelText}</span>
|
||||
<span className="ml-auto font-medium tabular-nums">
|
||||
{valueText}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
ChartTooltipContent.displayName = 'ChartTooltipContent';
|
||||
|
||||
export { ChartContainer, ChartTooltip, ChartTooltipContent };
|
||||
Reference in New Issue
Block a user