mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-06-04 11:25:52 +08:00
feat: client changes for query stats (#5706)
* feat: changes for the query stats websockets * chore: remove unwanted files * fix: work on random id rather than hash * fix: improve the icons and design * feat: webpack and docker file changes * fix: test cases * chore: format the units * chore: address review comments * chore: update the id to uuid package * fix: build issues * chore: remove docker file changes * chore: remove docker file changes
This commit is contained in:
parent
65280cf4e1
commit
1b9683d699
1
frontend/public/Icons/solid-x-circle.svg
Normal file
1
frontend/public/Icons/solid-x-circle.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#prefix__clip0_4062_7291)" stroke-width="1.167" stroke-linecap="round" stroke-linejoin="round"><path d="M7 12.833A5.833 5.833 0 107 1.167a5.833 5.833 0 000 11.666z" fill="#E5484D" stroke="#E5484D"/><path d="M8.75 5.25l-3.5 3.5M5.25 5.25l3.5 3.5" stroke="#121317"/></g><defs><clipPath id="prefix__clip0_4062_7291"><path fill="#fff" d="M0 0h14v14H0z"/></clipPath></defs></svg>
|
After Width: | Height: | Size: 467 B |
32
frontend/src/api/common/getQueryStats.ts
Normal file
32
frontend/src/api/common/getQueryStats.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
|
||||
export interface WsDataEvent {
|
||||
read_rows: number;
|
||||
read_bytes: number;
|
||||
elapsed_ms: number;
|
||||
}
|
||||
interface GetQueryStatsProps {
|
||||
queryId: string;
|
||||
setData: React.Dispatch<React.SetStateAction<WsDataEvent | undefined>>;
|
||||
}
|
||||
|
||||
export function getQueryStats(props: GetQueryStatsProps): void {
|
||||
const { queryId, setData } = props;
|
||||
|
||||
const token = getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN) || '';
|
||||
const socket = new WebSocket(
|
||||
`${ENVIRONMENT.wsURL}api/v3/query_progress?q=${queryId}`,
|
||||
token,
|
||||
);
|
||||
|
||||
socket.addEventListener('message', (event) => {
|
||||
try {
|
||||
const parsedData = JSON.parse(event?.data);
|
||||
setData(parsedData);
|
||||
} catch {
|
||||
setData(event?.data);
|
||||
}
|
||||
});
|
||||
}
|
@ -12,10 +12,13 @@ export const getMetricsQueryRange = async (
|
||||
props: QueryRangePayload,
|
||||
version: string,
|
||||
signal: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => {
|
||||
try {
|
||||
if (version && version === ENTITY_VERSION_V4) {
|
||||
const response = await ApiV4Instance.post('/query_range', props, { signal });
|
||||
const response = await ApiV4Instance.post('/query_range', props, {
|
||||
signal,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
@ -26,7 +29,10 @@ export const getMetricsQueryRange = async (
|
||||
};
|
||||
}
|
||||
|
||||
const response = await ApiV3Instance.post('/query_range', props, { signal });
|
||||
const response = await ApiV3Instance.post('/query_range', props, {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
|
@ -3,4 +3,5 @@ export const ENVIRONMENT = {
|
||||
process?.env?.FRONTEND_API_ENDPOINT ||
|
||||
process?.env?.GITPOD_WORKSPACE_URL?.replace('://', '://8080-') ||
|
||||
'',
|
||||
wsURL: process?.env?.WEBSOCKET_API_ENDPOINT || 'ws://localhost:8080/',
|
||||
};
|
||||
|
@ -80,6 +80,36 @@
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
.query-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
.rows {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
letter-spacing: 0.36px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
background: #242834;
|
||||
}
|
||||
|
||||
.time {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
letter-spacing: 0.36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logs-actions-container {
|
||||
@ -149,6 +179,15 @@
|
||||
background: var(--bg-robin-400);
|
||||
}
|
||||
}
|
||||
.query-stats {
|
||||
.rows {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.time {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,4 @@
|
||||
.query-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
42
frontend/src/container/LogsExplorerViews/QueryStatus.tsx
Normal file
42
frontend/src/container/LogsExplorerViews/QueryStatus.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import './QueryStatus.styles.scss';
|
||||
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Spin } from 'antd';
|
||||
import { CircleCheck } from 'lucide-react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
interface IQueryStatusProps {
|
||||
loading: boolean;
|
||||
error: boolean;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export default function QueryStatus(
|
||||
props: IQueryStatusProps,
|
||||
): React.ReactElement {
|
||||
const { loading, error, success } = props;
|
||||
|
||||
const content = useMemo((): React.ReactElement => {
|
||||
if (loading) {
|
||||
return <Spin spinning size="small" indicator={<LoadingOutlined spin />} />;
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
<img
|
||||
src="/Icons/solid-x-circle.svg"
|
||||
alt="header"
|
||||
className="error"
|
||||
style={{ height: '14px', width: '14px' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (success) {
|
||||
return (
|
||||
<CircleCheck className="success" size={14} fill={Color.BG_ROBIN_500} />
|
||||
);
|
||||
}
|
||||
return <div />;
|
||||
}, [error, loading, success]);
|
||||
return <div className="query-status">{content}</div>;
|
||||
}
|
@ -1,8 +1,10 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import './LogsExplorerViews.styles.scss';
|
||||
|
||||
import { Button } from 'antd';
|
||||
import { Button, Typography } from 'antd';
|
||||
import { getQueryStats, WsDataEvent } from 'api/common/getQueryStats';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
@ -77,6 +79,8 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import QueryStatus from './QueryStatus';
|
||||
|
||||
function LogsExplorerViews({
|
||||
selectedView,
|
||||
showFrequencyChart,
|
||||
@ -130,6 +134,8 @@ function LogsExplorerViews({
|
||||
const [logs, setLogs] = useState<ILog[]>([]);
|
||||
const [requestData, setRequestData] = useState<Query | null>(null);
|
||||
const [showFormatMenuItems, setShowFormatMenuItems] = useState(false);
|
||||
const [queryId, setQueryId] = useState<string>(v4());
|
||||
const [queryStats, setQueryStats] = useState<WsDataEvent>();
|
||||
|
||||
const handleAxisError = useAxiosError();
|
||||
|
||||
@ -233,7 +239,13 @@ function LogsExplorerViews({
|
||||
chartQueryKeyRef,
|
||||
);
|
||||
|
||||
const { data, isLoading, isFetching, isError } = useGetExplorerQueryRange(
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
isFetching,
|
||||
isError,
|
||||
isSuccess,
|
||||
} = useGetExplorerQueryRange(
|
||||
requestData,
|
||||
panelType,
|
||||
DEFAULT_ENTITY_VERSION,
|
||||
@ -251,6 +263,9 @@ function LogsExplorerViews({
|
||||
},
|
||||
undefined,
|
||||
listQueryKeyRef,
|
||||
{
|
||||
...(!isEmpty(queryId) && { 'X-SIGNOZ-QUERY-ID': queryId }),
|
||||
},
|
||||
);
|
||||
|
||||
const getRequestData = useCallback(
|
||||
@ -337,6 +352,23 @@ function LogsExplorerViews({
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setQueryId(v4());
|
||||
}, [isError, isSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isEmpty(queryId) &&
|
||||
(isLoading || isFetching) &&
|
||||
selectedPanelType !== PANEL_TYPES.LIST
|
||||
) {
|
||||
setQueryStats(undefined);
|
||||
setTimeout(() => {
|
||||
getQueryStats({ queryId, setData: setQueryStats });
|
||||
}, 500);
|
||||
}
|
||||
}, [queryId, isLoading, isFetching, selectedPanelType]);
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current && !isUndefined(data?.payload)) {
|
||||
@ -703,6 +735,30 @@ function LogsExplorerViews({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(selectedPanelType === PANEL_TYPES.TIME_SERIES ||
|
||||
selectedPanelType === PANEL_TYPES.TABLE) && (
|
||||
<div className="query-stats">
|
||||
<QueryStatus
|
||||
loading={isLoading || isFetching}
|
||||
error={isError}
|
||||
success={isSuccess}
|
||||
/>
|
||||
{queryStats?.read_rows && (
|
||||
<Typography.Text className="rows">
|
||||
{getYAxisFormattedValue(queryStats.read_rows?.toString(), 'short')}{' '}
|
||||
rows
|
||||
</Typography.Text>
|
||||
)}
|
||||
{queryStats?.elapsed_ms && (
|
||||
<>
|
||||
<div className="divider" />
|
||||
<Typography.Text className="time">
|
||||
{getYAxisFormattedValue(queryStats?.elapsed_ms?.toString(), 'ms')}
|
||||
</Typography.Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -46,6 +46,8 @@ jest.mock(
|
||||
},
|
||||
);
|
||||
|
||||
jest.mock('api/common/getQueryStats', () => jest.fn());
|
||||
|
||||
jest.mock('constants/panelTypes', () => ({
|
||||
AVAILABLE_EXPORT_PANEL_TYPES: ['graph', 'table'],
|
||||
}));
|
||||
|
@ -20,6 +20,7 @@ export const useGetExplorerQueryRange = (
|
||||
params?: Record<string, unknown>,
|
||||
isDependentOnQB = true,
|
||||
keyRef?: MutableRefObject<any>,
|
||||
headers?: Record<string, string>,
|
||||
): UseQueryResult<SuccessResponse<MetricRangePayloadProps>, Error> => {
|
||||
const { isEnabledQuery } = useQueryBuilder();
|
||||
const { selectedTime: globalSelectedInterval, minTime, maxTime } = useSelector<
|
||||
@ -61,5 +62,6 @@ export const useGetExplorerQueryRange = (
|
||||
queryKey: [key, globalSelectedInterval, requestData, minTime, maxTime],
|
||||
enabled: isEnabled,
|
||||
},
|
||||
headers,
|
||||
);
|
||||
};
|
||||
|
@ -13,12 +13,14 @@ type UseGetQueryRange = (
|
||||
requestData: GetQueryResultsProps,
|
||||
version: string,
|
||||
options?: UseQueryOptions<SuccessResponse<MetricRangePayloadProps>, Error>,
|
||||
headers?: Record<string, string>,
|
||||
) => UseQueryResult<SuccessResponse<MetricRangePayloadProps>, Error>;
|
||||
|
||||
export const useGetQueryRange: UseGetQueryRange = (
|
||||
requestData,
|
||||
version,
|
||||
options,
|
||||
headers,
|
||||
) => {
|
||||
const newRequestData: GetQueryResultsProps = useMemo(
|
||||
() => ({
|
||||
@ -45,7 +47,7 @@ export const useGetQueryRange: UseGetQueryRange = (
|
||||
|
||||
return useQuery<SuccessResponse<MetricRangePayloadProps>, Error>({
|
||||
queryFn: async ({ signal }) =>
|
||||
GetMetricQueryRange(requestData, version, signal),
|
||||
GetMetricQueryRange(requestData, version, signal, headers),
|
||||
...options,
|
||||
queryKey,
|
||||
});
|
||||
|
@ -23,6 +23,7 @@ export async function GetMetricQueryRange(
|
||||
props: GetQueryResultsProps,
|
||||
version: string,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
): Promise<SuccessResponse<MetricRangePayloadProps>> {
|
||||
const { legendMap, queryPayload } = prepareQueryRangePayload(props);
|
||||
|
||||
@ -30,6 +31,7 @@ export async function GetMetricQueryRange(
|
||||
queryPayload,
|
||||
version || 'v3',
|
||||
signal,
|
||||
headers,
|
||||
);
|
||||
|
||||
if (response.statusCode >= 400) {
|
||||
|
@ -3,6 +3,7 @@ declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
FRONTEND_API_ENDPOINT: string | undefined;
|
||||
WEBSOCKET_API_ENDPOINT: string | undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ const plugins = [
|
||||
'process.env': JSON.stringify({
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
FRONTEND_API_ENDPOINT: process.env.FRONTEND_API_ENDPOINT,
|
||||
WEBSOCKET_API_ENDPOINT: process.env.WEBSOCKET_API_ENDPOINT,
|
||||
INTERCOM_APP_ID: process.env.INTERCOM_APP_ID,
|
||||
SEGMENT_ID: process.env.SEGMENT_ID,
|
||||
POSTHOG_KEY: process.env.POSTHOG_KEY,
|
||||
|
@ -48,6 +48,7 @@ const plugins = [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': JSON.stringify({
|
||||
FRONTEND_API_ENDPOINT: process.env.FRONTEND_API_ENDPOINT,
|
||||
WEBSOCKET_API_ENDPOINT: process.env.WEBSOCKET_API_ENDPOINT,
|
||||
INTERCOM_APP_ID: process.env.INTERCOM_APP_ID,
|
||||
SEGMENT_ID: process.env.SEGMENT_ID,
|
||||
POSTHOG_KEY: process.env.POSTHOG_KEY,
|
||||
|
Loading…
x
Reference in New Issue
Block a user