Compare commits

...

28 Commits

Author SHA1 Message Date
Sudeep MP
33541a2ac0 feat: add view templates option to dashboard menu (#5696)
* feat: add view templates option to dashboard menu

* feat: increase dropdown overlay width
Set the dropdown overlay width to 200px to provide breathing space for the dropdown button.
Added flex to wrap the dropdown button to create space between the right icon and the left elements.

---------

Co-authored-by: Pranay Prateek <pranay@signoz.io>
2024-08-23 15:55:04 +05:30
SagarRajput-7
947b5bdefb fix: handled defaultTraceSelected for traces list view (#5752)
* fix: handled defaultTraceSelected for traces list view

* fix: added metaData id
2024-08-23 15:15:30 +05:30
Vibhu Pandey
bd7d14b1ca feat(render): add render package (#5751)
### Summary

Add `render` package

#### Related Issues / PR's

https://github.com/SigNoz/signoz/pull/5710
2024-08-23 13:07:10 +05:30
Yunus M
43ed49f9d9 fix: dashboard names invisible due to same background color (#5758) 2024-08-23 12:24:06 +05:30
Vikrant Gupta
758b10f1bf fix: raw view css condense fix for line clamp (#5755) 2024-08-23 00:54:30 +05:30
Vikrant Gupta
ab1caf13fc feat: add support for group by attribute in log details (#5753)
* feat: add support for group by attribute in log details

* feat: auto shift to qb from search on adding groupBY

* feat: update icon and styles
2024-08-22 23:59:22 +05:30
Vikrant Gupta
96b81817e0 feat: add support for changing the font size in logs (#5739)
* feat: add support for changing the font size in logs

* fix: build issues and logs context

* chore: fix build issues

* feat: scale all the spaces

* chore: handle light mode designs

* feat: set small as the default
2024-08-22 23:56:51 +05:30
Vibhu Pandey
bfeceb0ed2 feat(web): add web package (#5743)
### Summary

Add a web package for serving frontend

#### Related Issues / PR's

https://github.com/SigNoz/signoz/pull/5710
2024-08-22 20:56:15 +05:30
Vibhu Pandey
c322fc72d9 feat(errors): add errors package (#5741)
### Summary

Add errors package

#### Related Issues / PR's

https://github.com/SigNoz/signoz/pull/5710
2024-08-22 15:19:32 +05:30
Vibhu Pandey
e7b5410c5b feat(packages): add registry and http packages (#5740)
### Summary

Add packages for Registry and HTTP

#### Related Issues / PR's

https://github.com/SigNoz/signoz/pull/5710
2024-08-22 14:24:02 +05:30
Srikanth Chekuri
072693d57d fix: nan and inf values in formula result (#5733) 2024-08-21 17:55:16 +05:30
SagarRajput-7
a20794040a chore: added trace explorer test (#5531)
* feat: added trace filter test cases

* feat: added trace filter test cases - initial render

* feat: added test cases - query sync, filter section behaviour etc

* feat: deleted mock-data files

* feat: added test cases of undefined filters and items

* feat: deleted tsconfig

* feat: added clear and rest btn test cases for traces filters

* feat: added collapse and uncollapse test for traces filters

* chore: added trace explorer tests
2024-08-21 15:04:42 +05:30
Vibhu Pandey
ab4a8dfbea feat(packages): add first dedicated confmap, config, version and instrumentation packages (#5727)
### Summary

A config package based on https://github.com/open-telemetry/opentelemetry-collector/blob/main/confmap/confmap.go for signoz.

#### Related Issues / PR's

This is a part of https://github.com/SigNoz/signoz/pull/5710
2024-08-21 14:18:44 +05:30
Vishal Sharma
fa0a065b95 chore: chat block events (#5725)
Also add go to integration event
2024-08-20 18:41:34 +05:30
Vibhu Pandey
abc8096a39 chore(codeowners): update codeowners to team (#5726) 2024-08-20 18:16:07 +05:30
SagarRajput-7
7cff07333f fix: added onDragSelect to DBCall and External metric app (#5694)
* fix: added onDragSelect to DBCall and External metric app

* fix: handled back navigation
2024-08-20 17:45:22 +05:30
Vikrant Gupta
5796d6cb8c feat: rewrite the query builder search component (#5659)
* feat: make the query builder search extensible

* feat: setup the framework and necessary states needed

* feat: cover the happy path of selects

* chore: forward typing flow handled

* chore: add antd select

* chore: add antd select

* chore: handle forward and backward flows

* feat: added tag properites to the search bar and multi tag partial handling

* feat: handle tag on blur and body contains changes

* feat: handle tag deselect

* feat: multi tag handling

* feat: multi tag handling

* fix: jest test cases

* chore: update the key

* chore: add edit tag not working as expected

* feat: handle cases for exists and nexists

* fix: handle has / nhas operators

* chore: fix usability issues

* chore: remove the usage for the new bar

* fix: flaky build issues

* feat: client changes for consumption and design changes for where clause in logs explorer  (#5712)

* feat: query search new ui

* feat: suggestions changes in v2

* feat: dropdown and tags ui touch up

* feat: added missing keyboard shortcuts

* fix: race condition issues

* feat: remove usage

* fix: operator select fix

* fix: handle example queries click changes

* chore: design sync

* chore: handle boolean selects

* chore: address review comments
2024-08-20 17:09:17 +05:30
Srikanth Chekuri
98367fd054 fix: add missing selected time range variables (#5714) 2024-08-20 15:08:29 +05:30
Raj Kamal Singh
ff8df5dc36 chore: use base prefix of /ws for websocket paths (#5719)
Co-authored-by: Vikrant Gupta <vikrant.thomso@gmail.com>
2024-08-20 13:26:34 +05:30
Vikrant Gupta
f0c9f12897 fix: do not use relative URLs for ws connections (#5715)
* fix: do not use relative URLs for ws connections

* fix: handle local env better

* chore: update the websocket endpoint

* chore: handle OSS/Docker installations
2024-08-20 13:17:56 +05:30
Vikrant Gupta
79e96e544f chore: added leading slash for for ws URL (#5709) 2024-08-16 18:51:02 +05:30
Vishal Sharma
871e5ada9e chore: dashboard and alert names (#5705)
* chore: dashboard names

* chore: fix panel count
2024-08-16 18:08:59 +05:30
Vikrant Gupta
0401c27dbc chore: remove the base URL from the ws config (#5708) 2024-08-16 17:41:19 +05:30
Vibhu Pandey
57c45f22d6 feat(premium-support): add premium-support feature (#5707) 2024-08-16 16:50:40 +05:30
Srikanth Chekuri
29f1883edd chore: add telemetry for dashboards/alerts with tsv2 table (#5677) 2024-08-16 16:16:12 +05:30
Shaheer Kochai
5d903b5487 NOOP to Count in alert creation from logs (#5464)
* fix: change NOOP to count on creating alert from Logs and traces

* fix: change 'count' back to 'noop' in Traces page, in case there is a single query

* fix: handle the query modification in useGetCompositeQueryParam instead of Filter

* chore: use values StringOperators enum instead of hard coded strings

* Revert "fix: handle the query modification in useGetCompositeQueryParam instead of Filter"

This reverts commit 5bb837ec27.

* Revert "fix: change 'count' back to 'noop' in Traces page, in case there is a single query"

This reverts commit 5e506dbd35.
2024-08-16 14:12:22 +04:30
Vikrant Gupta
1b9683d699 feat: client changes for query stats (#5706)
* feat: changes for the query stats websockets

* chore: remove unwanted files

* fix: work on random id rather than hash

* fix: improve the icons and design

* feat: webpack and docker file changes

* fix: test cases

* chore: format the units

* chore: address review comments

* chore: update the id to uuid package

* fix: build issues

* chore: remove docker file changes

* chore: remove docker file changes
2024-08-16 15:07:06 +05:30
Vikrant Gupta
65280cf4e1 feat: support for attribute key suggestions and example queries in logs explorer query builder (#5608)
* feat: qb-suggestions base setup

* chore: make the dropdown a little similar to the designs

* chore: move out example queries from og and add to renderer

* chore: added the handlers for example queries

* chore: hide the example queries as soon as the user starts typing

* feat: handle changes for cancel query

* chore: remove stupid concept of option group

* chore: show only first 3 items and add option to show all filters

* chore: minor css changes and remove transitions

* feat: integrate suggestions api and control re-renders

* feat: added keyboard shortcuts for the dropdown

* fix: design cleanups and touchups

* fix: build issues and tests

* chore: extra safety check for base64 and fix tests

* fix: qs doesn't handle padding in base64 strings, added client logic

* chore: some code comments

* chore: some code comments

* chore: increase the height of the bar when key is set

* chore: address minor designs

* chore: update the keyboard shortcut to cmd+/

* feat: correct the option render for logs for tooltip

* chore: search bar to not loose focus on btn click

* fix: update the spacing and icon for search bar

* chore: address review comments
2024-08-16 13:11:39 +05:30
176 changed files with 7654 additions and 780 deletions

6
.github/CODEOWNERS vendored
View File

@@ -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
View File

@@ -67,3 +67,6 @@ e2e/.auth
# go
vendor/
**/main/**
# git-town
.git-branches.toml

View File

@@ -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()

View File

@@ -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: "",
},
}

View 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

View 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

View File

@@ -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",

View 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);
}
});
}

View File

@@ -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,

View File

@@ -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,

View 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);
}
};

View 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;

View File

@@ -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);

View File

@@ -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,
});

View File

@@ -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,

View File

@@ -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'> &

View File

@@ -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}

View File

@@ -1,3 +1,16 @@
.addToQueryContainer {
cursor: pointer;
display: flex;
align-items: center;
&.small {
height: 16px;
}
&.medium {
height: 20px;
}
&.large {
height: 24px;
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}
}

View File

@@ -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}
/>
)}
</>

View File

@@ -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>`

View File

@@ -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);
}

View File

@@ -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'),

View File

@@ -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>
);
}

View File

@@ -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>

View File

@@ -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'};

View File

@@ -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;

View File

@@ -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;`}
`;

View File

@@ -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;
};

View File

@@ -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;
}
}

View File

@@ -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 };

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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 || '',
};

View File

@@ -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 = {

View File

@@ -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',
};

View File

@@ -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>,

View File

@@ -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>

View File

@@ -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]));

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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')}&nbsp;
<ExternalLink size={14} />
</Button>
</a>
</Flex>
<Button
// disabled={editorValue.length === 0}

View File

@@ -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}
/>
</>

View File

@@ -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 && (

View File

@@ -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>(

View File

@@ -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}

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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);
}
}
}
}

View File

@@ -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,
};

View File

@@ -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 => ({

View File

@@ -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}
/>
),
[],

View File

@@ -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

View File

@@ -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>

View File

@@ -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',
};
}

View File

@@ -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}
/>
</>
);

View File

@@ -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};
`;

View File

@@ -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}
/>
</>

View File

@@ -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);
}
}
}
}
}

View File

@@ -0,0 +1,4 @@
.query-status {
display: flex;
align-items: center;
}

View 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>;
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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]));

View File

@@ -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);
};

View File

@@ -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>

View File

@@ -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,
},
];

View File

@@ -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'

View File

@@ -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,
],
);

View File

@@ -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>
);

View File

@@ -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,
};

View File

@@ -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;
}
}
}

View File

@@ -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');

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}
}
}
}
}
}

View File

@@ -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>
);

View File

@@ -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(' , '),
}));
}

View File

@@ -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,
};

View File

@@ -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);
}
}
}
}
}

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -15,4 +15,5 @@ export type Option = {
label: string;
selected?: boolean;
dataType?: string;
isIndexed?: boolean;
};

View File

@@ -28,4 +28,9 @@ export type UseActiveLog = {
isJSON?: boolean,
dataType?: DataTypes,
) => void;
onGroupByAttribute: (
fieldKey: string,
isJSON?: boolean,
dataType?: DataTypes,
) => Promise<void>;
};

View File

@@ -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,
};
};

View File

@@ -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[];
}

View File

@@ -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,
};
};

View 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,
});
};

View File

@@ -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,
});
};

View File

@@ -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,
);
};

View File

@@ -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,
});

View File

@@ -2,7 +2,7 @@ import { useMemo } from 'react';
import { OperatorType } from './useOperatorType';
const validationMapper: Record<
export const validationMapper: Record<
OperatorType,
(resultLength: number) => boolean
> = {

View File

@@ -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