diff --git a/web-frontend/src/main/v3/packages/ui/src/components/AgentActiveThread/AgentActiveChart.tsx b/web-frontend/src/main/v3/packages/ui/src/components/AgentActiveThread/AgentActiveChart.tsx index bd1e6b55649b..17145eabafdd 100644 --- a/web-frontend/src/main/v3/packages/ui/src/components/AgentActiveThread/AgentActiveChart.tsx +++ b/web-frontend/src/main/v3/packages/ui/src/components/AgentActiveThread/AgentActiveChart.tsx @@ -23,33 +23,36 @@ import { AgentActiveData } from './AgentActiveTable'; export const DefaultValue = { yMax: 100 }; export interface AgentActiveChartProps { + loading?: boolean; data?: AgentActiveData[]; setting?: AgentActiveSettingType; clickedActiveThread?: string; setClickedActiveThread?: React.Dispatch>; } +const TICK_COUNT = 4; const DefaultReferenceLineLength = 50; const chartConfig = { - slow: { - label: 'slow', - color: '#e67f22', - }, - '5s': { - label: '5s', - color: '#ffba00', + '1s': { + label: '1s', + color: '#34b994', }, '3s': { label: '3s', color: '#51afdf', }, - '1s': { - label: '1s', - color: '#34b994', + '5s': { + label: '5s', + color: '#ffba00', + }, + slow: { + label: 'slow', + color: '#e67f22', }, } satisfies ChartConfig; export const AgentActiveChart = ({ + loading, data, setting, clickedActiveThread, @@ -91,28 +94,41 @@ export const AgentActiveChart = ({ tickLine={false} axisLine={false} tickMargin={10} + ticks={Array.from({ length: TICK_COUNT + 1 }, (_, index) => + Math.ceil((yMax / TICK_COUNT) * index), + )} allowDecimals={false} domain={() => [0, yMax]} /> - } /> - } /> - {Object.keys(chartConfig).map((key) => ( - - {data?.map((entry, index) => ( - - ))} - - ))} + ((value as number) < 0 ? '-' : value)} + /> + } + /> + } /> + {Object.keys(chartConfig) + .reverse() + .map((key) => ( + + {data?.map((entry, index) => ( + + ))} + + ))} {Array.from({ length: ReferenceLineLength }, (_, index) => { return ( ; @@ -51,6 +52,7 @@ export const AgentActiveSetting = ({ defaultValues: { yMax: defaultValues?.yMax, isSplit: defaultValues?.isSplit, + inactivityThreshold: defaultValues?.inactivityThreshold, }, }); @@ -77,7 +79,7 @@ export const AgentActiveSetting = ({ >
Agent request chart Setting
- + Max of Y axis - + @@ -111,6 +119,27 @@ export const AgentActiveSetting = ({ )} /> + ( + + + Inactivity Threshold (m) + + + + + + + )} + />
+ {true && ( + + + +
+ +
+
+ + +

{message}

+
+
+
+
+ )} +
+ ); + }, }, { accessorKey: 'slow', @@ -40,6 +86,11 @@ export const AgentActiveTable = ({ }, cell: ({ getValue }) => { const value = getValue() || 0; + + if (value === -1) { + return '-'; + } + return ( { const value = getValue() || 0; + if (value === -1) { + return '-'; + } return ( { + const value = getValue() || 0; + if (value === -1) { + return '-'; + } + return value; + }, }, { accessorKey: '1s', @@ -89,15 +150,23 @@ export const AgentActiveTable = ({ headerClassName: 'flex justify-end', cellClassName: 'text-right', }, + cell: ({ getValue }) => { + const value = getValue() || 0; + if (value === -1) { + return '-'; + } + return value; + }, }, ]; return (
{ if (row?.id === String(focusRowId)) { diff --git a/web-frontend/src/main/v3/packages/ui/src/components/AgentActiveThread/AgentActiveThreadFetcher.tsx b/web-frontend/src/main/v3/packages/ui/src/components/AgentActiveThread/AgentActiveThreadFetcher.tsx index c9bc5bdfecd5..07900f210f3a 100644 --- a/web-frontend/src/main/v3/packages/ui/src/components/AgentActiveThread/AgentActiveThreadFetcher.tsx +++ b/web-frontend/src/main/v3/packages/ui/src/components/AgentActiveThread/AgentActiveThreadFetcher.tsx @@ -148,6 +148,7 @@ export const AgentActiveThreadFetcher = () => {
diff --git a/web-frontend/src/main/v3/packages/ui/src/components/AgentActiveThread/AgentActiveThreadView.tsx b/web-frontend/src/main/v3/packages/ui/src/components/AgentActiveThread/AgentActiveThreadView.tsx index 9f85bee715d9..2be0d764540f 100644 --- a/web-frontend/src/main/v3/packages/ui/src/components/AgentActiveThread/AgentActiveThreadView.tsx +++ b/web-frontend/src/main/v3/packages/ui/src/components/AgentActiveThread/AgentActiveThreadView.tsx @@ -3,63 +3,141 @@ import { AgentActiveThread } from '@pinpoint-fe/ui/constants'; import { AgentActiveTable, AgentActiveData } from './AgentActiveTable'; import { AgentActiveSettingType } from './AgentActiveSetting'; import { AgentActiveChart } from './AgentActiveChart'; + export interface AgentActiveThreadViewProps { + applicationName?: string; activeThreadCounts?: AgentActiveThread.Result; setting?: AgentActiveSettingType; } export const AgentActiveThreadView = ({ + applicationName, activeThreadCounts, setting, }: AgentActiveThreadViewProps) => { - const prevActiveThreadCounts = React.useRef<{ [server: string]: number[] }>({}); - const [activeThreadCountsData, setActiveThreadCountsData] = React.useState( - getActiveThreadCountsData(activeThreadCounts?.activeThreadCounts || {}), - ); - const [clickedActiveThread, setClickedActiveThread] = React.useState(''); - + /// + // const [activeThreadCounts, setActiveThreadCounts] = React.useState(); // React.useEffect(() => { - // const interval = setInterval(() => { - // // 새로운 activeThreadCountsData 생성 로직 - // const newData = Array.from({ length: 30 }, (_, index) => ({ - // server: `pd-my2-6cf95bf889-zzkls${index + 1}`, - // '1s': Math.floor(Math.random() * 25), - // '3s': Math.floor(Math.random() * 25), - // '5s': Math.floor(Math.random() * 25), - // slow: Math.floor(Math.random() * 25), - // })); - // setActiveThreadCountsData(newData); + // const interval = setTimeout(() => { + // const now = new Date().getTime(); + // const activeThreadCounts = {} as AgentActiveThread.ActiveThreadCounts; + + // for (let i = 0; i < 2; i++) { + // activeThreadCounts[`s${Math.floor(Math.random() * 5)}`] = { + // code: 0, + // message: 'TIMEOUT', + // // message: Math.random() > 0.5 ? 'OK' : 'TIMEOUT', + // status: [ + // Math.floor(Math.random() * 25), + // Math.floor(Math.random() * 25), + // Math.floor(Math.random() * 25), + // Math.floor(Math.random() * 25), + // ], + // }; + // } + + // setActiveThreadCounts({ + // timeStamp: now, + // applicationName: 'test', + // activeThreadCounts, + // }); // }, 1000); // return () => clearInterval(interval); // }, []); + /// + + const dataMap = React.useRef( + new Map(), + ); + const [loading, setLoading] = React.useState(false); + const [activeThreadCountsData, setActiveThreadCountsData] = React.useState([]); + const [clickedActiveThread, setClickedActiveThread] = React.useState(''); React.useEffect(() => { - setActiveThreadCountsData( - getActiveThreadCountsData(activeThreadCounts?.activeThreadCounts || {}), - ); - }, [activeThreadCounts]); - - function getActiveThreadCountsData( - activeThreadCounts: AgentActiveThread.ActiveThreadCounts, - ): AgentActiveData[] { - return Object.keys(activeThreadCounts).map((server) => { - const { message, status = [0, 0, 0, 0] } = activeThreadCounts[server] || {}; - if (message && message === 'OK') { - const [oneS, threeS, fiveS, slow] = status; - - prevActiveThreadCounts.current = { ...prevActiveThreadCounts.current, [server]: status }; - return { server, '1s': oneS, '3s': threeS, '5s': fiveS, slow: slow }; + if (!activeThreadCounts || activeThreadCounts.applicationName !== applicationName) { + dataMap.current.clear(); + setClickedActiveThread(''); + setLoading(true); + return; + } + + setActiveThreadCountsData(getActiveThreadCountsData(activeThreadCounts)); + setLoading(false); + }, [applicationName, activeThreadCounts]); + + function getActiveThreadCountsData({ + activeThreadCounts, + timeStamp, + }: AgentActiveThread.Result): AgentActiveData[] { + if (!activeThreadCounts) { + return []; + } + + // Set dataMap with new activeThreadCounts + Object.keys(activeThreadCounts || {}).forEach((key) => { + const serverData = activeThreadCounts[key]; + if (serverData?.message === 'OK') { + // If "OK" the status will be updated. + dataMap.current.set(key, { + status: serverData.status || [0, 0, 0, 0], + lastOKTimeStamp: timeStamp, + message: '', + }); + } else { + // If it is not “OK”, only the message is updated. + const dataMapData = dataMap.current.get(key) || { + status: [-1, -1, -1, -1], + lastOKTimeStamp: timeStamp, + }; + + const timeDiff = timeStamp - dataMapData.lastOKTimeStamp; + + // If the time difference is greater than 3 seconds, the status is updated to -1. + if (timeDiff > 3000) { + dataMap.current.set(key, { + ...dataMapData, + status: [-1, -1, -1, -1], + message: serverData?.message, + }); + return; + } + + // If the time difference is greater than the inactivityThreshold, the status is removed. + if (timeDiff >= (setting?.inactivityThreshold || 5) * 60 * 1000) { + dataMap.current.delete(key); + setClickedActiveThread(''); + return; + } + + dataMap.current.set(key, { + ...dataMapData, + message: serverData?.message, + }); } - const [oneS, threeS, fiveS, slow] = prevActiveThreadCounts?.current?.[server] || [0, 0, 0, 0]; - return { server, '1s': oneS, '3s': threeS, '5s': fiveS, slow: slow }; }); + + const newData = Array.from(dataMap.current.keys()).map((server) => { + const mapData = dataMap.current.get(server); + const [oneS, threeS, fiveS, slow] = mapData?.status || [-1, -1, -1, -1]; + return { + server, + '1s': oneS, + '3s': threeS, + '5s': fiveS, + slow, + message: mapData?.message || '', + }; + }); + + return newData; } return ( <> {activeThreadCountsData?.length < 100 || !setting?.isSplit ? (
)} - + ); }; diff --git a/web-frontend/src/main/v3/packages/ui/src/components/DataTable/VirtualizedDataTable.tsx b/web-frontend/src/main/v3/packages/ui/src/components/DataTable/VirtualizedDataTable.tsx index 0455c19379e7..d29a2ab305f6 100644 --- a/web-frontend/src/main/v3/packages/ui/src/components/DataTable/VirtualizedDataTable.tsx +++ b/web-frontend/src/main/v3/packages/ui/src/components/DataTable/VirtualizedDataTable.tsx @@ -22,6 +22,7 @@ import { cn } from '../../lib'; type ExpandableTData = T & { subRows?: ExpandableTData[] }; export interface VirtualizedDataTableProps { + loading?: boolean; data: TData[]; columns: ColumnDef[]; focusRowIndex?: number; @@ -47,6 +48,7 @@ type MetaType = { }; export function VirtualizedDataTable({ + loading, data, columns, focusRowIndex, @@ -292,7 +294,7 @@ export function VirtualizedDataTable({ ) : ( - No results. + {loading ? 'Loading...' : 'No results.'} )} diff --git a/web-frontend/src/main/v3/packages/ui/src/components/ui/chart.tsx b/web-frontend/src/main/v3/packages/ui/src/components/ui/chart.tsx index 3d5ddc49a3a8..982064b60f81 100644 --- a/web-frontend/src/main/v3/packages/ui/src/components/ui/chart.tsx +++ b/web-frontend/src/main/v3/packages/ui/src/components/ui/chart.tsx @@ -6,6 +6,7 @@ import * as TooltipPrimitive from '@radix-ui/react-tooltip'; import { cn } from '../../lib/utils'; import { toCssVariable } from '../../lib/charts'; import { Payload } from 'recharts/types/component/DefaultLegendContent'; +import { reverse } from 'lodash'; // Format: { THEME_NAME: CSS_SELECTOR } const THEMES = { light: '', dark: '.dark' } as const; @@ -100,15 +101,19 @@ const ChartTooltipContent = React.forwardRef< HTMLDivElement, React.ComponentProps & React.ComponentProps<'div'> & { + isReverse?: boolean; hideLabel?: boolean; hideIndicator?: boolean; indicator?: 'line' | 'dot' | 'dashed'; nameKey?: string; labelKey?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + valueFormatter?: (value: any, payload: Payload[]) => React.ReactNode; } >( ( { + isReverse, active, payload, className, @@ -118,6 +123,7 @@ const ChartTooltipContent = React.forwardRef< label, labelFormatter, labelClassName, + valueFormatter, formatter, color, nameKey, @@ -158,6 +164,7 @@ const ChartTooltipContent = React.forwardRef< } const nestLabel = payload.length === 1 && indicator !== 'dot'; + const payloadForRender = isReverse ? reverse(payload) : payload; return (
{!nestLabel ? tooltipLabel : null}
- {payload.map((item, index) => { + {payloadForRender.map((item, index) => { const key = `${nameKey || item.name || item.dataKey || 'value'}`; const itemConfig = getPayloadConfigFromPayload(config, item, key); const indicatorColor = color || item.payload.fill || item.color; @@ -224,7 +231,9 @@ const ChartTooltipContent = React.forwardRef<
{item.value && ( - {item.value.toLocaleString()} + {valueFormatter + ? valueFormatter(item.value, item.payload) + : item.value.toLocaleString()} )}
@@ -246,6 +255,7 @@ const ChartLegendContent = React.forwardRef< HTMLDivElement, React.ComponentProps<'div'> & Pick & { + isReverse?: boolean; hideIcon?: boolean; nameKey?: string; mouseHoverDataKey?: Payload['dataKey'] | undefined; @@ -254,6 +264,7 @@ const ChartLegendContent = React.forwardRef< >( ( { + isReverse, className, hideIcon = false, payload, @@ -270,6 +281,8 @@ const ChartLegendContent = React.forwardRef< if (!payload?.length) { return null; } + + const payloadForRender = isReverse ? reverse(payload) : payload; return (
- {payload.map((item) => { + {payloadForRender.map((item) => { const key = `${nameKey || item.dataKey || 'value'}`; const itemConfig = getPayloadConfigFromPayload(config, item, key);