Compare commits
28 Commits
v0.52.0
...
v0.53.0-cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33541a2ac0 | ||
|
|
947b5bdefb | ||
|
|
bd7d14b1ca | ||
|
|
43ed49f9d9 | ||
|
|
758b10f1bf | ||
|
|
ab1caf13fc | ||
|
|
96b81817e0 | ||
|
|
bfeceb0ed2 | ||
|
|
c322fc72d9 | ||
|
|
e7b5410c5b | ||
|
|
072693d57d | ||
|
|
a20794040a | ||
|
|
ab4a8dfbea | ||
|
|
fa0a065b95 | ||
|
|
abc8096a39 | ||
|
|
7cff07333f | ||
|
|
5796d6cb8c | ||
|
|
98367fd054 | ||
|
|
ff8df5dc36 | ||
|
|
f0c9f12897 | ||
|
|
79e96e544f | ||
|
|
871e5ada9e | ||
|
|
0401c27dbc | ||
|
|
57c45f22d6 | ||
|
|
29f1883edd | ||
|
|
5d903b5487 | ||
|
|
1b9683d699 | ||
|
|
65280cf4e1 |
6
.github/CODEOWNERS
vendored
6
.github/CODEOWNERS
vendored
@@ -5,6 +5,6 @@
|
||||
/frontend/ @YounixM
|
||||
/frontend/src/container/MetricsApplication @srikanthccv
|
||||
/frontend/src/container/NewWidget/RightContainer/types.ts @srikanthccv
|
||||
/deploy/ @prashant-shahi
|
||||
/sample-apps/ @prashant-shahi
|
||||
.github @prashant-shahi
|
||||
/deploy/ @SigNoz/devops
|
||||
/sample-apps/ @SigNoz/devops
|
||||
.github @SigNoz/devops
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -67,3 +67,6 @@ e2e/.auth
|
||||
# go
|
||||
vendor/
|
||||
**/main/**
|
||||
|
||||
# git-town
|
||||
.git-branches.toml
|
||||
|
||||
@@ -359,6 +359,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
|
||||
apiHandler.RegisterIntegrationRoutes(r, am)
|
||||
apiHandler.RegisterQueryRangeV3Routes(r, am)
|
||||
apiHandler.RegisterQueryRangeV4Routes(r, am)
|
||||
apiHandler.RegisterWebSocketPaths(r, am)
|
||||
|
||||
c := cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
@@ -375,6 +376,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TODO(remove): Implemented at pkg/http/middleware/logging.go
|
||||
// loggingMiddleware is used for logging public api calls
|
||||
func loggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -386,6 +388,7 @@ func loggingMiddleware(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
// TODO(remove): Implemented at pkg/http/middleware/logging.go
|
||||
// loggingMiddlewarePrivate is used for logging private api calls
|
||||
// from internal services like alert manager
|
||||
func loggingMiddlewarePrivate(next http.Handler) http.Handler {
|
||||
@@ -398,27 +401,32 @@ func loggingMiddlewarePrivate(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
// TODO(remove): Implemented at pkg/http/middleware/logging.go
|
||||
type loggingResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
}
|
||||
|
||||
// TODO(remove): Implemented at pkg/http/middleware/logging.go
|
||||
func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter {
|
||||
// WriteHeader(int) is not called if our response implicitly returns 200 OK, so
|
||||
// we default to that status code.
|
||||
return &loggingResponseWriter{w, http.StatusOK}
|
||||
}
|
||||
|
||||
// TODO(remove): Implemented at pkg/http/middleware/logging.go
|
||||
func (lrw *loggingResponseWriter) WriteHeader(code int) {
|
||||
lrw.statusCode = code
|
||||
lrw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
// TODO(remove): Implemented at pkg/http/middleware/logging.go
|
||||
// Flush implements the http.Flush interface.
|
||||
func (lrw *loggingResponseWriter) Flush() {
|
||||
lrw.ResponseWriter.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// TODO(remove): Implemented at pkg/http/middleware/logging.go
|
||||
// Support websockets
|
||||
func (lrw *loggingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
h, ok := lrw.ResponseWriter.(http.Hijacker)
|
||||
@@ -564,6 +572,7 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
// TODO(remove): Implemented at pkg/http/middleware/timeout.go
|
||||
func setTimeoutMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
@@ -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/groupBy.svg
Normal file
1
frontend/public/Icons/groupBy.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_4344_1236)" stroke="#C0C1C3" stroke-width="1.167" stroke-linecap="round" stroke-linejoin="round"><path d="M4.667 1.167H2.333c-.644 0-1.166.522-1.166 1.166v2.334c0 .644.522 1.166 1.166 1.166h2.334c.644 0 1.166-.522 1.166-1.166V2.333c0-.644-.522-1.166-1.166-1.166zM8.167 1.167a1.17 1.17 0 011.166 1.166v2.334a1.17 1.17 0 01-1.166 1.166M11.667 1.167a1.17 1.17 0 011.166 1.166v2.334a1.17 1.17 0 01-1.166 1.166M5.833 10.5H2.917c-.992 0-1.75-.758-1.75-1.75v-.583"/><path d="M4.083 12.25l1.75-1.75-1.75-1.75M11.667 8.167H9.333c-.644 0-1.166.522-1.166 1.166v2.334c0 .644.522 1.166 1.166 1.166h2.334c.644 0 1.166-.522 1.166-1.166V9.333c0-.644-.522-1.166-1.166-1.166z"/></g><defs><clipPath id="prefix__clip0_4344_1236"><path fill="#fff" d="M0 0h14v14H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 878 B |
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 |
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"create_dashboard": "Create Dashboard",
|
||||
"import_json": "Import Dashboard JSON",
|
||||
"view_template": "View templates",
|
||||
"import_grafana_json": "Import Grafana JSON",
|
||||
"copy_to_clipboard": "Copy To ClipBoard",
|
||||
"download_json": "Download JSON",
|
||||
|
||||
49
frontend/src/api/common/getQueryStats.ts
Normal file
49
frontend/src/api/common/getQueryStats.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
|
||||
export interface WsDataEvent {
|
||||
read_rows: number;
|
||||
read_bytes: number;
|
||||
elapsed_ms: number;
|
||||
}
|
||||
interface GetQueryStatsProps {
|
||||
queryId: string;
|
||||
setData: React.Dispatch<React.SetStateAction<WsDataEvent | undefined>>;
|
||||
}
|
||||
|
||||
function getURL(baseURL: string, queryId: string): URL | string {
|
||||
if (baseURL && !isEmpty(baseURL)) {
|
||||
return `${baseURL}/ws/query_progress?q=${queryId}`;
|
||||
}
|
||||
const url = new URL(`/ws/query_progress?q=${queryId}`, window.location.href);
|
||||
|
||||
if (window.location.protocol === 'http:') {
|
||||
url.protocol = 'ws';
|
||||
} else {
|
||||
url.protocol = 'wss';
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function getQueryStats(props: GetQueryStatsProps): void {
|
||||
const { queryId, setData } = props;
|
||||
|
||||
const token = getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN) || '';
|
||||
|
||||
// https://github.com/whatwg/websockets/issues/20 reason for not using the relative URLs
|
||||
const url = getURL(ENVIRONMENT.wsURL, queryId);
|
||||
|
||||
const socket = new WebSocket(url, token);
|
||||
|
||||
socket.addEventListener('message', (event) => {
|
||||
try {
|
||||
const parsedData = JSON.parse(event?.data);
|
||||
setData(parsedData);
|
||||
} catch {
|
||||
setData(event?.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ApiV2Instance as axios } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||
import store from 'store';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
Props,
|
||||
@@ -11,7 +13,26 @@ const dashboardVariablesQuery = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<VariableResponseProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post(`/variables/query`, props);
|
||||
const { globalTime } = store.getState();
|
||||
const { start, end } = getStartEndRangeTime({
|
||||
type: 'GLOBAL_TIME',
|
||||
interval: globalTime.selectedTime,
|
||||
});
|
||||
|
||||
const timeVariables: Record<string, number> = {
|
||||
start_timestamp_ms: parseInt(start, 10) * 1e3,
|
||||
end_timestamp_ms: parseInt(end, 10) * 1e3,
|
||||
start_timestamp_nano: parseInt(start, 10) * 1e9,
|
||||
end_timestamp_nano: parseInt(end, 10) * 1e9,
|
||||
start_timestamp: parseInt(start, 10),
|
||||
end_timestamp: parseInt(end, 10),
|
||||
};
|
||||
|
||||
const payload = { ...props };
|
||||
|
||||
payload.variables = { ...payload.variables, ...timeVariables };
|
||||
|
||||
const response = await axios.post(`/variables/query`, payload);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
27
frontend/src/assets/CustomIcons/GroupByIcon.tsx
Normal file
27
frontend/src/assets/CustomIcons/GroupByIcon.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
function GroupByIcon(): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
return (
|
||||
<svg width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g
|
||||
clipPath="url(#prefix__clip0_4344_1236)"
|
||||
stroke={isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500}
|
||||
strokeWidth="1.167"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M4.667 1.167H2.333c-.644 0-1.166.522-1.166 1.166v2.334c0 .644.522 1.166 1.166 1.166h2.334c.644 0 1.166-.522 1.166-1.166V2.333c0-.644-.522-1.166-1.166-1.166zM8.167 1.167a1.17 1.17 0 011.166 1.166v2.334a1.17 1.17 0 01-1.166 1.166M11.667 1.167a1.17 1.17 0 011.166 1.166v2.334a1.17 1.17 0 01-1.166 1.166M5.833 10.5H2.917c-.992 0-1.75-.758-1.75-1.75v-.583" />
|
||||
<path d="M4.083 12.25l1.75-1.75-1.75-1.75M11.667 8.167H9.333c-.644 0-1.166.522-1.166 1.166v2.334c0 .644.522 1.166 1.166 1.166h2.334c.644 0 1.166-.522 1.166-1.166V9.333c0-.644-.522-1.166-1.166-1.166z" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="prefix__clip0_4344_1236">
|
||||
<path fill="#fff" d="M0 0h14v14H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default GroupByIcon;
|
||||
@@ -7,6 +7,7 @@ import { useNotifications } from 'hooks/useNotifications';
|
||||
import { CreditCard, X } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||
import { License } from 'types/api/licenses/def';
|
||||
@@ -57,11 +58,11 @@ export default function ChatSupportGateway(): JSX.Element {
|
||||
onError: handleBillingOnError,
|
||||
},
|
||||
);
|
||||
|
||||
const { pathname } = useLocation();
|
||||
const handleAddCreditCard = (): void => {
|
||||
logEvent('Add Credit card modal: Clicked', {
|
||||
source: `intercom icon`,
|
||||
page: '',
|
||||
page: pathname,
|
||||
});
|
||||
|
||||
updateCreditCard({
|
||||
@@ -79,7 +80,7 @@ export default function ChatSupportGateway(): JSX.Element {
|
||||
onClick={(): void => {
|
||||
logEvent('Disabled Chat Support: Clicked', {
|
||||
source: `intercom icon`,
|
||||
page: '',
|
||||
page: pathname,
|
||||
});
|
||||
|
||||
setIsAddCreditCardModalOpen(true);
|
||||
|
||||
@@ -13,6 +13,7 @@ import { defaultTo } from 'lodash-es';
|
||||
import { CreditCard, HelpCircle, X } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||
import { License } from 'types/api/licenses/def';
|
||||
@@ -47,6 +48,7 @@ function LaunchChatSupport({
|
||||
false,
|
||||
);
|
||||
|
||||
const { pathname } = useLocation();
|
||||
const isPremiumChatSupportEnabled =
|
||||
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
|
||||
|
||||
@@ -65,6 +67,11 @@ function LaunchChatSupport({
|
||||
|
||||
const handleFacingIssuesClick = (): void => {
|
||||
if (showAddCreditCardModal) {
|
||||
logEvent('Disabled Chat Support: Clicked', {
|
||||
source: `facing issues button`,
|
||||
page: pathname,
|
||||
...attributes,
|
||||
});
|
||||
setIsAddCreditCardModalOpen(true);
|
||||
} else {
|
||||
logEvent(eventName, attributes);
|
||||
@@ -105,7 +112,7 @@ function LaunchChatSupport({
|
||||
const handleAddCreditCard = (): void => {
|
||||
logEvent('Add Credit card modal: Clicked', {
|
||||
source: `facing issues button`,
|
||||
page: '',
|
||||
page: pathname,
|
||||
...attributes,
|
||||
});
|
||||
|
||||
|
||||
@@ -41,6 +41,21 @@ I need help with managing alerts.
|
||||
|
||||
Thanks`;
|
||||
|
||||
export const onboardingHelpMessage = (
|
||||
dataSourceName: string,
|
||||
moduleId: string,
|
||||
): string => `Hi Team,
|
||||
|
||||
I am facing issues sending data to SigNoz. Here are my application details
|
||||
|
||||
Data Source: ${dataSourceName}
|
||||
Framework:
|
||||
Environment:
|
||||
Module: ${moduleId}
|
||||
|
||||
Thanks
|
||||
`;
|
||||
|
||||
export const alertHelpMessage = (
|
||||
alertDef: AlertDef,
|
||||
ruleId: number,
|
||||
|
||||
@@ -3,12 +3,18 @@ import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
|
||||
import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { VIEWS } from './constants';
|
||||
|
||||
export type LogDetailProps = {
|
||||
log: ILog | null;
|
||||
selectedTab: VIEWS;
|
||||
onGroupByAttribute?: (
|
||||
fieldKey: string,
|
||||
isJSON?: boolean,
|
||||
dataType?: DataTypes,
|
||||
) => Promise<void>;
|
||||
isListViewPanel?: boolean;
|
||||
listViewPanelSelectedFields?: IField[] | null;
|
||||
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
|
||||
|
||||
@@ -37,6 +37,7 @@ function LogDetail({
|
||||
log,
|
||||
onClose,
|
||||
onAddToQuery,
|
||||
onGroupByAttribute,
|
||||
onClickActionItem,
|
||||
selectedTab,
|
||||
isListViewPanel = false,
|
||||
@@ -209,6 +210,7 @@ function LogDetail({
|
||||
logData={log}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onClickActionItem}
|
||||
onGroupByAttribute={onGroupByAttribute}
|
||||
isListViewPanel={isListViewPanel}
|
||||
selectedOptions={options}
|
||||
listViewPanelSelectedFields={listViewPanelSelectedFields}
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
.addToQueryContainer {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&.small {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
&.large {
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import './AddToQueryHOC.styles.scss';
|
||||
|
||||
import { Popover } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { memo, MouseEvent, ReactNode, useMemo } from 'react';
|
||||
|
||||
function AddToQueryHOC({
|
||||
fieldKey,
|
||||
fieldValue,
|
||||
onAddToQuery,
|
||||
fontSize,
|
||||
children,
|
||||
}: AddToQueryHOCProps): JSX.Element {
|
||||
const handleQueryAdd = (event: MouseEvent<HTMLDivElement>): void => {
|
||||
@@ -21,7 +24,7 @@ function AddToQueryHOC({
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<div className="addToQueryContainer" onClick={handleQueryAdd}>
|
||||
<div className={cx('addToQueryContainer', fontSize)} onClick={handleQueryAdd}>
|
||||
<Popover placement="top" content={popOverContent}>
|
||||
{children}
|
||||
</Popover>
|
||||
@@ -33,6 +36,7 @@ export interface AddToQueryHOCProps {
|
||||
fieldKey: string;
|
||||
fieldValue: string;
|
||||
onAddToQuery: (fieldKey: string, fieldValue: string, operator: string) => void;
|
||||
fontSize: FontSize;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,21 @@
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 128.571% */
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
&.small {
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&.large {
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
.log-value {
|
||||
color: var(--text-vanilla-400, #c0c1c3);
|
||||
@@ -14,6 +29,21 @@
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 128.571% */
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
&.small {
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&.large {
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
.log-line {
|
||||
display: flex;
|
||||
@@ -40,6 +70,20 @@
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 128.571% */
|
||||
letter-spacing: -0.07px;
|
||||
&.small {
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&.large {
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.selected-log-value {
|
||||
@@ -52,12 +96,37 @@
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
font-size: 14px;
|
||||
&.small {
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&.large {
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.selected-log-kv {
|
||||
min-height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&.small {
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
&.large {
|
||||
min-height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@ import './ListLogView.styles.scss';
|
||||
import { blue } from '@ant-design/colors';
|
||||
import Convert from 'ansi-to-html';
|
||||
import { Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import dayjs from 'dayjs';
|
||||
import dompurify from 'dompurify';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
@@ -39,6 +41,7 @@ interface LogFieldProps {
|
||||
fieldKey: string;
|
||||
fieldValue: string;
|
||||
linesPerRow?: number;
|
||||
fontSize: FontSize;
|
||||
}
|
||||
|
||||
type LogSelectedFieldProps = Omit<LogFieldProps, 'linesPerRow'> &
|
||||
@@ -48,6 +51,7 @@ function LogGeneralField({
|
||||
fieldKey,
|
||||
fieldValue,
|
||||
linesPerRow = 1,
|
||||
fontSize,
|
||||
}: LogFieldProps): JSX.Element {
|
||||
const html = useMemo(
|
||||
() => ({
|
||||
@@ -62,12 +66,12 @@ function LogGeneralField({
|
||||
|
||||
return (
|
||||
<TextContainer>
|
||||
<Text ellipsis type="secondary" className="log-field-key">
|
||||
<Text ellipsis type="secondary" className={cx('log-field-key', fontSize)}>
|
||||
{`${fieldKey} : `}
|
||||
</Text>
|
||||
<LogText
|
||||
dangerouslySetInnerHTML={html}
|
||||
className="log-value"
|
||||
className={cx('log-value', fontSize)}
|
||||
linesPerRow={linesPerRow > 1 ? linesPerRow : undefined}
|
||||
/>
|
||||
</TextContainer>
|
||||
@@ -78,6 +82,7 @@ function LogSelectedField({
|
||||
fieldKey = '',
|
||||
fieldValue = '',
|
||||
onAddToQuery,
|
||||
fontSize,
|
||||
}: LogSelectedFieldProps): JSX.Element {
|
||||
return (
|
||||
<div className="log-selected-fields">
|
||||
@@ -85,16 +90,22 @@ function LogSelectedField({
|
||||
fieldKey={fieldKey}
|
||||
fieldValue={fieldValue}
|
||||
onAddToQuery={onAddToQuery}
|
||||
fontSize={fontSize}
|
||||
>
|
||||
<Typography.Text>
|
||||
<span style={{ color: blue[4] }} className="selected-log-field-key">
|
||||
<span
|
||||
style={{ color: blue[4] }}
|
||||
className={cx('selected-log-field-key', fontSize)}
|
||||
>
|
||||
{fieldKey}
|
||||
</span>
|
||||
</Typography.Text>
|
||||
</AddToQueryHOC>
|
||||
<Typography.Text ellipsis className="selected-log-kv">
|
||||
<span className="selected-log-field-key">{': '}</span>
|
||||
<span className="selected-log-value">{fieldValue || "''"}</span>
|
||||
<Typography.Text ellipsis className={cx('selected-log-kv', fontSize)}>
|
||||
<span className={cx('selected-log-field-key', fontSize)}>{': '}</span>
|
||||
<span className={cx('selected-log-value', fontSize)}>
|
||||
{fieldValue || "''"}
|
||||
</span>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
@@ -107,6 +118,7 @@ type ListLogViewProps = {
|
||||
onAddToQuery: AddToQueryHOCProps['onAddToQuery'];
|
||||
activeLog?: ILog | null;
|
||||
linesPerRow: number;
|
||||
fontSize: FontSize;
|
||||
};
|
||||
|
||||
function ListLogView({
|
||||
@@ -116,6 +128,7 @@ function ListLogView({
|
||||
onAddToQuery,
|
||||
activeLog,
|
||||
linesPerRow,
|
||||
fontSize,
|
||||
}: ListLogViewProps): JSX.Element {
|
||||
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
|
||||
|
||||
@@ -128,6 +141,7 @@ function ListLogView({
|
||||
onAddToQuery: handleAddToQuery,
|
||||
onSetActiveLog: handleSetActiveContextLog,
|
||||
onClearActiveLog: handleClearActiveContextLog,
|
||||
onGroupByAttribute,
|
||||
} = useActiveLog();
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
@@ -185,6 +199,7 @@ function ListLogView({
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={handleDetailedView}
|
||||
fontSize={fontSize}
|
||||
>
|
||||
<div className="log-line">
|
||||
<LogStateIndicator
|
||||
@@ -192,18 +207,28 @@ function ListLogView({
|
||||
isActive={
|
||||
activeLog?.id === logData.id || activeContextLog?.id === logData.id
|
||||
}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
<div>
|
||||
<LogContainer>
|
||||
<LogContainer fontSize={fontSize}>
|
||||
<LogGeneralField
|
||||
fieldKey="Log"
|
||||
fieldValue={flattenLogData.body}
|
||||
linesPerRow={linesPerRow}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
{flattenLogData.stream && (
|
||||
<LogGeneralField fieldKey="Stream" fieldValue={flattenLogData.stream} />
|
||||
<LogGeneralField
|
||||
fieldKey="Stream"
|
||||
fieldValue={flattenLogData.stream}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
)}
|
||||
<LogGeneralField fieldKey="Timestamp" fieldValue={timestampValue} />
|
||||
<LogGeneralField
|
||||
fieldKey="Timestamp"
|
||||
fieldValue={timestampValue}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
|
||||
{updatedSelecedFields.map((field) =>
|
||||
isValidLogField(flattenLogData[field.name] as never) ? (
|
||||
@@ -212,6 +237,7 @@ function ListLogView({
|
||||
fieldKey={field.name}
|
||||
fieldValue={flattenLogData[field.name] as never}
|
||||
onAddToQuery={onAddToQuery}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
) : null,
|
||||
)}
|
||||
@@ -232,6 +258,7 @@ function ListLogView({
|
||||
onAddToQuery={handleAddToQuery}
|
||||
selectedTab={VIEW_TYPES.CONTEXT}
|
||||
onClose={handlerClearActiveContextLog}
|
||||
onGroupByAttribute={onGroupByAttribute}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,21 +1,46 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Card, Typography } from 'antd';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface LogTextProps {
|
||||
linesPerRow?: number;
|
||||
}
|
||||
|
||||
interface LogContainerProps {
|
||||
fontSize: FontSize;
|
||||
}
|
||||
|
||||
export const Container = styled(Card)<{
|
||||
$isActiveLog: boolean;
|
||||
$isDarkMode: boolean;
|
||||
fontSize: FontSize;
|
||||
}>`
|
||||
width: 100% !important;
|
||||
margin-bottom: 0.3rem;
|
||||
|
||||
${({ fontSize }): string =>
|
||||
fontSize === FontSize.SMALL
|
||||
? `margin-bottom:0.1rem;`
|
||||
: fontSize === FontSize.MEDIUM
|
||||
? `margin-bottom: 0.2rem;`
|
||||
: fontSize === FontSize.LARGE
|
||||
? `margin-bottom:0.3rem;`
|
||||
: ``}
|
||||
cursor: pointer;
|
||||
.ant-card-body {
|
||||
padding: 0.3rem 0.6rem;
|
||||
|
||||
${({ fontSize }): string =>
|
||||
fontSize === FontSize.SMALL
|
||||
? `padding:0.1rem 0.6rem;`
|
||||
: fontSize === FontSize.MEDIUM
|
||||
? `padding: 0.2rem 0.6rem;`
|
||||
: fontSize === FontSize.LARGE
|
||||
? `padding:0.3rem 0.6rem;`
|
||||
: ``}
|
||||
|
||||
${({ $isActiveLog, $isDarkMode }): string =>
|
||||
$isActiveLog
|
||||
? `background-color: ${
|
||||
@@ -38,11 +63,17 @@ export const TextContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const LogContainer = styled.div`
|
||||
export const LogContainer = styled.div<LogContainerProps>`
|
||||
margin-left: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
${({ fontSize }): string =>
|
||||
fontSize === FontSize.SMALL
|
||||
? `gap: 2px;`
|
||||
: fontSize === FontSize.MEDIUM
|
||||
? ` gap:4px;`
|
||||
: `gap:6px;`}
|
||||
`;
|
||||
|
||||
export const LogText = styled.div<LogTextProps>`
|
||||
|
||||
@@ -9,11 +9,24 @@
|
||||
border-radius: 50px;
|
||||
background-color: transparent;
|
||||
|
||||
&.small {
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
&.large {
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
&.INFO {
|
||||
background-color: var(--bg-slate-400);
|
||||
}
|
||||
|
||||
&.WARNING, &.WARN {
|
||||
&.WARNING,
|
||||
&.WARN {
|
||||
background-color: var(--bg-amber-500);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
|
||||
import LogStateIndicator from './LogStateIndicator';
|
||||
|
||||
describe('LogStateIndicator', () => {
|
||||
it('renders correctly with default props', () => {
|
||||
const { container } = render(<LogStateIndicator type="INFO" />);
|
||||
const { container } = render(
|
||||
<LogStateIndicator type="INFO" fontSize={FontSize.MEDIUM} />,
|
||||
);
|
||||
const indicator = container.firstChild as HTMLElement;
|
||||
expect(indicator.classList.contains('log-state-indicator')).toBe(true);
|
||||
expect(indicator.classList.contains('isActive')).toBe(false);
|
||||
@@ -15,28 +18,30 @@ describe('LogStateIndicator', () => {
|
||||
});
|
||||
|
||||
it('renders correctly when isActive is true', () => {
|
||||
const { container } = render(<LogStateIndicator type="INFO" isActive />);
|
||||
const { container } = render(
|
||||
<LogStateIndicator type="INFO" isActive fontSize={FontSize.MEDIUM} />,
|
||||
);
|
||||
const indicator = container.firstChild as HTMLElement;
|
||||
expect(indicator.classList.contains('isActive')).toBe(true);
|
||||
});
|
||||
|
||||
it('renders correctly with different types', () => {
|
||||
const { container: containerInfo } = render(
|
||||
<LogStateIndicator type="INFO" />,
|
||||
<LogStateIndicator type="INFO" fontSize={FontSize.MEDIUM} />,
|
||||
);
|
||||
expect(containerInfo.querySelector('.line')?.classList.contains('INFO')).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
const { container: containerWarning } = render(
|
||||
<LogStateIndicator type="WARNING" />,
|
||||
<LogStateIndicator type="WARNING" fontSize={FontSize.MEDIUM} />,
|
||||
);
|
||||
expect(
|
||||
containerWarning.querySelector('.line')?.classList.contains('WARNING'),
|
||||
).toBe(true);
|
||||
|
||||
const { container: containerError } = render(
|
||||
<LogStateIndicator type="ERROR" />,
|
||||
<LogStateIndicator type="ERROR" fontSize={FontSize.MEDIUM} />,
|
||||
);
|
||||
expect(
|
||||
containerError.querySelector('.line')?.classList.contains('ERROR'),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import './LogStateIndicator.styles.scss';
|
||||
|
||||
import cx from 'classnames';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
|
||||
export const SEVERITY_TEXT_TYPE = {
|
||||
TRACE: 'TRACE',
|
||||
@@ -44,13 +45,15 @@ export const LogType = {
|
||||
function LogStateIndicator({
|
||||
type,
|
||||
isActive,
|
||||
fontSize,
|
||||
}: {
|
||||
type: string;
|
||||
fontSize: FontSize;
|
||||
isActive?: boolean;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className={cx('log-state-indicator', isActive ? 'isActive' : '')}>
|
||||
<div className={cx('line', type)}> </div>
|
||||
<div className={cx('line', type, fontSize)}> </div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ function RawLogView({
|
||||
linesPerRow,
|
||||
isTextOverflowEllipsisDisabled,
|
||||
selectedFields = [],
|
||||
fontSize,
|
||||
}: RawLogViewProps): JSX.Element {
|
||||
const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink(
|
||||
data.id,
|
||||
@@ -54,6 +55,7 @@ function RawLogView({
|
||||
onSetActiveLog,
|
||||
onClearActiveLog,
|
||||
onAddToQuery,
|
||||
onGroupByAttribute,
|
||||
} = useActiveLog();
|
||||
|
||||
const [hasActionButtons, setHasActionButtons] = useState<boolean>(false);
|
||||
@@ -160,6 +162,7 @@ function RawLogView({
|
||||
$isActiveLog={isActiveLog}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
fontSize={fontSize}
|
||||
>
|
||||
<LogStateIndicator
|
||||
type={logType}
|
||||
@@ -168,6 +171,7 @@ function RawLogView({
|
||||
activeContextLog?.id === data.id ||
|
||||
isActiveLog
|
||||
}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
|
||||
<RawLogContent
|
||||
@@ -176,6 +180,7 @@ function RawLogView({
|
||||
$isDarkMode={isDarkMode}
|
||||
$isTextOverflowEllipsisDisabled={isTextOverflowEllipsisDisabled}
|
||||
linesPerRow={linesPerRow}
|
||||
fontSize={fontSize}
|
||||
dangerouslySetInnerHTML={html}
|
||||
/>
|
||||
|
||||
@@ -199,6 +204,7 @@ function RawLogView({
|
||||
onClose={handleCloseLogDetail}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
onGroupByAttribute={onGroupByAttribute}
|
||||
/>
|
||||
)}
|
||||
</RawLogViewContainer>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { blue } from '@ant-design/colors';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Col, Row, Space } from 'antd';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import styled from 'styled-components';
|
||||
import { getActiveLogBackground, getDefaultLogBackground } from 'utils/logs';
|
||||
|
||||
@@ -11,6 +13,7 @@ export const RawLogViewContainer = styled(Row)<{
|
||||
$isReadOnly?: boolean;
|
||||
$isActiveLog?: boolean;
|
||||
$isHightlightedLog: boolean;
|
||||
fontSize: FontSize;
|
||||
}>`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@@ -22,6 +25,13 @@ export const RawLogViewContainer = styled(Row)<{
|
||||
|
||||
.log-state-indicator {
|
||||
margin: 4px 0;
|
||||
|
||||
${({ fontSize }): string =>
|
||||
fontSize === FontSize.SMALL
|
||||
? `margin: 1px 0;`
|
||||
: fontSize === FontSize.MEDIUM
|
||||
? `margin: 1px 0;`
|
||||
: `margin: 2px 0;`}
|
||||
}
|
||||
|
||||
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
|
||||
@@ -50,8 +60,8 @@ export const RawLogContent = styled.div<RawLogContentProps>`
|
||||
margin-bottom: 0;
|
||||
font-family: 'SF Mono', monospace;
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.07px;
|
||||
padding: 4px;
|
||||
text-align: left;
|
||||
color: ${({ $isDarkMode }): string =>
|
||||
$isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400};
|
||||
@@ -66,9 +76,15 @@ export const RawLogContent = styled.div<RawLogContentProps>`
|
||||
line-clamp: ${linesPerRow};
|
||||
-webkit-box-orient: vertical;`};
|
||||
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
letter-spacing: -0.07px;
|
||||
padding: 4px;
|
||||
${({ fontSize }): string =>
|
||||
fontSize === FontSize.SMALL
|
||||
? `font-size:11px; line-height:16px; padding:1px;`
|
||||
: fontSize === FontSize.MEDIUM
|
||||
? `font-size:13px; line-height:20px; padding:1px;`
|
||||
: `font-size:14px; line-height:24px; padding:2px;`}
|
||||
|
||||
cursor: ${({ $isActiveLog, $isReadOnly }): string =>
|
||||
$isActiveLog || $isReadOnly ? 'initial' : 'pointer'};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
@@ -7,11 +8,13 @@ export interface RawLogViewProps {
|
||||
isTextOverflowEllipsisDisabled?: boolean;
|
||||
data: ILog;
|
||||
linesPerRow: number;
|
||||
fontSize: FontSize;
|
||||
selectedFields?: IField[];
|
||||
}
|
||||
|
||||
export interface RawLogContentProps {
|
||||
linesPerRow: number;
|
||||
fontSize: FontSize;
|
||||
$isReadOnly?: boolean;
|
||||
$isActiveLog?: boolean;
|
||||
$isDarkMode?: boolean;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface TableBodyContentProps {
|
||||
linesPerRow: number;
|
||||
fontSize: FontSize;
|
||||
isDarkMode?: boolean;
|
||||
}
|
||||
|
||||
@@ -20,4 +23,10 @@ export const TableBodyContent = styled.div<TableBodyContentProps>`
|
||||
-webkit-line-clamp: ${(props): number => props.linesPerRow};
|
||||
line-clamp: ${(props): number => props.linesPerRow};
|
||||
-webkit-box-orient: vertical;
|
||||
${({ fontSize }): string =>
|
||||
fontSize === FontSize.SMALL
|
||||
? `font-size:11px; line-height:16px;`
|
||||
: fontSize === FontSize.MEDIUM
|
||||
? `font-size:13px; line-height:20px;`
|
||||
: `font-size:14px; line-height:24px;`}
|
||||
`;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ColumnsType, ColumnType } from 'antd/es/table';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
@@ -10,6 +11,7 @@ export type LogsTableViewProps = {
|
||||
logs: ILog[];
|
||||
fields: IField[];
|
||||
linesPerRow: number;
|
||||
fontSize: FontSize;
|
||||
onClickExpand?: (log: ILog) => void;
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,21 @@
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 128.571% */
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
&.small {
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&.large {
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-timestamp {
|
||||
@@ -25,3 +40,21 @@
|
||||
color: var(--bg-slate-400);
|
||||
}
|
||||
}
|
||||
|
||||
.paragraph {
|
||||
padding: 0px !important;
|
||||
&.small {
|
||||
font-size: 11px !important;
|
||||
line-height: 16px !important;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
font-size: 13px !important;
|
||||
line-height: 20px !important;
|
||||
}
|
||||
|
||||
&.large {
|
||||
font-size: 14px !important;
|
||||
line-height: 24px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import './useTableView.styles.scss';
|
||||
import Convert from 'ansi-to-html';
|
||||
import { Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import cx from 'classnames';
|
||||
import dayjs from 'dayjs';
|
||||
import dompurify from 'dompurify';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
@@ -31,6 +32,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
logs,
|
||||
fields,
|
||||
linesPerRow,
|
||||
fontSize,
|
||||
appendTo = 'center',
|
||||
activeContextLog,
|
||||
activeLog,
|
||||
@@ -57,7 +59,10 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
: getDefaultCellStyle(isDarkMode),
|
||||
},
|
||||
children: (
|
||||
<Typography.Paragraph ellipsis={{ rows: linesPerRow }}>
|
||||
<Typography.Paragraph
|
||||
ellipsis={{ rows: linesPerRow }}
|
||||
className={cx('paragraph', fontSize)}
|
||||
>
|
||||
{field}
|
||||
</Typography.Paragraph>
|
||||
),
|
||||
@@ -87,8 +92,9 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
isActive={
|
||||
activeLog?.id === item.id || activeContextLog?.id === item.id
|
||||
}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
<Typography.Paragraph ellipsis className="text">
|
||||
<Typography.Paragraph ellipsis className={cx('text', fontSize)}>
|
||||
{date}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
@@ -114,6 +120,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
}),
|
||||
),
|
||||
}}
|
||||
fontSize={fontSize}
|
||||
linesPerRow={linesPerRow}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
@@ -130,6 +137,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
linesPerRow,
|
||||
activeLog?.id,
|
||||
activeContextLog?.id,
|
||||
fontSize,
|
||||
]);
|
||||
|
||||
return { columns, dataSource: flattenLogData };
|
||||
|
||||
@@ -17,17 +17,126 @@
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(20px);
|
||||
|
||||
.font-size-dropdown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.back-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 12px;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.text {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: 0.14px;
|
||||
}
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background-color: unset !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.option-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
justify-content: space-between;
|
||||
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.text {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal; /* 142.857% */
|
||||
letter-spacing: 0.14px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.text:hover {
|
||||
color: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
|
||||
.option-btn:hover {
|
||||
background-color: unset !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.font-size-container {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.title {
|
||||
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;
|
||||
}
|
||||
|
||||
.value {
|
||||
display: flex;
|
||||
height: 20px;
|
||||
padding: 4px 0px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border: none !important;
|
||||
.font-value {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
letter-spacing: 0.14px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.icon {
|
||||
}
|
||||
}
|
||||
|
||||
.value:hover {
|
||||
background-color: unset !important;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-container {
|
||||
padding: 12px;
|
||||
|
||||
.title {
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.08em;
|
||||
text-align: left;
|
||||
color: #52575c;
|
||||
color: var(--bg-slate-50);
|
||||
}
|
||||
|
||||
.menu-items {
|
||||
@@ -65,11 +174,11 @@
|
||||
padding: 12px;
|
||||
|
||||
.title {
|
||||
color: #52575c;
|
||||
color: var(--bg-slate-50);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
@@ -149,11 +258,11 @@
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #52575c;
|
||||
color: var(--bg-slate-50);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
@@ -299,6 +408,38 @@
|
||||
|
||||
box-shadow: 4px 10px 16px 2px rgba(255, 255, 255, 0.2);
|
||||
|
||||
.font-size-dropdown {
|
||||
.back-btn {
|
||||
.text {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
.option-btn {
|
||||
.text {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.text:hover {
|
||||
color: var(--bg-ink-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.font-size-container {
|
||||
.title {
|
||||
color: var(--bg-ink-100);
|
||||
}
|
||||
|
||||
.value {
|
||||
.font-value {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.horizontal-line {
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import './LogsFormatOptionsMenu.styles.scss';
|
||||
|
||||
import { Divider, Input, InputNumber, Tooltip } from 'antd';
|
||||
import { Button, Divider, Input, InputNumber, Tooltip, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
import { OptionsMenuConfig } from 'container/OptionsMenu/types';
|
||||
import { FontSize, OptionsMenuConfig } from 'container/OptionsMenu/types';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { Check, Minus, Plus, X } from 'lucide-react';
|
||||
import { Check, ChevronLeft, ChevronRight, Minus, Plus, X } from 'lucide-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
interface LogsFormatOptionsMenuProps {
|
||||
@@ -24,10 +24,16 @@ export default function LogsFormatOptionsMenu({
|
||||
selectedOptionFormat,
|
||||
config,
|
||||
}: LogsFormatOptionsMenuProps): JSX.Element {
|
||||
const { maxLines, format, addColumn } = config;
|
||||
const { maxLines, format, addColumn, fontSize } = config;
|
||||
const [selectedItem, setSelectedItem] = useState(selectedOptionFormat);
|
||||
const maxLinesNumber = (maxLines?.value as number) || 1;
|
||||
const [maxLinesPerRow, setMaxLinesPerRow] = useState<number>(maxLinesNumber);
|
||||
const [fontSizeValue, setFontSizeValue] = useState<FontSize>(
|
||||
fontSize?.value || FontSize.SMALL,
|
||||
);
|
||||
const [isFontSizeOptionsOpen, setIsFontSizeOptionsOpen] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
|
||||
const [addNewColumn, setAddNewColumn] = useState(false);
|
||||
|
||||
@@ -88,6 +94,12 @@ export default function LogsFormatOptionsMenu({
|
||||
}
|
||||
}, [maxLinesPerRow]);
|
||||
|
||||
useEffect(() => {
|
||||
if (fontSizeValue && config && config.fontSize?.onChange) {
|
||||
config.fontSize.onChange(fontSizeValue);
|
||||
}
|
||||
}, [fontSizeValue]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx('nested-menu-container', addNewColumn ? 'active' : '')}
|
||||
@@ -96,145 +108,213 @@ export default function LogsFormatOptionsMenu({
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className="menu-container">
|
||||
<div className="title"> {title} </div>
|
||||
|
||||
<div className="menu-items">
|
||||
{items.map(
|
||||
(item: any): JSX.Element => (
|
||||
<div
|
||||
className="item"
|
||||
key={item.label}
|
||||
onClick={(): void => handleMenuItemClick(item.key)}
|
||||
>
|
||||
<div className={cx('item-label')}>
|
||||
{item.label}
|
||||
|
||||
{selectedItem === item.key && <Check size={12} />}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
{isFontSizeOptionsOpen ? (
|
||||
<div className="font-size-dropdown">
|
||||
<Button
|
||||
onClick={(): void => setIsFontSizeOptionsOpen(false)}
|
||||
className="back-btn"
|
||||
type="text"
|
||||
>
|
||||
<ChevronLeft size={14} className="icon" />
|
||||
<Typography.Text className="text">Select font size</Typography.Text>
|
||||
</Button>
|
||||
<div className="horizontal-line" />
|
||||
<div className="content">
|
||||
<Button
|
||||
onClick={(): void => {
|
||||
setFontSizeValue(FontSize.SMALL);
|
||||
}}
|
||||
className="option-btn"
|
||||
type="text"
|
||||
>
|
||||
<Typography.Text className="text">{FontSize.SMALL}</Typography.Text>
|
||||
{fontSizeValue === FontSize.SMALL && (
|
||||
<Check size={14} className="icon" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={(): void => {
|
||||
setFontSizeValue(FontSize.MEDIUM);
|
||||
}}
|
||||
className="option-btn"
|
||||
type="text"
|
||||
>
|
||||
<Typography.Text className="text">{FontSize.MEDIUM}</Typography.Text>
|
||||
{fontSizeValue === FontSize.MEDIUM && (
|
||||
<Check size={14} className="icon" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={(): void => {
|
||||
setFontSizeValue(FontSize.LARGE);
|
||||
}}
|
||||
className="option-btn"
|
||||
type="text"
|
||||
>
|
||||
<Typography.Text className="text">{FontSize.LARGE}</Typography.Text>
|
||||
{fontSizeValue === FontSize.LARGE && (
|
||||
<Check size={14} className="icon" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedItem && (
|
||||
) : (
|
||||
<>
|
||||
<>
|
||||
<div className="horizontal-line" />
|
||||
<div className="max-lines-per-row">
|
||||
<div className="title"> max lines per row </div>
|
||||
<div className="raw-format max-lines-per-row-input">
|
||||
<button
|
||||
type="button"
|
||||
className="periscope-btn"
|
||||
onClick={decrementMaxLinesPerRow}
|
||||
>
|
||||
{' '}
|
||||
<Minus size={12} />{' '}
|
||||
</button>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={10}
|
||||
value={maxLinesPerRow}
|
||||
onChange={handleLinesPerRowChange}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="periscope-btn"
|
||||
onClick={incrementMaxLinesPerRow}
|
||||
>
|
||||
{' '}
|
||||
<Plus size={12} />{' '}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<div className="font-size-container">
|
||||
<div className="title">Font Size</div>
|
||||
<Button
|
||||
className="value"
|
||||
type="text"
|
||||
onClick={(): void => {
|
||||
setIsFontSizeOptionsOpen(true);
|
||||
}}
|
||||
>
|
||||
<Typography.Text className="font-value">{fontSizeValue}</Typography.Text>
|
||||
<ChevronRight size={14} className="icon" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="horizontal-line" />
|
||||
<div className="menu-container">
|
||||
<div className="title"> {title} </div>
|
||||
|
||||
<div className="selected-item-content-container active">
|
||||
{!addNewColumn && <div className="horizontal-line" />}
|
||||
<div className="menu-items">
|
||||
{items.map(
|
||||
(item: any): JSX.Element => (
|
||||
<div
|
||||
className="item"
|
||||
key={item.label}
|
||||
onClick={(): void => handleMenuItemClick(item.key)}
|
||||
>
|
||||
<div className={cx('item-label')}>
|
||||
{item.label}
|
||||
|
||||
{addNewColumn && (
|
||||
<div className="add-new-column-header">
|
||||
<div className="title">
|
||||
{' '}
|
||||
columns
|
||||
<X size={14} onClick={handleToggleAddNewColumn} />{' '}
|
||||
</div>
|
||||
|
||||
<Input
|
||||
tabIndex={0}
|
||||
type="text"
|
||||
autoFocus
|
||||
onFocus={addColumn?.onFocus}
|
||||
onChange={handleSearchValueChange}
|
||||
placeholder="Search..."
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="item-content">
|
||||
{!addNewColumn && (
|
||||
<div className="title">
|
||||
columns
|
||||
<Plus size={14} onClick={handleToggleAddNewColumn} />{' '}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="column-format">
|
||||
{addColumn?.value?.map(({ key, id }) => (
|
||||
<div className="column-name" key={id}>
|
||||
<div className="name">
|
||||
<Tooltip placement="left" title={key}>
|
||||
{key}
|
||||
</Tooltip>
|
||||
{selectedItem === item.key && <Check size={12} />}
|
||||
</div>
|
||||
<X
|
||||
className="delete-btn"
|
||||
size={14}
|
||||
onClick={(): void => addColumn.onRemove(id as string)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{addColumn?.isFetching && (
|
||||
<div className="loading-container"> Loading ... </div>
|
||||
)}
|
||||
|
||||
{addNewColumn &&
|
||||
addColumn &&
|
||||
addColumn.value.length > 0 &&
|
||||
addColumn.options &&
|
||||
addColumn?.options?.length > 0 && (
|
||||
<Divider className="column-divider" />
|
||||
)}
|
||||
|
||||
{addNewColumn && (
|
||||
<div className="column-format-new-options">
|
||||
{addColumn?.options?.map(({ label, value }) => (
|
||||
<div
|
||||
className="column-name"
|
||||
key={value}
|
||||
onClick={(eve): void => {
|
||||
eve.stopPropagation();
|
||||
|
||||
if (addColumn && addColumn?.onSelect) {
|
||||
addColumn?.onSelect(value, { label, disabled: false });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="name">
|
||||
<Tooltip placement="left" title={label}>
|
||||
{label}
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedItem && (
|
||||
<>
|
||||
<>
|
||||
<div className="horizontal-line" />
|
||||
<div className="max-lines-per-row">
|
||||
<div className="title"> max lines per row </div>
|
||||
<div className="raw-format max-lines-per-row-input">
|
||||
<button
|
||||
type="button"
|
||||
className="periscope-btn"
|
||||
onClick={decrementMaxLinesPerRow}
|
||||
>
|
||||
{' '}
|
||||
<Minus size={12} />{' '}
|
||||
</button>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={10}
|
||||
value={maxLinesPerRow}
|
||||
onChange={handleLinesPerRowChange}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="periscope-btn"
|
||||
onClick={incrementMaxLinesPerRow}
|
||||
>
|
||||
{' '}
|
||||
<Plus size={12} />{' '}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
<div className="selected-item-content-container active">
|
||||
{!addNewColumn && <div className="horizontal-line" />}
|
||||
|
||||
{addNewColumn && (
|
||||
<div className="add-new-column-header">
|
||||
<div className="title">
|
||||
{' '}
|
||||
columns
|
||||
<X size={14} onClick={handleToggleAddNewColumn} />{' '}
|
||||
</div>
|
||||
|
||||
<Input
|
||||
tabIndex={0}
|
||||
type="text"
|
||||
autoFocus
|
||||
onFocus={addColumn?.onFocus}
|
||||
onChange={handleSearchValueChange}
|
||||
placeholder="Search..."
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="item-content">
|
||||
{!addNewColumn && (
|
||||
<div className="title">
|
||||
columns
|
||||
<Plus size={14} onClick={handleToggleAddNewColumn} />{' '}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="column-format">
|
||||
{addColumn?.value?.map(({ key, id }) => (
|
||||
<div className="column-name" key={id}>
|
||||
<div className="name">
|
||||
<Tooltip placement="left" title={key}>
|
||||
{key}
|
||||
</Tooltip>
|
||||
</div>
|
||||
<X
|
||||
className="delete-btn"
|
||||
size={14}
|
||||
onClick={(): void => addColumn.onRemove(id as string)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{addColumn?.isFetching && (
|
||||
<div className="loading-container"> Loading ... </div>
|
||||
)}
|
||||
|
||||
{addNewColumn &&
|
||||
addColumn &&
|
||||
addColumn.value.length > 0 &&
|
||||
addColumn.options &&
|
||||
addColumn?.options?.length > 0 && (
|
||||
<Divider className="column-divider" />
|
||||
)}
|
||||
|
||||
{addNewColumn && (
|
||||
<div className="column-format-new-options">
|
||||
{addColumn?.options?.map(({ label, value }) => (
|
||||
<div
|
||||
className="column-name"
|
||||
key={value}
|
||||
onClick={(eve): void => {
|
||||
eve.stopPropagation();
|
||||
|
||||
if (addColumn && addColumn?.onSelect) {
|
||||
addColumn?.onSelect(value, { label, disabled: false });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="name">
|
||||
<Tooltip placement="left" title={label}>
|
||||
{label}
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
@@ -482,6 +501,7 @@ function ExplorerOptions({
|
||||
shape="circle"
|
||||
onClick={hideToolbar}
|
||||
icon={<PanelBottomClose size={16} />}
|
||||
data-testid="hide-toolbar"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -511,6 +531,7 @@ function ExplorerOptions({
|
||||
icon={<Check size={16} />}
|
||||
onClick={onSaveHandler}
|
||||
disabled={isSaveViewLoading}
|
||||
data-testid="save-view-btn"
|
||||
>
|
||||
Save this view
|
||||
</Button>,
|
||||
|
||||
@@ -65,6 +65,7 @@ function ExplorerOptionsHideArea({
|
||||
// style={{ alignSelf: 'center', marginRight: 'calc(10% - 20px)' }}
|
||||
className="explorer-show-btn"
|
||||
onClick={handleShowExplorerOption}
|
||||
data-testid="show-explorer-option"
|
||||
>
|
||||
<div className="menu-bar" />
|
||||
</Button>
|
||||
|
||||
@@ -194,7 +194,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
|
||||
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
|
||||
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
|
||||
history.replace(generatedUrl);
|
||||
history.push(generatedUrl);
|
||||
|
||||
if (startTimestamp !== endTimestamp) {
|
||||
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
|
||||
|
||||
@@ -590,6 +590,8 @@
|
||||
}
|
||||
|
||||
.new-dashboard-menu {
|
||||
width: 200px;
|
||||
|
||||
.create-dashboard-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1067,7 +1069,7 @@
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
.subtitle {
|
||||
color: var(--bg-vanilla-400);
|
||||
color: var(--bg-ink-300);
|
||||
}
|
||||
|
||||
.ant-table-row {
|
||||
@@ -1087,6 +1089,10 @@
|
||||
|
||||
.dashboard-title {
|
||||
color: var(--bg-slate-300);
|
||||
|
||||
.title {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
.title-with-action {
|
||||
|
||||
@@ -45,6 +45,8 @@ import {
|
||||
Ellipsis,
|
||||
EllipsisVertical,
|
||||
Expand,
|
||||
ExternalLink,
|
||||
Github,
|
||||
HdmiPort,
|
||||
LayoutGrid,
|
||||
Link2,
|
||||
@@ -53,6 +55,8 @@ import {
|
||||
RotateCw,
|
||||
Search,
|
||||
} from 'lucide-react';
|
||||
// #TODO: lucide will be removing brand icons like Github in future, in that case we can use simple icons
|
||||
// see more: https://github.com/lucide-icons/lucide/issues/94
|
||||
import { handleContactSupport } from 'pages/Integrations/utils';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import {
|
||||
@@ -600,6 +604,28 @@ function DashboardsList(): JSX.Element {
|
||||
),
|
||||
key: '1',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<a
|
||||
href="https://github.com/SigNoz/dashboards"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Flex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
style={{ width: '100%' }}
|
||||
gap="small"
|
||||
>
|
||||
<div className="create-dashboard-menu-item">
|
||||
<Github size={14} /> View templates
|
||||
</div>
|
||||
<ExternalLink size={14} />
|
||||
</Flex>
|
||||
</a>
|
||||
),
|
||||
key: '2',
|
||||
},
|
||||
];
|
||||
|
||||
if (createNewDashboard) {
|
||||
|
||||
@@ -4,7 +4,15 @@ import { red } from '@ant-design/colors';
|
||||
import { ExclamationCircleTwoTone } from '@ant-design/icons';
|
||||
import MEditor, { Monaco } from '@monaco-editor/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Modal, Space, Typography, Upload, UploadProps } from 'antd';
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
Modal,
|
||||
Space,
|
||||
Typography,
|
||||
Upload,
|
||||
UploadProps,
|
||||
} from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import createDashboard from 'api/dashboard/create';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -13,7 +21,9 @@ import { MESSAGE } from 'hooks/useFeatureFlag';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
|
||||
import history from 'lib/history';
|
||||
import { MonitorDot, MoveRight, X } from 'lucide-react';
|
||||
import { ExternalLink, Github, MonitorDot, MoveRight, X } from 'lucide-react';
|
||||
// #TODO: Lucide will be removing brand icons like GitHub in the future. In that case, we can use Simple Icons. https://simpleicons.org/
|
||||
// See more: https://github.com/lucide-icons/lucide/issues/94
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
@@ -174,27 +184,43 @@ function ImportJSON({
|
||||
)}
|
||||
|
||||
<div className="action-btns-container">
|
||||
<Upload
|
||||
accept=".json"
|
||||
showUploadList={false}
|
||||
multiple={false}
|
||||
onChange={onChangeHandler}
|
||||
beforeUpload={(): boolean => false}
|
||||
action="none"
|
||||
data={jsonData}
|
||||
>
|
||||
<Button
|
||||
type="default"
|
||||
className="periscope-btn"
|
||||
icon={<MonitorDot size={14} />}
|
||||
onClick={(): void => {
|
||||
logEvent('Dashboard List: Upload JSON file clicked', {});
|
||||
}}
|
||||
<Flex gap="small">
|
||||
<Upload
|
||||
accept=".json"
|
||||
showUploadList={false}
|
||||
multiple={false}
|
||||
onChange={onChangeHandler}
|
||||
beforeUpload={(): boolean => false}
|
||||
action="none"
|
||||
data={jsonData}
|
||||
>
|
||||
{' '}
|
||||
{t('upload_json_file')}
|
||||
</Button>
|
||||
</Upload>
|
||||
<Button
|
||||
type="default"
|
||||
className="periscope-btn"
|
||||
icon={<MonitorDot size={14} />}
|
||||
onClick={(): void => {
|
||||
logEvent('Dashboard List: Upload JSON file clicked', {});
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
{t('upload_json_file')}
|
||||
</Button>
|
||||
</Upload>
|
||||
<a
|
||||
href="https://github.com/SigNoz/dashboards"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button
|
||||
type="default"
|
||||
className="periscope-btn"
|
||||
icon={<Github size={14} />}
|
||||
>
|
||||
{t('view_template')}
|
||||
<ExternalLink size={14} />
|
||||
</Button>
|
||||
</a>
|
||||
</Flex>
|
||||
|
||||
<Button
|
||||
// disabled={editorValue.length === 0}
|
||||
|
||||
@@ -38,6 +38,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
activeLog,
|
||||
onClearActiveLog,
|
||||
onAddToQuery,
|
||||
onGroupByAttribute,
|
||||
onSetActiveLog,
|
||||
} = useActiveLog();
|
||||
|
||||
@@ -63,6 +64,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
data={log}
|
||||
linesPerRow={options.maxLines}
|
||||
selectedFields={selectedFields}
|
||||
fontSize={options.fontSize}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -75,12 +77,14 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
linesPerRow={options.maxLines}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onSetActiveLog={onSetActiveLog}
|
||||
fontSize={options.fontSize}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[
|
||||
onAddToQuery,
|
||||
onSetActiveLog,
|
||||
options.fontSize,
|
||||
options.format,
|
||||
options.maxLines,
|
||||
selectedFields,
|
||||
@@ -123,6 +127,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
logs,
|
||||
fields: selectedFields,
|
||||
linesPerRow: options.maxLines,
|
||||
fontSize: options.fontSize,
|
||||
appendTo: 'end',
|
||||
activeLogIndex,
|
||||
}}
|
||||
@@ -147,6 +152,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
log={activeLog}
|
||||
onClose={onClearActiveLog}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onGroupByAttribute={onGroupByAttribute}
|
||||
onClickActionItem={onAddToQuery}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -3,12 +3,17 @@ import './ContextLogRenderer.styles.scss';
|
||||
import { Skeleton } from 'antd';
|
||||
import RawLogView from 'components/Logs/RawLogView';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import ShowButton from 'container/LogsContextList/ShowButton';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
|
||||
import { useContextLogData } from './useContextLogData';
|
||||
|
||||
@@ -22,6 +27,20 @@ function ContextLogRenderer({
|
||||
const [afterLogPage, setAfterLogPage] = useState<number>(1);
|
||||
const [logs, setLogs] = useState<ILog[]>([log]);
|
||||
|
||||
const { initialDataSource, stagedQuery } = useQueryBuilder();
|
||||
|
||||
const listQuery = useMemo(() => {
|
||||
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
|
||||
|
||||
return stagedQuery.builder.queryData.find((item) => !item.disabled) || null;
|
||||
}, [stagedQuery]);
|
||||
|
||||
const { options } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
dataSource: initialDataSource || DataSource.METRICS,
|
||||
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
|
||||
});
|
||||
|
||||
const {
|
||||
logs: previousLogs,
|
||||
isFetching: isPreviousLogsFetching,
|
||||
@@ -34,6 +53,7 @@ function ContextLogRenderer({
|
||||
order: ORDERBY_FILTERS.ASC,
|
||||
page: prevLogPage,
|
||||
setPage: setPrevLogPage,
|
||||
fontSize: options.fontSize,
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -48,6 +68,7 @@ function ContextLogRenderer({
|
||||
order: ORDERBY_FILTERS.DESC,
|
||||
page: afterLogPage,
|
||||
setPage: setAfterLogPage,
|
||||
fontSize: options.fontSize,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -65,6 +86,19 @@ function ContextLogRenderer({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [filters]);
|
||||
|
||||
const lengthMultipier = useMemo(() => {
|
||||
switch (options.fontSize) {
|
||||
case FontSize.SMALL:
|
||||
return 24;
|
||||
case FontSize.MEDIUM:
|
||||
return 28;
|
||||
case FontSize.LARGE:
|
||||
return 32;
|
||||
default:
|
||||
return 32;
|
||||
}
|
||||
}, [options.fontSize]);
|
||||
|
||||
const getItemContent = useCallback(
|
||||
(_: number, logTorender: ILog): JSX.Element => (
|
||||
<RawLogView
|
||||
@@ -74,9 +108,10 @@ function ContextLogRenderer({
|
||||
key={logTorender.id}
|
||||
data={logTorender}
|
||||
linesPerRow={1}
|
||||
fontSize={options.fontSize}
|
||||
/>
|
||||
),
|
||||
[log.id],
|
||||
[log.id, options.fontSize],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -101,7 +136,7 @@ function ContextLogRenderer({
|
||||
initialTopMostItemIndex={0}
|
||||
data={logs}
|
||||
itemContent={getItemContent}
|
||||
style={{ height: `calc(${logs.length} * 32px)` }}
|
||||
style={{ height: `calc(${logs.length} * ${lengthMultipier}px)` }}
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
{isAfterLogsFetching && (
|
||||
|
||||
@@ -4,9 +4,11 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import {
|
||||
getOrderByTimestamp,
|
||||
INITIAL_PAGE_SIZE,
|
||||
INITIAL_PAGE_SIZE_SMALL_FONT,
|
||||
LOGS_MORE_PAGE_SIZE,
|
||||
} from 'container/LogsContextList/configs';
|
||||
import { getRequestData } from 'container/LogsContextList/utils';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
|
||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||
import {
|
||||
@@ -30,6 +32,7 @@ export const useContextLogData = ({
|
||||
filters,
|
||||
page,
|
||||
setPage,
|
||||
fontSize,
|
||||
}: {
|
||||
log: ILog;
|
||||
query: Query;
|
||||
@@ -38,6 +41,7 @@ export const useContextLogData = ({
|
||||
filters: TagFilter | null;
|
||||
page: number;
|
||||
setPage: Dispatch<SetStateAction<number>>;
|
||||
fontSize?: FontSize;
|
||||
}): {
|
||||
logs: ILog[];
|
||||
handleShowNextLines: () => void;
|
||||
@@ -54,9 +58,14 @@ export const useContextLogData = ({
|
||||
const logsMorePageSize = useMemo(() => (page - 1) * LOGS_MORE_PAGE_SIZE, [
|
||||
page,
|
||||
]);
|
||||
|
||||
const initialPageSize =
|
||||
fontSize && fontSize === FontSize.SMALL
|
||||
? INITIAL_PAGE_SIZE_SMALL_FONT
|
||||
: INITIAL_PAGE_SIZE;
|
||||
const pageSize = useMemo(
|
||||
() => (page <= 1 ? INITIAL_PAGE_SIZE : logsMorePageSize + INITIAL_PAGE_SIZE),
|
||||
[page, logsMorePageSize],
|
||||
() => (page <= 1 ? initialPageSize : logsMorePageSize + initialPageSize),
|
||||
[page, initialPageSize, logsMorePageSize],
|
||||
);
|
||||
const isDisabledFetch = useMemo(() => logs.length < pageSize, [
|
||||
logs.length,
|
||||
@@ -77,8 +86,16 @@ export const useContextLogData = ({
|
||||
log: lastLog,
|
||||
orderByTimestamp,
|
||||
page,
|
||||
pageSize: initialPageSize,
|
||||
}),
|
||||
[currentStagedQueryData, query, lastLog, orderByTimestamp, page],
|
||||
[
|
||||
currentStagedQueryData,
|
||||
query,
|
||||
lastLog,
|
||||
orderByTimestamp,
|
||||
page,
|
||||
initialPageSize,
|
||||
],
|
||||
);
|
||||
|
||||
const [requestData, setRequestData] = useState<Query | null>(
|
||||
|
||||
@@ -2,6 +2,7 @@ import './LogContext.styles.scss';
|
||||
|
||||
import RawLogView from 'components/Logs/RawLogView';
|
||||
import LogsContextList from 'container/LogsContextList';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
@@ -37,6 +38,7 @@ function LogContext({
|
||||
isTextOverflowEllipsisDisabled={false}
|
||||
data={log}
|
||||
linesPerRow={1}
|
||||
fontSize={FontSize.SMALL}
|
||||
/>
|
||||
<LogsContextList
|
||||
order={ORDERBY_FILTERS.DESC}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { ChevronDown, ChevronRight, Search } from 'lucide-react';
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { ActionItemProps } from './ActionItem';
|
||||
import TableView from './TableView';
|
||||
@@ -27,6 +28,11 @@ interface OverviewProps {
|
||||
isListViewPanel?: boolean;
|
||||
selectedOptions: OptionsQuery;
|
||||
listViewPanelSelectedFields?: IField[] | null;
|
||||
onGroupByAttribute?: (
|
||||
fieldKey: string,
|
||||
isJSON?: boolean,
|
||||
dataType?: DataTypes,
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
type Props = OverviewProps &
|
||||
@@ -39,6 +45,7 @@ function Overview({
|
||||
onClickActionItem,
|
||||
isListViewPanel = false,
|
||||
selectedOptions,
|
||||
onGroupByAttribute,
|
||||
listViewPanelSelectedFields,
|
||||
}: Props): JSX.Element {
|
||||
const [isWrapWord, setIsWrapWord] = useState<boolean>(true);
|
||||
@@ -204,6 +211,7 @@ function Overview({
|
||||
logData={logData}
|
||||
onAddToQuery={onAddToQuery}
|
||||
fieldSearchInput={fieldSearchInput}
|
||||
onGroupByAttribute={onGroupByAttribute}
|
||||
onClickActionItem={onClickActionItem}
|
||||
isListViewPanel={isListViewPanel}
|
||||
selectedOptions={selectedOptions}
|
||||
@@ -222,6 +230,7 @@ function Overview({
|
||||
Overview.defaultProps = {
|
||||
isListViewPanel: false,
|
||||
listViewPanelSelectedFields: null,
|
||||
onGroupByAttribute: undefined,
|
||||
};
|
||||
|
||||
export default Overview;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
top: 50%;
|
||||
right: 16px;
|
||||
transform: translateY(-50%);
|
||||
gap: 8px;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,8 +76,10 @@
|
||||
box-shadow: none;
|
||||
border-radius: 2px;
|
||||
background: var(--bg-slate-400);
|
||||
|
||||
height: 24px;
|
||||
padding: 2px 3px;
|
||||
gap: 3px;
|
||||
height: 18px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,23 +4,21 @@ import './TableView.styles.scss';
|
||||
|
||||
import { LinkOutlined } from '@ant-design/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Space, Spin, Tooltip, Tree, Typography } from 'antd';
|
||||
import { Button, Space, Tooltip, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import cx from 'classnames';
|
||||
import AddToQueryHOC, {
|
||||
AddToQueryHOCProps,
|
||||
} from 'components/Logs/AddToQueryHOC';
|
||||
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import { FontSize, OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import history from 'lib/history';
|
||||
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
|
||||
import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { ArrowDownToDot, ArrowUpFromDot, Pin } from 'lucide-react';
|
||||
import { Pin } from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
@@ -29,17 +27,12 @@ import AppActions from 'types/actions';
|
||||
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { ActionItemProps } from './ActionItem';
|
||||
import FieldRenderer from './FieldRenderer';
|
||||
import {
|
||||
filterKeyForField,
|
||||
findKeyPath,
|
||||
flattenObject,
|
||||
jsonToDataNodes,
|
||||
recursiveParseJSON,
|
||||
removeEscapeCharacters,
|
||||
} from './utils';
|
||||
import { TableViewActions } from './TableView/TableViewActions';
|
||||
import { filterKeyForField, findKeyPath, flattenObject } from './utils';
|
||||
|
||||
// Fields which should be restricted from adding it to query
|
||||
const RESTRICTED_FIELDS = ['timestamp'];
|
||||
@@ -50,6 +43,11 @@ interface TableViewProps {
|
||||
selectedOptions: OptionsQuery;
|
||||
isListViewPanel?: boolean;
|
||||
listViewPanelSelectedFields?: IField[] | null;
|
||||
onGroupByAttribute?: (
|
||||
fieldKey: string,
|
||||
isJSON?: boolean,
|
||||
dataType?: DataTypes,
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
type Props = TableViewProps &
|
||||
@@ -63,6 +61,7 @@ function TableView({
|
||||
onClickActionItem,
|
||||
isListViewPanel = false,
|
||||
selectedOptions,
|
||||
onGroupByAttribute,
|
||||
listViewPanelSelectedFields,
|
||||
}: Props): JSX.Element | null {
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
@@ -256,6 +255,7 @@ function TableView({
|
||||
fieldKey={fieldFilterKey}
|
||||
fieldValue={flattenLogData[field]}
|
||||
onAddToQuery={onAddToQuery}
|
||||
fontSize={FontSize.SMALL}
|
||||
>
|
||||
{renderedField}
|
||||
</AddToQueryHOC>
|
||||
@@ -270,75 +270,17 @@ function TableView({
|
||||
width: 70,
|
||||
ellipsis: false,
|
||||
className: 'value-field-container attribute-value',
|
||||
render: (fieldData: Record<string, string>, record): JSX.Element => {
|
||||
const textToCopy = fieldData.value.slice(1, -1);
|
||||
|
||||
if (record.field === 'body') {
|
||||
const parsedBody = recursiveParseJSON(fieldData.value);
|
||||
if (!isEmpty(parsedBody)) {
|
||||
return (
|
||||
<Tree defaultExpandAll showLine treeData={jsonToDataNodes(parsedBody)} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const fieldFilterKey = filterKeyForField(fieldData.field);
|
||||
|
||||
return (
|
||||
<div className="value-field">
|
||||
<CopyClipboardHOC textToCopy={textToCopy}>
|
||||
<span
|
||||
style={{
|
||||
color: Color.BG_SIENNA_400,
|
||||
whiteSpace: 'pre-wrap',
|
||||
tabSize: 4,
|
||||
}}
|
||||
>
|
||||
{removeEscapeCharacters(fieldData.value)}
|
||||
</span>
|
||||
</CopyClipboardHOC>
|
||||
|
||||
{!isListViewPanel && (
|
||||
<span className="action-btn">
|
||||
<Tooltip title="Filter for value">
|
||||
<Button
|
||||
className="filter-btn periscope-btn"
|
||||
icon={
|
||||
isfilterInLoading ? (
|
||||
<Spin size="small" />
|
||||
) : (
|
||||
<ArrowDownToDot size={14} style={{ transform: 'rotate(90deg)' }} />
|
||||
)
|
||||
}
|
||||
onClick={onClickHandler(
|
||||
OPERATORS.IN,
|
||||
fieldFilterKey,
|
||||
fieldData.value,
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Filter out value">
|
||||
<Button
|
||||
className="filter-btn periscope-btn"
|
||||
icon={
|
||||
isfilterOutLoading ? (
|
||||
<Spin size="small" />
|
||||
) : (
|
||||
<ArrowUpFromDot size={14} style={{ transform: 'rotate(90deg)' }} />
|
||||
)
|
||||
}
|
||||
onClick={onClickHandler(
|
||||
OPERATORS.NIN,
|
||||
fieldFilterKey,
|
||||
fieldData.value,
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
render: (fieldData: Record<string, string>, record): JSX.Element => (
|
||||
<TableViewActions
|
||||
fieldData={fieldData}
|
||||
record={record}
|
||||
isListViewPanel={isListViewPanel}
|
||||
isfilterInLoading={isfilterInLoading}
|
||||
isfilterOutLoading={isfilterOutLoading}
|
||||
onClickHandler={onClickHandler}
|
||||
onGroupByAttribute={onGroupByAttribute}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
function sortPinnedAttributes(
|
||||
@@ -379,9 +321,10 @@ function TableView({
|
||||
TableView.defaultProps = {
|
||||
isListViewPanel: false,
|
||||
listViewPanelSelectedFields: null,
|
||||
onGroupByAttribute: undefined,
|
||||
};
|
||||
|
||||
interface DataType {
|
||||
export interface DataType {
|
||||
key: string;
|
||||
field: string;
|
||||
value: string;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
.open-popover {
|
||||
&.value-field {
|
||||
.action-btn {
|
||||
display: flex !important;
|
||||
position: absolute !important;
|
||||
top: 50% !important;
|
||||
right: 16px !important;
|
||||
transform: translateY(-50%) !important;
|
||||
gap: 4px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-view-actions-content {
|
||||
.ant-popover-inner {
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
rgba(18, 19, 23, 0.8) 0%,
|
||||
rgba(18, 19, 23, 0.9) 98.68%
|
||||
);
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(20px);
|
||||
padding: 0px;
|
||||
.group-by-clause {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
letter-spacing: 0.14px;
|
||||
padding: 12px 18px 12px 14px;
|
||||
|
||||
.ant-btn-icon {
|
||||
margin-inline-end: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.group-by-clause:hover {
|
||||
background-color: unset !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.table-view-actions-content {
|
||||
.ant-popover-inner {
|
||||
border: 1px solid var(--bg-vanilla-400);
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
|
||||
.group-by-clause {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import './TableViewActions.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Popover, Spin, Tooltip, Tree } from 'antd';
|
||||
import GroupByIcon from 'assets/CustomIcons/GroupByIcon';
|
||||
import cx from 'classnames';
|
||||
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { DataType } from '../TableView';
|
||||
import {
|
||||
filterKeyForField,
|
||||
jsonToDataNodes,
|
||||
recursiveParseJSON,
|
||||
removeEscapeCharacters,
|
||||
} from '../utils';
|
||||
|
||||
interface ITableViewActionsProps {
|
||||
fieldData: Record<string, string>;
|
||||
record: DataType;
|
||||
isListViewPanel: boolean;
|
||||
isfilterInLoading: boolean;
|
||||
isfilterOutLoading: boolean;
|
||||
onGroupByAttribute?: (
|
||||
fieldKey: string,
|
||||
isJSON?: boolean,
|
||||
dataType?: DataTypes,
|
||||
) => Promise<void>;
|
||||
onClickHandler: (
|
||||
operator: string,
|
||||
fieldKey: string,
|
||||
fieldValue: string,
|
||||
) => () => void;
|
||||
}
|
||||
|
||||
export function TableViewActions(
|
||||
props: ITableViewActionsProps,
|
||||
): React.ReactElement {
|
||||
const {
|
||||
fieldData,
|
||||
record,
|
||||
isListViewPanel,
|
||||
isfilterInLoading,
|
||||
isfilterOutLoading,
|
||||
onClickHandler,
|
||||
onGroupByAttribute,
|
||||
} = props;
|
||||
|
||||
const { pathname } = useLocation();
|
||||
|
||||
// there is no option for where clause in old logs explorer and live logs page
|
||||
const isOldLogsExplorerOrLiveLogsPage = useMemo(
|
||||
() => pathname === ROUTES.OLD_LOGS_EXPLORER || pathname === ROUTES.LIVE_LOGS,
|
||||
[pathname],
|
||||
);
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const textToCopy = fieldData.value.slice(1, -1);
|
||||
|
||||
if (record.field === 'body') {
|
||||
const parsedBody = recursiveParseJSON(fieldData.value);
|
||||
if (!isEmpty(parsedBody)) {
|
||||
return (
|
||||
<Tree defaultExpandAll showLine treeData={jsonToDataNodes(parsedBody)} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const fieldFilterKey = filterKeyForField(fieldData.field);
|
||||
|
||||
return (
|
||||
<div className={cx('value-field', isOpen ? 'open-popover' : '')}>
|
||||
<CopyClipboardHOC textToCopy={textToCopy}>
|
||||
<span
|
||||
style={{
|
||||
color: Color.BG_SIENNA_400,
|
||||
whiteSpace: 'pre-wrap',
|
||||
tabSize: 4,
|
||||
}}
|
||||
>
|
||||
{removeEscapeCharacters(fieldData.value)}
|
||||
</span>
|
||||
</CopyClipboardHOC>
|
||||
|
||||
{!isListViewPanel && (
|
||||
<span className="action-btn">
|
||||
<Tooltip title="Filter for value">
|
||||
<Button
|
||||
className="filter-btn periscope-btn"
|
||||
icon={
|
||||
isfilterInLoading ? (
|
||||
<Spin size="small" />
|
||||
) : (
|
||||
<ArrowDownToDot size={14} style={{ transform: 'rotate(90deg)' }} />
|
||||
)
|
||||
}
|
||||
onClick={onClickHandler(OPERATORS.IN, fieldFilterKey, fieldData.value)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Filter out value">
|
||||
<Button
|
||||
className="filter-btn periscope-btn"
|
||||
icon={
|
||||
isfilterOutLoading ? (
|
||||
<Spin size="small" />
|
||||
) : (
|
||||
<ArrowUpFromDot size={14} style={{ transform: 'rotate(90deg)' }} />
|
||||
)
|
||||
}
|
||||
onClick={onClickHandler(OPERATORS.NIN, fieldFilterKey, fieldData.value)}
|
||||
/>
|
||||
</Tooltip>
|
||||
{!isOldLogsExplorerOrLiveLogsPage && (
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
arrow={false}
|
||||
content={
|
||||
<div>
|
||||
<Button
|
||||
className="group-by-clause"
|
||||
type="text"
|
||||
icon={<GroupByIcon />}
|
||||
onClick={(): Promise<void> | void =>
|
||||
onGroupByAttribute?.(fieldFilterKey)
|
||||
}
|
||||
>
|
||||
Group By Attribute
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
rootClassName="table-view-actions-content"
|
||||
trigger="hover"
|
||||
placement="bottomLeft"
|
||||
>
|
||||
<Button
|
||||
icon={<Ellipsis size={14} />}
|
||||
className="filter-btn periscope-btn"
|
||||
/>
|
||||
</Popover>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TableViewActions.defaultProps = {
|
||||
onGroupByAttribute: undefined,
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export const INITIAL_PAGE_SIZE = 10;
|
||||
export const INITIAL_PAGE_SIZE_SMALL_FONT = 12;
|
||||
export const LOGS_MORE_PAGE_SIZE = 10;
|
||||
|
||||
export const getOrderByTimestamp = (order: string): OrderByPayload => ({
|
||||
|
||||
@@ -5,6 +5,7 @@ import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
|
||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
@@ -167,6 +168,7 @@ function LogsContextList({
|
||||
key={log.id}
|
||||
data={log}
|
||||
linesPerRow={1}
|
||||
fontSize={FontSize.SMALL}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
|
||||
@@ -2,6 +2,7 @@ import { EditFilled } from '@ant-design/icons';
|
||||
import { Modal, Typography } from 'antd';
|
||||
import RawLogView from 'components/Logs/RawLogView';
|
||||
import LogsContextList from 'container/LogsContextList';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
|
||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
@@ -99,6 +100,7 @@ function LogsExplorerContext({
|
||||
isTextOverflowEllipsisDisabled
|
||||
data={log}
|
||||
linesPerRow={1}
|
||||
fontSize={FontSize.SMALL}
|
||||
/>
|
||||
</LogContainer>
|
||||
<LogsContextList
|
||||
|
||||
@@ -3,6 +3,7 @@ import './TableRow.styles.scss';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import LogLinesActionButtons from 'components/Logs/LogLinesActionButtons/LogLinesActionButtons';
|
||||
import { ColumnTypeRender } from 'components/Logs/TableView/types';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import {
|
||||
@@ -24,6 +25,7 @@ interface TableRowProps {
|
||||
handleSetActiveContextLog: (log: ILog) => void;
|
||||
logs: ILog[];
|
||||
hasActions: boolean;
|
||||
fontSize: FontSize;
|
||||
}
|
||||
|
||||
export default function TableRow({
|
||||
@@ -33,6 +35,7 @@ export default function TableRow({
|
||||
handleSetActiveContextLog,
|
||||
logs,
|
||||
hasActions,
|
||||
fontSize,
|
||||
}: TableRowProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
@@ -78,6 +81,7 @@ export default function TableRow({
|
||||
$isDragColumn={false}
|
||||
$isDarkMode={isDarkMode}
|
||||
key={column.key}
|
||||
fontSize={fontSize}
|
||||
>
|
||||
{cloneElement(children, props)}
|
||||
</TableCellStyled>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { CSSProperties } from 'react';
|
||||
|
||||
export const infinityDefaultStyles: CSSProperties = {
|
||||
@@ -5,3 +7,16 @@ export const infinityDefaultStyles: CSSProperties = {
|
||||
overflowX: 'scroll',
|
||||
marginTop: '15px',
|
||||
};
|
||||
|
||||
export function getInfinityDefaultStyles(fontSize: FontSize): CSSProperties {
|
||||
return {
|
||||
width: '100%',
|
||||
overflowX: 'scroll',
|
||||
marginTop:
|
||||
fontSize === FontSize.SMALL
|
||||
? '10px'
|
||||
: fontSize === FontSize.MEDIUM
|
||||
? '12px'
|
||||
: '15px',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from 'react-virtuoso';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import { infinityDefaultStyles } from './config';
|
||||
import { getInfinityDefaultStyles } from './config';
|
||||
import { LogsCustomTable } from './LogsCustomTable';
|
||||
import { TableHeaderCellStyled, TableRowStyled } from './styles';
|
||||
import TableRow from './TableRow';
|
||||
@@ -59,6 +59,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
||||
onSetActiveLog,
|
||||
onClearActiveLog,
|
||||
onAddToQuery,
|
||||
onGroupByAttribute,
|
||||
} = useActiveLog();
|
||||
|
||||
const { dataSource, columns } = useTableView({
|
||||
@@ -95,9 +96,15 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
||||
handleSetActiveContextLog={handleSetActiveContextLog}
|
||||
logs={tableViewProps.logs}
|
||||
hasActions
|
||||
fontSize={tableViewProps.fontSize}
|
||||
/>
|
||||
),
|
||||
[handleSetActiveContextLog, tableColumns, tableViewProps.logs],
|
||||
[
|
||||
handleSetActiveContextLog,
|
||||
tableColumns,
|
||||
tableViewProps.fontSize,
|
||||
tableViewProps.logs,
|
||||
],
|
||||
);
|
||||
|
||||
const tableHeader = useCallback(
|
||||
@@ -112,6 +119,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
||||
$isDarkMode={isDarkMode}
|
||||
$isDragColumn={isDragColumn}
|
||||
key={column.key}
|
||||
fontSize={tableViewProps?.fontSize}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...(isDragColumn && { className: 'dragHandler' })}
|
||||
>
|
||||
@@ -121,7 +129,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
||||
})}
|
||||
</tr>
|
||||
),
|
||||
[tableColumns, isDarkMode],
|
||||
[tableColumns, isDarkMode, tableViewProps?.fontSize],
|
||||
);
|
||||
|
||||
const handleClickExpand = (index: number): void => {
|
||||
@@ -137,7 +145,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
||||
initialTopMostItemIndex={
|
||||
tableViewProps.activeLogIndex !== -1 ? tableViewProps.activeLogIndex : 0
|
||||
}
|
||||
style={infinityDefaultStyles}
|
||||
style={getInfinityDefaultStyles(tableViewProps.fontSize)}
|
||||
data={dataSource}
|
||||
components={{
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
@@ -165,6 +173,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
||||
onClose={handleClearActiveContextLog}
|
||||
onAddToQuery={handleAddToQuery}
|
||||
selectedTab={VIEW_TYPES.CONTEXT}
|
||||
onGroupByAttribute={onGroupByAttribute}
|
||||
/>
|
||||
)}
|
||||
<LogDetail
|
||||
@@ -173,6 +182,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
||||
onClose={onClearActiveLog}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
onGroupByAttribute={onGroupByAttribute}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import styled from 'styled-components';
|
||||
import { getActiveLogBackground } from 'utils/logs';
|
||||
|
||||
@@ -7,6 +9,7 @@ interface TableHeaderCellStyledProps {
|
||||
$isDragColumn: boolean;
|
||||
$isDarkMode: boolean;
|
||||
$isTimestamp?: boolean;
|
||||
fontSize?: FontSize;
|
||||
}
|
||||
|
||||
export const TableStyled = styled.table`
|
||||
@@ -15,6 +18,14 @@ export const TableStyled = styled.table`
|
||||
|
||||
export const TableCellStyled = styled.td<TableHeaderCellStyledProps>`
|
||||
padding: 0.5rem;
|
||||
${({ fontSize }): string =>
|
||||
fontSize === FontSize.SMALL
|
||||
? `padding:0.3rem;`
|
||||
: fontSize === FontSize.MEDIUM
|
||||
? `padding:0.4rem;`
|
||||
: fontSize === FontSize.LARGE
|
||||
? `padding:0.5rem;`
|
||||
: ``}
|
||||
background-color: ${(props): string =>
|
||||
props.$isDarkMode ? 'inherit' : themeColors.whiteCream};
|
||||
|
||||
@@ -33,7 +44,7 @@ export const TableRowStyled = styled.tr<{
|
||||
? `background-color: ${
|
||||
$isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300
|
||||
} !important`
|
||||
: ''}
|
||||
: ''};
|
||||
}
|
||||
|
||||
cursor: pointer;
|
||||
@@ -66,9 +77,17 @@ export const TableHeaderCellStyled = styled.th<TableHeaderCellStyledProps>`
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
background: ${(props): string => (props.$isDarkMode ? '#0b0c0d' : '#fdfdfd')};
|
||||
${({ $isTimestamp }): string => ($isTimestamp ? 'padding-left: 24px;' : '')}
|
||||
${({ $isDragColumn }): string => ($isDragColumn ? 'cursor: col-resize;' : '')}
|
||||
|
||||
${({ fontSize }): string =>
|
||||
fontSize === FontSize.SMALL
|
||||
? `font-size:11px; line-height:16px; padding: 0.1rem;`
|
||||
: fontSize === FontSize.MEDIUM
|
||||
? `font-size:13px; line-height:20px; padding:0.3rem;`
|
||||
: fontSize === FontSize.LARGE
|
||||
? `font-size:14px; line-height:24px; padding: 0.5rem;`
|
||||
: ``};
|
||||
${({ $isTimestamp }): string => ($isTimestamp ? 'padding-left: 24px;' : '')}
|
||||
color: ${(props): string =>
|
||||
props.$isDarkMode ? 'var(--bg-vanilla-100, #fff)' : themeColors.bckgGrey};
|
||||
`;
|
||||
|
||||
@@ -14,6 +14,7 @@ import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
|
||||
import LogsError from 'container/LogsError/LogsError';
|
||||
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
@@ -50,6 +51,7 @@ function LogsExplorerList({
|
||||
activeLog,
|
||||
onClearActiveLog,
|
||||
onAddToQuery,
|
||||
onGroupByAttribute,
|
||||
onSetActiveLog,
|
||||
} = useActiveLog();
|
||||
|
||||
@@ -79,6 +81,7 @@ function LogsExplorerList({
|
||||
data={log}
|
||||
linesPerRow={options.maxLines}
|
||||
selectedFields={selectedFields}
|
||||
fontSize={options.fontSize}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -91,6 +94,7 @@ function LogsExplorerList({
|
||||
onAddToQuery={onAddToQuery}
|
||||
onSetActiveLog={onSetActiveLog}
|
||||
activeLog={activeLog}
|
||||
fontSize={options.fontSize}
|
||||
linesPerRow={options.maxLines}
|
||||
/>
|
||||
);
|
||||
@@ -99,6 +103,7 @@ function LogsExplorerList({
|
||||
activeLog,
|
||||
onAddToQuery,
|
||||
onSetActiveLog,
|
||||
options.fontSize,
|
||||
options.format,
|
||||
options.maxLines,
|
||||
selectedFields,
|
||||
@@ -121,6 +126,7 @@ function LogsExplorerList({
|
||||
logs,
|
||||
fields: selectedFields,
|
||||
linesPerRow: options.maxLines,
|
||||
fontSize: options.fontSize,
|
||||
appendTo: 'end',
|
||||
activeLogIndex,
|
||||
}}
|
||||
@@ -129,9 +135,22 @@ function LogsExplorerList({
|
||||
);
|
||||
}
|
||||
|
||||
function getMarginTop(): string {
|
||||
switch (options.fontSize) {
|
||||
case FontSize.SMALL:
|
||||
return '10px';
|
||||
case FontSize.MEDIUM:
|
||||
return '12px';
|
||||
case FontSize.LARGE:
|
||||
return '15px';
|
||||
default:
|
||||
return '15px';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
style={{ width: '100%', marginTop: '20px' }}
|
||||
style={{ width: '100%', marginTop: getMarginTop() }}
|
||||
bodyStyle={CARD_BODY_STYLE}
|
||||
>
|
||||
<OverlayScrollbar isVirtuoso>
|
||||
@@ -151,6 +170,7 @@ function LogsExplorerList({
|
||||
isLoading,
|
||||
options.format,
|
||||
options.maxLines,
|
||||
options.fontSize,
|
||||
activeLogIndex,
|
||||
logs,
|
||||
onEndReached,
|
||||
@@ -189,6 +209,7 @@ function LogsExplorerList({
|
||||
log={activeLog}
|
||||
onClose={onClearActiveLog}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onGroupByAttribute={onGroupByAttribute}
|
||||
onClickActionItem={onAddToQuery}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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,9 @@ function LogsExplorerViews({
|
||||
end: timeRange.end,
|
||||
}),
|
||||
},
|
||||
undefined,
|
||||
listQueryKeyRef,
|
||||
{},
|
||||
);
|
||||
|
||||
const getRequestData = useCallback(
|
||||
@@ -318,6 +350,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 +618,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 +733,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,10 @@ jest.mock(
|
||||
},
|
||||
);
|
||||
|
||||
jest.mock('api/common/getQueryStats', () => ({
|
||||
getQueryStats: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('constants/panelTypes', () => ({
|
||||
AVAILABLE_EXPORT_PANEL_TYPES: ['graph', 'table'],
|
||||
}));
|
||||
@@ -79,6 +83,9 @@ const renderer = (): RenderResult =>
|
||||
<LogsExplorerViews
|
||||
selectedView={SELECTED_VIEWS.SEARCH}
|
||||
showFrequencyChart
|
||||
setIsLoadingQueries={(): void => {}}
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
/>
|
||||
</VirtuosoMockContext.Provider>
|
||||
</QueryBuilderProvider>
|
||||
|
||||
@@ -108,6 +108,7 @@ function LogsPanelComponent({
|
||||
onSetActiveLog,
|
||||
onClearActiveLog,
|
||||
onAddToQuery,
|
||||
onGroupByAttribute,
|
||||
} = useActiveLog();
|
||||
|
||||
const handleRow = useCallback(
|
||||
@@ -244,6 +245,7 @@ function LogsPanelComponent({
|
||||
onClose={onClearActiveLog}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
onGroupByAttribute={onGroupByAttribute}
|
||||
isListViewPanel
|
||||
listViewPanelSelectedFields={widget?.selectedLogFields}
|
||||
/>
|
||||
|
||||
@@ -10,6 +10,7 @@ import LogsTableView from 'components/Logs/TableView';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { CARD_BODY_STYLE } from 'constants/card';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
@@ -35,6 +36,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
|
||||
activeLog,
|
||||
onClearActiveLog,
|
||||
onAddToQuery,
|
||||
onGroupByAttribute,
|
||||
onSetActiveLog,
|
||||
} = useActiveLog();
|
||||
|
||||
@@ -66,6 +68,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
|
||||
data={log}
|
||||
linesPerRow={linesPerRow}
|
||||
selectedFields={selected}
|
||||
fontSize={FontSize.SMALL}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -78,6 +81,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
|
||||
linesPerRow={linesPerRow}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onSetActiveLog={onSetActiveLog}
|
||||
fontSize={FontSize.SMALL}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -92,6 +96,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
|
||||
logs={logs}
|
||||
fields={selected}
|
||||
linesPerRow={linesPerRow}
|
||||
fontSize={FontSize.SMALL}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -126,6 +131,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
log={activeLog}
|
||||
onClose={onClearActiveLog}
|
||||
onGroupByAttribute={onGroupByAttribute}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Col } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import Graph from 'container/GridCardLayout/GridCard';
|
||||
import {
|
||||
@@ -12,8 +13,12 @@ import {
|
||||
convertRawQueriesToTraceSelectedTags,
|
||||
resourceAttributesToTagFilterItems,
|
||||
} from 'hooks/useResourceAttribute/utils';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
@@ -37,6 +42,26 @@ function DBCall(): JSX.Element {
|
||||
const servicename = decodeURIComponent(encodedServiceName);
|
||||
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
|
||||
const { queries } = useResourceAttribute();
|
||||
const urlQuery = useUrlQuery();
|
||||
const { pathname } = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onDragSelect = useCallback(
|
||||
(start: number, end: number) => {
|
||||
const startTimestamp = Math.trunc(start);
|
||||
const endTimestamp = Math.trunc(end);
|
||||
|
||||
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
|
||||
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
|
||||
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
|
||||
history.push(generatedUrl);
|
||||
|
||||
if (startTimestamp !== endTimestamp) {
|
||||
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
|
||||
}
|
||||
},
|
||||
[dispatch, pathname, urlQuery],
|
||||
);
|
||||
|
||||
const tagFilterItems: TagFilterItem[] = useMemo(
|
||||
() =>
|
||||
@@ -150,6 +175,7 @@ function DBCall(): JSX.Element {
|
||||
'database_call_rps',
|
||||
);
|
||||
}}
|
||||
onDragSelect={onDragSelect}
|
||||
version={ENTITY_VERSION_V4}
|
||||
/>
|
||||
</GraphContainer>
|
||||
@@ -185,6 +211,7 @@ function DBCall(): JSX.Element {
|
||||
'database_call_avg_duration',
|
||||
);
|
||||
}}
|
||||
onDragSelect={onDragSelect}
|
||||
version={ENTITY_VERSION_V4}
|
||||
/>
|
||||
</GraphContainer>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Col } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import Graph from 'container/GridCardLayout/GridCard';
|
||||
import {
|
||||
@@ -14,8 +15,12 @@ import {
|
||||
convertRawQueriesToTraceSelectedTags,
|
||||
resourceAttributesToTagFilterItems,
|
||||
} from 'hooks/useResourceAttribute/utils';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
@@ -40,6 +45,27 @@ function External(): JSX.Element {
|
||||
const servicename = decodeURIComponent(encodedServiceName);
|
||||
const { queries } = useResourceAttribute();
|
||||
|
||||
const urlQuery = useUrlQuery();
|
||||
const { pathname } = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onDragSelect = useCallback(
|
||||
(start: number, end: number) => {
|
||||
const startTimestamp = Math.trunc(start);
|
||||
const endTimestamp = Math.trunc(end);
|
||||
|
||||
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
|
||||
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
|
||||
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
|
||||
history.push(generatedUrl);
|
||||
|
||||
if (startTimestamp !== endTimestamp) {
|
||||
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
|
||||
}
|
||||
},
|
||||
[dispatch, pathname, urlQuery],
|
||||
);
|
||||
|
||||
const tagFilterItems = useMemo(
|
||||
() =>
|
||||
handleNonInQueryRange(resourceAttributesToTagFilterItems(queries)) || [],
|
||||
@@ -214,6 +240,7 @@ function External(): JSX.Element {
|
||||
'external_call_error_percentage',
|
||||
);
|
||||
}}
|
||||
onDragSelect={onDragSelect}
|
||||
version={ENTITY_VERSION_V4}
|
||||
/>
|
||||
</GraphContainer>
|
||||
@@ -249,6 +276,7 @@ function External(): JSX.Element {
|
||||
'external_call_duration',
|
||||
);
|
||||
}}
|
||||
onDragSelect={onDragSelect}
|
||||
version={ENTITY_VERSION_V4}
|
||||
/>
|
||||
</GraphContainer>
|
||||
@@ -285,6 +313,7 @@ function External(): JSX.Element {
|
||||
'external_call_rps_by_address',
|
||||
)
|
||||
}
|
||||
onDragSelect={onDragSelect}
|
||||
version={ENTITY_VERSION_V4}
|
||||
/>
|
||||
</GraphContainer>
|
||||
@@ -320,6 +349,7 @@ function External(): JSX.Element {
|
||||
'external_call_duration_by_address',
|
||||
);
|
||||
}}
|
||||
onDragSelect={onDragSelect}
|
||||
version={ENTITY_VERSION_V4}
|
||||
/>
|
||||
</GraphContainer>
|
||||
|
||||
@@ -185,7 +185,7 @@ function Application(): JSX.Element {
|
||||
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
|
||||
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
|
||||
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
|
||||
history.replace(generatedUrl);
|
||||
history.push(generatedUrl);
|
||||
|
||||
if (startTimestamp !== endTimestamp) {
|
||||
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
|
||||
|
||||
@@ -131,6 +131,11 @@ export default function DataSource(): JSX.Element {
|
||||
};
|
||||
|
||||
const goToIntegrationsPage = (): void => {
|
||||
logEvent('Onboarding V2: Go to integrations', {
|
||||
module: selectedModule?.id,
|
||||
dataSource: selectedDataSource?.name,
|
||||
framework: selectedFramework,
|
||||
});
|
||||
history.push(ROUTES.INTEGRATIONS);
|
||||
};
|
||||
|
||||
|
||||
@@ -11,13 +11,15 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Space, Steps, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
|
||||
import { onboardingHelpMessage } from 'components/LaunchChatSupport/util';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { stepsMap } from 'container/OnboardingContainer/constants/stepsConfig';
|
||||
import { DataSourceType } from 'container/OnboardingContainer/Steps/DataSource/DataSource';
|
||||
import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUtils';
|
||||
import history from 'lib/history';
|
||||
import { isEmpty, isNull } from 'lodash-es';
|
||||
import { HelpCircle, UserPlus } from 'lucide-react';
|
||||
import { UserPlus } from 'lucide-react';
|
||||
import { SetStateAction, useState } from 'react';
|
||||
|
||||
import { useOnboardingContext } from '../../context/OnboardingContext';
|
||||
@@ -381,31 +383,6 @@ export default function ModuleStepsContainer({
|
||||
history.push('/');
|
||||
};
|
||||
|
||||
const handleFacingIssuesClick = (): void => {
|
||||
logEvent('Onboarding V2: Facing Issues Sending Data to SigNoz', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
framework: selectedFramework,
|
||||
environment: selectedEnvironment,
|
||||
module: activeStep?.module?.id,
|
||||
step: activeStep?.step?.id,
|
||||
});
|
||||
|
||||
const message = `Hi Team,
|
||||
|
||||
I am facing issues sending data to SigNoz. Here are my application details
|
||||
|
||||
Data Source: ${selectedDataSource?.name}
|
||||
Framework:
|
||||
Environment:
|
||||
Module: ${activeStep?.module?.id}
|
||||
|
||||
Thanks
|
||||
`;
|
||||
if (window.Intercom) {
|
||||
window.Intercom('showNewMessage', message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="onboarding-module-steps">
|
||||
<div className="steps-container">
|
||||
@@ -493,19 +470,26 @@ Thanks
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button onClick={handleNext} type="primary" icon={<ArrowRightOutlined />}>
|
||||
{current < lastStepIndex ? 'Continue to next step' : 'Done'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="periscope-btn"
|
||||
onClick={handleFacingIssuesClick}
|
||||
danger
|
||||
icon={<HelpCircle size={14} />}
|
||||
>
|
||||
Facing issues sending data to SigNoz?
|
||||
</Button>
|
||||
<LaunchChatSupport
|
||||
attributes={{
|
||||
dataSource: selectedDataSource?.id,
|
||||
framework: selectedFramework,
|
||||
environment: selectedEnvironment,
|
||||
module: activeStep?.module?.id,
|
||||
step: activeStep?.step?.id,
|
||||
screen: 'Onboarding',
|
||||
}}
|
||||
eventName="Onboarding V2: Facing Issues Sending Data to SigNoz"
|
||||
message={onboardingHelpMessage(
|
||||
selectedDataSource?.name || '',
|
||||
activeStep?.module?.id,
|
||||
)}
|
||||
buttonText="Facing issues sending data to SigNoz?"
|
||||
onHoverText="Click here to get help with sending data to SigNoz"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { OptionsQuery } from './types';
|
||||
import { FontSize, OptionsQuery } from './types';
|
||||
|
||||
export const URL_OPTIONS = 'options';
|
||||
|
||||
@@ -8,6 +8,7 @@ export const defaultOptionsQuery: OptionsQuery = {
|
||||
selectColumns: [],
|
||||
maxLines: 2,
|
||||
format: 'list',
|
||||
fontSize: FontSize.SMALL,
|
||||
};
|
||||
|
||||
export const defaultTraceSelectedColumns = [
|
||||
@@ -18,6 +19,7 @@ export const defaultTraceSelectedColumns = [
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'serviceName--string--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
@@ -26,6 +28,7 @@ export const defaultTraceSelectedColumns = [
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'name--string--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
{
|
||||
key: 'durationNano',
|
||||
@@ -34,6 +37,7 @@ export const defaultTraceSelectedColumns = [
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'durationNano--float64--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
{
|
||||
key: 'httpMethod',
|
||||
@@ -42,6 +46,7 @@ export const defaultTraceSelectedColumns = [
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'httpMethod--string--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
{
|
||||
key: 'responseStatusCode',
|
||||
@@ -50,5 +55,6 @@ export const defaultTraceSelectedColumns = [
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'responseStatusCode--string--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -2,10 +2,21 @@ import { InputNumberProps, RadioProps, SelectProps } from 'antd';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
export enum FontSize {
|
||||
SMALL = 'small',
|
||||
MEDIUM = 'medium',
|
||||
LARGE = 'large',
|
||||
}
|
||||
|
||||
interface FontSizeProps {
|
||||
value: FontSize;
|
||||
onChange: (val: FontSize) => void;
|
||||
}
|
||||
export interface OptionsQuery {
|
||||
selectColumns: BaseAutocompleteData[];
|
||||
maxLines: number;
|
||||
format: LogViewMode;
|
||||
fontSize: FontSize;
|
||||
}
|
||||
|
||||
export interface InitialOptions
|
||||
@@ -18,6 +29,7 @@ export type OptionsMenuConfig = {
|
||||
onChange: (value: LogViewMode) => void;
|
||||
};
|
||||
maxLines?: Pick<InputNumberProps, 'value' | 'onChange'>;
|
||||
fontSize?: FontSizeProps;
|
||||
addColumn?: Pick<
|
||||
SelectProps,
|
||||
'options' | 'onSelect' | 'onFocus' | 'onSearch' | 'onBlur'
|
||||
|
||||
@@ -7,6 +7,10 @@ import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import {
|
||||
AllTraceFilterKeys,
|
||||
AllTraceFilterKeyValue,
|
||||
} from 'pages/TracesExplorer/Filter/filterUtils';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQueries } from 'react-query';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
@@ -21,7 +25,12 @@ import {
|
||||
defaultTraceSelectedColumns,
|
||||
URL_OPTIONS,
|
||||
} from './constants';
|
||||
import { InitialOptions, OptionsMenuConfig, OptionsQuery } from './types';
|
||||
import {
|
||||
FontSize,
|
||||
InitialOptions,
|
||||
OptionsMenuConfig,
|
||||
OptionsQuery,
|
||||
} from './types';
|
||||
import { getOptionsFromKeys } from './utils';
|
||||
|
||||
interface UseOptionsMenuProps {
|
||||
@@ -106,15 +115,40 @@ const useOptionsMenu = ({
|
||||
[] as BaseAutocompleteData[],
|
||||
);
|
||||
|
||||
return (
|
||||
(initialOptions.selectColumns
|
||||
?.map((column) => attributesData.find(({ key }) => key === column))
|
||||
.filter(Boolean) as BaseAutocompleteData[]) || []
|
||||
);
|
||||
let initialSelected = initialOptions.selectColumns
|
||||
?.map((column) => attributesData.find(({ key }) => key === column))
|
||||
.filter(Boolean) as BaseAutocompleteData[];
|
||||
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
initialSelected = initialSelected
|
||||
?.map((col) => {
|
||||
if (col && Object.keys(AllTraceFilterKeyValue).includes(col?.key)) {
|
||||
const metaData = defaultTraceSelectedColumns.find(
|
||||
(coln) => coln.key === (col.key as AllTraceFilterKeys),
|
||||
);
|
||||
|
||||
return {
|
||||
...metaData,
|
||||
key: metaData?.key,
|
||||
dataType: metaData?.dataType,
|
||||
type: metaData?.type,
|
||||
isColumn: metaData?.isColumn,
|
||||
isJSON: metaData?.isJSON,
|
||||
id: metaData?.id,
|
||||
};
|
||||
}
|
||||
return col;
|
||||
})
|
||||
.filter(Boolean) as BaseAutocompleteData[];
|
||||
}
|
||||
|
||||
return initialSelected || [];
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
isFetchedInitialAttributes,
|
||||
initialOptions?.selectColumns,
|
||||
initialAttributesResult,
|
||||
dataSource,
|
||||
]);
|
||||
|
||||
const {
|
||||
@@ -248,6 +282,17 @@ const useOptionsMenu = ({
|
||||
},
|
||||
[handleRedirectWithOptionsData, optionsQueryData],
|
||||
);
|
||||
const handleFontSizeChange = useCallback(
|
||||
(value: FontSize) => {
|
||||
const optionsData: OptionsQuery = {
|
||||
...optionsQueryData,
|
||||
fontSize: value,
|
||||
};
|
||||
|
||||
handleRedirectWithOptionsData(optionsData);
|
||||
},
|
||||
[handleRedirectWithOptionsData, optionsQueryData],
|
||||
);
|
||||
|
||||
const handleSearchAttribute = useCallback((value: string) => {
|
||||
setSearchText(value);
|
||||
@@ -282,18 +327,24 @@ const useOptionsMenu = ({
|
||||
value: optionsQueryData.maxLines || defaultOptionsQuery.maxLines,
|
||||
onChange: handleMaxLinesChange,
|
||||
},
|
||||
fontSize: {
|
||||
value: optionsQueryData?.fontSize || defaultOptionsQuery.fontSize,
|
||||
onChange: handleFontSizeChange,
|
||||
},
|
||||
}),
|
||||
[
|
||||
optionsFromAttributeKeys,
|
||||
optionsQueryData?.maxLines,
|
||||
optionsQueryData?.format,
|
||||
optionsQueryData?.selectColumns,
|
||||
isSearchedAttributesFetching,
|
||||
handleSearchAttribute,
|
||||
optionsQueryData?.selectColumns,
|
||||
optionsQueryData.format,
|
||||
optionsQueryData.maxLines,
|
||||
optionsQueryData?.fontSize,
|
||||
optionsFromAttributeKeys,
|
||||
handleSelectColumns,
|
||||
handleRemoveSelectedColumn,
|
||||
handleSearchAttribute,
|
||||
handleFormatChange,
|
||||
handleMaxLinesChange,
|
||||
handleFontSizeChange,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ function LogsList({ logs }: LogsListProps): JSX.Element {
|
||||
onSetActiveLog,
|
||||
onClearActiveLog,
|
||||
onAddToQuery,
|
||||
onGroupByAttribute,
|
||||
} = useActiveLog();
|
||||
|
||||
const makeLogDetailsHandler = (log: ILog) => (): void => onSetActiveLog(log);
|
||||
@@ -42,6 +43,7 @@ function LogsList({ logs }: LogsListProps): JSX.Element {
|
||||
onClose={onClearActiveLog}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
onGroupByAttribute={onGroupByAttribute}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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(' , '),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { Typography } from 'antd';
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
ChevronUp,
|
||||
Command,
|
||||
CornerDownLeft,
|
||||
Slash,
|
||||
} from 'lucide-react';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
|
||||
|
||||
import ExampleQueriesRendererForLogs from '../QueryBuilderSearch/ExampleQueriesRendererForLogs';
|
||||
import { convertExampleQueriesToOptions } from '../QueryBuilderSearch/utils';
|
||||
import { ITag, Option } from './QueryBuilderSearchV2';
|
||||
|
||||
interface ICustomDropdownProps {
|
||||
menu: React.ReactElement;
|
||||
searchValue: string;
|
||||
tags: ITag[];
|
||||
options: Option[];
|
||||
exampleQueries: TagFilter[];
|
||||
onChange: (value: TagFilter) => void;
|
||||
currentFilterItem?: ITag;
|
||||
}
|
||||
|
||||
export default function QueryBuilderSearchDropdown(
|
||||
props: ICustomDropdownProps,
|
||||
): React.ReactElement {
|
||||
const {
|
||||
menu,
|
||||
currentFilterItem,
|
||||
searchValue,
|
||||
tags,
|
||||
exampleQueries,
|
||||
options,
|
||||
onChange,
|
||||
} = props;
|
||||
const userOs = getUserOperatingSystem();
|
||||
return (
|
||||
<>
|
||||
<div className="content">
|
||||
{!currentFilterItem?.key ? (
|
||||
<div className="suggested-filters">Suggested Filters</div>
|
||||
) : !currentFilterItem?.op ? (
|
||||
<div className="operator-for">
|
||||
<Typography.Text className="operator-for-text">
|
||||
Operator for{' '}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="operator-for-value">
|
||||
{currentFilterItem?.key?.key}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
) : (
|
||||
<div className="value-for">
|
||||
<Typography.Text className="value-for-text">
|
||||
Value(s) for{' '}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="value-for-value">
|
||||
{currentFilterItem?.key?.key} {currentFilterItem?.op}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
{menu}
|
||||
{!searchValue && 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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{!currentFilterItem?.key && options.length > 3 && (
|
||||
<section className="show-all-filter-items">
|
||||
{userOs === UserOperatingSystem.MACOS ? (
|
||||
<Command size={14} className="icons" />
|
||||
) : (
|
||||
<ChevronUp size={14} className="icons" />
|
||||
)}
|
||||
+
|
||||
<Slash size={14} className="icons" />
|
||||
<span className="keyboard-text">Show all filter items</span>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
QueryBuilderSearchDropdown.defaultProps = {
|
||||
currentFilterItem: undefined,
|
||||
};
|
||||
@@ -0,0 +1,261 @@
|
||||
.query-builder-search-v2 {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
.show-all-filters {
|
||||
.content {
|
||||
.rc-virtual-list-holder {
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
.suggested-filters {
|
||||
color: var(--bg-slate-50);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
padding: 12px 0px 8px 14px;
|
||||
}
|
||||
|
||||
.operator-for {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 12px 0px 8px 14px;
|
||||
|
||||
.operator-for-text {
|
||||
color: var(--bg-slate-200);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.operator-for-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
padding: 0px 8px;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
border-radius: 50px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
}
|
||||
|
||||
.value-for {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 12px 0px 8px 14px;
|
||||
.value-for-text {
|
||||
color: var(--bg-slate-200);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.value-for-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
padding: 0px 8px;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
border-radius: 50px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
}
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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(--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(--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;
|
||||
}
|
||||
.show-all-filter-items {
|
||||
padding-left: 12px;
|
||||
border-left: 1px solid #1d212d;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 12px;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.qb-search-bar-tokenised-tags {
|
||||
.ant-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 2px 0px 0px 2px;
|
||||
border: 1px solid var(--bg-slate-300);
|
||||
background: var(--bg-slate-300);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
padding: 0px;
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-family: Inter;
|
||||
font-size: 14px !important;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.ant-tag-close-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
width: 20px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
margin-inline-start: 0px !important;
|
||||
margin-inline-end: 0px !important;
|
||||
}
|
||||
|
||||
&.resource {
|
||||
border: 1px solid rgba(242, 71, 105, 0.2);
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-sakura-400);
|
||||
background: rgba(245, 108, 135, 0.1);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ant-tag-close-icon {
|
||||
background: rgba(245, 108, 135, 0.1);
|
||||
}
|
||||
}
|
||||
&.tag {
|
||||
border: 1px solid rgba(189, 153, 121, 0.2);
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-sienna-400);
|
||||
background: rgba(189, 153, 121, 0.1);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ant-tag-close-icon {
|
||||
background: rgba(189, 153, 121, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,862 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import './QueryBuilderSearchV2.styles.scss';
|
||||
|
||||
import { Select, Spin, Tag, Tooltip } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
OPERATORS,
|
||||
QUERY_BUILDER_OPERATORS_BY_TYPES,
|
||||
QUERY_BUILDER_SEARCH_VALUES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
|
||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
import { WhereClauseConfig } from 'hooks/queryBuilder/useAutoComplete';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
|
||||
import { useGetAttributeSuggestions } from 'hooks/queryBuilder/useGetAttributeSuggestions';
|
||||
import { validationMapper } from 'hooks/queryBuilder/useIsValidTag';
|
||||
import { operatorTypeMapper } from 'hooks/queryBuilder/useOperatorType';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useDebounceValue from 'hooks/useDebounce';
|
||||
import {
|
||||
cloneDeep,
|
||||
isArray,
|
||||
isEmpty,
|
||||
isEqual,
|
||||
isObject,
|
||||
isUndefined,
|
||||
unset,
|
||||
} from 'lodash-es';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import type { BaseSelectRef } from 'rc-select';
|
||||
import {
|
||||
KeyboardEvent,
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
TagFilter,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||
import { PLACEHOLDER } from '../QueryBuilderSearch/constant';
|
||||
import { TypographyText } from '../QueryBuilderSearch/style';
|
||||
import { getTagToken, isInNInOperator } from '../QueryBuilderSearch/utils';
|
||||
import QueryBuilderSearchDropdown from './QueryBuilderSearchDropdown';
|
||||
import Suggestions from './Suggestions';
|
||||
|
||||
export interface ITag {
|
||||
id?: string;
|
||||
key: BaseAutocompleteData;
|
||||
op: string;
|
||||
value: string[] | string | number | boolean;
|
||||
}
|
||||
|
||||
interface CustomTagProps {
|
||||
label: React.ReactNode;
|
||||
value: string;
|
||||
disabled: boolean;
|
||||
onClose: () => void;
|
||||
closable: boolean;
|
||||
}
|
||||
|
||||
interface QueryBuilderSearchV2Props {
|
||||
query: IBuilderQuery;
|
||||
onChange: (value: TagFilter) => void;
|
||||
whereClauseConfig?: WhereClauseConfig;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
suffixIcon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
label: string;
|
||||
value: BaseAutocompleteData | string;
|
||||
}
|
||||
|
||||
export enum DropdownState {
|
||||
ATTRIBUTE_KEY = 'ATTRIBUTE_KEY',
|
||||
OPERATOR = 'OPERATOR',
|
||||
ATTRIBUTE_VALUE = 'ATTRIBUTE_VALUE',
|
||||
}
|
||||
|
||||
function getInitTags(query: IBuilderQuery): ITag[] {
|
||||
return query.filters.items.map((item) => ({
|
||||
id: item.id,
|
||||
key: item.key as BaseAutocompleteData,
|
||||
op: item.op,
|
||||
value: `${item.value}`,
|
||||
}));
|
||||
}
|
||||
|
||||
function QueryBuilderSearchV2(
|
||||
props: QueryBuilderSearchV2Props,
|
||||
): React.ReactElement {
|
||||
const {
|
||||
query,
|
||||
onChange,
|
||||
placeholder,
|
||||
className,
|
||||
suffixIcon,
|
||||
whereClauseConfig,
|
||||
} = props;
|
||||
|
||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||
|
||||
const { handleRunQuery, currentQuery } = useQueryBuilder();
|
||||
|
||||
const selectRef = useRef<BaseSelectRef>(null);
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
// create the tags from the initial query here, this should only be computed on the first load as post that tags and query will be always in sync.
|
||||
const [tags, setTags] = useState<ITag[]>(() => getInitTags(query));
|
||||
|
||||
// this will maintain the current state of in process filter item
|
||||
const [currentFilterItem, setCurrentFilterItem] = useState<ITag | undefined>();
|
||||
|
||||
const [currentState, setCurrentState] = useState<DropdownState>(
|
||||
DropdownState.ATTRIBUTE_KEY,
|
||||
);
|
||||
|
||||
// to maintain the current running state until the tokenization happens for the tag
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
|
||||
const [dropdownOptions, setDropdownOptions] = useState<Option[]>([]);
|
||||
|
||||
const [showAllFilters, setShowAllFilters] = useState<boolean>(false);
|
||||
|
||||
const { pathname } = useLocation();
|
||||
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
|
||||
pathname,
|
||||
]);
|
||||
|
||||
const memoizedSearchParams = useMemo(
|
||||
() => [
|
||||
searchValue,
|
||||
query.dataSource,
|
||||
query.aggregateOperator,
|
||||
query.aggregateAttribute.key,
|
||||
],
|
||||
[
|
||||
searchValue,
|
||||
query.dataSource,
|
||||
query.aggregateOperator,
|
||||
query.aggregateAttribute.key,
|
||||
],
|
||||
);
|
||||
|
||||
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(
|
||||
() => [searchValue, query.dataSource, queryFiltersWithoutId],
|
||||
[query.dataSource, queryFiltersWithoutId, searchValue],
|
||||
);
|
||||
|
||||
const memoizedValueParams = useMemo(
|
||||
() => [
|
||||
query.aggregateOperator,
|
||||
query.dataSource,
|
||||
query.aggregateAttribute.key,
|
||||
currentFilterItem?.key?.key || '',
|
||||
currentFilterItem?.key?.dataType,
|
||||
currentFilterItem?.key?.type ?? '',
|
||||
isArray(currentFilterItem?.value)
|
||||
? currentFilterItem?.value?.[currentFilterItem.value.length - 1]
|
||||
: currentFilterItem?.value,
|
||||
],
|
||||
[
|
||||
query.aggregateOperator,
|
||||
query.dataSource,
|
||||
query.aggregateAttribute.key,
|
||||
currentFilterItem?.key?.key,
|
||||
currentFilterItem?.key?.dataType,
|
||||
currentFilterItem?.key?.type,
|
||||
currentFilterItem?.value,
|
||||
],
|
||||
);
|
||||
|
||||
const searchParams = useDebounceValue(memoizedSearchParams, DEBOUNCE_DELAY);
|
||||
|
||||
const valueParams = useDebounceValue(memoizedValueParams, DEBOUNCE_DELAY);
|
||||
|
||||
const suggestionsParams = useDebounceValue(
|
||||
memoizedSuggestionsParams,
|
||||
DEBOUNCE_DELAY,
|
||||
);
|
||||
|
||||
const isQueryEnabled = useMemo(() => {
|
||||
if (currentState === DropdownState.ATTRIBUTE_KEY) {
|
||||
return query.dataSource === DataSource.METRICS
|
||||
? !!query.aggregateOperator &&
|
||||
!!query.dataSource &&
|
||||
!!query.aggregateAttribute.dataType
|
||||
: true;
|
||||
}
|
||||
return false;
|
||||
}, [
|
||||
currentState,
|
||||
query.aggregateAttribute.dataType,
|
||||
query.aggregateOperator,
|
||||
query.dataSource,
|
||||
]);
|
||||
|
||||
const { data, isFetching } = useGetAggregateKeys(
|
||||
{
|
||||
searchText: searchValue,
|
||||
dataSource: query.dataSource,
|
||||
aggregateOperator: query.aggregateOperator,
|
||||
aggregateAttribute: query.aggregateAttribute.key,
|
||||
tagType: query.aggregateAttribute.type ?? null,
|
||||
},
|
||||
{
|
||||
queryKey: [searchParams],
|
||||
enabled: isQueryEnabled && !isLogsExplorerPage,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: suggestionsData,
|
||||
isFetching: isFetchingSuggestions,
|
||||
} = useGetAttributeSuggestions(
|
||||
{
|
||||
searchText: searchValue.split(' ')[0],
|
||||
dataSource: query.dataSource,
|
||||
filters: query.filters,
|
||||
},
|
||||
{
|
||||
queryKey: [suggestionsParams],
|
||||
enabled: isQueryEnabled && isLogsExplorerPage,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: attributeValues,
|
||||
isFetching: isFetchingAttributeValues,
|
||||
} = useGetAggregateValues(
|
||||
{
|
||||
aggregateOperator: query.aggregateOperator,
|
||||
dataSource: query.dataSource,
|
||||
aggregateAttribute: query.aggregateAttribute.key,
|
||||
attributeKey: currentFilterItem?.key?.key || '',
|
||||
filterAttributeKeyDataType:
|
||||
currentFilterItem?.key?.dataType ?? DataTypes.EMPTY,
|
||||
tagType: currentFilterItem?.key?.type ?? '',
|
||||
searchText: isArray(currentFilterItem?.value)
|
||||
? currentFilterItem?.value?.[currentFilterItem.value.length - 1] || ''
|
||||
: currentFilterItem?.value?.toString() || '',
|
||||
},
|
||||
{
|
||||
enabled: currentState === DropdownState.ATTRIBUTE_VALUE,
|
||||
queryKey: [valueParams],
|
||||
},
|
||||
);
|
||||
|
||||
const handleDropdownSelect = useCallback(
|
||||
(value: string) => {
|
||||
let parsedValue: BaseAutocompleteData | string;
|
||||
|
||||
try {
|
||||
parsedValue = JSON.parse(value);
|
||||
} catch {
|
||||
parsedValue = value;
|
||||
}
|
||||
if (currentState === DropdownState.ATTRIBUTE_KEY) {
|
||||
setCurrentFilterItem((prev) => ({
|
||||
...prev,
|
||||
key: parsedValue as BaseAutocompleteData,
|
||||
op: '',
|
||||
value: '',
|
||||
}));
|
||||
setCurrentState(DropdownState.OPERATOR);
|
||||
setSearchValue((parsedValue as BaseAutocompleteData)?.key);
|
||||
} else if (currentState === DropdownState.OPERATOR) {
|
||||
if (value === OPERATORS.EXISTS || value === OPERATORS.NOT_EXISTS) {
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
{
|
||||
key: currentFilterItem?.key,
|
||||
op: value,
|
||||
value: '',
|
||||
} as ITag,
|
||||
]);
|
||||
setCurrentFilterItem(undefined);
|
||||
setSearchValue('');
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
} else {
|
||||
setCurrentFilterItem((prev) => ({
|
||||
key: prev?.key as BaseAutocompleteData,
|
||||
op: value as string,
|
||||
value: '',
|
||||
}));
|
||||
setCurrentState(DropdownState.ATTRIBUTE_VALUE);
|
||||
setSearchValue(`${currentFilterItem?.key?.key} ${value}`);
|
||||
}
|
||||
} else if (currentState === DropdownState.ATTRIBUTE_VALUE) {
|
||||
const operatorType =
|
||||
operatorTypeMapper[currentFilterItem?.op || ''] || 'NOT_VALID';
|
||||
const isMulti = operatorType === QUERY_BUILDER_SEARCH_VALUES.MULTIPLY;
|
||||
|
||||
if (isMulti) {
|
||||
const { tagKey, tagOperator, tagValue } = getTagToken(searchValue);
|
||||
const newSearch = [...tagValue];
|
||||
newSearch[newSearch.length === 0 ? 0 : newSearch.length - 1] = value;
|
||||
const newSearchValue = newSearch.join(',');
|
||||
setSearchValue(`${tagKey} ${tagOperator} ${newSearchValue},`);
|
||||
} else {
|
||||
setSearchValue('');
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
setCurrentFilterItem(undefined);
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
{
|
||||
key: currentFilterItem?.key,
|
||||
op: currentFilterItem?.op,
|
||||
value,
|
||||
} as ITag,
|
||||
]);
|
||||
}
|
||||
}
|
||||
},
|
||||
[currentFilterItem?.key, currentFilterItem?.op, currentState, searchValue],
|
||||
);
|
||||
|
||||
const handleSearch = useCallback((value: string) => {
|
||||
setSearchValue(value);
|
||||
}, []);
|
||||
|
||||
const onInputKeyDownHandler = useCallback(
|
||||
(event: KeyboardEvent<Element>): void => {
|
||||
if (event.key === 'Backspace' && !searchValue) {
|
||||
event.stopPropagation();
|
||||
setTags((prev) => prev.slice(0, -1));
|
||||
}
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === '/') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setShowAllFilters((prev) => !prev);
|
||||
}
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleRunQuery();
|
||||
setIsOpen(false);
|
||||
}
|
||||
},
|
||||
[handleRunQuery, searchValue],
|
||||
);
|
||||
|
||||
const handleOnBlur = useCallback((): void => {
|
||||
if (searchValue) {
|
||||
const operatorType =
|
||||
operatorTypeMapper[currentFilterItem?.op || ''] || 'NOT_VALID';
|
||||
if (
|
||||
currentFilterItem?.key &&
|
||||
isEmpty(currentFilterItem?.op) &&
|
||||
whereClauseConfig?.customKey === 'body' &&
|
||||
whereClauseConfig?.customOp === OPERATORS.CONTAINS
|
||||
) {
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
{
|
||||
key: {
|
||||
key: 'body',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'body--string----true',
|
||||
},
|
||||
op: OPERATORS.CONTAINS,
|
||||
value: currentFilterItem?.key?.key,
|
||||
},
|
||||
]);
|
||||
setCurrentFilterItem(undefined);
|
||||
setSearchValue('');
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
} else if (
|
||||
currentFilterItem?.op === OPERATORS.EXISTS ||
|
||||
currentFilterItem?.op === OPERATORS.NOT_EXISTS
|
||||
) {
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
{
|
||||
key: currentFilterItem?.key,
|
||||
op: currentFilterItem?.op,
|
||||
value: '',
|
||||
},
|
||||
]);
|
||||
setCurrentFilterItem(undefined);
|
||||
setSearchValue('');
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
} else if (
|
||||
validationMapper[operatorType]?.(
|
||||
isArray(currentFilterItem?.value)
|
||||
? currentFilterItem?.value.length || 0
|
||||
: 1,
|
||||
)
|
||||
) {
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
{
|
||||
key: currentFilterItem?.key as BaseAutocompleteData,
|
||||
op: currentFilterItem?.op as string,
|
||||
value: currentFilterItem?.value || '',
|
||||
},
|
||||
]);
|
||||
setCurrentFilterItem(undefined);
|
||||
setSearchValue('');
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
currentFilterItem?.key,
|
||||
currentFilterItem?.op,
|
||||
currentFilterItem?.value,
|
||||
searchValue,
|
||||
whereClauseConfig?.customKey,
|
||||
whereClauseConfig?.customOp,
|
||||
]);
|
||||
|
||||
// this useEffect takes care of tokenisation based on the search state
|
||||
useEffect(() => {
|
||||
if (isFetchingSuggestions) {
|
||||
return;
|
||||
}
|
||||
if (!searchValue) {
|
||||
setCurrentFilterItem(undefined);
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
}
|
||||
const { tagKey, tagOperator, tagValue } = getTagToken(searchValue);
|
||||
|
||||
if (tagKey && isUndefined(currentFilterItem?.key)) {
|
||||
let currentRunningAttributeKey;
|
||||
const isSuggestedKeyInAutocomplete = suggestionsData?.payload?.attributes?.some(
|
||||
(value) => value.key === tagKey.split(' ')[0],
|
||||
);
|
||||
|
||||
if (isSuggestedKeyInAutocomplete) {
|
||||
const allAttributesMatchingTheKey =
|
||||
suggestionsData?.payload?.attributes?.filter(
|
||||
(value) => value.key === tagKey.split(' ')[0],
|
||||
) || [];
|
||||
|
||||
if (allAttributesMatchingTheKey?.length === 1) {
|
||||
[currentRunningAttributeKey] = allAttributesMatchingTheKey;
|
||||
}
|
||||
if (allAttributesMatchingTheKey?.length > 1) {
|
||||
// the priority logic goes here
|
||||
[currentRunningAttributeKey] = allAttributesMatchingTheKey;
|
||||
}
|
||||
|
||||
if (currentRunningAttributeKey) {
|
||||
setCurrentFilterItem({
|
||||
key: currentRunningAttributeKey,
|
||||
op: '',
|
||||
value: '',
|
||||
});
|
||||
|
||||
setCurrentState(DropdownState.OPERATOR);
|
||||
}
|
||||
}
|
||||
if (suggestionsData?.payload?.attributes?.length === 0) {
|
||||
setCurrentFilterItem({
|
||||
key: {
|
||||
key: tagKey.split(' ')[0],
|
||||
// update this for has and nhas operator , check the useEffect of source keys in older component for details
|
||||
dataType: DataTypes.EMPTY,
|
||||
type: '',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '',
|
||||
value: '',
|
||||
});
|
||||
setCurrentState(DropdownState.OPERATOR);
|
||||
}
|
||||
} else if (
|
||||
currentFilterItem?.key &&
|
||||
currentFilterItem?.key?.key !== tagKey.split(' ')[0]
|
||||
) {
|
||||
setCurrentFilterItem(undefined);
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
} else if (tagOperator && isEmpty(currentFilterItem?.op)) {
|
||||
if (
|
||||
tagOperator === OPERATORS.EXISTS ||
|
||||
tagOperator === OPERATORS.NOT_EXISTS
|
||||
) {
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
{
|
||||
key: currentFilterItem?.key,
|
||||
op: tagOperator,
|
||||
value: '',
|
||||
} as ITag,
|
||||
]);
|
||||
setCurrentFilterItem(undefined);
|
||||
setSearchValue('');
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
} else {
|
||||
setCurrentFilterItem((prev) => ({
|
||||
key: prev?.key as BaseAutocompleteData,
|
||||
op: tagOperator,
|
||||
value: '',
|
||||
}));
|
||||
|
||||
setCurrentState(DropdownState.ATTRIBUTE_VALUE);
|
||||
}
|
||||
} else if (
|
||||
!isEmpty(currentFilterItem?.op) &&
|
||||
tagOperator !== currentFilterItem?.op
|
||||
) {
|
||||
setCurrentFilterItem((prev) => ({
|
||||
key: prev?.key as BaseAutocompleteData,
|
||||
op: '',
|
||||
value: '',
|
||||
}));
|
||||
setCurrentState(DropdownState.OPERATOR);
|
||||
} else if (!isEmpty(tagValue)) {
|
||||
const currentValue = {
|
||||
key: currentFilterItem?.key as BaseAutocompleteData,
|
||||
operator: currentFilterItem?.op as string,
|
||||
value: tagValue,
|
||||
};
|
||||
if (!isEqual(currentValue, currentFilterItem)) {
|
||||
setCurrentFilterItem((prev) => ({
|
||||
key: prev?.key as BaseAutocompleteData,
|
||||
op: prev?.op as string,
|
||||
value: tagValue,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}, [
|
||||
currentFilterItem,
|
||||
currentFilterItem?.key,
|
||||
currentFilterItem?.op,
|
||||
suggestionsData?.payload?.attributes,
|
||||
searchValue,
|
||||
isFetchingSuggestions,
|
||||
]);
|
||||
|
||||
// the useEffect takes care of setting the dropdown values correctly on change of the current state
|
||||
useEffect(() => {
|
||||
if (currentState === DropdownState.ATTRIBUTE_KEY) {
|
||||
if (isLogsExplorerPage) {
|
||||
setDropdownOptions(
|
||||
suggestionsData?.payload?.attributes?.map((key) => ({
|
||||
label: key.key,
|
||||
value: key,
|
||||
})) || [],
|
||||
);
|
||||
} else {
|
||||
setDropdownOptions(
|
||||
data?.payload?.attributeKeys?.map((key) => ({
|
||||
label: key.key,
|
||||
value: key,
|
||||
})) || [],
|
||||
);
|
||||
}
|
||||
}
|
||||
if (currentState === DropdownState.OPERATOR) {
|
||||
const keyOperator = searchValue.split(' ');
|
||||
const partialOperator = keyOperator?.[1];
|
||||
const strippedKey = keyOperator?.[0];
|
||||
|
||||
let operatorOptions;
|
||||
if (currentFilterItem?.key?.dataType) {
|
||||
operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES[
|
||||
currentFilterItem.key
|
||||
.dataType as keyof typeof QUERY_BUILDER_OPERATORS_BY_TYPES
|
||||
].map((operator) => ({
|
||||
label: operator,
|
||||
value: operator,
|
||||
}));
|
||||
|
||||
if (partialOperator) {
|
||||
operatorOptions = operatorOptions.filter((op) =>
|
||||
op.label.startsWith(partialOperator.toLocaleUpperCase()),
|
||||
);
|
||||
}
|
||||
setDropdownOptions(operatorOptions);
|
||||
} else if (strippedKey.endsWith('[*]') && strippedKey.startsWith('body.')) {
|
||||
operatorOptions = [OPERATORS.HAS, OPERATORS.NHAS].map((operator) => ({
|
||||
label: operator,
|
||||
value: operator,
|
||||
}));
|
||||
setDropdownOptions(operatorOptions);
|
||||
} else {
|
||||
operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES.universal.map(
|
||||
(operator) => ({
|
||||
label: operator,
|
||||
value: operator,
|
||||
}),
|
||||
);
|
||||
|
||||
if (partialOperator) {
|
||||
operatorOptions = operatorOptions.filter((op) =>
|
||||
op.label.startsWith(partialOperator.toLocaleUpperCase()),
|
||||
);
|
||||
}
|
||||
setDropdownOptions(operatorOptions);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentState === DropdownState.ATTRIBUTE_VALUE) {
|
||||
const values: string[] =
|
||||
Object.values(attributeValues?.payload || {}).find((el) => !!el) || [];
|
||||
|
||||
const { tagValue } = getTagToken(searchValue);
|
||||
|
||||
if (values.length === 0) {
|
||||
if (isArray(tagValue)) {
|
||||
if (!isEmpty(tagValue[tagValue.length - 1]))
|
||||
values.push(tagValue[tagValue.length - 1]);
|
||||
} else if (!isEmpty(tagValue)) values.push(tagValue);
|
||||
}
|
||||
|
||||
setDropdownOptions(
|
||||
values.map((val) => ({
|
||||
label: val,
|
||||
value: val,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}, [
|
||||
attributeValues?.payload,
|
||||
currentFilterItem?.key.dataType,
|
||||
currentState,
|
||||
data?.payload?.attributeKeys,
|
||||
isLogsExplorerPage,
|
||||
searchValue,
|
||||
suggestionsData?.payload?.attributes,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const filterTags: IBuilderQuery['filters'] = {
|
||||
op: 'AND',
|
||||
items: [],
|
||||
};
|
||||
tags.forEach((tag) => {
|
||||
filterTags.items.push({
|
||||
id: tag.id || uuid().slice(0, 8),
|
||||
key: tag.key,
|
||||
op: tag.op,
|
||||
value: tag.value,
|
||||
});
|
||||
});
|
||||
|
||||
if (!isEqual(query.filters, filterTags)) {
|
||||
onChange(filterTags);
|
||||
setTags(filterTags.items as ITag[]);
|
||||
}
|
||||
}, [onChange, query.filters, tags]);
|
||||
|
||||
const isLastQuery = useMemo(
|
||||
() =>
|
||||
isEqual(
|
||||
currentQuery.builder.queryData[currentQuery.builder.queryData.length - 1],
|
||||
query,
|
||||
),
|
||||
[currentQuery, query],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLastQuery) {
|
||||
registerShortcut(LogsExplorerShortcuts.FocusTheSearchBar, () => {
|
||||
// set timeout is needed here else the select treats the hotkey as input value
|
||||
setTimeout(() => {
|
||||
selectRef.current?.focus();
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
|
||||
return (): void =>
|
||||
deregisterShortcut(LogsExplorerShortcuts.FocusTheSearchBar);
|
||||
}, [deregisterShortcut, isLastQuery, registerShortcut]);
|
||||
|
||||
const loading = useMemo(
|
||||
() => isFetching || isFetchingAttributeValues || isFetchingSuggestions,
|
||||
[isFetching, isFetchingAttributeValues, isFetchingSuggestions],
|
||||
);
|
||||
|
||||
const isMetricsDataSource = useMemo(
|
||||
() => query.dataSource === DataSource.METRICS,
|
||||
[query.dataSource],
|
||||
);
|
||||
|
||||
const queryTags = useMemo(
|
||||
() => tags.map((tag) => `${tag.key.key} ${tag.op} ${tag.value}`),
|
||||
[tags],
|
||||
);
|
||||
|
||||
const onTagRender = ({
|
||||
value,
|
||||
closable,
|
||||
onClose,
|
||||
}: CustomTagProps): React.ReactElement => {
|
||||
const { tagOperator } = getTagToken(value);
|
||||
const isInNin = isInNInOperator(tagOperator);
|
||||
const chipValue = isInNin
|
||||
? value?.trim()?.replace(/,\s*$/, '')
|
||||
: value?.trim();
|
||||
|
||||
const indexInQueryTags = queryTags.findIndex((qTag) => isEqual(qTag, value));
|
||||
const tagDetails = tags[indexInQueryTags];
|
||||
|
||||
const onCloseHandler = (): void => {
|
||||
onClose();
|
||||
setSearchValue('');
|
||||
setTags((prev) => prev.filter((t) => !isEqual(t, tagDetails)));
|
||||
};
|
||||
|
||||
const tagEditHandler = (value: string): void => {
|
||||
setCurrentFilterItem(tagDetails);
|
||||
setSearchValue(value);
|
||||
setCurrentState(DropdownState.ATTRIBUTE_VALUE);
|
||||
setTags((prev) => prev.filter((t) => !isEqual(t, tagDetails)));
|
||||
};
|
||||
|
||||
const isDisabled = !!searchValue;
|
||||
|
||||
return (
|
||||
<span className="qb-search-bar-tokenised-tags">
|
||||
<Tag
|
||||
closable={!searchValue && closable}
|
||||
onClose={onCloseHandler}
|
||||
className={tagDetails?.key?.type || ''}
|
||||
>
|
||||
<Tooltip title={chipValue}>
|
||||
<TypographyText
|
||||
ellipsis
|
||||
$isInNin={isInNin}
|
||||
disabled={isDisabled}
|
||||
$isEnabled={!!searchValue}
|
||||
onClick={(): void => {
|
||||
if (!isDisabled) tagEditHandler(value);
|
||||
}}
|
||||
>
|
||||
{chipValue}
|
||||
</TypographyText>
|
||||
</Tooltip>
|
||||
</Tag>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="query-builder-search-v2">
|
||||
<Select
|
||||
ref={selectRef}
|
||||
getPopupContainer={popupContainer}
|
||||
virtual={false}
|
||||
showSearch
|
||||
tagRender={onTagRender}
|
||||
transitionName=""
|
||||
choiceTransitionName=""
|
||||
filterOption={false}
|
||||
open={isOpen}
|
||||
suffixIcon={
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
!isUndefined(suffixIcon) ? (
|
||||
suffixIcon
|
||||
) : isOpen ? (
|
||||
<ChevronUp size={14} />
|
||||
) : (
|
||||
<ChevronDown size={14} />
|
||||
)
|
||||
}
|
||||
onDropdownVisibleChange={setIsOpen}
|
||||
autoClearSearchValue={false}
|
||||
mode="multiple"
|
||||
placeholder={placeholder}
|
||||
value={queryTags}
|
||||
searchValue={searchValue}
|
||||
className={cx(
|
||||
!currentFilterItem?.key && !showAllFilters && dropdownOptions.length > 3
|
||||
? 'show-all-filters'
|
||||
: '',
|
||||
className,
|
||||
)}
|
||||
rootClassName="query-builder-search"
|
||||
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
|
||||
style={selectStyle}
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleDropdownSelect}
|
||||
onInputKeyDown={onInputKeyDownHandler}
|
||||
notFoundContent={loading ? <Spin size="small" /> : null}
|
||||
showAction={['focus']}
|
||||
onBlur={handleOnBlur}
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
dropdownRender={(menu): ReactElement => (
|
||||
<QueryBuilderSearchDropdown
|
||||
menu={menu}
|
||||
options={dropdownOptions}
|
||||
onChange={(val: TagFilter): void => {
|
||||
setTags((prev) => [...prev, ...(val.items as ITag[])]);
|
||||
}}
|
||||
searchValue={searchValue}
|
||||
exampleQueries={suggestionsData?.payload?.example_queries || []}
|
||||
tags={tags}
|
||||
currentFilterItem={currentFilterItem}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{dropdownOptions.map((option) => {
|
||||
let val = option.value;
|
||||
try {
|
||||
if (isObject(option.value)) {
|
||||
val = JSON.stringify(option.value);
|
||||
} else {
|
||||
val = option.value;
|
||||
}
|
||||
} catch {
|
||||
val = option.value;
|
||||
}
|
||||
return (
|
||||
<Select.Option key={isObject(val) ? `select-option` : val} value={val}>
|
||||
<Suggestions
|
||||
label={option.label}
|
||||
value={option.value}
|
||||
option={currentState}
|
||||
/>
|
||||
</Select.Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
QueryBuilderSearchV2.defaultProps = {
|
||||
placeholder: PLACEHOLDER,
|
||||
className: '',
|
||||
suffixIcon: null,
|
||||
whereClauseConfig: {},
|
||||
};
|
||||
|
||||
export default QueryBuilderSearchV2;
|
||||
@@ -0,0 +1,147 @@
|
||||
.text {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
height: 5px;
|
||||
width: 5px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--bg-slate-300);
|
||||
}
|
||||
|
||||
.option {
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.left-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 90%;
|
||||
gap: 8px;
|
||||
|
||||
.value {
|
||||
}
|
||||
}
|
||||
.right-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.data-type {
|
||||
display: flex;
|
||||
height: 20px;
|
||||
padding: 4px 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.type-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
padding: 0px 6px;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
border-radius: 50px;
|
||||
text-transform: capitalize;
|
||||
|
||||
&.tag {
|
||||
border-radius: 50px;
|
||||
background: rgba(189, 153, 121, 0.1) !important;
|
||||
color: var(--bg-sienna-400) !important;
|
||||
|
||||
.dot {
|
||||
background-color: var(--bg-sienna-400);
|
||||
}
|
||||
.text {
|
||||
color: var(--bg-sienna-400);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
}
|
||||
|
||||
&.resource {
|
||||
border-radius: 50px;
|
||||
background: rgba(245, 108, 135, 0.1) !important;
|
||||
color: var(--bg-sakura-400) !important;
|
||||
|
||||
.dot {
|
||||
background-color: var(--bg-sakura-400);
|
||||
}
|
||||
.text {
|
||||
color: var(--bg-sakura-400);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.option-meta-data-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.container-without-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.OPERATOR {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: 'Space Mono';
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
text-transform: uppercase;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.VALUE {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: 'Space Mono';
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
text-transform: uppercase;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.option:hover {
|
||||
.container {
|
||||
.left-section {
|
||||
.value {
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
.container-without-tag {
|
||||
.value {
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import './Suggestions.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Tooltip, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { isEmpty, isObject } from 'lodash-es';
|
||||
import { Zap } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { DropdownState } from './QueryBuilderSearchV2';
|
||||
|
||||
interface ISuggestionsProps {
|
||||
label: string;
|
||||
value: BaseAutocompleteData | string;
|
||||
option: DropdownState;
|
||||
}
|
||||
|
||||
function Suggestions(props: ISuggestionsProps): React.ReactElement {
|
||||
const { label, value, option } = props;
|
||||
|
||||
const optionType = useMemo(() => {
|
||||
if (isObject(value)) {
|
||||
return value.type;
|
||||
}
|
||||
return '';
|
||||
}, [value]);
|
||||
|
||||
const [truncated, setTruncated] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<div className="option">
|
||||
{!isEmpty(optionType) && isObject(value) ? (
|
||||
<Tooltip title={truncated ? `${value.key}` : ''} placement="topLeft">
|
||||
<div className="container">
|
||||
<section className="left-section">
|
||||
{value.isIndexed ? (
|
||||
<Zap size={12} fill={Color.BG_AMBER_500} />
|
||||
) : (
|
||||
<div className="dot" />
|
||||
)}
|
||||
<Typography.Text
|
||||
className="text value"
|
||||
ellipsis={{ onEllipsis: (ellipsis): void => setTruncated(ellipsis) }}
|
||||
>
|
||||
{label}
|
||||
</Typography.Text>
|
||||
</section>
|
||||
<section className="right-section">
|
||||
<Typography.Text className="data-type">{value.dataType}</Typography.Text>
|
||||
<section className={cx('type-tag', value.type)}>
|
||||
<div className="dot" />
|
||||
<Typography.Text className="text">{value.type}</Typography.Text>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title={truncated ? label : ''} placement="topLeft">
|
||||
<div className="container-without-tag">
|
||||
<div className="dot" />
|
||||
<Typography.Text
|
||||
className={cx('text value', option)}
|
||||
ellipsis={{ onEllipsis: (ellipsis): void => setTruncated(ellipsis) }}
|
||||
>
|
||||
{`${label}`}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Suggestions;
|
||||
@@ -15,4 +15,5 @@ export type Option = {
|
||||
label: string;
|
||||
selected?: boolean;
|
||||
dataType?: string;
|
||||
isIndexed?: boolean;
|
||||
};
|
||||
|
||||
@@ -28,4 +28,9 @@ export type UseActiveLog = {
|
||||
isJSON?: boolean,
|
||||
dataType?: DataTypes,
|
||||
) => void;
|
||||
onGroupByAttribute: (
|
||||
fieldKey: string,
|
||||
isJSON?: boolean,
|
||||
dataType?: DataTypes,
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -128,6 +128,54 @@ export const useActiveLog = (): UseActiveLog => {
|
||||
[currentQuery, notifications, queryClient, redirectWithQueryBuilderData],
|
||||
);
|
||||
|
||||
const onGroupByAttribute = useCallback(
|
||||
async (
|
||||
fieldKey: string,
|
||||
isJSON?: boolean,
|
||||
dataType?: DataTypes,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const keysAutocompleteResponse = await queryClient.fetchQuery(
|
||||
[QueryBuilderKeys.GET_AGGREGATE_KEYS, fieldKey],
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
async () =>
|
||||
getAggregateKeys({
|
||||
searchText: fieldKey,
|
||||
aggregateOperator: currentQuery.builder.queryData[0].aggregateOperator,
|
||||
dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||
aggregateAttribute:
|
||||
currentQuery.builder.queryData[0].aggregateAttribute.key,
|
||||
}),
|
||||
);
|
||||
|
||||
const keysAutocomplete: BaseAutocompleteData[] =
|
||||
keysAutocompleteResponse.payload?.attributeKeys || [];
|
||||
|
||||
const existAutocompleteKey = chooseAutocompleteFromCustomValue(
|
||||
keysAutocomplete,
|
||||
fieldKey,
|
||||
isJSON,
|
||||
dataType,
|
||||
);
|
||||
|
||||
const nextQuery: Query = {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
groupBy: [...item.groupBy, existAutocompleteKey],
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
redirectWithQueryBuilderData(nextQuery);
|
||||
} catch {
|
||||
notifications.error({ message: SOMETHING_WENT_WRONG });
|
||||
}
|
||||
},
|
||||
[currentQuery, notifications, queryClient, redirectWithQueryBuilderData],
|
||||
);
|
||||
const onAddToQueryLogs = useCallback(
|
||||
(fieldKey: string, fieldValue: string, operator: string) => {
|
||||
const updatedQueryString = getGeneratedFilterQueryString(
|
||||
@@ -147,5 +195,6 @@ export const useActiveLog = (): UseActiveLog => {
|
||||
onSetActiveLog,
|
||||
onClearActiveLog,
|
||||
onAddToQuery: isLogsPage ? onAddToQueryLogs : onAddToQueryExplorer,
|
||||
onGroupByAttribute,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
33
frontend/src/hooks/queryBuilder/useGetAggregateValues.ts
Normal file
33
frontend/src/hooks/queryBuilder/useGetAggregateValues.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
IAttributeValuesResponse,
|
||||
IGetAttributeValuesPayload,
|
||||
} from 'types/api/queryBuilder/getAttributesValues';
|
||||
|
||||
type UseGetAttributeValues = (
|
||||
requestData: IGetAttributeValuesPayload,
|
||||
options?: UseQueryOptions<
|
||||
SuccessResponse<IAttributeValuesResponse> | ErrorResponse
|
||||
>,
|
||||
) => UseQueryResult<SuccessResponse<IAttributeValuesResponse> | ErrorResponse>;
|
||||
|
||||
export const useGetAggregateValues: UseGetAttributeValues = (
|
||||
requestData,
|
||||
options,
|
||||
) => {
|
||||
const queryKey = useMemo(() => {
|
||||
if (options?.queryKey && Array.isArray(options.queryKey)) {
|
||||
return [...options.queryKey];
|
||||
}
|
||||
return [requestData];
|
||||
}, [options?.queryKey, requestData]);
|
||||
|
||||
return useQuery<SuccessResponse<IAttributeValuesResponse> | ErrorResponse>({
|
||||
queryKey,
|
||||
queryFn: () => getAttributesValues(requestData),
|
||||
...options,
|
||||
});
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
||||
|
||||
import { OperatorType } from './useOperatorType';
|
||||
|
||||
const validationMapper: Record<
|
||||
export const validationMapper: Record<
|
||||
OperatorType,
|
||||
(resultLength: number) => boolean
|
||||
> = {
|
||||
|
||||
@@ -6,7 +6,7 @@ export type OperatorType =
|
||||
| 'NON_VALUE'
|
||||
| 'NOT_VALID';
|
||||
|
||||
const operatorTypeMapper: Record<string, OperatorType> = {
|
||||
export const operatorTypeMapper: Record<string, OperatorType> = {
|
||||
[OPERATORS.IN]: 'MULTIPLY_VALUE',
|
||||
[OPERATORS.NIN]: 'MULTIPLY_VALUE',
|
||||
[OPERATORS.EXISTS]: 'NON_VALUE',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user