Compare commits
8 Commits
v0.52.0
...
v0.52.0-cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79e96e544f | ||
|
|
871e5ada9e | ||
|
|
0401c27dbc | ||
|
|
57c45f22d6 | ||
|
|
29f1883edd | ||
|
|
5d903b5487 | ||
|
|
1b9683d699 | ||
|
|
65280cf4e1 |
@@ -12,6 +12,7 @@ const DisableUpsell = "DISABLE_UPSELL"
|
||||
const Onboarding = "ONBOARDING"
|
||||
const ChatSupport = "CHAT_SUPPORT"
|
||||
const Gateway = "GATEWAY"
|
||||
const PremiumSupport = "PREMIUM_SUPPORT"
|
||||
|
||||
var BasicPlan = basemodel.FeatureSet{
|
||||
basemodel.Feature{
|
||||
@@ -119,6 +120,13 @@ var BasicPlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: PremiumSupport,
|
||||
Active: false,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
}
|
||||
|
||||
var ProPlan = basemodel.FeatureSet{
|
||||
@@ -220,6 +228,13 @@ var ProPlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: PremiumSupport,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
}
|
||||
|
||||
var EnterprisePlan = basemodel.FeatureSet{
|
||||
@@ -335,4 +350,11 @@ var EnterprisePlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: PremiumSupport,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
63
frontend/src/api/queryBuilder/getAttributeSuggestions.ts
Normal file
63
frontend/src/api/queryBuilder/getAttributeSuggestions.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { ApiV3Instance } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { baseAutoCompleteIdKeysOrder } from 'constants/queryBuilder';
|
||||
import { encode } from 'js-base64';
|
||||
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
IGetAttributeSuggestionsPayload,
|
||||
IGetAttributeSuggestionsSuccessResponse,
|
||||
} from 'types/api/queryBuilder/getAttributeSuggestions';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
export const getAttributeSuggestions = async ({
|
||||
searchText,
|
||||
dataSource,
|
||||
filters,
|
||||
}: IGetAttributeSuggestionsPayload): Promise<
|
||||
SuccessResponse<IGetAttributeSuggestionsSuccessResponse> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
let base64EncodedFiltersString;
|
||||
try {
|
||||
// the replace function is to remove the padding at the end of base64 encoded string which is auto added to make it a multiple of 4
|
||||
// why ? because the current working of qs doesn't work well with padding
|
||||
base64EncodedFiltersString = encode(JSON.stringify(filters)).replace(
|
||||
/=+$/,
|
||||
'',
|
||||
);
|
||||
} catch {
|
||||
// default base64 encoded string for empty filters object
|
||||
base64EncodedFiltersString = 'eyJpdGVtcyI6W10sIm9wIjoiQU5EIn0';
|
||||
}
|
||||
const response: AxiosResponse<{
|
||||
data: IGetAttributeSuggestionsSuccessResponse;
|
||||
}> = await ApiV3Instance.get(
|
||||
`/filter_suggestions?${createQueryParams({
|
||||
searchText,
|
||||
dataSource,
|
||||
existingFilter: base64EncodedFiltersString,
|
||||
})}`,
|
||||
);
|
||||
|
||||
const payload: BaseAutocompleteData[] =
|
||||
response.data.data.attributes?.map(({ id: _, ...item }) => ({
|
||||
...item,
|
||||
id: createIdFromObjectFields(item, baseAutoCompleteIdKeysOrder),
|
||||
})) || [];
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.statusText,
|
||||
payload: {
|
||||
attributes: payload,
|
||||
example_queries: response.data.data.example_queries,
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
return ErrorResponseHandler(e as AxiosError);
|
||||
}
|
||||
};
|
||||
@@ -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 || '',
|
||||
};
|
||||
|
||||
@@ -52,7 +52,7 @@ export const selectValueDivider = '__';
|
||||
|
||||
export const baseAutoCompleteIdKeysOrder: (keyof Omit<
|
||||
BaseAutocompleteData,
|
||||
'id' | 'isJSON'
|
||||
'id' | 'isJSON' | 'isIndexed'
|
||||
>)[] = ['key', 'dataType', 'type', 'isColumn'];
|
||||
|
||||
export const autocompleteType: Record<AutocompleteType, AutocompleteType> = {
|
||||
@@ -71,6 +71,7 @@ export const alphabet: string[] = alpha.map((str) => String.fromCharCode(str));
|
||||
export enum QueryBuilderKeys {
|
||||
GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE',
|
||||
GET_AGGREGATE_KEYS = 'GET_AGGREGATE_KEYS',
|
||||
GET_ATTRIBUTE_SUGGESTIONS = 'GET_ATTRIBUTE_SUGGESTIONS',
|
||||
}
|
||||
|
||||
export const mapOfOperators = {
|
||||
|
||||
@@ -4,6 +4,7 @@ const userOS = getUserOperatingSystem();
|
||||
export const LogsExplorerShortcuts = {
|
||||
StageAndRunQuery: 'enter+meta',
|
||||
FocusTheSearchBar: 's',
|
||||
ShowAllFilters: '/+meta',
|
||||
};
|
||||
|
||||
export const LogsExplorerShortcutsName = {
|
||||
@@ -11,9 +12,11 @@ export const LogsExplorerShortcutsName = {
|
||||
userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'
|
||||
}+enter`,
|
||||
FocusTheSearchBar: 's',
|
||||
ShowAllFilters: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'}+/`,
|
||||
};
|
||||
|
||||
export const LogsExplorerShortcutsDescription = {
|
||||
StageAndRunQuery: 'Stage and Run the current query',
|
||||
FocusTheSearchBar: 'Shift the focus to the last query filter bar',
|
||||
ShowAllFilters: 'Toggle all filters in the filters dropdown',
|
||||
};
|
||||
|
||||
@@ -33,6 +33,7 @@ import useErrorNotification from 'hooks/useErrorNotification';
|
||||
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import {
|
||||
Check,
|
||||
ConciergeBell,
|
||||
@@ -56,7 +57,7 @@ import { useHistory } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
@@ -120,6 +121,21 @@ function ExplorerOptions({
|
||||
|
||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
const handleConditionalQueryModification = useCallback((): string => {
|
||||
if (
|
||||
query?.builder?.queryData?.[0]?.aggregateOperator !== StringOperators.NOOP
|
||||
) {
|
||||
return JSON.stringify(query);
|
||||
}
|
||||
|
||||
// Modify aggregateOperator to count, as noop is not supported in alerts
|
||||
const modifiedQuery = cloneDeep(query);
|
||||
|
||||
modifiedQuery.builder.queryData[0].aggregateOperator = StringOperators.COUNT;
|
||||
|
||||
return JSON.stringify(modifiedQuery);
|
||||
}, [query]);
|
||||
|
||||
const onCreateAlertsHandler = useCallback(() => {
|
||||
if (sourcepage === DataSource.TRACES) {
|
||||
logEvent('Traces Explorer: Create alert', {
|
||||
@@ -130,13 +146,16 @@ function ExplorerOptions({
|
||||
panelType,
|
||||
});
|
||||
}
|
||||
|
||||
const stringifiedQuery = handleConditionalQueryModification();
|
||||
|
||||
history.push(
|
||||
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
|
||||
JSON.stringify(query),
|
||||
stringifiedQuery,
|
||||
)}`,
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [history, query]);
|
||||
}, [handleConditionalQueryModification, history]);
|
||||
|
||||
const onCancel = (value: boolean) => (): void => {
|
||||
onModalToggle(value);
|
||||
|
||||
@@ -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';
|
||||
@@ -48,7 +50,15 @@ import {
|
||||
} from 'lodash-es';
|
||||
import { Sliders } from 'lucide-react';
|
||||
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
memo,
|
||||
MutableRefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -69,12 +79,20 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import QueryStatus from './QueryStatus';
|
||||
|
||||
function LogsExplorerViews({
|
||||
selectedView,
|
||||
showFrequencyChart,
|
||||
setIsLoadingQueries,
|
||||
listQueryKeyRef,
|
||||
chartQueryKeyRef,
|
||||
}: {
|
||||
selectedView: SELECTED_VIEWS;
|
||||
showFrequencyChart: boolean;
|
||||
setIsLoadingQueries: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
listQueryKeyRef: MutableRefObject<any>;
|
||||
chartQueryKeyRef: MutableRefObject<any>;
|
||||
}): JSX.Element {
|
||||
const { notifications } = useNotifications();
|
||||
const history = useHistory();
|
||||
@@ -116,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();
|
||||
|
||||
@@ -214,9 +234,18 @@ function LogsExplorerViews({
|
||||
{
|
||||
enabled: !!listChartQuery && panelType === PANEL_TYPES.LIST,
|
||||
},
|
||||
{},
|
||||
undefined,
|
||||
chartQueryKeyRef,
|
||||
);
|
||||
|
||||
const { data, isLoading, isFetching, isError } = useGetExplorerQueryRange(
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
isFetching,
|
||||
isError,
|
||||
isSuccess,
|
||||
} = useGetExplorerQueryRange(
|
||||
requestData,
|
||||
panelType,
|
||||
DEFAULT_ENTITY_VERSION,
|
||||
@@ -232,6 +261,11 @@ function LogsExplorerViews({
|
||||
end: timeRange.end,
|
||||
}),
|
||||
},
|
||||
undefined,
|
||||
listQueryKeyRef,
|
||||
{
|
||||
...(!isEmpty(queryId) && { 'X-SIGNOZ-QUERY-ID': queryId }),
|
||||
},
|
||||
);
|
||||
|
||||
const getRequestData = useCallback(
|
||||
@@ -318,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)) {
|
||||
@@ -569,6 +620,25 @@ function LogsExplorerViews({
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isLoading ||
|
||||
isFetching ||
|
||||
isLoadingListChartData ||
|
||||
isFetchingListChartData
|
||||
) {
|
||||
setIsLoadingQueries(true);
|
||||
} else {
|
||||
setIsLoadingQueries(false);
|
||||
}
|
||||
}, [
|
||||
isLoading,
|
||||
isFetching,
|
||||
isFetchingListChartData,
|
||||
isLoadingListChartData,
|
||||
setIsLoadingQueries,
|
||||
]);
|
||||
|
||||
const flattenLogData = useMemo(
|
||||
() =>
|
||||
logs.map((log) => {
|
||||
@@ -665,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'],
|
||||
}));
|
||||
@@ -79,6 +81,9 @@ const renderer = (): RenderResult =>
|
||||
<LogsExplorerViews
|
||||
selectedView={SELECTED_VIEWS.SEARCH}
|
||||
showFrequencyChart
|
||||
setIsLoadingQueries={(): void => {}}
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
/>
|
||||
</VirtuosoMockContext.Provider>
|
||||
</QueryBuilderProvider>
|
||||
|
||||
@@ -18,6 +18,7 @@ export const defaultTraceSelectedColumns = [
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'serviceName--string--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
@@ -26,6 +27,7 @@ export const defaultTraceSelectedColumns = [
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'name--string--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
{
|
||||
key: 'durationNano',
|
||||
@@ -34,6 +36,7 @@ export const defaultTraceSelectedColumns = [
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'durationNano--float64--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
{
|
||||
key: 'httpMethod',
|
||||
@@ -42,6 +45,7 @@ export const defaultTraceSelectedColumns = [
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'httpMethod--string--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
{
|
||||
key: 'responseStatusCode',
|
||||
@@ -50,5 +54,6 @@ export const defaultTraceSelectedColumns = [
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'responseStatusCode--string--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -3,18 +3,27 @@ import './ToolbarActions.styles.scss';
|
||||
import { Button } from 'antd';
|
||||
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
|
||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
import { Play } from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
import { Play, X } from 'lucide-react';
|
||||
import { MutableRefObject, useEffect } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
|
||||
interface RightToolbarActionsProps {
|
||||
onStageRunQuery: () => void;
|
||||
isLoadingQueries?: boolean;
|
||||
listQueryKeyRef?: MutableRefObject<any>;
|
||||
chartQueryKeyRef?: MutableRefObject<any>;
|
||||
}
|
||||
|
||||
export default function RightToolbarActions({
|
||||
onStageRunQuery,
|
||||
isLoadingQueries,
|
||||
listQueryKeyRef,
|
||||
chartQueryKeyRef,
|
||||
}: RightToolbarActionsProps): JSX.Element {
|
||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
registerShortcut(LogsExplorerShortcuts.StageAndRunQuery, onStageRunQuery);
|
||||
|
||||
@@ -25,14 +34,41 @@ export default function RightToolbarActions({
|
||||
}, [onStageRunQuery]);
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
type="primary"
|
||||
className="right-toolbar"
|
||||
onClick={onStageRunQuery}
|
||||
icon={<Play size={14} />}
|
||||
>
|
||||
Stage & Run Query
|
||||
</Button>
|
||||
{isLoadingQueries ? (
|
||||
<div className="loading-container">
|
||||
<Button className="loading-btn" loading={isLoadingQueries} />
|
||||
<Button
|
||||
icon={<X size={14} />}
|
||||
className="cancel-run"
|
||||
onClick={(): void => {
|
||||
if (listQueryKeyRef?.current) {
|
||||
queryClient.cancelQueries(listQueryKeyRef.current);
|
||||
}
|
||||
if (chartQueryKeyRef?.current) {
|
||||
queryClient.cancelQueries(chartQueryKeyRef.current);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Cancel Run
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
className="right-toolbar"
|
||||
disabled={isLoadingQueries}
|
||||
onClick={onStageRunQuery}
|
||||
icon={<Play size={14} />}
|
||||
>
|
||||
Stage & Run Query
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
RightToolbarActions.defaultProps = {
|
||||
isLoadingQueries: false,
|
||||
listQueryKeyRef: null,
|
||||
chartQueryKeyRef: null,
|
||||
};
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
.left-toolbar-query-actions {
|
||||
display: flex;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400, #1d212d);
|
||||
background: var(--bg-ink-300, #16181d);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
flex-direction: row;
|
||||
|
||||
.prom-ql-icon {
|
||||
@@ -24,7 +24,7 @@
|
||||
border-radius: 0;
|
||||
|
||||
&.active-tab {
|
||||
background-color: #1d212d;
|
||||
background-color: var(--bg-slate-400);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@@ -33,7 +33,7 @@
|
||||
}
|
||||
}
|
||||
.action-btn + .action-btn {
|
||||
border-left: 1px solid var(--bg-slate-400, #1d212d);
|
||||
border-left: 1px solid var(--bg-slate-400);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,50 @@
|
||||
background-color: var(--bg-robin-600);
|
||||
}
|
||||
|
||||
.right-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.loading-btn {
|
||||
display: flex;
|
||||
width: 32px;
|
||||
height: 33px;
|
||||
padding: 4px 10px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 2px;
|
||||
background: var(--bg-slate-300);
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cancel-run {
|
||||
display: flex;
|
||||
height: 33px;
|
||||
padding: 4px 10px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex: 1 0 0;
|
||||
border-radius: 2px;
|
||||
background: var(--bg-cherry-500);
|
||||
border-color: none;
|
||||
}
|
||||
.cancel-run:hover {
|
||||
background-color: #ff7875 !important;
|
||||
color: var(--bg-vanilla-100) !important;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.left-toolbar {
|
||||
.left-toolbar-query-actions {
|
||||
@@ -68,4 +112,17 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.loading-container {
|
||||
.loading-btn {
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.cancel-run {
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.cancel-run:hover {
|
||||
background-color: #ff7875;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
|
||||
import LeftToolbarActions from '../LeftToolbarActions';
|
||||
import RightToolbarActions from '../RightToolbarActions';
|
||||
@@ -94,7 +95,9 @@ describe('ToolbarActions', () => {
|
||||
it('RightToolbarActions - render correctly with props', async () => {
|
||||
const onStageRunQuery = jest.fn();
|
||||
const { queryByText } = render(
|
||||
<RightToolbarActions onStageRunQuery={onStageRunQuery} />,
|
||||
<MockQueryClientProvider>
|
||||
<RightToolbarActions onStageRunQuery={onStageRunQuery} />,
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
|
||||
const stageNRunBtn = queryByText('Stage & Run Query');
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import './QueryBuilderSearch.styles.scss';
|
||||
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
function ExampleQueriesRendererForLogs({
|
||||
label,
|
||||
value,
|
||||
handleAddTag,
|
||||
}: ExampleQueriesRendererForLogsProps): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
className="example-query-container"
|
||||
onClick={(): void => {
|
||||
handleAddTag(value);
|
||||
}}
|
||||
>
|
||||
<span className="example-query">{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ExampleQueriesRendererForLogsProps {
|
||||
label: string;
|
||||
value: TagFilter;
|
||||
handleAddTag: (value: TagFilter) => void;
|
||||
}
|
||||
|
||||
export default ExampleQueriesRendererForLogs;
|
||||
@@ -0,0 +1,77 @@
|
||||
import './QueryBuilderSearch.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Tooltip, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { Zap } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { getOptionType } from './utils';
|
||||
|
||||
function OptionRendererForLogs({
|
||||
label,
|
||||
value,
|
||||
dataType,
|
||||
isIndexed,
|
||||
setDynamicPlaceholder,
|
||||
}: OptionRendererProps): JSX.Element {
|
||||
const [truncated, setTruncated] = useState<boolean>(false);
|
||||
const optionType = getOptionType(label);
|
||||
|
||||
return (
|
||||
<span
|
||||
className="option"
|
||||
onMouseEnter={(): void => setDynamicPlaceholder(value)}
|
||||
onFocus={(): void => setDynamicPlaceholder(value)}
|
||||
>
|
||||
{optionType ? (
|
||||
<Tooltip title={truncated ? `${value}` : ''} placement="topLeft">
|
||||
<div className="logs-options-select">
|
||||
<section className="left-section">
|
||||
{isIndexed ? (
|
||||
<Zap size={12} fill={Color.BG_AMBER_500} />
|
||||
) : (
|
||||
<div className="dot" />
|
||||
)}
|
||||
<Typography.Text
|
||||
className="text value"
|
||||
ellipsis={{ onEllipsis: (ellipsis): void => setTruncated(ellipsis) }}
|
||||
>
|
||||
{value}
|
||||
</Typography.Text>
|
||||
</section>
|
||||
<section className="right-section">
|
||||
<div className="text tags data-type-tag">{dataType}</div>
|
||||
<div className={cx('text tags option-type-tag', optionType)}>
|
||||
<div className="dot" />
|
||||
{optionType}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title={truncated ? `${label}` : ''} placement="topLeft">
|
||||
<div className="without-option-type">
|
||||
<div className="dot" />
|
||||
<Typography.Text
|
||||
className="text"
|
||||
ellipsis={{ onEllipsis: (ellipsis): void => setTruncated(ellipsis) }}
|
||||
>
|
||||
{label}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
interface OptionRendererProps {
|
||||
label: string;
|
||||
value: string;
|
||||
dataType: string;
|
||||
isIndexed: boolean;
|
||||
setDynamicPlaceholder: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
export default OptionRendererForLogs;
|
||||
@@ -11,6 +11,290 @@
|
||||
}
|
||||
}
|
||||
|
||||
.logs-popup {
|
||||
&.hide-scroll {
|
||||
.rc-virtual-list-holder {
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logs-explorer-popup {
|
||||
padding: 0px;
|
||||
.ant-select-item-group {
|
||||
padding: 12px 14px 8px 14px;
|
||||
color: var(--bg-slate-50);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.show-all-filter-props {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 13px;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.left-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.text {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.text:hover {
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
.right-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
.keyboard-shortcut-slash {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 2.286px;
|
||||
border-top: 1.143px solid var(--bg-ink-200);
|
||||
border-right: 1.143px solid var(--bg-ink-200);
|
||||
border-bottom: 2.286px solid var(--bg-ink-200);
|
||||
border-left: 1.143px solid var(--bg-ink-200);
|
||||
background: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.show-all-filter-props:hover {
|
||||
background: rgba(255, 255, 255, 0.04) !important;
|
||||
}
|
||||
|
||||
.example-queries {
|
||||
cursor: default;
|
||||
.heading {
|
||||
padding: 12px 14px 8px 14px;
|
||||
color: var(--bg-slate-50);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.query-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 0px 12px 12px 12px;
|
||||
cursor: pointer;
|
||||
|
||||
.example-query {
|
||||
display: flex;
|
||||
padding: 4px 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border-radius: 2px;
|
||||
background: var(--bg-ink-200);
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
letter-spacing: -0.07px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.example-query:hover {
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-item-option-grouped {
|
||||
padding-inline-start: 0px;
|
||||
padding: 7px 13px;
|
||||
}
|
||||
|
||||
.keyboard-shortcuts {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 0px 0px 4px 4px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
padding: 11px 16px;
|
||||
cursor: default;
|
||||
|
||||
.icons {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 2.286px;
|
||||
border-top: 1.143px solid var(--Ink-200, #23262e);
|
||||
border-right: 1.143px solid var(--Ink-200, #23262e);
|
||||
border-bottom: 2.286px solid var(--Ink-200, #23262e);
|
||||
border-left: 1.143px solid var(--Ink-200, #23262e);
|
||||
background: var(--Ink-400, #121317);
|
||||
}
|
||||
|
||||
.keyboard-text {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.navigate {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 12px;
|
||||
gap: 4px;
|
||||
border-right: 1px solid #1d212d;
|
||||
}
|
||||
|
||||
.update-query {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 12px;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.without-option-type {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
.dot {
|
||||
height: 5px;
|
||||
width: 5px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--bg-slate-300);
|
||||
}
|
||||
}
|
||||
|
||||
.logs-options-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.text {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
height: 20px;
|
||||
padding: 4px 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
height: 5px;
|
||||
width: 5px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.left-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 90%;
|
||||
|
||||
.dot {
|
||||
background-color: var(--bg-slate-300);
|
||||
}
|
||||
|
||||
.value {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.right-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.data-type-tag {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.option-type-tag {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
padding: 0px 6px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.tag {
|
||||
border-radius: 50px;
|
||||
background: rgba(189, 153, 121, 0.1);
|
||||
color: var(--bg-sienna-400);
|
||||
|
||||
.dot {
|
||||
background-color: var(--bg-sienna-400);
|
||||
}
|
||||
}
|
||||
|
||||
.resource {
|
||||
border-radius: 50px;
|
||||
background: rgba(245, 108, 135, 0.1);
|
||||
color: var(--bg-sakura-400);
|
||||
|
||||
.dot {
|
||||
background-color: var(--bg-sakura-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-item-option-active {
|
||||
.logs-options-select {
|
||||
.left-section {
|
||||
.value {
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.query-builder-search {
|
||||
.ant-select-dropdown {
|
||||
@@ -21,4 +305,108 @@
|
||||
background-color: var(--bg-vanilla-200) !important;
|
||||
}
|
||||
}
|
||||
.logs-explorer-popup {
|
||||
.ant-select-item-group {
|
||||
color: var(--bg-slate-50);
|
||||
}
|
||||
|
||||
.show-all-filter-props {
|
||||
.content {
|
||||
.left-section {
|
||||
.text {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.text:hover {
|
||||
color: var(--bg-slate-100);
|
||||
}
|
||||
}
|
||||
.right-section {
|
||||
.keyboard-shortcut-slash {
|
||||
border-top: 1.143px solid var(--bg-ink-200);
|
||||
border-right: 1.143px solid var(--bg-ink-200);
|
||||
border-bottom: 2.286px solid var(--bg-ink-200);
|
||||
border-left: 1.143px solid var(--bg-ink-200);
|
||||
background: var(--bg-vanilla-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.show-all-filter-props:hover {
|
||||
background: var(--bg-vanilla-200) !important;
|
||||
}
|
||||
|
||||
.example-queries {
|
||||
.heading {
|
||||
color: var(--bg-slate-50);
|
||||
}
|
||||
|
||||
.query-container {
|
||||
.example-query-container {
|
||||
.example-query {
|
||||
background: var(--bg-vanilla-200);
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.example-query:hover {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.keyboard-shortcuts {
|
||||
border: 1px solid var(--bg-vanilla-400);
|
||||
background: var(--bg-vanilla-200);
|
||||
|
||||
.icons {
|
||||
border-top: 1.143px solid var(--Ink-200, #23262e);
|
||||
border-right: 1.143px solid var(--Ink-200, #23262e);
|
||||
border-bottom: 2.286px solid var(--Ink-200, #23262e);
|
||||
border-left: 1.143px solid var(--Ink-200, #23262e);
|
||||
background: var(--bg-vanilla-200);
|
||||
}
|
||||
|
||||
.keyboard-text {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.navigate {
|
||||
border-right: 1px solid #1d212d;
|
||||
}
|
||||
}
|
||||
|
||||
.logs-options-select {
|
||||
.text {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.right-section {
|
||||
.data-type-tag {
|
||||
background: var(--bg-vanilla-200);
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: rgba(189, 153, 121, 0.1);
|
||||
color: var(--bg-sienna-400);
|
||||
}
|
||||
|
||||
.resource {
|
||||
background: rgba(245, 108, 135, 0.1);
|
||||
color: var(--bg-sakura-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-item-option-active {
|
||||
.logs-options-select {
|
||||
.left-section {
|
||||
.value {
|
||||
color: var(--bg-ink-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
/* eslint-disable react/no-unstable-nested-components */
|
||||
import './QueryBuilderSearch.styles.scss';
|
||||
|
||||
import { Select, Spin, Tag, Tooltip } from 'antd';
|
||||
import { Button, Select, Spin, Tag, Tooltip, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
|
||||
import { getDataTypes } from 'container/LogDetailedView/utils';
|
||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
@@ -11,7 +14,17 @@ import {
|
||||
} from 'hooks/queryBuilder/useAutoComplete';
|
||||
import { useFetchKeysAndValues } from 'hooks/queryBuilder/useFetchKeysAndValues';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { isEqual, isUndefined } from 'lodash-es';
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Command,
|
||||
CornerDownLeft,
|
||||
Filter,
|
||||
Slash,
|
||||
} from 'lucide-react';
|
||||
import type { BaseSelectRef } from 'rc-select';
|
||||
import {
|
||||
KeyboardEvent,
|
||||
@@ -23,6 +36,7 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
@@ -32,14 +46,18 @@ import {
|
||||
TagFilter,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { selectStyle } from './config';
|
||||
import { PLACEHOLDER } from './constant';
|
||||
import ExampleQueriesRendererForLogs from './ExampleQueriesRendererForLogs';
|
||||
import OptionRenderer from './OptionRenderer';
|
||||
import OptionRendererForLogs from './OptionRendererForLogs';
|
||||
import { StyledCheckOutlined, TypographyText } from './style';
|
||||
import {
|
||||
convertExampleQueriesToOptions,
|
||||
getOperatorValue,
|
||||
getRemovePrefixFromKey,
|
||||
getTagToken,
|
||||
@@ -55,6 +73,10 @@ function QueryBuilderSearch({
|
||||
placeholder,
|
||||
suffixIcon,
|
||||
}: QueryBuilderSearchProps): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
|
||||
pathname,
|
||||
]);
|
||||
const {
|
||||
updateTag,
|
||||
handleClearTag,
|
||||
@@ -69,14 +91,20 @@ function QueryBuilderSearch({
|
||||
isFetching,
|
||||
setSearchKey,
|
||||
searchKey,
|
||||
} = useAutoComplete(query, whereClauseConfig);
|
||||
|
||||
key,
|
||||
exampleQueries,
|
||||
} = useAutoComplete(query, whereClauseConfig, isLogsExplorerPage);
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [showAllFilters, setShowAllFilters] = useState<boolean>(false);
|
||||
const [dynamicPlacholder, setDynamicPlaceholder] = useState<string>(
|
||||
placeholder || '',
|
||||
);
|
||||
const selectRef = useRef<BaseSelectRef>(null);
|
||||
const { sourceKeys, handleRemoveSourceKey } = useFetchKeysAndValues(
|
||||
searchValue,
|
||||
query,
|
||||
searchKey,
|
||||
isLogsExplorerPage,
|
||||
);
|
||||
|
||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||
@@ -140,6 +168,12 @@ function QueryBuilderSearch({
|
||||
handleRunQuery();
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === '/') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setShowAllFilters((prev) => !prev);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeselect = useCallback(
|
||||
@@ -229,6 +263,28 @@ function QueryBuilderSearch({
|
||||
deregisterShortcut(LogsExplorerShortcuts.FocusTheSearchBar);
|
||||
}, [deregisterShortcut, isLastQuery, registerShortcut]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setDynamicPlaceholder(placeholder || '');
|
||||
}
|
||||
}, [isOpen, placeholder]);
|
||||
|
||||
const userOs = getUserOperatingSystem();
|
||||
|
||||
// conditional changes here to use a seperate component to render the example queries based on the option group label
|
||||
const customRendererForLogsExplorer = options.map((option) => (
|
||||
<Select.Option key={option.label} value={option.value}>
|
||||
<OptionRendererForLogs
|
||||
label={option.label}
|
||||
value={option.value}
|
||||
dataType={option.dataType || ''}
|
||||
isIndexed={option.isIndexed || false}
|
||||
setDynamicPlaceholder={setDynamicPlaceholder}
|
||||
/>
|
||||
{option.selected && <StyledCheckOutlined />}
|
||||
</Select.Option>
|
||||
));
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -238,7 +294,9 @@ function QueryBuilderSearch({
|
||||
<Select
|
||||
ref={selectRef}
|
||||
getPopupContainer={popupContainer}
|
||||
virtual
|
||||
transitionName=""
|
||||
choiceTransitionName=""
|
||||
virtual={false}
|
||||
showSearch
|
||||
tagRender={onTagRender}
|
||||
filterOption={false}
|
||||
@@ -246,10 +304,14 @@ function QueryBuilderSearch({
|
||||
onDropdownVisibleChange={setIsOpen}
|
||||
autoClearSearchValue={false}
|
||||
mode="multiple"
|
||||
placeholder={placeholder}
|
||||
placeholder={dynamicPlacholder}
|
||||
value={queryTags}
|
||||
searchValue={searchValue}
|
||||
className={className}
|
||||
className={cx(
|
||||
className,
|
||||
isLogsExplorerPage ? 'logs-popup' : '',
|
||||
!showAllFilters && options.length > 3 && !key ? 'hide-scroll' : '',
|
||||
)}
|
||||
rootClassName="query-builder-search"
|
||||
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
|
||||
style={selectStyle}
|
||||
@@ -259,20 +321,99 @@ function QueryBuilderSearch({
|
||||
onDeselect={handleDeselect}
|
||||
onInputKeyDown={onInputKeyDownHandler}
|
||||
notFoundContent={isFetching ? <Spin size="small" /> : null}
|
||||
suffixIcon={suffixIcon}
|
||||
suffixIcon={
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
!isUndefined(suffixIcon) ? (
|
||||
suffixIcon
|
||||
) : isOpen ? (
|
||||
<ChevronUp size={14} />
|
||||
) : (
|
||||
<ChevronDown size={14} />
|
||||
)
|
||||
}
|
||||
showAction={['focus']}
|
||||
onBlur={handleOnBlur}
|
||||
popupClassName={isLogsExplorerPage ? 'logs-explorer-popup' : ''}
|
||||
dropdownRender={(menu): ReactElement => (
|
||||
<div>
|
||||
{!searchKey && isLogsExplorerPage && (
|
||||
<div className="ant-select-item-group ">Suggested Filters</div>
|
||||
)}
|
||||
{menu}
|
||||
{isLogsExplorerPage && (
|
||||
<div>
|
||||
{!searchKey && tags.length === 0 && (
|
||||
<div className="example-queries">
|
||||
<div className="heading"> Example Queries </div>
|
||||
<div className="query-container">
|
||||
{convertExampleQueriesToOptions(exampleQueries).map((query) => (
|
||||
<ExampleQueriesRendererForLogs
|
||||
key={query.label}
|
||||
label={query.label}
|
||||
value={query.value}
|
||||
handleAddTag={onChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!key && !isFetching && !showAllFilters && options.length > 3 && (
|
||||
<Button
|
||||
type="text"
|
||||
className="show-all-filter-props"
|
||||
onClick={(): void => {
|
||||
setShowAllFilters(true);
|
||||
// when clicking on the button the search bar looses the focus
|
||||
selectRef?.current?.focus();
|
||||
}}
|
||||
>
|
||||
<div className="content">
|
||||
<section className="left-section">
|
||||
<Filter size={14} />
|
||||
<Typography.Text className="text">
|
||||
Show all filters properties
|
||||
</Typography.Text>
|
||||
</section>
|
||||
<section className="right-section">
|
||||
{userOs === UserOperatingSystem.MACOS ? (
|
||||
<Command size={14} className="keyboard-shortcut-slash" />
|
||||
) : (
|
||||
<ChevronUp size={14} className="keyboard-shortcut-slash" />
|
||||
)}
|
||||
+
|
||||
<Slash size={14} className="keyboard-shortcut-slash" />
|
||||
</section>
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
<div className="keyboard-shortcuts">
|
||||
<section className="navigate">
|
||||
<ArrowDown size={10} className="icons" />
|
||||
<ArrowUp size={10} className="icons" />
|
||||
<span className="keyboard-text">to navigate</span>
|
||||
</section>
|
||||
<section className="update-query">
|
||||
<CornerDownLeft size={10} className="icons" />
|
||||
<span className="keyboard-text">to update query</span>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<Select.Option key={option.label} value={option.value}>
|
||||
<OptionRenderer
|
||||
label={option.label}
|
||||
value={option.value}
|
||||
dataType={option.dataType || ''}
|
||||
/>
|
||||
{option.selected && <StyledCheckOutlined />}
|
||||
</Select.Option>
|
||||
))}
|
||||
{isLogsExplorerPage
|
||||
? customRendererForLogsExplorer
|
||||
: options.map((option) => (
|
||||
<Select.Option key={option.label} value={option.value}>
|
||||
<OptionRenderer
|
||||
label={option.label}
|
||||
value={option.value}
|
||||
dataType={option.dataType || ''}
|
||||
/>
|
||||
{option.selected && <StyledCheckOutlined />}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import { queryFilterTags } from 'hooks/queryBuilder/useTag';
|
||||
import { parse } from 'papaparse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { orderByValueDelimiter } from '../OrderByFilter/utils';
|
||||
|
||||
@@ -162,3 +164,17 @@ export function getOptionType(label: string): MetricsType | undefined {
|
||||
|
||||
return optionType;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param exampleQueries the example queries based on recommendation engine
|
||||
* @returns the data formatted to the Option[]
|
||||
*/
|
||||
export function convertExampleQueriesToOptions(
|
||||
exampleQueries: TagFilter[],
|
||||
): { label: string; value: TagFilter }[] {
|
||||
return exampleQueries.map((query) => ({
|
||||
value: query,
|
||||
label: queryFilterTags(query).join(' , '),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -15,4 +15,5 @@ export type Option = {
|
||||
label: string;
|
||||
selected?: boolean;
|
||||
dataType?: string;
|
||||
isIndexed?: boolean;
|
||||
};
|
||||
|
||||
@@ -8,7 +8,10 @@ import {
|
||||
import { Option } from 'container/QueryBuilder/type';
|
||||
import { parse } from 'papaparse';
|
||||
import { KeyboardEvent, useCallback, useState } from 'react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
TagFilter,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { useFetchKeysAndValues } from './useFetchKeysAndValues';
|
||||
import { useOptions, WHERE_CLAUSE_CUSTOM_SUFFIX } from './useOptions';
|
||||
@@ -24,14 +27,16 @@ export type WhereClauseConfig = {
|
||||
export const useAutoComplete = (
|
||||
query: IBuilderQuery,
|
||||
whereClauseConfig?: WhereClauseConfig,
|
||||
shouldUseSuggestions?: boolean,
|
||||
): IAutoComplete => {
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const [searchKey, setSearchKey] = useState<string>('');
|
||||
|
||||
const { keys, results, isFetching } = useFetchKeysAndValues(
|
||||
const { keys, results, isFetching, exampleQueries } = useFetchKeysAndValues(
|
||||
searchValue,
|
||||
query,
|
||||
searchKey,
|
||||
shouldUseSuggestions,
|
||||
);
|
||||
|
||||
const [key, operator, result] = useSetCurrentKeyAndOperator(searchValue, keys);
|
||||
@@ -144,6 +149,8 @@ export const useAutoComplete = (
|
||||
isFetching,
|
||||
setSearchKey,
|
||||
searchKey,
|
||||
key,
|
||||
exampleQueries,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -161,4 +168,6 @@ interface IAutoComplete {
|
||||
isFetching: boolean;
|
||||
setSearchKey: (value: string) => void;
|
||||
searchKey: string;
|
||||
key: string;
|
||||
exampleQueries: TagFilter[];
|
||||
}
|
||||
|
||||
@@ -6,17 +6,21 @@ import {
|
||||
isInNInOperator,
|
||||
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import useDebounceValue from 'hooks/useDebounce';
|
||||
import { isEqual, uniqWith } from 'lodash-es';
|
||||
import { cloneDeep, isEqual, uniqWith, unset } from 'lodash-es';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useDebounce } from 'react-use';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
TagFilter,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { useGetAggregateKeys } from './useGetAggregateKeys';
|
||||
import { useGetAttributeSuggestions } from './useGetAttributeSuggestions';
|
||||
|
||||
type IuseFetchKeysAndValues = {
|
||||
keys: BaseAutocompleteData[];
|
||||
@@ -24,6 +28,7 @@ type IuseFetchKeysAndValues = {
|
||||
isFetching: boolean;
|
||||
sourceKeys: BaseAutocompleteData[];
|
||||
handleRemoveSourceKey: (newSourceKey: string) => void;
|
||||
exampleQueries: TagFilter[];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -37,8 +42,10 @@ export const useFetchKeysAndValues = (
|
||||
searchValue: string,
|
||||
query: IBuilderQuery,
|
||||
searchKey: string,
|
||||
shouldUseSuggestions?: boolean,
|
||||
): IuseFetchKeysAndValues => {
|
||||
const [keys, setKeys] = useState<BaseAutocompleteData[]>([]);
|
||||
const [exampleQueries, setExampleQueries] = useState<TagFilter[]>([]);
|
||||
const [sourceKeys, setSourceKeys] = useState<BaseAutocompleteData[]>([]);
|
||||
const [results, setResults] = useState<string[]>([]);
|
||||
const [isAggregateFetching, setAggregateFetching] = useState<boolean>(false);
|
||||
@@ -60,6 +67,28 @@ export const useFetchKeysAndValues = (
|
||||
|
||||
const searchParams = useDebounceValue(memoizedSearchParams, DEBOUNCE_DELAY);
|
||||
|
||||
const queryFiltersWithoutId = useMemo(
|
||||
() => ({
|
||||
...query.filters,
|
||||
items: query.filters.items.map((item) => {
|
||||
const filterWithoutId = cloneDeep(item);
|
||||
unset(filterWithoutId, 'id');
|
||||
return filterWithoutId;
|
||||
}),
|
||||
}),
|
||||
[query.filters],
|
||||
);
|
||||
|
||||
const memoizedSuggestionsParams = useMemo(
|
||||
() => [searchKey, query.dataSource, queryFiltersWithoutId],
|
||||
[query.dataSource, queryFiltersWithoutId, searchKey],
|
||||
);
|
||||
|
||||
const suggestionsParams = useDebounceValue(
|
||||
memoizedSuggestionsParams,
|
||||
DEBOUNCE_DELAY,
|
||||
);
|
||||
|
||||
const isQueryEnabled = useMemo(
|
||||
() =>
|
||||
query.dataSource === DataSource.METRICS
|
||||
@@ -82,7 +111,26 @@ export const useFetchKeysAndValues = (
|
||||
aggregateAttribute: query.aggregateAttribute.key,
|
||||
tagType: query.aggregateAttribute.type ?? null,
|
||||
},
|
||||
{ queryKey: [searchParams], enabled: isQueryEnabled },
|
||||
{
|
||||
queryKey: [searchParams],
|
||||
enabled: isQueryEnabled && !shouldUseSuggestions,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: suggestionsData,
|
||||
isFetching: isFetchingSuggestions,
|
||||
status: fetchingSuggestionsStatus,
|
||||
} = useGetAttributeSuggestions(
|
||||
{
|
||||
searchText: searchKey,
|
||||
dataSource: query.dataSource,
|
||||
filters: query.filters,
|
||||
},
|
||||
{
|
||||
queryKey: [suggestionsParams],
|
||||
enabled: isQueryEnabled && shouldUseSuggestions,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -162,11 +210,41 @@ export const useFetchKeysAndValues = (
|
||||
}
|
||||
}, [data?.payload?.attributeKeys, status]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
fetchingSuggestionsStatus === 'success' &&
|
||||
suggestionsData?.payload?.attributes
|
||||
) {
|
||||
setKeys(suggestionsData.payload.attributes);
|
||||
setSourceKeys((prevState) =>
|
||||
uniqWith(
|
||||
[...(suggestionsData.payload.attributes ?? []), ...prevState],
|
||||
isEqual,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setKeys([]);
|
||||
}
|
||||
if (
|
||||
fetchingSuggestionsStatus === 'success' &&
|
||||
suggestionsData?.payload?.example_queries
|
||||
) {
|
||||
setExampleQueries(suggestionsData.payload.example_queries);
|
||||
} else {
|
||||
setExampleQueries([]);
|
||||
}
|
||||
}, [
|
||||
suggestionsData?.payload?.attributes,
|
||||
fetchingSuggestionsStatus,
|
||||
suggestionsData?.payload?.example_queries,
|
||||
]);
|
||||
|
||||
return {
|
||||
keys,
|
||||
results,
|
||||
isFetching: isFetching || isAggregateFetching,
|
||||
isFetching: isFetching || isAggregateFetching || isFetchingSuggestions,
|
||||
sourceKeys,
|
||||
handleRemoveSourceKey,
|
||||
exampleQueries,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { getAttributeSuggestions } from 'api/queryBuilder/getAttributeSuggestions';
|
||||
import { QueryBuilderKeys } from 'constants/queryBuilder';
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
IGetAttributeSuggestionsPayload,
|
||||
IGetAttributeSuggestionsSuccessResponse,
|
||||
} from 'types/api/queryBuilder/getAttributeSuggestions';
|
||||
|
||||
type UseGetAttributeSuggestions = (
|
||||
requestData: IGetAttributeSuggestionsPayload,
|
||||
options?: UseQueryOptions<
|
||||
SuccessResponse<IGetAttributeSuggestionsSuccessResponse> | ErrorResponse
|
||||
>,
|
||||
) => UseQueryResult<
|
||||
SuccessResponse<IGetAttributeSuggestionsSuccessResponse> | ErrorResponse
|
||||
>;
|
||||
|
||||
export const useGetAttributeSuggestions: UseGetAttributeSuggestions = (
|
||||
requestData,
|
||||
options,
|
||||
) => {
|
||||
const queryKey = useMemo(() => {
|
||||
if (options?.queryKey && Array.isArray(options.queryKey)) {
|
||||
return [QueryBuilderKeys.GET_ATTRIBUTE_SUGGESTIONS, ...options.queryKey];
|
||||
}
|
||||
return [QueryBuilderKeys.GET_ATTRIBUTE_SUGGESTIONS, requestData];
|
||||
}, [options?.queryKey, requestData]);
|
||||
|
||||
return useQuery<
|
||||
SuccessResponse<IGetAttributeSuggestionsSuccessResponse> | ErrorResponse
|
||||
>({
|
||||
queryKey,
|
||||
queryFn: () => getAttributeSuggestions(requestData),
|
||||
...options,
|
||||
});
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useMemo } from 'react';
|
||||
import { MutableRefObject, useMemo } from 'react';
|
||||
import { UseQueryOptions, UseQueryResult } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -19,6 +19,8 @@ export const useGetExplorerQueryRange = (
|
||||
options?: UseQueryOptions<SuccessResponse<MetricRangePayloadProps>, Error>,
|
||||
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<
|
||||
@@ -40,6 +42,11 @@ export const useGetExplorerQueryRange = (
|
||||
return isEnabledQuery;
|
||||
}, [options, isEnabledQuery, isDependentOnQB]);
|
||||
|
||||
if (keyRef) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
keyRef.current = [key, globalSelectedInterval, requestData, minTime, maxTime];
|
||||
}
|
||||
|
||||
return useGetQueryRange(
|
||||
{
|
||||
graphType: panelType || PANEL_TYPES.LIST,
|
||||
@@ -55,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,
|
||||
});
|
||||
|
||||
@@ -45,6 +45,7 @@ export const useOptions = (
|
||||
label: `${getLabel(item)}`,
|
||||
value: item.key,
|
||||
dataType: item.dataType,
|
||||
isIndexed: item?.isIndexed,
|
||||
})),
|
||||
[getLabel],
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import RightToolbarActions from 'container/QueryBuilder/components/ToolbarAction
|
||||
import Toolbar from 'container/Toolbar/Toolbar';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { WrapperStyled } from './styles';
|
||||
@@ -23,6 +23,12 @@ function LogsExplorer(): JSX.Element {
|
||||
|
||||
const { handleRunQuery, currentQuery } = useQueryBuilder();
|
||||
|
||||
const listQueryKeyRef = useRef<any>();
|
||||
|
||||
const chartQueryKeyRef = useRef<any>();
|
||||
|
||||
const [isLoadingQueries, setIsLoadingQueries] = useState<boolean>(false);
|
||||
|
||||
const handleToggleShowFrequencyChart = (): void => {
|
||||
setShowFrequencyChart(!showFrequencyChart);
|
||||
};
|
||||
@@ -82,7 +88,14 @@ function LogsExplorer(): JSX.Element {
|
||||
showFrequencyChart={showFrequencyChart}
|
||||
/>
|
||||
}
|
||||
rightActions={<RightToolbarActions onStageRunQuery={handleRunQuery} />}
|
||||
rightActions={
|
||||
<RightToolbarActions
|
||||
onStageRunQuery={handleRunQuery}
|
||||
listQueryKeyRef={listQueryKeyRef}
|
||||
chartQueryKeyRef={chartQueryKeyRef}
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
/>
|
||||
}
|
||||
showOldCTA
|
||||
/>
|
||||
|
||||
@@ -97,6 +110,9 @@ function LogsExplorer(): JSX.Element {
|
||||
<LogsExplorerViews
|
||||
selectedView={selectedView}
|
||||
showFrequencyChart={showFrequencyChart}
|
||||
listQueryKeyRef={listQueryKeyRef}
|
||||
chartQueryKeyRef={chartQueryKeyRef}
|
||||
setIsLoadingQueries={setIsLoadingQueries}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { BaseAutocompleteData } from './queryAutocompleteResponse';
|
||||
import { TagFilter } from './queryBuilderData';
|
||||
|
||||
export interface IGetAttributeSuggestionsPayload {
|
||||
dataSource: DataSource;
|
||||
searchText: string;
|
||||
filters: TagFilter;
|
||||
}
|
||||
|
||||
export interface IGetAttributeSuggestionsSuccessResponse {
|
||||
attributes: BaseAutocompleteData[];
|
||||
example_queries: TagFilter[];
|
||||
}
|
||||
@@ -21,6 +21,7 @@ export interface BaseAutocompleteData {
|
||||
key: string;
|
||||
type: AutocompleteType | string | null;
|
||||
isJSON?: boolean;
|
||||
isIndexed?: boolean;
|
||||
}
|
||||
|
||||
export interface IQueryAutocompleteResponse {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -3346,17 +3346,40 @@ func (r *ClickHouseReader) GetDashboardsInfo(ctx context.Context) (*model.Dashbo
|
||||
return &dashboardsInfo, err
|
||||
}
|
||||
totalDashboardsWithPanelAndName := 0
|
||||
var dashboardNames []string
|
||||
count := 0
|
||||
for _, dashboard := range dashboardsData {
|
||||
if isDashboardWithPanelAndName(dashboard.Data) {
|
||||
totalDashboardsWithPanelAndName = totalDashboardsWithPanelAndName + 1
|
||||
}
|
||||
dashboardsInfo = countPanelsInDashboard(dashboard.Data)
|
||||
dashboardName := extractDashboardName(dashboard.Data)
|
||||
if dashboardName != "" {
|
||||
dashboardNames = append(dashboardNames, dashboardName)
|
||||
}
|
||||
dashboardInfo := countPanelsInDashboard(dashboard.Data)
|
||||
dashboardsInfo.LogsBasedPanels += dashboardInfo.LogsBasedPanels
|
||||
dashboardsInfo.TracesBasedPanels += dashboardInfo.TracesBasedPanels
|
||||
dashboardsInfo.MetricBasedPanels += dashboardsInfo.MetricBasedPanels
|
||||
if isDashboardWithTSV2(dashboard.Data) {
|
||||
count = count + 1
|
||||
}
|
||||
}
|
||||
|
||||
dashboardsInfo.DashboardNames = dashboardNames
|
||||
dashboardsInfo.TotalDashboards = len(dashboardsData)
|
||||
dashboardsInfo.TotalDashboardsWithPanelAndName = totalDashboardsWithPanelAndName
|
||||
dashboardsInfo.QueriesWithTSV2 = count
|
||||
return &dashboardsInfo, nil
|
||||
}
|
||||
|
||||
func isDashboardWithTSV2(data map[string]interface{}) bool {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(string(jsonData), "time_series_v2")
|
||||
}
|
||||
|
||||
func isDashboardWithPanelAndName(data map[string]interface{}) bool {
|
||||
isDashboardName := false
|
||||
isDashboardWithPanelAndName := false
|
||||
@@ -3376,6 +3399,19 @@ func isDashboardWithPanelAndName(data map[string]interface{}) bool {
|
||||
|
||||
return isDashboardWithPanelAndName
|
||||
}
|
||||
|
||||
func extractDashboardName(data map[string]interface{}) string {
|
||||
|
||||
if data != nil && data["title"] != nil {
|
||||
title, ok := data["title"].(string)
|
||||
if ok {
|
||||
return title
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func countPanelsInDashboard(data map[string]interface{}) model.DashboardsInfo {
|
||||
var logsPanelCount, tracesPanelCount, metricsPanelCount int
|
||||
// totalPanels := 0
|
||||
|
||||
@@ -634,20 +634,22 @@ type TagsInfo struct {
|
||||
}
|
||||
|
||||
type AlertsInfo struct {
|
||||
TotalAlerts int `json:"totalAlerts"`
|
||||
LogsBasedAlerts int `json:"logsBasedAlerts"`
|
||||
MetricBasedAlerts int `json:"metricBasedAlerts"`
|
||||
TracesBasedAlerts int `json:"tracesBasedAlerts"`
|
||||
SlackChannels int `json:"slackChannels"`
|
||||
WebHookChannels int `json:"webHookChannels"`
|
||||
PagerDutyChannels int `json:"pagerDutyChannels"`
|
||||
OpsGenieChannels int `json:"opsGenieChannels"`
|
||||
EmailChannels int `json:"emailChannels"`
|
||||
MSTeamsChannels int `json:"microsoftTeamsChannels"`
|
||||
MetricsBuilderQueries int `json:"metricsBuilderQueries"`
|
||||
MetricsClickHouseQueries int `json:"metricsClickHouseQueries"`
|
||||
MetricsPrometheusQueries int `json:"metricsPrometheusQueries"`
|
||||
SpanMetricsPrometheusQueries int `json:"spanMetricsPrometheusQueries"`
|
||||
TotalAlerts int `json:"totalAlerts"`
|
||||
LogsBasedAlerts int `json:"logsBasedAlerts"`
|
||||
MetricBasedAlerts int `json:"metricBasedAlerts"`
|
||||
TracesBasedAlerts int `json:"tracesBasedAlerts"`
|
||||
SlackChannels int `json:"slackChannels"`
|
||||
WebHookChannels int `json:"webHookChannels"`
|
||||
PagerDutyChannels int `json:"pagerDutyChannels"`
|
||||
OpsGenieChannels int `json:"opsGenieChannels"`
|
||||
EmailChannels int `json:"emailChannels"`
|
||||
MSTeamsChannels int `json:"microsoftTeamsChannels"`
|
||||
MetricsBuilderQueries int `json:"metricsBuilderQueries"`
|
||||
MetricsClickHouseQueries int `json:"metricsClickHouseQueries"`
|
||||
MetricsPrometheusQueries int `json:"metricsPrometheusQueries"`
|
||||
SpanMetricsPrometheusQueries int `json:"spanMetricsPrometheusQueries"`
|
||||
AlertNames []string `json:"alertNames"`
|
||||
AlertsWithTSV2 int `json:"alertsWithTSv2"`
|
||||
}
|
||||
|
||||
type SavedViewsInfo struct {
|
||||
@@ -657,11 +659,13 @@ type SavedViewsInfo struct {
|
||||
}
|
||||
|
||||
type DashboardsInfo struct {
|
||||
TotalDashboards int `json:"totalDashboards"`
|
||||
TotalDashboardsWithPanelAndName int `json:"totalDashboardsWithPanelAndName"` // dashboards with panel and name without sample title
|
||||
LogsBasedPanels int `json:"logsBasedPanels"`
|
||||
MetricBasedPanels int `json:"metricBasedPanels"`
|
||||
TracesBasedPanels int `json:"tracesBasedPanels"`
|
||||
TotalDashboards int `json:"totalDashboards"`
|
||||
TotalDashboardsWithPanelAndName int `json:"totalDashboardsWithPanelAndName"` // dashboards with panel and name without sample title
|
||||
LogsBasedPanels int `json:"logsBasedPanels"`
|
||||
MetricBasedPanels int `json:"metricBasedPanels"`
|
||||
TracesBasedPanels int `json:"tracesBasedPanels"`
|
||||
DashboardNames []string `json:"dashboardNames"`
|
||||
QueriesWithTSV2 int `json:"queriesWithTSV2"`
|
||||
}
|
||||
|
||||
type TagTelemetryData struct {
|
||||
|
||||
@@ -308,6 +308,7 @@ func (r *ruleDB) GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error) {
|
||||
// fetch alerts from rules db
|
||||
query := "SELECT data FROM rules"
|
||||
var alertsData []string
|
||||
var alertNames []string
|
||||
err := r.Select(&alertsData, query)
|
||||
if err != nil {
|
||||
zap.L().Error("Error in processing sql query", zap.Error(err))
|
||||
@@ -315,11 +316,15 @@ func (r *ruleDB) GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error) {
|
||||
}
|
||||
for _, alert := range alertsData {
|
||||
var rule GettableRule
|
||||
if strings.Contains(alert, "time_series_v2") {
|
||||
alertsInfo.AlertsWithTSV2 = alertsInfo.AlertsWithTSV2 + 1
|
||||
}
|
||||
err = json.Unmarshal([]byte(alert), &rule)
|
||||
if err != nil {
|
||||
zap.L().Error("invalid rule data", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
alertNames = append(alertNames, rule.AlertName)
|
||||
if rule.AlertType == "LOGS_BASED_ALERT" {
|
||||
alertsInfo.LogsBasedAlerts = alertsInfo.LogsBasedAlerts + 1
|
||||
} else if rule.AlertType == "METRIC_BASED_ALERT" {
|
||||
@@ -343,6 +348,6 @@ func (r *ruleDB) GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error) {
|
||||
}
|
||||
alertsInfo.TotalAlerts = alertsInfo.TotalAlerts + 1
|
||||
}
|
||||
|
||||
alertsInfo.AlertNames = alertNames
|
||||
return &alertsInfo, nil
|
||||
}
|
||||
|
||||
@@ -314,10 +314,14 @@ func createTelemetry() {
|
||||
dashboardsAlertsData := map[string]interface{}{
|
||||
"totalDashboards": dashboardsInfo.TotalDashboards,
|
||||
"totalDashboardsWithPanelAndName": dashboardsInfo.TotalDashboardsWithPanelAndName,
|
||||
"dashboardNames": dashboardsInfo.DashboardNames,
|
||||
"alertNames": alertsInfo.AlertNames,
|
||||
"logsBasedPanels": dashboardsInfo.LogsBasedPanels,
|
||||
"metricBasedPanels": dashboardsInfo.MetricBasedPanels,
|
||||
"tracesBasedPanels": dashboardsInfo.TracesBasedPanels,
|
||||
"dashboardsWithTSV2": dashboardsInfo.QueriesWithTSV2,
|
||||
"totalAlerts": alertsInfo.TotalAlerts,
|
||||
"alertsWithTSV2": alertsInfo.AlertsWithTSV2,
|
||||
"logsBasedAlerts": alertsInfo.LogsBasedAlerts,
|
||||
"metricBasedAlerts": alertsInfo.MetricBasedAlerts,
|
||||
"tracesBasedAlerts": alertsInfo.TracesBasedAlerts,
|
||||
|
||||
Reference in New Issue
Block a user