mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
feat(ui): add floating action controls and trace refresh for provider workflows
This commit is contained in:
@@ -82,3 +82,51 @@
|
||||
gap: $spacing-lg;
|
||||
}
|
||||
|
||||
.contentWithFloatingAction {
|
||||
padding-bottom: calc(
|
||||
var(--secondary-shell-floating-action-height, 56px) + 12px + env(safe-area-inset-bottom)
|
||||
);
|
||||
}
|
||||
|
||||
.floatingActionContainer {
|
||||
position: fixed;
|
||||
left: var(--content-center-x, 50%);
|
||||
bottom: calc(12px + env(safe-area-inset-bottom));
|
||||
transform: translateX(-50%);
|
||||
z-index: 50;
|
||||
pointer-events: none;
|
||||
width: fit-content;
|
||||
max-width: calc(100vw - 24px);
|
||||
}
|
||||
|
||||
.floatingActionSurface {
|
||||
pointer-events: auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--bg-primary) 82%, transparent);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 60%, transparent);
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
:global([data-theme='dark']) {
|
||||
.floatingActionSurface {
|
||||
background: color-mix(in srgb, var(--bg-primary) 82%, transparent);
|
||||
border-color: color-mix(in srgb, var(--border-color) 55%, transparent);
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.floatingActionContainer {
|
||||
max-width: calc(100vw - 16px);
|
||||
}
|
||||
|
||||
.floatingActionSurface {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { forwardRef, type ReactNode } from 'react';
|
||||
import { forwardRef, useLayoutEffect, useRef, type ReactNode } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
||||
import { IconChevronLeft } from '@/components/ui/icons';
|
||||
import { usePageTransitionLayer } from './PageTransitionLayer';
|
||||
import styles from './SecondaryScreenShell.module.scss';
|
||||
|
||||
export type SecondaryScreenShellProps = {
|
||||
@@ -10,6 +12,9 @@ export type SecondaryScreenShellProps = {
|
||||
backLabel?: string;
|
||||
backAriaLabel?: string;
|
||||
rightAction?: ReactNode;
|
||||
hideTopBarBackButton?: boolean;
|
||||
hideTopBarRightAction?: boolean;
|
||||
floatingAction?: ReactNode;
|
||||
isLoading?: boolean;
|
||||
loadingLabel?: ReactNode;
|
||||
className?: string;
|
||||
@@ -25,6 +30,9 @@ export const SecondaryScreenShell = forwardRef<HTMLDivElement, SecondaryScreenSh
|
||||
backLabel = 'Back',
|
||||
backAriaLabel,
|
||||
rightAction,
|
||||
hideTopBarBackButton = false,
|
||||
hideTopBarRightAction = false,
|
||||
floatingAction,
|
||||
isLoading = false,
|
||||
loadingLabel = 'Loading...',
|
||||
className = '',
|
||||
@@ -34,45 +42,94 @@ export const SecondaryScreenShell = forwardRef<HTMLDivElement, SecondaryScreenSh
|
||||
ref
|
||||
) {
|
||||
const containerClassName = [styles.container, className].filter(Boolean).join(' ');
|
||||
const contentClasses = [styles.content, contentClassName].filter(Boolean).join(' ');
|
||||
const contentClasses = [
|
||||
styles.content,
|
||||
floatingAction ? styles.contentWithFloatingAction : '',
|
||||
contentClassName,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
const titleTooltip = typeof title === 'string' ? title : undefined;
|
||||
const resolvedBackAriaLabel = backAriaLabel ?? backLabel;
|
||||
const pageTransitionLayer = usePageTransitionLayer();
|
||||
const isCurrentLayer = pageTransitionLayer ? pageTransitionLayer.status === 'current' : true;
|
||||
const shouldRenderFloatingAction = Boolean(floatingAction) && isCurrentLayer;
|
||||
const floatingActionRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!shouldRenderFloatingAction) return;
|
||||
|
||||
const element = floatingActionRef.current;
|
||||
if (!element) return;
|
||||
|
||||
const updateHeight = () => {
|
||||
const height = element.getBoundingClientRect().height;
|
||||
document.documentElement.style.setProperty(
|
||||
'--secondary-shell-floating-action-height',
|
||||
`${height}px`
|
||||
);
|
||||
};
|
||||
|
||||
updateHeight();
|
||||
window.addEventListener('resize', updateHeight);
|
||||
|
||||
const resizeObserver =
|
||||
typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(updateHeight);
|
||||
resizeObserver?.observe(element);
|
||||
|
||||
return () => {
|
||||
resizeObserver?.disconnect();
|
||||
window.removeEventListener('resize', updateHeight);
|
||||
document.documentElement.style.removeProperty('--secondary-shell-floating-action-height');
|
||||
};
|
||||
}, [shouldRenderFloatingAction]);
|
||||
|
||||
return (
|
||||
<div className={containerClassName} ref={ref}>
|
||||
<div className={styles.topBar}>
|
||||
{onBack ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onBack}
|
||||
className={styles.backButton}
|
||||
aria-label={resolvedBackAriaLabel}
|
||||
>
|
||||
<span className={styles.backIcon}>
|
||||
<IconChevronLeft size={18} />
|
||||
</span>
|
||||
<span className={styles.backText}>{backLabel}</span>
|
||||
</Button>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<div className={styles.topBarTitle} title={titleTooltip}>
|
||||
{title}
|
||||
<>
|
||||
<div className={containerClassName} ref={ref}>
|
||||
<div className={styles.topBar}>
|
||||
{onBack && !hideTopBarBackButton ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onBack}
|
||||
className={styles.backButton}
|
||||
aria-label={resolvedBackAriaLabel}
|
||||
>
|
||||
<span className={styles.backIcon}>
|
||||
<IconChevronLeft size={18} />
|
||||
</span>
|
||||
<span className={styles.backText}>{backLabel}</span>
|
||||
</Button>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<div className={styles.topBarTitle} title={titleTooltip}>
|
||||
{title}
|
||||
</div>
|
||||
<div className={styles.rightSlot}>{hideTopBarRightAction ? null : rightAction}</div>
|
||||
</div>
|
||||
<div className={styles.rightSlot}>{rightAction}</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className={styles.loadingState}>
|
||||
<LoadingSpinner size={16} />
|
||||
<span>{loadingLabel}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className={contentClasses}>{children}</div>
|
||||
)}
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className={styles.loadingState}>
|
||||
<LoadingSpinner size={16} />
|
||||
<span>{loadingLabel}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className={contentClasses}>{children}</div>
|
||||
)}
|
||||
</div>
|
||||
{shouldRenderFloatingAction && typeof document !== 'undefined'
|
||||
? createPortal(
|
||||
<div className={styles.floatingActionContainer}>
|
||||
<div className={styles.floatingActionSurface} ref={floatingActionRef}>
|
||||
{floatingAction}
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -271,10 +271,28 @@ export function AiProvidersAmpcodeEditPage() {
|
||||
onBack={handleBack}
|
||||
backLabel={t('common.back')}
|
||||
backAriaLabel={t('common.back')}
|
||||
rightAction={
|
||||
<Button size="sm" onClick={() => void saveAmpcode()} loading={saving} disabled={!canSave}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
hideTopBarBackButton
|
||||
hideTopBarRightAction
|
||||
floatingAction={
|
||||
<div className={layoutStyles.floatingActions}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className={layoutStyles.floatingBackButton}
|
||||
>
|
||||
{t('common.back')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => void saveAmpcode()}
|
||||
loading={saving}
|
||||
disabled={!canSave}
|
||||
className={layoutStyles.floatingSaveButton}
|
||||
>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
isLoading={loading}
|
||||
loadingLabel={t('common.loading')}
|
||||
|
||||
@@ -272,10 +272,28 @@ export function AiProvidersClaudeEditPage() {
|
||||
onBack={handleBack}
|
||||
backLabel={t('common.back')}
|
||||
backAriaLabel={t('common.back')}
|
||||
rightAction={
|
||||
<Button size="sm" onClick={() => void handleSave()} loading={saving} disabled={!canSave}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
hideTopBarBackButton
|
||||
hideTopBarRightAction
|
||||
floatingAction={
|
||||
<div className={layoutStyles.floatingActions}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className={layoutStyles.floatingBackButton}
|
||||
>
|
||||
{t('common.back')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => void handleSave()}
|
||||
loading={saving}
|
||||
disabled={!canSave}
|
||||
className={layoutStyles.floatingSaveButton}
|
||||
>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
isLoading={loading}
|
||||
loadingLabel={t('common.loading')}
|
||||
|
||||
@@ -163,10 +163,27 @@ export function AiProvidersClaudeModelsPage() {
|
||||
onBack={handleBack}
|
||||
backLabel={t('common.back')}
|
||||
backAriaLabel={t('common.back')}
|
||||
rightAction={
|
||||
<Button size="sm" onClick={handleApply} disabled={!canApply}>
|
||||
{t('ai_providers.claude_models_fetch_apply')}
|
||||
</Button>
|
||||
hideTopBarBackButton
|
||||
hideTopBarRightAction
|
||||
floatingAction={
|
||||
<div className={layoutStyles.floatingActions}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className={layoutStyles.floatingBackButton}
|
||||
>
|
||||
{t('common.back')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleApply}
|
||||
disabled={!canApply}
|
||||
className={layoutStyles.floatingSaveButton}
|
||||
>
|
||||
{t('ai_providers.claude_models_fetch_apply')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
isLoading={initialLoading}
|
||||
loadingLabel={t('common.loading')}
|
||||
|
||||
@@ -417,10 +417,28 @@ export function AiProvidersCodexEditPage() {
|
||||
onBack={handleBack}
|
||||
backLabel={t('common.back')}
|
||||
backAriaLabel={t('common.back')}
|
||||
rightAction={
|
||||
<Button size="sm" onClick={handleSave} loading={saving} disabled={!canSave}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
hideTopBarBackButton
|
||||
hideTopBarRightAction
|
||||
floatingAction={
|
||||
<div className={layoutStyles.floatingActions}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className={layoutStyles.floatingBackButton}
|
||||
>
|
||||
{t('common.back')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
loading={saving}
|
||||
disabled={!canSave}
|
||||
className={layoutStyles.floatingSaveButton}
|
||||
>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
isLoading={loading}
|
||||
loadingLabel={t('common.loading')}
|
||||
|
||||
@@ -4,6 +4,20 @@
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.floatingActions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.floatingBackButton {
|
||||
min-width: 82px;
|
||||
}
|
||||
|
||||
.floatingSaveButton {
|
||||
min-width: 88px;
|
||||
}
|
||||
|
||||
.upstreamApiKeyRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -411,10 +411,28 @@ export function AiProvidersGeminiEditPage() {
|
||||
onBack={handleBack}
|
||||
backLabel={t('common.back')}
|
||||
backAriaLabel={t('common.back')}
|
||||
rightAction={
|
||||
<Button size="sm" onClick={handleSave} loading={saving} disabled={!canSave}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
hideTopBarBackButton
|
||||
hideTopBarRightAction
|
||||
floatingAction={
|
||||
<div className={layoutStyles.floatingActions}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className={layoutStyles.floatingBackButton}
|
||||
>
|
||||
{t('common.back')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
loading={saving}
|
||||
disabled={!canSave}
|
||||
className={layoutStyles.floatingSaveButton}
|
||||
>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
isLoading={loading}
|
||||
loadingLabel={t('common.loading')}
|
||||
|
||||
@@ -501,10 +501,28 @@ export function AiProvidersOpenAIEditPage() {
|
||||
onBack={handleBack}
|
||||
backLabel={t('common.back')}
|
||||
backAriaLabel={t('common.back')}
|
||||
rightAction={
|
||||
<Button size="sm" onClick={() => void handleSave()} loading={saving} disabled={!canSave}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
hideTopBarBackButton
|
||||
hideTopBarRightAction
|
||||
floatingAction={
|
||||
<div className={layoutStyles.floatingActions}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className={layoutStyles.floatingBackButton}
|
||||
>
|
||||
{t('common.back')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => void handleSave()}
|
||||
loading={saving}
|
||||
disabled={!canSave}
|
||||
className={layoutStyles.floatingSaveButton}
|
||||
>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
isLoading={loading}
|
||||
loadingLabel={t('common.loading')}
|
||||
|
||||
@@ -144,10 +144,27 @@ export function AiProvidersOpenAIModelsPage() {
|
||||
onBack={handleBack}
|
||||
backLabel={t('common.back')}
|
||||
backAriaLabel={t('common.back')}
|
||||
rightAction={
|
||||
<Button size="sm" onClick={handleApply} disabled={!canApply}>
|
||||
{t('ai_providers.openai_models_fetch_apply')}
|
||||
</Button>
|
||||
hideTopBarBackButton
|
||||
hideTopBarRightAction
|
||||
floatingAction={
|
||||
<div className={layoutStyles.floatingActions}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className={layoutStyles.floatingBackButton}
|
||||
>
|
||||
{t('common.back')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleApply}
|
||||
disabled={!canApply}
|
||||
className={layoutStyles.floatingSaveButton}
|
||||
>
|
||||
{t('ai_providers.openai_models_fetch_apply')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
isLoading={initialLoading}
|
||||
loadingLabel={t('common.loading')}
|
||||
|
||||
@@ -258,10 +258,28 @@ export function AiProvidersVertexEditPage() {
|
||||
onBack={handleBack}
|
||||
backLabel={t('common.back')}
|
||||
backAriaLabel={t('common.back')}
|
||||
rightAction={
|
||||
<Button size="sm" onClick={handleSave} loading={saving} disabled={!canSave}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
hideTopBarBackButton
|
||||
hideTopBarRightAction
|
||||
floatingAction={
|
||||
<div className={layoutStyles.floatingActions}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className={layoutStyles.floatingBackButton}
|
||||
>
|
||||
{t('common.back')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
loading={saving}
|
||||
disabled={!canSave}
|
||||
className={layoutStyles.floatingSaveButton}
|
||||
>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
isLoading={loading}
|
||||
loadingLabel={t('common.loading')}
|
||||
|
||||
@@ -612,6 +612,13 @@
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.traceCandidatesHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.traceInfoGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
+14
-1
@@ -947,7 +947,20 @@ export function LogsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className={styles.traceSectionTitle}>{t('logs.trace_candidates_title')}</h3>
|
||||
<div className={styles.traceCandidatesHeader}>
|
||||
<h3 className={styles.traceSectionTitle}>{t('logs.trace_candidates_title')}</h3>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
void trace.refreshTraceUsageDetails().catch(() => {});
|
||||
}}
|
||||
loading={trace.traceLoading}
|
||||
disabled={requestLogDownloading}
|
||||
>
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
{trace.traceLoading ? (
|
||||
<div className="hint">{t('logs.trace_loading')}</div>
|
||||
) : trace.traceError ? (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { authFilesApi } from '@/services/api/authFiles';
|
||||
import { usageApi } from '@/services/api/usage';
|
||||
import { USAGE_STATS_STALE_TIME_MS, useUsageStatsStore } from '@/stores';
|
||||
import type { AuthFileItem, Config } from '@/types';
|
||||
import type { CredentialInfo, SourceInfo } from '@/types/sourceInfo';
|
||||
import { buildSourceInfoMap, resolveSourceDisplay } from '@/utils/sourceResolver';
|
||||
@@ -21,7 +21,7 @@ export type TraceCandidate = {
|
||||
timeDeltaMs: number | null;
|
||||
};
|
||||
|
||||
const TRACE_USAGE_CACHE_MS = 60 * 1000;
|
||||
const TRACE_AUTH_CACHE_MS = 60 * 1000;
|
||||
const TRACE_MATCH_STRONG_WINDOW_MS = 3 * 1000;
|
||||
const TRACE_MATCH_WINDOW_MS = 10 * 1000;
|
||||
const TRACE_MATCH_MAX_WINDOW_MS = 30 * 1000;
|
||||
@@ -138,6 +138,7 @@ interface UseTraceResolverReturn {
|
||||
traceCandidates: TraceCandidate[];
|
||||
resolveTraceSourceInfo: (sourceRaw: string, authIndex: unknown) => SourceInfo;
|
||||
loadTraceUsageDetails: () => Promise<void>;
|
||||
refreshTraceUsageDetails: () => Promise<void>;
|
||||
openTraceModal: (line: ParsedLogLine) => void;
|
||||
closeTraceModal: () => void;
|
||||
}
|
||||
@@ -145,25 +146,30 @@ interface UseTraceResolverReturn {
|
||||
export function useTraceResolver(options: UseTraceResolverOptions): UseTraceResolverReturn {
|
||||
const { traceScopeKey, connectionStatus, config, requestLogDownloading } = options;
|
||||
const { t } = useTranslation();
|
||||
const usageSnapshot = useUsageStatsStore((state) => state.usage);
|
||||
const usageScopeKey = useUsageStatsStore((state) => state.scopeKey);
|
||||
const loadUsageStats = useUsageStatsStore((state) => state.loadUsageStats);
|
||||
|
||||
const [traceLogLine, setTraceLogLine] = useState<ParsedLogLine | null>(null);
|
||||
const [traceUsageDetails, setTraceUsageDetails] = useState<UsageDetailWithEndpoint[]>([]);
|
||||
const [traceAuthFileMap, setTraceAuthFileMap] = useState<Map<string, CredentialInfo>>(new Map());
|
||||
const [traceLoading, setTraceLoading] = useState(false);
|
||||
const [traceError, setTraceError] = useState('');
|
||||
|
||||
const traceUsageLoadedAtRef = useRef(0);
|
||||
const traceAuthLoadedAtRef = useRef(0);
|
||||
const traceScopeKeyRef = useRef('');
|
||||
|
||||
const scopedUsageSnapshot = usageScopeKey === traceScopeKey ? usageSnapshot : null;
|
||||
const traceUsageDetails = useMemo<UsageDetailWithEndpoint[]>(
|
||||
() => collectUsageDetailsWithEndpoint(scopedUsageSnapshot),
|
||||
[scopedUsageSnapshot]
|
||||
);
|
||||
|
||||
const traceSourceInfoMap = useMemo(() => buildSourceInfoMap(config ?? {}), [config]);
|
||||
|
||||
const loadTraceUsageDetails = useCallback(async () => {
|
||||
const loadTraceUsageDetailsInternal = useCallback(async (forceUsage: boolean) => {
|
||||
if (traceScopeKeyRef.current !== traceScopeKey) {
|
||||
traceScopeKeyRef.current = traceScopeKey;
|
||||
traceUsageLoadedAtRef.current = 0;
|
||||
traceAuthLoadedAtRef.current = 0;
|
||||
setTraceUsageDetails([]);
|
||||
setTraceAuthFileMap(new Map());
|
||||
setTraceError('');
|
||||
}
|
||||
@@ -171,27 +177,20 @@ export function useTraceResolver(options: UseTraceResolverOptions): UseTraceReso
|
||||
if (traceLoading) return;
|
||||
|
||||
const now = Date.now();
|
||||
const usageFresh =
|
||||
traceUsageLoadedAtRef.current > 0 && now - traceUsageLoadedAtRef.current < TRACE_USAGE_CACHE_MS;
|
||||
const authFresh =
|
||||
traceAuthLoadedAtRef.current > 0 && now - traceAuthLoadedAtRef.current < TRACE_USAGE_CACHE_MS;
|
||||
if (usageFresh && authFresh) return;
|
||||
traceAuthLoadedAtRef.current > 0 && now - traceAuthLoadedAtRef.current < TRACE_AUTH_CACHE_MS;
|
||||
|
||||
setTraceLoading(true);
|
||||
setTraceError('');
|
||||
try {
|
||||
const [usageResponse, authFilesResponse] = await Promise.all([
|
||||
usageFresh ? Promise.resolve(null) : usageApi.getUsage(),
|
||||
const [, authFilesResponse] = await Promise.all([
|
||||
loadUsageStats({
|
||||
force: forceUsage,
|
||||
staleTimeMs: USAGE_STATS_STALE_TIME_MS
|
||||
}),
|
||||
authFresh ? Promise.resolve(null) : authFilesApi.list().catch(() => null)
|
||||
]);
|
||||
|
||||
if (usageResponse !== null) {
|
||||
const usageData = usageResponse?.usage ?? usageResponse;
|
||||
const details = collectUsageDetailsWithEndpoint(usageData);
|
||||
setTraceUsageDetails(details);
|
||||
traceUsageLoadedAtRef.current = now;
|
||||
}
|
||||
|
||||
if (authFilesResponse !== null) {
|
||||
const files = Array.isArray(authFilesResponse)
|
||||
? authFilesResponse
|
||||
@@ -207,7 +206,7 @@ export function useTraceResolver(options: UseTraceResolverOptions): UseTraceReso
|
||||
});
|
||||
});
|
||||
setTraceAuthFileMap(map);
|
||||
traceAuthLoadedAtRef.current = now;
|
||||
traceAuthLoadedAtRef.current = Date.now();
|
||||
}
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
@@ -215,14 +214,20 @@ export function useTraceResolver(options: UseTraceResolverOptions): UseTraceReso
|
||||
} finally {
|
||||
setTraceLoading(false);
|
||||
}
|
||||
}, [t, traceLoading, traceScopeKey]);
|
||||
}, [loadUsageStats, t, traceLoading, traceScopeKey]);
|
||||
|
||||
const loadTraceUsageDetails = useCallback(async () => {
|
||||
await loadTraceUsageDetailsInternal(false);
|
||||
}, [loadTraceUsageDetailsInternal]);
|
||||
|
||||
const refreshTraceUsageDetails = useCallback(async () => {
|
||||
await loadTraceUsageDetailsInternal(true);
|
||||
}, [loadTraceUsageDetailsInternal]);
|
||||
|
||||
useEffect(() => {
|
||||
if (connectionStatus === 'connected') {
|
||||
traceScopeKeyRef.current = traceScopeKey;
|
||||
traceUsageLoadedAtRef.current = 0;
|
||||
traceAuthLoadedAtRef.current = 0;
|
||||
setTraceUsageDetails([]);
|
||||
setTraceAuthFileMap(new Map());
|
||||
setTraceLoading(false);
|
||||
setTraceError('');
|
||||
@@ -271,6 +276,7 @@ export function useTraceResolver(options: UseTraceResolverOptions): UseTraceReso
|
||||
traceCandidates,
|
||||
resolveTraceSourceInfo,
|
||||
loadTraceUsageDetails,
|
||||
refreshTraceUsageDetails,
|
||||
openTraceModal,
|
||||
closeTraceModal
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user