fix: dashboard colors, users rank
This commit is contained in:
@@ -35,6 +35,7 @@
|
|||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
|
"culori": "^4.0.2",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
"dompurify": "^3.2.6",
|
"dompurify": "^3.2.6",
|
||||||
"driver.js": "^1.3.1",
|
"driver.js": "^1.3.1",
|
||||||
|
|||||||
Generated
+9
@@ -77,6 +77,9 @@ importers:
|
|||||||
crypto-js:
|
crypto-js:
|
||||||
specifier: ^4.2.0
|
specifier: ^4.2.0
|
||||||
version: 4.2.0
|
version: 4.2.0
|
||||||
|
culori:
|
||||||
|
specifier: ^4.0.2
|
||||||
|
version: 4.0.2
|
||||||
dayjs:
|
dayjs:
|
||||||
specifier: ^1.11.11
|
specifier: ^1.11.11
|
||||||
version: 1.11.20
|
version: 1.11.20
|
||||||
@@ -3810,6 +3813,10 @@ packages:
|
|||||||
csstype@3.2.3:
|
csstype@3.2.3:
|
||||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==, tarball: https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz}
|
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==, tarball: https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz}
|
||||||
|
|
||||||
|
culori@4.0.2:
|
||||||
|
resolution: {integrity: sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw==, tarball: https://registry.npmjs.org/culori/-/culori-4.0.2.tgz}
|
||||||
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
||||||
current-script-polyfill@1.0.0:
|
current-script-polyfill@1.0.0:
|
||||||
resolution: {integrity: sha512-qv8s+G47V6Hq+g2kRE5th+ASzzrL7b6l+tap1DHKK25ZQJv3yIFhH96XaQ7NGL+zRW3t/RDbweJf/dJDe5Z5KA==, tarball: https://registry.npmjs.org/current-script-polyfill/-/current-script-polyfill-1.0.0.tgz}
|
resolution: {integrity: sha512-qv8s+G47V6Hq+g2kRE5th+ASzzrL7b6l+tap1DHKK25ZQJv3yIFhH96XaQ7NGL+zRW3t/RDbweJf/dJDe5Z5KA==, tarball: https://registry.npmjs.org/current-script-polyfill/-/current-script-polyfill-1.0.0.tgz}
|
||||||
|
|
||||||
@@ -13699,6 +13706,8 @@ snapshots:
|
|||||||
|
|
||||||
csstype@3.2.3: {}
|
csstype@3.2.3: {}
|
||||||
|
|
||||||
|
culori@4.0.2: {}
|
||||||
|
|
||||||
current-script-polyfill@1.0.0: {}
|
current-script-polyfill@1.0.0: {}
|
||||||
|
|
||||||
d@1.0.2:
|
d@1.0.2:
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
|
||||||
|
import { converter, modeHsl, modeRgb, useMode } from 'culori/fn';
|
||||||
|
|
||||||
|
const toHsl = converter('hsl');
|
||||||
|
|
||||||
|
const DEFAULT_BRAND_HUE = 211; // brand color
|
||||||
|
const COOL_HUE_START = 180;
|
||||||
|
const COOL_HUE_END = 280;
|
||||||
|
const COOL_HUE_RANGE = COOL_HUE_END - COOL_HUE_START;
|
||||||
|
const GOLDEN_RATIO_CONJUGATE = 0.618033988749895;
|
||||||
|
|
||||||
|
function colorStringToHue(input?: string): number | undefined {
|
||||||
|
if (!input) return undefined;
|
||||||
|
const color = toHsl(input);
|
||||||
|
if (!color || typeof color.h !== 'number') return undefined;
|
||||||
|
return color.h;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useCoolColors() {
|
||||||
|
// const brandHue = useMemo(
|
||||||
|
// () => colorStringToHue(userSettings.colorPrimary) ?? DEFAULT_BRAND_HUE,
|
||||||
|
// [userSettings.colorPrimary]
|
||||||
|
// );
|
||||||
|
useMode(modeRgb);
|
||||||
|
useMode(modeHsl);
|
||||||
|
|
||||||
|
const brandHue = DEFAULT_BRAND_HUE;
|
||||||
|
|
||||||
|
return useMemoizedFn((count: number): string[] => {
|
||||||
|
if (count <= 0) return [];
|
||||||
|
|
||||||
|
const colors: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
let hue: number;
|
||||||
|
|
||||||
|
if (i === 0) {
|
||||||
|
hue = brandHue - 5;
|
||||||
|
} else {
|
||||||
|
const offset = (i * GOLDEN_RATIO_CONJUGATE) % 1;
|
||||||
|
hue = COOL_HUE_START + offset * COOL_HUE_RANGE;
|
||||||
|
|
||||||
|
if (Math.abs(hue - brandHue) < 5) {
|
||||||
|
hue = (hue + 10) % COOL_HUE_END;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const s = i === 0 ? 100 : 75 + (i % 3) * 5;
|
||||||
|
const l = i === 0 ? 50 : 55 + (i % 2) * 5;
|
||||||
|
|
||||||
|
colors.push(`hsl(${Math.round(hue)}, ${s}%, ${l}%)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return colors;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,27 +1,10 @@
|
|||||||
|
import useCoolColors from '@/hooks/use-cool-colors';
|
||||||
import { Chart } from '@gpustack/core-ui';
|
import { Chart } from '@gpustack/core-ui';
|
||||||
import { formatLargeNumber } from '@gpustack/core-ui/utils';
|
import { formatLargeNumber } from '@gpustack/core-ui/utils';
|
||||||
import { theme } from 'antd';
|
import { theme } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { useEffect, useMemo, useRef } from 'react';
|
import React, { useEffect, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
const COOL_HUE_START = 180;
|
|
||||||
const COOL_HUE_END = 280;
|
|
||||||
const COOL_HUE_RANGE = COOL_HUE_END - COOL_HUE_START;
|
|
||||||
// golden angle (137.508) mod COOL_HUE_RANGE — coprime offset for even spread
|
|
||||||
const HUE_STEP = 37.508;
|
|
||||||
|
|
||||||
export function generateCoolColors(count: number): string[] {
|
|
||||||
if (count <= 0) return [];
|
|
||||||
const colors: string[] = [];
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const hue = COOL_HUE_START + ((i * HUE_STEP) % COOL_HUE_RANGE);
|
|
||||||
const saturation = 55 + (i % 3) * 5;
|
|
||||||
const lightness = 60 + (i % 2) * 6;
|
|
||||||
colors.push(`hsl(${Math.round(hue)}, ${saturation}%, ${lightness}%)`);
|
|
||||||
}
|
|
||||||
return colors;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BarSeriesItem {
|
export interface BarSeriesItem {
|
||||||
name: string;
|
name: string;
|
||||||
data: any[];
|
data: any[];
|
||||||
@@ -55,10 +38,11 @@ const BarChart: React.FC<BarChartProps> = (props) => {
|
|||||||
} = props;
|
} = props;
|
||||||
const { token } = theme.useToken();
|
const { token } = theme.useToken();
|
||||||
const chartRef = useRef<{ chart: any } | null>(null);
|
const chartRef = useRef<{ chart: any } | null>(null);
|
||||||
|
const generateCoolColors = useCoolColors();
|
||||||
|
|
||||||
const dynamicColors = useMemo(
|
const dynamicColors = useMemo(
|
||||||
() => generateCoolColors(seriesData.length),
|
() => generateCoolColors(seriesData.length),
|
||||||
[seriesData.length]
|
[seriesData.length, generateCoolColors]
|
||||||
);
|
);
|
||||||
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import useCoolColors from '@/hooks/use-cool-colors';
|
||||||
import { Chart } from '@gpustack/core-ui';
|
import { Chart } from '@gpustack/core-ui';
|
||||||
import { formatLargeNumber } from '@gpustack/core-ui/utils';
|
import { formatLargeNumber } from '@gpustack/core-ui/utils';
|
||||||
import { Empty, Spin, theme } from 'antd';
|
import { Empty, Spin, theme } from 'antd';
|
||||||
import React, { useMemo, useRef } from 'react';
|
import React, { useMemo, useRef } from 'react';
|
||||||
import { generateCoolColors } from '../bar-chart';
|
|
||||||
|
|
||||||
export interface PieChartItem {
|
export interface PieChartItem {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -30,11 +30,12 @@ const PieChart: React.FC<PieChartProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { token } = theme.useToken();
|
const { token } = theme.useToken();
|
||||||
const chartRef = useRef<{ chart: any } | null>(null);
|
const chartRef = useRef<{ chart: any } | null>(null);
|
||||||
|
const generateCoolColors = useCoolColors();
|
||||||
|
|
||||||
const colors = useMemo(() => {
|
const colors = useMemo(() => {
|
||||||
const generatedColors = generateCoolColors(data.length + colorOffset);
|
const generatedColors = generateCoolColors(data.length + colorOffset);
|
||||||
return generatedColors.slice(colorOffset);
|
return generatedColors.slice(colorOffset);
|
||||||
}, [colorOffset, data.length]);
|
}, [colorOffset, data.length, generateCoolColors]);
|
||||||
|
|
||||||
const options = useMemo(
|
const options = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
import { activeModelsAtom } from '@/atoms/dashboard';
|
|
||||||
import useQueryBreakdownList from '@/pages/usage/services/use-query-breakdown-list';
|
import useQueryBreakdownList from '@/pages/usage/services/use-query-breakdown-list';
|
||||||
import useQueryTimeSeriesData from '@/pages/usage/services/use-query-timeseries-data';
|
import useQueryTimeSeriesData from '@/pages/usage/services/use-query-timeseries-data';
|
||||||
import { formatLargeNumber } from '@/utils';
|
|
||||||
import { PageTools } from '@gpustack/core-ui';
|
import { PageTools } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Col, Row } from 'antd';
|
import { Col, Row } from 'antd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { useContext, useEffect, useMemo } from 'react';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import {
|
import { DashboardUsageCommonParams } from '../config';
|
||||||
baseColorMap,
|
|
||||||
DashboardUsageCommonParams,
|
|
||||||
getUsageSummary
|
|
||||||
} from '../config';
|
|
||||||
import { DashboardContext } from '../config/dashboard-context';
|
|
||||||
import ApiRequestsByModel from './usage-charts/api-requests-by-model';
|
import ApiRequestsByModel from './usage-charts/api-requests-by-model';
|
||||||
import TokenUsageByModel from './usage-charts/token-usage-by-model';
|
import TokenUsageByModel from './usage-charts/token-usage-by-model';
|
||||||
import TopTokenUsageByApiKey from './usage-charts/top-token-usage-by-api-key';
|
import TopTokenUsageByApiKey from './usage-charts/top-token-usage-by-api-key';
|
||||||
@@ -26,9 +18,6 @@ const Section = styled.div`
|
|||||||
|
|
||||||
const NewUsage = () => {
|
const NewUsage = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const activeModelsFromAtom = useAtomValue(activeModelsAtom);
|
|
||||||
const { active_models: activeModelsFromContext = [] } =
|
|
||||||
useContext(DashboardContext);
|
|
||||||
const tokenByModelQuery = useQueryTimeSeriesData({
|
const tokenByModelQuery = useQueryTimeSeriesData({
|
||||||
key: 'tokenUsageByModelData'
|
key: 'tokenUsageByModelData'
|
||||||
});
|
});
|
||||||
@@ -54,10 +43,6 @@ const NewUsage = () => {
|
|||||||
[dateRange]
|
[dateRange]
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeModels = activeModelsFromAtom.length
|
|
||||||
? activeModelsFromAtom
|
|
||||||
: activeModelsFromContext;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -66,15 +51,6 @@ const NewUsage = () => {
|
|||||||
metric: 'total_tokens',
|
metric: 'total_tokens',
|
||||||
group_by: ['date', 'model'],
|
group_by: ['date', 'model'],
|
||||||
sort_by: '-total_tokens'
|
sort_by: '-total_tokens'
|
||||||
}),
|
|
||||||
activeUsersQuery.fetchData({
|
|
||||||
...dateRange,
|
|
||||||
scope: 'all',
|
|
||||||
group_by: ['user'],
|
|
||||||
page: 1,
|
|
||||||
perPage: 10,
|
|
||||||
sort_by: '-total_tokens',
|
|
||||||
filters: {}
|
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
@@ -82,41 +58,6 @@ const NewUsage = () => {
|
|||||||
fetchData().catch(() => {});
|
fetchData().catch(() => {});
|
||||||
}, [commonParams, dateRange]);
|
}, [commonParams, dateRange]);
|
||||||
|
|
||||||
const summaryCards = useMemo(() => {
|
|
||||||
const summary = getUsageSummary(tokenByModelQuery.detailData) || {
|
|
||||||
total_tokens: 0,
|
|
||||||
api_requests: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: formatLargeNumber(summary.total_tokens) as string,
|
|
||||||
value: intl.formatMessage({ id: 'dashboard.tokens' }),
|
|
||||||
color: baseColorMap.base
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: formatLargeNumber(summary.api_requests) as string,
|
|
||||||
value: intl.formatMessage({ id: 'dashboard.apirequest' }),
|
|
||||||
color: baseColorMap.baseR1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: formatLargeNumber(activeModels.length) as string,
|
|
||||||
value: intl.formatMessage({ id: 'dashboard.activeModels' }),
|
|
||||||
color: baseColorMap.baseR2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: formatLargeNumber(activeUsersQuery.dataSource.total) as string,
|
|
||||||
value: intl.formatMessage({ id: 'dashboard.activeUsers' }),
|
|
||||||
color: baseColorMap.baseR3
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}, [
|
|
||||||
activeModels.length,
|
|
||||||
activeUsersQuery.dataSource.total,
|
|
||||||
intl,
|
|
||||||
tokenByModelQuery.detailData
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageTools
|
<PageTools
|
||||||
@@ -143,10 +84,10 @@ const NewUsage = () => {
|
|||||||
<Section>
|
<Section>
|
||||||
<Row gutter={[20, 20]}>
|
<Row gutter={[20, 20]}>
|
||||||
<Col xs={24} sm={24} md={24} lg={12}>
|
<Col xs={24} sm={24} md={24} lg={12}>
|
||||||
<TopTokenUsageByUser commonParams={commonParams} height={450} />
|
<TopTokenUsageByUser commonParams={commonParams} height={460} />
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={24} md={24} lg={12}>
|
<Col xs={24} sm={24} md={24} lg={12}>
|
||||||
<TopTokenUsageByApiKey commonParams={commonParams} height={450} />
|
<TopTokenUsageByApiKey commonParams={commonParams} height={460} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const ApiRequestsByModel: React.FC<ApiRequestsByModelProps> = ({
|
|||||||
data={chartData}
|
data={chartData}
|
||||||
height={usageChartHeight}
|
height={usageChartHeight}
|
||||||
loading={query.loading}
|
loading={query.loading}
|
||||||
colorOffset={1}
|
colorOffset={5}
|
||||||
total={total}
|
total={total}
|
||||||
/>
|
/>
|
||||||
</UsageChartCard>
|
</UsageChartCard>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const TokenUsageByModel: React.FC<TokenUsageByModelProps> = ({
|
|||||||
data={chartData}
|
data={chartData}
|
||||||
height={usageChartHeight}
|
height={usageChartHeight}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
colorOffset={3}
|
colorOffset={2}
|
||||||
total={total}
|
total={total}
|
||||||
/>
|
/>
|
||||||
</UsageChartCard>
|
</UsageChartCard>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import BarChart, { generateCoolColors } from '@/pages/_components/bar-chart';
|
import useCoolColors from '@/hooks/use-cool-colors';
|
||||||
|
import BarChart from '@/pages/_components/bar-chart';
|
||||||
import { BaseSelect, CardWrapper } from '@gpustack/core-ui';
|
import { BaseSelect, CardWrapper } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Segmented } from 'antd';
|
import { Segmented } from 'antd';
|
||||||
@@ -78,6 +79,7 @@ const DailyUsage: React.FC<DailyUsageProps> = (props) => {
|
|||||||
onGroupByChange,
|
onGroupByChange,
|
||||||
onGranularityChange
|
onGranularityChange
|
||||||
} = props;
|
} = props;
|
||||||
|
const generateCoolColors = useCoolColors();
|
||||||
|
|
||||||
const labelFormatter = (v: any) => {
|
const labelFormatter = (v: any) => {
|
||||||
if (granularity === 'month') {
|
if (granularity === 'month') {
|
||||||
@@ -237,7 +239,7 @@ const DailyUsage: React.FC<DailyUsageProps> = (props) => {
|
|||||||
xAxisData: xAxis,
|
xAxisData: xAxis,
|
||||||
legendData: dedupedLegend
|
legendData: dedupedLegend
|
||||||
};
|
};
|
||||||
}, [timeSeriesData, intl]);
|
}, [timeSeriesData, intl, generateCoolColors]);
|
||||||
|
|
||||||
const handleOnGroupByChange = (value: string) => {
|
const handleOnGroupByChange = (value: string) => {
|
||||||
onGroupByChange(value || null);
|
onGroupByChange(value || null);
|
||||||
|
|||||||
Reference in New Issue
Block a user