Merge pull request #5800 from SigNoz/release/v0.53.x

Release/v0.53.x
This commit is contained in:
Prashant Shahi 2024-08-30 15:20:27 +05:30 committed by GitHub
commit 262beef8f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
259 changed files with 13196 additions and 1474 deletions

6
.github/CODEOWNERS vendored
View File

@ -5,6 +5,6 @@
/frontend/ @YounixM /frontend/ @YounixM
/frontend/src/container/MetricsApplication @srikanthccv /frontend/src/container/MetricsApplication @srikanthccv
/frontend/src/container/NewWidget/RightContainer/types.ts @srikanthccv /frontend/src/container/NewWidget/RightContainer/types.ts @srikanthccv
/deploy/ @prashant-shahi /deploy/ @SigNoz/devops
/sample-apps/ @prashant-shahi /sample-apps/ @SigNoz/devops
.github @prashant-shahi .github @SigNoz/devops

3
.gitignore vendored
View File

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

View File

@ -649,12 +649,12 @@
See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/#creating-replicated-tables See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/#creating-replicated-tables
--> -->
<!--
<macros> <macros>
<shard>01</shard> <shard>01</shard>
<replica>example01-01-1</replica> <replica>example01-01-1</replica>
</macros> </macros>
-->
<!-- Reloading interval for embedded dictionaries, in seconds. Default: 3600. --> <!-- Reloading interval for embedded dictionaries, in seconds. Default: 3600. -->

View File

@ -146,7 +146,7 @@ services:
condition: on-failure condition: on-failure
query-service: query-service:
image: signoz/query-service:0.52.0 image: signoz/query-service:0.53.0
command: command:
[ [
"-config=/root/config/prometheus.yml", "-config=/root/config/prometheus.yml",
@ -186,7 +186,7 @@ services:
<<: *db-depend <<: *db-depend
frontend: frontend:
image: signoz/frontend:0.52.0 image: signoz/frontend:0.53.0
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
@ -199,7 +199,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:0.102.4 image: signoz/signoz-otel-collector:0.102.7
command: command:
[ [
"--config=/etc/otel-collector-config.yaml", "--config=/etc/otel-collector-config.yaml",
@ -238,7 +238,7 @@ services:
- query-service - query-service
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:0.102.4 image: signoz/signoz-schema-migrator:0.102.7
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure

View File

@ -649,12 +649,12 @@
See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/#creating-replicated-tables See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/#creating-replicated-tables
--> -->
<!--
<macros> <macros>
<shard>01</shard> <shard>01</shard>
<replica>example01-01-1</replica> <replica>example01-01-1</replica>
</macros> </macros>
-->
<!-- Reloading interval for embedded dictionaries, in seconds. Default: 3600. --> <!-- Reloading interval for embedded dictionaries, in seconds. Default: 3600. -->

View File

@ -66,7 +66,7 @@ services:
- --storage.path=/data - --storage.path=/data
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.4} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.7}
container_name: otel-migrator container_name: otel-migrator
command: command:
- "--dsn=tcp://clickhouse:9000" - "--dsn=tcp://clickhouse:9000"
@ -81,7 +81,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
otel-collector: otel-collector:
container_name: signoz-otel-collector container_name: signoz-otel-collector
image: signoz/signoz-otel-collector:0.102.4 image: signoz/signoz-otel-collector:0.102.7
command: command:
[ [
"--config=/etc/otel-collector-config.yaml", "--config=/etc/otel-collector-config.yaml",

View File

@ -164,7 +164,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service: query-service:
image: signoz/query-service:${DOCKER_TAG:-0.52.0} image: signoz/query-service:${DOCKER_TAG:-0.53.0}
container_name: signoz-query-service container_name: signoz-query-service
command: command:
[ [
@ -204,7 +204,7 @@ services:
<<: *db-depend <<: *db-depend
frontend: frontend:
image: signoz/frontend:${DOCKER_TAG:-0.52.0} image: signoz/frontend:${DOCKER_TAG:-0.53.0}
container_name: signoz-frontend container_name: signoz-frontend
restart: on-failure restart: on-failure
depends_on: depends_on:
@ -216,7 +216,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.4} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.7}
container_name: otel-migrator container_name: otel-migrator
command: command:
- "--dsn=tcp://clickhouse:9000" - "--dsn=tcp://clickhouse:9000"
@ -230,7 +230,7 @@ services:
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.4} image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.7}
container_name: signoz-otel-collector container_name: signoz-otel-collector
command: command:
[ [

View File

@ -164,7 +164,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service: query-service:
image: signoz/query-service:${DOCKER_TAG:-0.52.0} image: signoz/query-service:${DOCKER_TAG:-0.53.0}
container_name: signoz-query-service container_name: signoz-query-service
command: command:
[ [
@ -203,7 +203,7 @@ services:
<<: *db-depend <<: *db-depend
frontend: frontend:
image: signoz/frontend:${DOCKER_TAG:-0.52.0} image: signoz/frontend:${DOCKER_TAG:-0.53.0}
container_name: signoz-frontend container_name: signoz-frontend
restart: on-failure restart: on-failure
depends_on: depends_on:
@ -215,7 +215,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.4} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.7}
container_name: otel-migrator container_name: otel-migrator
command: command:
- "--dsn=tcp://clickhouse:9000" - "--dsn=tcp://clickhouse:9000"
@ -229,7 +229,7 @@ services:
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.4} image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.7}
container_name: signoz-otel-collector container_name: signoz-otel-collector
command: command:
[ [

View File

@ -1,3 +1,8 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server { server {
listen 3301; listen 3301;
server_name _; server_name _;
@ -42,6 +47,14 @@ server {
proxy_read_timeout 600s; proxy_read_timeout 600s;
} }
location /ws {
proxy_pass http://query-service:8080/ws;
proxy_http_version 1.1;
proxy_set_header Upgrade "websocket";
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
# redirect server error pages to the static page /50x.html # redirect server error pages to the static page /50x.html
# #
error_page 500 502 503 504 /50x.html; error_page 500 502 503 504 /50x.html;

View File

@ -359,6 +359,8 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
apiHandler.RegisterIntegrationRoutes(r, am) apiHandler.RegisterIntegrationRoutes(r, am)
apiHandler.RegisterQueryRangeV3Routes(r, am) apiHandler.RegisterQueryRangeV3Routes(r, am)
apiHandler.RegisterQueryRangeV4Routes(r, am) apiHandler.RegisterQueryRangeV4Routes(r, am)
apiHandler.RegisterWebSocketPaths(r, am)
apiHandler.RegisterMessagingQueuesRoutes(r, am)
c := cors.New(cors.Options{ c := cors.New(cors.Options{
AllowedOrigins: []string{"*"}, AllowedOrigins: []string{"*"},
@ -375,6 +377,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
}, nil }, nil
} }
// TODO(remove): Implemented at pkg/http/middleware/logging.go
// loggingMiddleware is used for logging public api calls // loggingMiddleware is used for logging public api calls
func loggingMiddleware(next http.Handler) http.Handler { func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -386,6 +389,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 // loggingMiddlewarePrivate is used for logging private api calls
// from internal services like alert manager // from internal services like alert manager
func loggingMiddlewarePrivate(next http.Handler) http.Handler { func loggingMiddlewarePrivate(next http.Handler) http.Handler {
@ -398,27 +402,32 @@ func loggingMiddlewarePrivate(next http.Handler) http.Handler {
}) })
} }
// TODO(remove): Implemented at pkg/http/middleware/logging.go
type loggingResponseWriter struct { type loggingResponseWriter struct {
http.ResponseWriter http.ResponseWriter
statusCode int statusCode int
} }
// TODO(remove): Implemented at pkg/http/middleware/logging.go
func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter { func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter {
// WriteHeader(int) is not called if our response implicitly returns 200 OK, so // WriteHeader(int) is not called if our response implicitly returns 200 OK, so
// we default to that status code. // we default to that status code.
return &loggingResponseWriter{w, http.StatusOK} return &loggingResponseWriter{w, http.StatusOK}
} }
// TODO(remove): Implemented at pkg/http/middleware/logging.go
func (lrw *loggingResponseWriter) WriteHeader(code int) { func (lrw *loggingResponseWriter) WriteHeader(code int) {
lrw.statusCode = code lrw.statusCode = code
lrw.ResponseWriter.WriteHeader(code) lrw.ResponseWriter.WriteHeader(code)
} }
// TODO(remove): Implemented at pkg/http/middleware/logging.go
// Flush implements the http.Flush interface. // Flush implements the http.Flush interface.
func (lrw *loggingResponseWriter) Flush() { func (lrw *loggingResponseWriter) Flush() {
lrw.ResponseWriter.(http.Flusher).Flush() lrw.ResponseWriter.(http.Flusher).Flush()
} }
// TODO(remove): Implemented at pkg/http/middleware/logging.go
// Support websockets // Support websockets
func (lrw *loggingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { func (lrw *loggingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
h, ok := lrw.ResponseWriter.(http.Hijacker) h, ok := lrw.ResponseWriter.(http.Hijacker)
@ -564,6 +573,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 { func setTimeoutMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()

View File

@ -12,6 +12,7 @@ const DisableUpsell = "DISABLE_UPSELL"
const Onboarding = "ONBOARDING" const Onboarding = "ONBOARDING"
const ChatSupport = "CHAT_SUPPORT" const ChatSupport = "CHAT_SUPPORT"
const Gateway = "GATEWAY" const Gateway = "GATEWAY"
const PremiumSupport = "PREMIUM_SUPPORT"
var BasicPlan = basemodel.FeatureSet{ var BasicPlan = basemodel.FeatureSet{
basemodel.Feature{ basemodel.Feature{
@ -119,6 +120,13 @@ var BasicPlan = basemodel.FeatureSet{
UsageLimit: -1, UsageLimit: -1,
Route: "", Route: "",
}, },
basemodel.Feature{
Name: PremiumSupport,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
} }
var ProPlan = basemodel.FeatureSet{ var ProPlan = basemodel.FeatureSet{
@ -220,6 +228,13 @@ var ProPlan = basemodel.FeatureSet{
UsageLimit: -1, UsageLimit: -1,
Route: "", Route: "",
}, },
basemodel.Feature{
Name: PremiumSupport,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
} }
var EnterprisePlan = basemodel.FeatureSet{ var EnterprisePlan = basemodel.FeatureSet{
@ -335,4 +350,11 @@ var EnterprisePlan = basemodel.FeatureSet{
UsageLimit: -1, UsageLimit: -1,
Route: "", 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", "create_dashboard": "Create Dashboard",
"import_json": "Import Dashboard JSON", "import_json": "Import Dashboard JSON",
"view_template": "View templates",
"import_grafana_json": "Import Grafana JSON", "import_grafana_json": "Import Grafana JSON",
"copy_to_clipboard": "Copy To ClipBoard", "copy_to_clipboard": "Copy To ClipBoard",
"download_json": "Download JSON", "download_json": "Download JSON",

View File

@ -49,5 +49,6 @@
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views", "TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
"DEFAULT": "Open source Observability Platform | SigNoz", "DEFAULT": "Open source Observability Platform | SigNoz",
"SHORTCUTS": "SigNoz | Shortcuts", "SHORTCUTS": "SigNoz | Shortcuts",
"INTEGRATIONS": "SigNoz | Integrations" "INTEGRATIONS": "SigNoz | Integrations",
"MESSAGING_QUEUES": "SigNoz | Messaging Queues"
} }

View File

@ -204,3 +204,15 @@ export const InstalledIntegrations = Loadable(
/* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage' /* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage'
), ),
); );
export const MessagingQueues = Loadable(
() =>
import(/* webpackChunkName: "MessagingQueues" */ 'pages/MessagingQueues'),
);
export const MQDetailPage = Loadable(
() =>
import(
/* webpackChunkName: "MQDetailPage" */ 'pages/MessagingQueues/MQDetailPage'
),
);

View File

@ -23,6 +23,8 @@ import {
LogsExplorer, LogsExplorer,
LogsIndexToFields, LogsIndexToFields,
LogsSaveViews, LogsSaveViews,
MessagingQueues,
MQDetailPage,
MySettings, MySettings,
NewDashboardPage, NewDashboardPage,
OldLogsExplorer, OldLogsExplorer,
@ -351,6 +353,20 @@ const routes: AppRoutes[] = [
isPrivate: true, isPrivate: true,
key: 'INTEGRATIONS', key: 'INTEGRATIONS',
}, },
{
path: ROUTES.MESSAGING_QUEUES,
exact: true,
component: MessagingQueues,
key: 'MESSAGING_QUEUES',
isPrivate: true,
},
{
path: ROUTES.MESSAGING_QUEUES_DETAIL,
exact: true,
component: MQDetailPage,
key: 'MESSAGING_QUEUES_DETAIL',
isPrivate: true,
},
]; ];
export const SUPPORT_ROUTE: AppRoutes = { export const SUPPORT_ROUTE: AppRoutes = {

View File

@ -0,0 +1,62 @@
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);
}
});
socket.addEventListener('error', (event) => {
console.error(event);
});
socket.addEventListener('close', (event) => {
// 1000 is a normal closure status code
if (event.code !== 1000) {
console.error('WebSocket closed with error:', event);
} else {
console.error('WebSocket closed normally.');
}
});
}

View File

@ -1,6 +1,8 @@
import { ApiV2Instance as axios } from 'api'; import { ApiV2Instance as axios } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
import store from 'store';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
import { import {
Props, Props,
@ -11,7 +13,26 @@ const dashboardVariablesQuery = async (
props: Props, props: Props,
): Promise<SuccessResponse<VariableResponseProps> | ErrorResponse> => { ): Promise<SuccessResponse<VariableResponseProps> | ErrorResponse> => {
try { 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 { return {
statusCode: 200, statusCode: 200,

View File

@ -12,10 +12,13 @@ export const getMetricsQueryRange = async (
props: QueryRangePayload, props: QueryRangePayload,
version: string, version: string,
signal: AbortSignal, signal: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => { ): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => {
try { try {
if (version && version === ENTITY_VERSION_V4) { 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 { return {
statusCode: 200, 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 { return {
statusCode: 200, 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 { CreditCard, X } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useMutation } from 'react-query'; import { useMutation } from 'react-query';
import { useLocation } from 'react-router-dom';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout'; import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { License } from 'types/api/licenses/def'; import { License } from 'types/api/licenses/def';
@ -57,11 +58,11 @@ export default function ChatSupportGateway(): JSX.Element {
onError: handleBillingOnError, onError: handleBillingOnError,
}, },
); );
const { pathname } = useLocation();
const handleAddCreditCard = (): void => { const handleAddCreditCard = (): void => {
logEvent('Add Credit card modal: Clicked', { logEvent('Add Credit card modal: Clicked', {
source: `intercom icon`, source: `intercom icon`,
page: '', page: pathname,
}); });
updateCreditCard({ updateCreditCard({
@ -79,7 +80,7 @@ export default function ChatSupportGateway(): JSX.Element {
onClick={(): void => { onClick={(): void => {
logEvent('Disabled Chat Support: Clicked', { logEvent('Disabled Chat Support: Clicked', {
source: `intercom icon`, source: `intercom icon`,
page: '', page: pathname,
}); });
setIsAddCreditCardModalOpen(true); setIsAddCreditCardModalOpen(true);

View File

@ -13,6 +13,7 @@ import { defaultTo } from 'lodash-es';
import { CreditCard, HelpCircle, X } from 'lucide-react'; import { CreditCard, HelpCircle, X } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useMutation } from 'react-query'; import { useMutation } from 'react-query';
import { useLocation } from 'react-router-dom';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout'; import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { License } from 'types/api/licenses/def'; import { License } from 'types/api/licenses/def';
@ -47,6 +48,7 @@ function LaunchChatSupport({
false, false,
); );
const { pathname } = useLocation();
const isPremiumChatSupportEnabled = const isPremiumChatSupportEnabled =
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false; useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
@ -65,6 +67,11 @@ function LaunchChatSupport({
const handleFacingIssuesClick = (): void => { const handleFacingIssuesClick = (): void => {
if (showAddCreditCardModal) { if (showAddCreditCardModal) {
logEvent('Disabled Chat Support: Clicked', {
source: `facing issues button`,
page: pathname,
...attributes,
});
setIsAddCreditCardModalOpen(true); setIsAddCreditCardModalOpen(true);
} else { } else {
logEvent(eventName, attributes); logEvent(eventName, attributes);
@ -105,7 +112,7 @@ function LaunchChatSupport({
const handleAddCreditCard = (): void => { const handleAddCreditCard = (): void => {
logEvent('Add Credit card modal: Clicked', { logEvent('Add Credit card modal: Clicked', {
source: `facing issues button`, source: `facing issues button`,
page: '', page: pathname,
...attributes, ...attributes,
}); });

View File

@ -41,6 +41,21 @@ I need help with managing alerts.
Thanks`; 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 = ( export const alertHelpMessage = (
alertDef: AlertDef, alertDef: AlertDef,
ruleId: number, ruleId: number,

View File

@ -3,12 +3,18 @@ import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { ActionItemProps } from 'container/LogDetailedView/ActionItem'; import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
import { IField } from 'types/api/logs/fields'; import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log'; import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { VIEWS } from './constants'; import { VIEWS } from './constants';
export type LogDetailProps = { export type LogDetailProps = {
log: ILog | null; log: ILog | null;
selectedTab: VIEWS; selectedTab: VIEWS;
onGroupByAttribute?: (
fieldKey: string,
isJSON?: boolean,
dataType?: DataTypes,
) => Promise<void>;
isListViewPanel?: boolean; isListViewPanel?: boolean;
listViewPanelSelectedFields?: IField[] | null; listViewPanelSelectedFields?: IField[] | null;
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> & } & Pick<AddToQueryHOCProps, 'onAddToQuery'> &

View File

@ -2,6 +2,7 @@
import './LogDetails.styles.scss'; import './LogDetails.styles.scss';
import { Color, Spacing } from '@signozhq/design-tokens'; import { Color, Spacing } from '@signozhq/design-tokens';
import Convert from 'ansi-to-html';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd'; import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import { RadioChangeEvent } from 'antd/lib'; import { RadioChangeEvent } from 'antd/lib';
import cx from 'classnames'; import cx from 'classnames';
@ -10,8 +11,13 @@ import { LOCALSTORAGE } from 'constants/localStorage';
import ContextView from 'container/LogDetailedView/ContextView/ContextView'; import ContextView from 'container/LogDetailedView/ContextView/ContextView';
import JSONView from 'container/LogDetailedView/JsonView'; import JSONView from 'container/LogDetailedView/JsonView';
import Overview from 'container/LogDetailedView/Overview'; import Overview from 'container/LogDetailedView/Overview';
import { aggregateAttributesResourcesToString } from 'container/LogDetailedView/utils'; import {
aggregateAttributesResourcesToString,
removeEscapeCharacters,
unescapeString,
} from 'container/LogDetailedView/utils';
import { useOptionsMenu } from 'container/OptionsMenu'; import { useOptionsMenu } from 'container/OptionsMenu';
import dompurify from 'dompurify';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
@ -28,15 +34,19 @@ import { useMemo, useState } from 'react';
import { useCopyToClipboard } from 'react-use'; import { useCopyToClipboard } from 'react-use';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData'; import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder'; import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
import { VIEW_TYPES, VIEWS } from './constants'; import { VIEW_TYPES, VIEWS } from './constants';
import { LogDetailProps } from './LogDetail.interfaces'; import { LogDetailProps } from './LogDetail.interfaces';
import QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper'; import QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper';
const convert = new Convert();
function LogDetail({ function LogDetail({
log, log,
onClose, onClose,
onAddToQuery, onAddToQuery,
onGroupByAttribute,
onClickActionItem, onClickActionItem,
selectedTab, selectedTab,
isListViewPanel = false, isListViewPanel = false,
@ -89,6 +99,17 @@ function LogDetail({
} }
}; };
const htmlBody = useMemo(
() => ({
__html: convert.toHtml(
dompurify.sanitize(unescapeString(log?.body || ''), {
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
}),
),
}),
[log?.body],
);
const handleJSONCopy = (): void => { const handleJSONCopy = (): void => {
copyToClipboard(LogJsonData); copyToClipboard(LogJsonData);
notifications.success({ notifications.success({
@ -126,8 +147,8 @@ function LogDetail({
> >
<div className="log-detail-drawer__log"> <div className="log-detail-drawer__log">
<Divider type="vertical" className={cx('log-type-indicator', logType)} /> <Divider type="vertical" className={cx('log-type-indicator', logType)} />
<Tooltip title={log?.body} placement="left"> <Tooltip title={removeEscapeCharacters(log?.body)} placement="left">
<Typography.Text className="log-body">{log?.body}</Typography.Text> <div className="log-body" dangerouslySetInnerHTML={htmlBody} />
</Tooltip> </Tooltip>
<div className="log-overflow-shadow">&nbsp;</div> <div className="log-overflow-shadow">&nbsp;</div>
@ -209,6 +230,7 @@ function LogDetail({
logData={log} logData={log}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
onClickActionItem={onClickActionItem} onClickActionItem={onClickActionItem}
onGroupByAttribute={onGroupByAttribute}
isListViewPanel={isListViewPanel} isListViewPanel={isListViewPanel}
selectedOptions={options} selectedOptions={options}
listViewPanelSelectedFields={listViewPanelSelectedFields} listViewPanelSelectedFields={listViewPanelSelectedFields}

View File

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

View File

@ -1,13 +1,16 @@
import './AddToQueryHOC.styles.scss'; import './AddToQueryHOC.styles.scss';
import { Popover } from 'antd'; import { Popover } from 'antd';
import cx from 'classnames';
import { OPERATORS } from 'constants/queryBuilder'; import { OPERATORS } from 'constants/queryBuilder';
import { FontSize } from 'container/OptionsMenu/types';
import { memo, MouseEvent, ReactNode, useMemo } from 'react'; import { memo, MouseEvent, ReactNode, useMemo } from 'react';
function AddToQueryHOC({ function AddToQueryHOC({
fieldKey, fieldKey,
fieldValue, fieldValue,
onAddToQuery, onAddToQuery,
fontSize,
children, children,
}: AddToQueryHOCProps): JSX.Element { }: AddToQueryHOCProps): JSX.Element {
const handleQueryAdd = (event: MouseEvent<HTMLDivElement>): void => { const handleQueryAdd = (event: MouseEvent<HTMLDivElement>): void => {
@ -21,7 +24,7 @@ function AddToQueryHOC({
return ( return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions // 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}> <Popover placement="top" content={popOverContent}>
{children} {children}
</Popover> </Popover>
@ -33,6 +36,7 @@ export interface AddToQueryHOCProps {
fieldKey: string; fieldKey: string;
fieldValue: string; fieldValue: string;
onAddToQuery: (fieldKey: string, fieldValue: string, operator: string) => void; onAddToQuery: (fieldKey: string, fieldValue: string, operator: string) => void;
fontSize: FontSize;
children: ReactNode; children: ReactNode;
} }

View File

@ -4,6 +4,7 @@ import { ReactNode, useCallback, useEffect } from 'react';
import { useCopyToClipboard } from 'react-use'; import { useCopyToClipboard } from 'react-use';
function CopyClipboardHOC({ function CopyClipboardHOC({
entityKey,
textToCopy, textToCopy,
children, children,
}: CopyClipboardHOCProps): JSX.Element { }: CopyClipboardHOCProps): JSX.Element {
@ -11,11 +12,15 @@ function CopyClipboardHOC({
const { notifications } = useNotifications(); const { notifications } = useNotifications();
useEffect(() => { useEffect(() => {
if (value.value) { if (value.value) {
const key = entityKey || '';
const notificationMessage = `${key} copied to clipboard`;
notifications.success({ notifications.success({
message: 'Copied to clipboard', message: notificationMessage,
}); });
} }
}, [value, notifications]); }, [value, notifications, entityKey]);
const onClick = useCallback((): void => { const onClick = useCallback((): void => {
setCopy(textToCopy); setCopy(textToCopy);
@ -34,6 +39,7 @@ function CopyClipboardHOC({
} }
interface CopyClipboardHOCProps { interface CopyClipboardHOCProps {
entityKey: string | undefined;
textToCopy: string; textToCopy: string;
children: ReactNode; children: ReactNode;
} }

View File

@ -6,6 +6,21 @@
font-weight: 400; font-weight: 400;
line-height: 18px; /* 128.571% */ line-height: 18px; /* 128.571% */
letter-spacing: -0.07px; 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 { .log-value {
color: var(--text-vanilla-400, #c0c1c3); color: var(--text-vanilla-400, #c0c1c3);
@ -14,6 +29,21 @@
font-weight: 400; font-weight: 400;
line-height: 18px; /* 128.571% */ line-height: 18px; /* 128.571% */
letter-spacing: -0.07px; 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 { .log-line {
display: flex; display: flex;
@ -40,6 +70,20 @@
font-weight: 400; font-weight: 400;
line-height: 18px; /* 128.571% */ line-height: 18px; /* 128.571% */
letter-spacing: -0.07px; 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 { .selected-log-value {
@ -52,12 +96,37 @@
line-height: 18px; line-height: 18px;
letter-spacing: -0.07px; letter-spacing: -0.07px;
font-size: 14px; 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 { .selected-log-kv {
min-height: 24px; min-height: 24px;
display: flex; display: flex;
align-items: center; align-items: center;
&.small {
min-height: 16px;
}
&.medium {
min-height: 20px;
}
&.large {
min-height: 24px;
}
} }
} }

View File

@ -3,8 +3,11 @@ import './ListLogView.styles.scss';
import { blue } from '@ant-design/colors'; import { blue } from '@ant-design/colors';
import Convert from 'ansi-to-html'; import Convert from 'ansi-to-html';
import { Typography } from 'antd'; import { Typography } from 'antd';
import cx from 'classnames';
import LogDetail from 'components/LogDetail'; import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants'; import { VIEW_TYPES } from 'components/LogDetail/constants';
import { unescapeString } from 'container/LogDetailedView/utils';
import { FontSize } from 'container/OptionsMenu/types';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import dompurify from 'dompurify'; import dompurify from 'dompurify';
import { useActiveLog } from 'hooks/logs/useActiveLog'; import { useActiveLog } from 'hooks/logs/useActiveLog';
@ -39,6 +42,7 @@ interface LogFieldProps {
fieldKey: string; fieldKey: string;
fieldValue: string; fieldValue: string;
linesPerRow?: number; linesPerRow?: number;
fontSize: FontSize;
} }
type LogSelectedFieldProps = Omit<LogFieldProps, 'linesPerRow'> & type LogSelectedFieldProps = Omit<LogFieldProps, 'linesPerRow'> &
@ -48,11 +52,12 @@ function LogGeneralField({
fieldKey, fieldKey,
fieldValue, fieldValue,
linesPerRow = 1, linesPerRow = 1,
fontSize,
}: LogFieldProps): JSX.Element { }: LogFieldProps): JSX.Element {
const html = useMemo( const html = useMemo(
() => ({ () => ({
__html: convert.toHtml( __html: convert.toHtml(
dompurify.sanitize(fieldValue, { dompurify.sanitize(unescapeString(fieldValue), {
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS], FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
}), }),
), ),
@ -62,12 +67,12 @@ function LogGeneralField({
return ( return (
<TextContainer> <TextContainer>
<Text ellipsis type="secondary" className="log-field-key"> <Text ellipsis type="secondary" className={cx('log-field-key', fontSize)}>
{`${fieldKey} : `} {`${fieldKey} : `}
</Text> </Text>
<LogText <LogText
dangerouslySetInnerHTML={html} dangerouslySetInnerHTML={html}
className="log-value" className={cx('log-value', fontSize)}
linesPerRow={linesPerRow > 1 ? linesPerRow : undefined} linesPerRow={linesPerRow > 1 ? linesPerRow : undefined}
/> />
</TextContainer> </TextContainer>
@ -78,6 +83,7 @@ function LogSelectedField({
fieldKey = '', fieldKey = '',
fieldValue = '', fieldValue = '',
onAddToQuery, onAddToQuery,
fontSize,
}: LogSelectedFieldProps): JSX.Element { }: LogSelectedFieldProps): JSX.Element {
return ( return (
<div className="log-selected-fields"> <div className="log-selected-fields">
@ -85,16 +91,22 @@ function LogSelectedField({
fieldKey={fieldKey} fieldKey={fieldKey}
fieldValue={fieldValue} fieldValue={fieldValue}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
fontSize={fontSize}
> >
<Typography.Text> <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} {fieldKey}
</span> </span>
</Typography.Text> </Typography.Text>
</AddToQueryHOC> </AddToQueryHOC>
<Typography.Text ellipsis className="selected-log-kv"> <Typography.Text ellipsis className={cx('selected-log-kv', fontSize)}>
<span className="selected-log-field-key">{': '}</span> <span className={cx('selected-log-field-key', fontSize)}>{': '}</span>
<span className="selected-log-value">{fieldValue || "''"}</span> <span className={cx('selected-log-value', fontSize)}>
{fieldValue || "''"}
</span>
</Typography.Text> </Typography.Text>
</div> </div>
); );
@ -107,6 +119,7 @@ type ListLogViewProps = {
onAddToQuery: AddToQueryHOCProps['onAddToQuery']; onAddToQuery: AddToQueryHOCProps['onAddToQuery'];
activeLog?: ILog | null; activeLog?: ILog | null;
linesPerRow: number; linesPerRow: number;
fontSize: FontSize;
}; };
function ListLogView({ function ListLogView({
@ -116,6 +129,7 @@ function ListLogView({
onAddToQuery, onAddToQuery,
activeLog, activeLog,
linesPerRow, linesPerRow,
fontSize,
}: ListLogViewProps): JSX.Element { }: ListLogViewProps): JSX.Element {
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]); const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
@ -128,6 +142,7 @@ function ListLogView({
onAddToQuery: handleAddToQuery, onAddToQuery: handleAddToQuery,
onSetActiveLog: handleSetActiveContextLog, onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog, onClearActiveLog: handleClearActiveContextLog,
onGroupByAttribute,
} = useActiveLog(); } = useActiveLog();
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
@ -185,6 +200,7 @@ function ListLogView({
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
onClick={handleDetailedView} onClick={handleDetailedView}
fontSize={fontSize}
> >
<div className="log-line"> <div className="log-line">
<LogStateIndicator <LogStateIndicator
@ -192,18 +208,28 @@ function ListLogView({
isActive={ isActive={
activeLog?.id === logData.id || activeContextLog?.id === logData.id activeLog?.id === logData.id || activeContextLog?.id === logData.id
} }
fontSize={fontSize}
/> />
<div> <div>
<LogContainer> <LogContainer fontSize={fontSize}>
<LogGeneralField <LogGeneralField
fieldKey="Log" fieldKey="Log"
fieldValue={flattenLogData.body} fieldValue={flattenLogData.body}
linesPerRow={linesPerRow} linesPerRow={linesPerRow}
fontSize={fontSize}
/> />
{flattenLogData.stream && ( {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) => {updatedSelecedFields.map((field) =>
isValidLogField(flattenLogData[field.name] as never) ? ( isValidLogField(flattenLogData[field.name] as never) ? (
@ -212,6 +238,7 @@ function ListLogView({
fieldKey={field.name} fieldKey={field.name}
fieldValue={flattenLogData[field.name] as never} fieldValue={flattenLogData[field.name] as never}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
fontSize={fontSize}
/> />
) : null, ) : null,
)} )}
@ -232,6 +259,7 @@ function ListLogView({
onAddToQuery={handleAddToQuery} onAddToQuery={handleAddToQuery}
selectedTab={VIEW_TYPES.CONTEXT} selectedTab={VIEW_TYPES.CONTEXT}
onClose={handlerClearActiveContextLog} onClose={handlerClearActiveContextLog}
onGroupByAttribute={onGroupByAttribute}
/> />
)} )}
</> </>

View File

@ -1,21 +1,46 @@
/* eslint-disable no-nested-ternary */
import { Color } from '@signozhq/design-tokens'; import { Color } from '@signozhq/design-tokens';
import { Card, Typography } from 'antd'; import { Card, Typography } from 'antd';
import { FontSize } from 'container/OptionsMenu/types';
import styled from 'styled-components'; import styled from 'styled-components';
interface LogTextProps { interface LogTextProps {
linesPerRow?: number; linesPerRow?: number;
} }
interface LogContainerProps {
fontSize: FontSize;
}
export const Container = styled(Card)<{ export const Container = styled(Card)<{
$isActiveLog: boolean; $isActiveLog: boolean;
$isDarkMode: boolean; $isDarkMode: boolean;
fontSize: FontSize;
}>` }>`
width: 100% !important; width: 100% !important;
margin-bottom: 0.3rem; 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; cursor: pointer;
.ant-card-body { .ant-card-body {
padding: 0.3rem 0.6rem; 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, $isDarkMode }): string =>
$isActiveLog $isActiveLog
? `background-color: ${ ? `background-color: ${
@ -38,11 +63,17 @@ export const TextContainer = styled.div`
width: 100%; width: 100%;
`; `;
export const LogContainer = styled.div` export const LogContainer = styled.div<LogContainerProps>`
margin-left: 0.5rem; margin-left: 0.5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
${({ fontSize }): string =>
fontSize === FontSize.SMALL
? `gap: 2px;`
: fontSize === FontSize.MEDIUM
? ` gap:4px;`
: `gap:6px;`}
`; `;
export const LogText = styled.div<LogTextProps>` export const LogText = styled.div<LogTextProps>`

View File

@ -9,11 +9,24 @@
border-radius: 50px; border-radius: 50px;
background-color: transparent; background-color: transparent;
&.small {
min-height: 16px;
}
&.medium {
min-height: 20px;
}
&.large {
min-height: 24px;
}
&.INFO { &.INFO {
background-color: var(--bg-slate-400); background-color: var(--bg-slate-400);
} }
&.WARNING, &.WARN { &.WARNING,
&.WARN {
background-color: var(--bg-amber-500); background-color: var(--bg-amber-500);
} }

View File

@ -1,10 +1,13 @@
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import { FontSize } from 'container/OptionsMenu/types';
import LogStateIndicator from './LogStateIndicator'; import LogStateIndicator from './LogStateIndicator';
describe('LogStateIndicator', () => { describe('LogStateIndicator', () => {
it('renders correctly with default props', () => { 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; const indicator = container.firstChild as HTMLElement;
expect(indicator.classList.contains('log-state-indicator')).toBe(true); expect(indicator.classList.contains('log-state-indicator')).toBe(true);
expect(indicator.classList.contains('isActive')).toBe(false); expect(indicator.classList.contains('isActive')).toBe(false);
@ -15,28 +18,30 @@ describe('LogStateIndicator', () => {
}); });
it('renders correctly when isActive is true', () => { 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; const indicator = container.firstChild as HTMLElement;
expect(indicator.classList.contains('isActive')).toBe(true); expect(indicator.classList.contains('isActive')).toBe(true);
}); });
it('renders correctly with different types', () => { it('renders correctly with different types', () => {
const { container: containerInfo } = render( const { container: containerInfo } = render(
<LogStateIndicator type="INFO" />, <LogStateIndicator type="INFO" fontSize={FontSize.MEDIUM} />,
); );
expect(containerInfo.querySelector('.line')?.classList.contains('INFO')).toBe( expect(containerInfo.querySelector('.line')?.classList.contains('INFO')).toBe(
true, true,
); );
const { container: containerWarning } = render( const { container: containerWarning } = render(
<LogStateIndicator type="WARNING" />, <LogStateIndicator type="WARNING" fontSize={FontSize.MEDIUM} />,
); );
expect( expect(
containerWarning.querySelector('.line')?.classList.contains('WARNING'), containerWarning.querySelector('.line')?.classList.contains('WARNING'),
).toBe(true); ).toBe(true);
const { container: containerError } = render( const { container: containerError } = render(
<LogStateIndicator type="ERROR" />, <LogStateIndicator type="ERROR" fontSize={FontSize.MEDIUM} />,
); );
expect( expect(
containerError.querySelector('.line')?.classList.contains('ERROR'), containerError.querySelector('.line')?.classList.contains('ERROR'),

View File

@ -1,6 +1,7 @@
import './LogStateIndicator.styles.scss'; import './LogStateIndicator.styles.scss';
import cx from 'classnames'; import cx from 'classnames';
import { FontSize } from 'container/OptionsMenu/types';
export const SEVERITY_TEXT_TYPE = { export const SEVERITY_TEXT_TYPE = {
TRACE: 'TRACE', TRACE: 'TRACE',
@ -44,13 +45,15 @@ export const LogType = {
function LogStateIndicator({ function LogStateIndicator({
type, type,
isActive, isActive,
fontSize,
}: { }: {
type: string; type: string;
fontSize: FontSize;
isActive?: boolean; isActive?: boolean;
}): JSX.Element { }): JSX.Element {
return ( return (
<div className={cx('log-state-indicator', isActive ? 'isActive' : '')}> <div className={cx('log-state-indicator', isActive ? 'isActive' : '')}>
<div className={cx('line', type)}> </div> <div className={cx('line', type, fontSize)}> </div>
</div> </div>
); );
} }

View File

@ -4,6 +4,7 @@ import Convert from 'ansi-to-html';
import { DrawerProps } from 'antd'; import { DrawerProps } from 'antd';
import LogDetail from 'components/LogDetail'; import LogDetail from 'components/LogDetail';
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants'; import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
import { unescapeString } from 'container/LogDetailedView/utils';
import LogsExplorerContext from 'container/LogsExplorerContext'; import LogsExplorerContext from 'container/LogsExplorerContext';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import dompurify from 'dompurify'; import dompurify from 'dompurify';
@ -39,6 +40,7 @@ function RawLogView({
linesPerRow, linesPerRow,
isTextOverflowEllipsisDisabled, isTextOverflowEllipsisDisabled,
selectedFields = [], selectedFields = [],
fontSize,
}: RawLogViewProps): JSX.Element { }: RawLogViewProps): JSX.Element {
const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink( const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink(
data.id, data.id,
@ -54,6 +56,7 @@ function RawLogView({
onSetActiveLog, onSetActiveLog,
onClearActiveLog, onClearActiveLog,
onAddToQuery, onAddToQuery,
onGroupByAttribute,
} = useActiveLog(); } = useActiveLog();
const [hasActionButtons, setHasActionButtons] = useState<boolean>(false); const [hasActionButtons, setHasActionButtons] = useState<boolean>(false);
@ -143,7 +146,9 @@ function RawLogView({
const html = useMemo( const html = useMemo(
() => ({ () => ({
__html: convert.toHtml( __html: convert.toHtml(
dompurify.sanitize(text, { FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS] }), dompurify.sanitize(unescapeString(text), {
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
}),
), ),
}), }),
[text], [text],
@ -160,6 +165,7 @@ function RawLogView({
$isActiveLog={isActiveLog} $isActiveLog={isActiveLog}
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
fontSize={fontSize}
> >
<LogStateIndicator <LogStateIndicator
type={logType} type={logType}
@ -168,6 +174,7 @@ function RawLogView({
activeContextLog?.id === data.id || activeContextLog?.id === data.id ||
isActiveLog isActiveLog
} }
fontSize={fontSize}
/> />
<RawLogContent <RawLogContent
@ -176,6 +183,7 @@ function RawLogView({
$isDarkMode={isDarkMode} $isDarkMode={isDarkMode}
$isTextOverflowEllipsisDisabled={isTextOverflowEllipsisDisabled} $isTextOverflowEllipsisDisabled={isTextOverflowEllipsisDisabled}
linesPerRow={linesPerRow} linesPerRow={linesPerRow}
fontSize={fontSize}
dangerouslySetInnerHTML={html} dangerouslySetInnerHTML={html}
/> />
@ -199,6 +207,7 @@ function RawLogView({
onClose={handleCloseLogDetail} onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery} onClickActionItem={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
/> />
)} )}
</RawLogViewContainer> </RawLogViewContainer>

View File

@ -1,6 +1,8 @@
/* eslint-disable no-nested-ternary */
import { blue } from '@ant-design/colors'; import { blue } from '@ant-design/colors';
import { Color } from '@signozhq/design-tokens'; import { Color } from '@signozhq/design-tokens';
import { Col, Row, Space } from 'antd'; import { Col, Row, Space } from 'antd';
import { FontSize } from 'container/OptionsMenu/types';
import styled from 'styled-components'; import styled from 'styled-components';
import { getActiveLogBackground, getDefaultLogBackground } from 'utils/logs'; import { getActiveLogBackground, getDefaultLogBackground } from 'utils/logs';
@ -11,6 +13,7 @@ export const RawLogViewContainer = styled(Row)<{
$isReadOnly?: boolean; $isReadOnly?: boolean;
$isActiveLog?: boolean; $isActiveLog?: boolean;
$isHightlightedLog: boolean; $isHightlightedLog: boolean;
fontSize: FontSize;
}>` }>`
position: relative; position: relative;
width: 100%; width: 100%;
@ -22,6 +25,13 @@ export const RawLogViewContainer = styled(Row)<{
.log-state-indicator { .log-state-indicator {
margin: 4px 0; margin: 4px 0;
${({ fontSize }): string =>
fontSize === FontSize.SMALL
? `margin: 1px 0;`
: fontSize === FontSize.MEDIUM
? `margin: 1px 0;`
: `margin: 2px 0;`}
} }
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)} ${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
@ -50,8 +60,8 @@ export const RawLogContent = styled.div<RawLogContentProps>`
margin-bottom: 0; margin-bottom: 0;
font-family: 'SF Mono', monospace; font-family: 'SF Mono', monospace;
font-family: 'Geist Mono'; font-family: 'Geist Mono';
font-size: 13px; letter-spacing: -0.07px;
font-weight: 400; padding: 4px;
text-align: left; text-align: left;
color: ${({ $isDarkMode }): string => color: ${({ $isDarkMode }): string =>
$isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400}; $isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400};
@ -66,9 +76,15 @@ export const RawLogContent = styled.div<RawLogContentProps>`
line-clamp: ${linesPerRow}; line-clamp: ${linesPerRow};
-webkit-box-orient: vertical;`}; -webkit-box-orient: vertical;`};
font-size: 13px;
font-weight: 400;
line-height: 24px; line-height: 24px;
letter-spacing: -0.07px; ${({ fontSize }): string =>
padding: 4px; 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 => cursor: ${({ $isActiveLog, $isReadOnly }): string =>
$isActiveLog || $isReadOnly ? 'initial' : 'pointer'}; $isActiveLog || $isReadOnly ? 'initial' : 'pointer'};

View File

@ -1,3 +1,4 @@
import { FontSize } from 'container/OptionsMenu/types';
import { IField } from 'types/api/logs/fields'; import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log'; import { ILog } from 'types/api/logs/log';
@ -7,11 +8,13 @@ export interface RawLogViewProps {
isTextOverflowEllipsisDisabled?: boolean; isTextOverflowEllipsisDisabled?: boolean;
data: ILog; data: ILog;
linesPerRow: number; linesPerRow: number;
fontSize: FontSize;
selectedFields?: IField[]; selectedFields?: IField[];
} }
export interface RawLogContentProps { export interface RawLogContentProps {
linesPerRow: number; linesPerRow: number;
fontSize: FontSize;
$isReadOnly?: boolean; $isReadOnly?: boolean;
$isActiveLog?: boolean; $isActiveLog?: boolean;
$isDarkMode?: 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'; import styled from 'styled-components';
interface TableBodyContentProps { interface TableBodyContentProps {
linesPerRow: number; linesPerRow: number;
fontSize: FontSize;
isDarkMode?: boolean; isDarkMode?: boolean;
} }
@ -20,4 +23,10 @@ export const TableBodyContent = styled.div<TableBodyContentProps>`
-webkit-line-clamp: ${(props): number => props.linesPerRow}; -webkit-line-clamp: ${(props): number => props.linesPerRow};
line-clamp: ${(props): number => props.linesPerRow}; line-clamp: ${(props): number => props.linesPerRow};
-webkit-box-orient: vertical; -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 { ColumnsType, ColumnType } from 'antd/es/table';
import { FontSize } from 'container/OptionsMenu/types';
import { IField } from 'types/api/logs/fields'; import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log'; import { ILog } from 'types/api/logs/log';
@ -10,6 +11,7 @@ export type LogsTableViewProps = {
logs: ILog[]; logs: ILog[];
fields: IField[]; fields: IField[];
linesPerRow: number; linesPerRow: number;
fontSize: FontSize;
onClickExpand?: (log: ILog) => void; onClickExpand?: (log: ILog) => void;
}; };

View File

@ -5,6 +5,21 @@
font-weight: 400; font-weight: 400;
line-height: 18px; /* 128.571% */ line-height: 18px; /* 128.571% */
letter-spacing: -0.07px; 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 { .table-timestamp {
@ -25,3 +40,21 @@
color: var(--bg-slate-400); 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,8 @@ import './useTableView.styles.scss';
import Convert from 'ansi-to-html'; import Convert from 'ansi-to-html';
import { Typography } from 'antd'; import { Typography } from 'antd';
import { ColumnsType } from 'antd/es/table'; import { ColumnsType } from 'antd/es/table';
import cx from 'classnames';
import { unescapeString } from 'container/LogDetailedView/utils';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import dompurify from 'dompurify'; import dompurify from 'dompurify';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
@ -31,6 +33,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
logs, logs,
fields, fields,
linesPerRow, linesPerRow,
fontSize,
appendTo = 'center', appendTo = 'center',
activeContextLog, activeContextLog,
activeLog, activeLog,
@ -57,7 +60,10 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
: getDefaultCellStyle(isDarkMode), : getDefaultCellStyle(isDarkMode),
}, },
children: ( children: (
<Typography.Paragraph ellipsis={{ rows: linesPerRow }}> <Typography.Paragraph
ellipsis={{ rows: linesPerRow }}
className={cx('paragraph', fontSize)}
>
{field} {field}
</Typography.Paragraph> </Typography.Paragraph>
), ),
@ -87,8 +93,9 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
isActive={ isActive={
activeLog?.id === item.id || activeContextLog?.id === item.id activeLog?.id === item.id || activeContextLog?.id === item.id
} }
fontSize={fontSize}
/> />
<Typography.Paragraph ellipsis className="text"> <Typography.Paragraph ellipsis className={cx('text', fontSize)}>
{date} {date}
</Typography.Paragraph> </Typography.Paragraph>
</div> </div>
@ -109,11 +116,12 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
<TableBodyContent <TableBodyContent
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: convert.toHtml( __html: convert.toHtml(
dompurify.sanitize(field, { dompurify.sanitize(unescapeString(field), {
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS], FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
}), }),
), ),
}} }}
fontSize={fontSize}
linesPerRow={linesPerRow} linesPerRow={linesPerRow}
isDarkMode={isDarkMode} isDarkMode={isDarkMode}
/> />
@ -130,6 +138,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
linesPerRow, linesPerRow,
activeLog?.id, activeLog?.id,
activeContextLog?.id, activeContextLog?.id,
fontSize,
]); ]);
return { columns, dataSource: flattenLogData }; return { columns, dataSource: flattenLogData };

View File

@ -17,17 +17,126 @@
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2); box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px); 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 { .menu-container {
padding: 12px; padding: 12px;
.title { .title {
font-family: Inter; font-family: Inter;
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 500;
line-height: 18px; line-height: 18px;
letter-spacing: 0.08em; letter-spacing: 0.08em;
text-align: left; text-align: left;
color: #52575c; color: var(--bg-slate-50);
} }
.menu-items { .menu-items {
@ -65,11 +174,11 @@
padding: 12px; padding: 12px;
.title { .title {
color: #52575c; color: var(--bg-slate-50);
font-family: Inter; font-family: Inter;
font-size: 11px; font-size: 11px;
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 500;
line-height: 18px; /* 163.636% */ line-height: 18px; /* 163.636% */
letter-spacing: 0.88px; letter-spacing: 0.88px;
text-transform: uppercase; text-transform: uppercase;
@ -149,11 +258,11 @@
} }
.title { .title {
color: #52575c; color: var(--bg-slate-50);
font-family: Inter; font-family: Inter;
font-size: 11px; font-size: 11px;
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 500;
line-height: 18px; /* 163.636% */ line-height: 18px; /* 163.636% */
letter-spacing: 0.88px; letter-spacing: 0.88px;
text-transform: uppercase; text-transform: uppercase;
@ -299,6 +408,38 @@
box-shadow: 4px 10px 16px 2px rgba(255, 255, 255, 0.2); 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 { .horizontal-line {
background: var(--bg-vanilla-300); background: var(--bg-vanilla-300);
} }

View File

@ -3,12 +3,12 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */ /* eslint-disable jsx-a11y/click-events-have-key-events */
import './LogsFormatOptionsMenu.styles.scss'; 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 cx from 'classnames';
import { LogViewMode } from 'container/LogsTable'; import { LogViewMode } from 'container/LogsTable';
import { OptionsMenuConfig } from 'container/OptionsMenu/types'; import { FontSize, OptionsMenuConfig } from 'container/OptionsMenu/types';
import useDebouncedFn from 'hooks/useDebouncedFunction'; 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'; import { useCallback, useEffect, useState } from 'react';
interface LogsFormatOptionsMenuProps { interface LogsFormatOptionsMenuProps {
@ -24,10 +24,16 @@ export default function LogsFormatOptionsMenu({
selectedOptionFormat, selectedOptionFormat,
config, config,
}: LogsFormatOptionsMenuProps): JSX.Element { }: LogsFormatOptionsMenuProps): JSX.Element {
const { maxLines, format, addColumn } = config; const { maxLines, format, addColumn, fontSize } = config;
const [selectedItem, setSelectedItem] = useState(selectedOptionFormat); const [selectedItem, setSelectedItem] = useState(selectedOptionFormat);
const maxLinesNumber = (maxLines?.value as number) || 1; const maxLinesNumber = (maxLines?.value as number) || 1;
const [maxLinesPerRow, setMaxLinesPerRow] = useState<number>(maxLinesNumber); 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); const [addNewColumn, setAddNewColumn] = useState(false);
@ -88,6 +94,12 @@ export default function LogsFormatOptionsMenu({
} }
}, [maxLinesPerRow]); }, [maxLinesPerRow]);
useEffect(() => {
if (fontSizeValue && config && config.fontSize?.onChange) {
config.fontSize.onChange(fontSizeValue);
}
}, [fontSizeValue]);
return ( return (
<div <div
className={cx('nested-menu-container', addNewColumn ? 'active' : '')} className={cx('nested-menu-container', addNewColumn ? 'active' : '')}
@ -96,6 +108,72 @@ export default function LogsFormatOptionsMenu({
event.stopPropagation(); event.stopPropagation();
}} }}
> >
{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 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="menu-container">
<div className="title"> {title} </div> <div className="title"> {title} </div>
@ -237,6 +315,8 @@ export default function LogsFormatOptionsMenu({
</div> </div>
</> </>
)} )}
</>
)}
</div> </div>
); );
} }

View File

@ -3,4 +3,5 @@ export const ENVIRONMENT = {
process?.env?.FRONTEND_API_ENDPOINT || process?.env?.FRONTEND_API_ENDPOINT ||
process?.env?.GITPOD_WORKSPACE_URL?.replace('://', '://8080-') || process?.env?.GITPOD_WORKSPACE_URL?.replace('://', '://8080-') ||
'', '',
wsURL: process?.env?.WEBSOCKET_API_ENDPOINT || '',
}; };

View File

@ -32,4 +32,8 @@ export enum QueryParams {
relativeTime = 'relativeTime', relativeTime = 'relativeTime',
alertType = 'alertType', alertType = 'alertType',
ruleId = 'ruleId', ruleId = 'ruleId',
consumerGrp = 'consumerGrp',
topic = 'topic',
partition = 'partition',
selectedTimelineQuery = 'selectedTimelineQuery',
} }

View File

@ -52,7 +52,7 @@ export const selectValueDivider = '__';
export const baseAutoCompleteIdKeysOrder: (keyof Omit< export const baseAutoCompleteIdKeysOrder: (keyof Omit<
BaseAutocompleteData, BaseAutocompleteData,
'id' | 'isJSON' 'id' | 'isJSON' | 'isIndexed'
>)[] = ['key', 'dataType', 'type', 'isColumn']; >)[] = ['key', 'dataType', 'type', 'isColumn'];
export const autocompleteType: Record<AutocompleteType, AutocompleteType> = { export const autocompleteType: Record<AutocompleteType, AutocompleteType> = {
@ -71,6 +71,7 @@ export const alphabet: string[] = alpha.map((str) => String.fromCharCode(str));
export enum QueryBuilderKeys { export enum QueryBuilderKeys {
GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE', GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE',
GET_AGGREGATE_KEYS = 'GET_AGGREGATE_KEYS', GET_AGGREGATE_KEYS = 'GET_AGGREGATE_KEYS',
GET_ATTRIBUTE_SUGGESTIONS = 'GET_ATTRIBUTE_SUGGESTIONS',
} }
export const mapOfOperators = { export const mapOfOperators = {

View File

@ -8,4 +8,5 @@ export const REACT_QUERY_KEY = {
GET_FEATURES_FLAGS: 'GET_FEATURES_FLAGS', GET_FEATURES_FLAGS: 'GET_FEATURES_FLAGS',
DELETE_DASHBOARD: 'DELETE_DASHBOARD', DELETE_DASHBOARD: 'DELETE_DASHBOARD',
LOGS_PIPELINE_PREVIEW: 'LOGS_PIPELINE_PREVIEW', LOGS_PIPELINE_PREVIEW: 'LOGS_PIPELINE_PREVIEW',
GET_CONSUMER_LAG_DETAILS: 'GET_CONSUMER_LAG_DETAILS',
}; };

View File

@ -54,6 +54,8 @@ const ROUTES = {
WORKSPACE_LOCKED: '/workspace-locked', WORKSPACE_LOCKED: '/workspace-locked',
SHORTCUTS: '/shortcuts', SHORTCUTS: '/shortcuts',
INTEGRATIONS: '/integrations', INTEGRATIONS: '/integrations',
MESSAGING_QUEUES: '/messaging-queues',
MESSAGING_QUEUES_DETAIL: '/messaging-queues/detail',
} as const; } as const;
export default ROUTES; export default ROUTES;

View File

@ -9,6 +9,7 @@ export const GlobalShortcuts = {
NavigateToDashboards: 'd+shift', NavigateToDashboards: 'd+shift',
NavigateToAlerts: 'a+shift', NavigateToAlerts: 'a+shift',
NavigateToExceptions: 'e+shift', NavigateToExceptions: 'e+shift',
NavigateToMessagingQueues: 'm+shift',
}; };
export const GlobalShortcutsName = { export const GlobalShortcutsName = {
@ -19,6 +20,7 @@ export const GlobalShortcutsName = {
NavigateToDashboards: 'shift+d', NavigateToDashboards: 'shift+d',
NavigateToAlerts: 'shift+a', NavigateToAlerts: 'shift+a',
NavigateToExceptions: 'shift+e', NavigateToExceptions: 'shift+e',
NavigateToMessagingQueues: 'shift+m',
}; };
export const GlobalShortcutsDescription = { export const GlobalShortcutsDescription = {
@ -29,4 +31,5 @@ export const GlobalShortcutsDescription = {
NavigateToDashboards: 'Navigate to dashboards page', NavigateToDashboards: 'Navigate to dashboards page',
NavigateToAlerts: 'Navigate to alerts page', NavigateToAlerts: 'Navigate to alerts page',
NavigateToExceptions: 'Navigate to Exceptions page', NavigateToExceptions: 'Navigate to Exceptions page',
NavigateToMessagingQueues: 'Navigate to Messaging Queues page',
}; };

View File

@ -4,6 +4,7 @@ const userOS = getUserOperatingSystem();
export const LogsExplorerShortcuts = { export const LogsExplorerShortcuts = {
StageAndRunQuery: 'enter+meta', StageAndRunQuery: 'enter+meta',
FocusTheSearchBar: 's', FocusTheSearchBar: 's',
ShowAllFilters: '/+meta',
}; };
export const LogsExplorerShortcutsName = { export const LogsExplorerShortcutsName = {
@ -11,9 +12,11 @@ export const LogsExplorerShortcutsName = {
userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl' userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'
}+enter`, }+enter`,
FocusTheSearchBar: 's', FocusTheSearchBar: 's',
ShowAllFilters: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'}+/`,
}; };
export const LogsExplorerShortcutsDescription = { export const LogsExplorerShortcutsDescription = {
StageAndRunQuery: 'Stage and Run the current query', StageAndRunQuery: 'Stage and Run the current query',
FocusTheSearchBar: 'Shift the focus to the last query filter bar', FocusTheSearchBar: 'Shift the focus to the last query filter bar',
ShowAllFilters: 'Toggle all filters in the filters dropdown',
}; };

View File

@ -47,6 +47,7 @@ import {
UPDATE_LATEST_VERSION_ERROR, UPDATE_LATEST_VERSION_ERROR,
} from 'types/actions/app'; } from 'types/actions/app';
import AppReducer from 'types/reducer/app'; import AppReducer from 'types/reducer/app';
import { isCloudUser } from 'utils/app';
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils'; import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
import { ChildrenContainer, Layout, LayoutContent } from './styles'; import { ChildrenContainer, Layout, LayoutContent } from './styles';
@ -71,7 +72,14 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const isPremiumChatSupportEnabled = const isPremiumChatSupportEnabled =
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false; useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
const isChatSupportEnabled =
useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active || false;
const isCloudUserVal = isCloudUser();
const showAddCreditCardModal = const showAddCreditCardModal =
isChatSupportEnabled &&
isCloudUserVal &&
!isPremiumChatSupportEnabled && !isPremiumChatSupportEnabled &&
!licenseData?.payload?.trialConvertedToSubscription; !licenseData?.payload?.trialConvertedToSubscription;
@ -241,6 +249,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const isTracesView = (): boolean => const isTracesView = (): boolean =>
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS'; routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
const isMessagingQueues = (): boolean =>
routeKey === 'MESSAGING_QUEUES' || routeKey === 'MESSAGING_QUEUES_DETAIL';
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD'; const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
const isDashboardView = (): boolean => { const isDashboardView = (): boolean => {
/** /**
@ -329,7 +340,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
isTracesView() || isTracesView() ||
isDashboardView() || isDashboardView() ||
isDashboardWidgetView() || isDashboardWidgetView() ||
isDashboardListView() isDashboardListView() ||
isMessagingQueues()
? 0 ? 0
: '0 1rem', : '0 1rem',
}} }}

View File

@ -33,6 +33,7 @@ import useErrorNotification from 'hooks/useErrorNotification';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange'; import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery'; import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
import { cloneDeep } from 'lodash-es';
import { import {
Check, Check,
ConciergeBell, ConciergeBell,
@ -56,7 +57,7 @@ import { useHistory } from 'react-router-dom';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { Dashboard } from 'types/api/dashboard/getAll'; import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData'; 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 AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles'; import { USER_ROLES } from 'types/roles';
@ -120,6 +121,21 @@ function ExplorerOptions({
const { role } = useSelector<AppState, AppReducer>((state) => state.app); 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(() => { const onCreateAlertsHandler = useCallback(() => {
if (sourcepage === DataSource.TRACES) { if (sourcepage === DataSource.TRACES) {
logEvent('Traces Explorer: Create alert', { logEvent('Traces Explorer: Create alert', {
@ -130,13 +146,16 @@ function ExplorerOptions({
panelType, panelType,
}); });
} }
const stringifiedQuery = handleConditionalQueryModification();
history.push( history.push(
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent( `${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
JSON.stringify(query), stringifiedQuery,
)}`, )}`,
); );
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [history, query]); }, [handleConditionalQueryModification, history]);
const onCancel = (value: boolean) => (): void => { const onCancel = (value: boolean) => (): void => {
onModalToggle(value); onModalToggle(value);
@ -482,6 +501,7 @@ function ExplorerOptions({
shape="circle" shape="circle"
onClick={hideToolbar} onClick={hideToolbar}
icon={<PanelBottomClose size={16} />} icon={<PanelBottomClose size={16} />}
data-testid="hide-toolbar"
/> />
</Tooltip> </Tooltip>
</div> </div>
@ -511,6 +531,7 @@ function ExplorerOptions({
icon={<Check size={16} />} icon={<Check size={16} />}
onClick={onSaveHandler} onClick={onSaveHandler}
disabled={isSaveViewLoading} disabled={isSaveViewLoading}
data-testid="save-view-btn"
> >
Save this view Save this view
</Button>, </Button>,

View File

@ -65,6 +65,7 @@ function ExplorerOptionsHideArea({
// style={{ alignSelf: 'center', marginRight: 'calc(10% - 20px)' }} // style={{ alignSelf: 'center', marginRight: 'calc(10% - 20px)' }}
className="explorer-show-btn" className="explorer-show-btn"
onClick={handleShowExplorerOption} onClick={handleShowExplorerOption}
data-testid="show-explorer-option"
> >
<div className="menu-bar" /> <div className="menu-bar" />
</Button> </Button>

View File

@ -47,6 +47,7 @@ function WidgetGraphComponent({
setRequestData, setRequestData,
onClickHandler, onClickHandler,
onDragSelect, onDragSelect,
customTooltipElement,
}: WidgetGraphComponentProps): JSX.Element { }: WidgetGraphComponentProps): JSX.Element {
const [deleteModal, setDeleteModal] = useState(false); const [deleteModal, setDeleteModal] = useState(false);
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
@ -335,6 +336,7 @@ function WidgetGraphComponent({
onClickHandler={onClickHandler} onClickHandler={onClickHandler}
onDragSelect={onDragSelect} onDragSelect={onDragSelect}
tableProcessedDataRef={tableProcessedDataRef} tableProcessedDataRef={tableProcessedDataRef}
customTooltipElement={customTooltipElement}
/> />
</div> </div>
)} )}

View File

@ -33,6 +33,7 @@ function GridCardGraph({
version, version,
onClickHandler, onClickHandler,
onDragSelect, onDragSelect,
customTooltipElement,
}: GridCardGraphProps): JSX.Element { }: GridCardGraphProps): JSX.Element {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [errorMessage, setErrorMessage] = useState<string>(); const [errorMessage, setErrorMessage] = useState<string>();
@ -215,6 +216,7 @@ function GridCardGraph({
setRequestData={setRequestData} setRequestData={setRequestData}
onClickHandler={onClickHandler} onClickHandler={onClickHandler}
onDragSelect={onDragSelect} onDragSelect={onDragSelect}
customTooltipElement={customTooltipElement}
/> />
)} )}
</div> </div>

View File

@ -31,6 +31,7 @@ export interface WidgetGraphComponentProps {
setRequestData?: Dispatch<SetStateAction<GetQueryResultsProps>>; setRequestData?: Dispatch<SetStateAction<GetQueryResultsProps>>;
onClickHandler?: OnClickPluginOpts['onClick']; onClickHandler?: OnClickPluginOpts['onClick'];
onDragSelect: (start: number, end: number) => void; onDragSelect: (start: number, end: number) => void;
customTooltipElement?: HTMLDivElement;
} }
export interface GridCardGraphProps { export interface GridCardGraphProps {
@ -42,6 +43,7 @@ export interface GridCardGraphProps {
variables?: Dashboard['data']['variables']; variables?: Dashboard['data']['variables'];
version?: string; version?: string;
onDragSelect: (start: number, end: number) => void; onDragSelect: (start: number, end: number) => void;
customTooltipElement?: HTMLDivElement;
} }
export interface GetGraphVisibilityStateOnLegendClickProps { export interface GetGraphVisibilityStateOnLegendClickProps {

View File

@ -194,7 +194,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
urlQuery.set(QueryParams.startTime, startTimestamp.toString()); urlQuery.set(QueryParams.startTime, startTimestamp.toString());
urlQuery.set(QueryParams.endTime, endTimestamp.toString()); urlQuery.set(QueryParams.endTime, endTimestamp.toString());
const generatedUrl = `${pathname}?${urlQuery.toString()}`; const generatedUrl = `${pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl); history.push(generatedUrl);
if (startTimestamp !== endTimestamp) { if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp])); dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));

View File

@ -590,6 +590,8 @@
} }
.new-dashboard-menu { .new-dashboard-menu {
width: 200px;
.create-dashboard-menu-item { .create-dashboard-menu-item {
display: flex; display: flex;
align-items: center; align-items: center;
@ -1067,7 +1069,7 @@
color: var(--bg-ink-500); color: var(--bg-ink-500);
} }
.subtitle { .subtitle {
color: var(--bg-vanilla-400); color: var(--bg-ink-300);
} }
.ant-table-row { .ant-table-row {
@ -1087,6 +1089,10 @@
.dashboard-title { .dashboard-title {
color: var(--bg-slate-300); color: var(--bg-slate-300);
.title {
color: var(--bg-ink-500);
}
} }
.title-with-action { .title-with-action {

View File

@ -45,6 +45,8 @@ import {
Ellipsis, Ellipsis,
EllipsisVertical, EllipsisVertical,
Expand, Expand,
ExternalLink,
Github,
HdmiPort, HdmiPort,
LayoutGrid, LayoutGrid,
Link2, Link2,
@ -53,6 +55,8 @@ import {
RotateCw, RotateCw,
Search, Search,
} from 'lucide-react'; } 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 { handleContactSupport } from 'pages/Integrations/utils';
import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useDashboard } from 'providers/Dashboard/Dashboard';
import { import {
@ -600,6 +604,28 @@ function DashboardsList(): JSX.Element {
), ),
key: '1', 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) { if (createNewDashboard) {

View File

@ -4,7 +4,15 @@ import { red } from '@ant-design/colors';
import { ExclamationCircleTwoTone } from '@ant-design/icons'; import { ExclamationCircleTwoTone } from '@ant-design/icons';
import MEditor, { Monaco } from '@monaco-editor/react'; import MEditor, { Monaco } from '@monaco-editor/react';
import { Color } from '@signozhq/design-tokens'; 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 logEvent from 'api/common/logEvent';
import createDashboard from 'api/dashboard/create'; import createDashboard from 'api/dashboard/create';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
@ -13,7 +21,9 @@ import { MESSAGE } from 'hooks/useFeatureFlag';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout'; import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
import history from 'lib/history'; 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 { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom'; import { generatePath } from 'react-router-dom';
@ -174,6 +184,7 @@ function ImportJSON({
)} )}
<div className="action-btns-container"> <div className="action-btns-container">
<Flex gap="small">
<Upload <Upload
accept=".json" accept=".json"
showUploadList={false} showUploadList={false}
@ -195,6 +206,21 @@ function ImportJSON({
{t('upload_json_file')} {t('upload_json_file')}
</Button> </Button>
</Upload> </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 <Button
// disabled={editorValue.length === 0} // disabled={editorValue.length === 0}

View File

@ -38,6 +38,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
activeLog, activeLog,
onClearActiveLog, onClearActiveLog,
onAddToQuery, onAddToQuery,
onGroupByAttribute,
onSetActiveLog, onSetActiveLog,
} = useActiveLog(); } = useActiveLog();
@ -63,6 +64,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
data={log} data={log}
linesPerRow={options.maxLines} linesPerRow={options.maxLines}
selectedFields={selectedFields} selectedFields={selectedFields}
fontSize={options.fontSize}
/> />
); );
} }
@ -75,12 +77,14 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
linesPerRow={options.maxLines} linesPerRow={options.maxLines}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
onSetActiveLog={onSetActiveLog} onSetActiveLog={onSetActiveLog}
fontSize={options.fontSize}
/> />
); );
}, },
[ [
onAddToQuery, onAddToQuery,
onSetActiveLog, onSetActiveLog,
options.fontSize,
options.format, options.format,
options.maxLines, options.maxLines,
selectedFields, selectedFields,
@ -123,6 +127,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
logs, logs,
fields: selectedFields, fields: selectedFields,
linesPerRow: options.maxLines, linesPerRow: options.maxLines,
fontSize: options.fontSize,
appendTo: 'end', appendTo: 'end',
activeLogIndex, activeLogIndex,
}} }}
@ -147,6 +152,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
log={activeLog} log={activeLog}
onClose={onClearActiveLog} onClose={onClearActiveLog}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
onClickActionItem={onAddToQuery} onClickActionItem={onAddToQuery}
/> />
</> </>

View File

@ -3,12 +3,17 @@ import './ContextLogRenderer.styles.scss';
import { Skeleton } from 'antd'; import { Skeleton } from 'antd';
import RawLogView from 'components/Logs/RawLogView'; import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { LOCALSTORAGE } from 'constants/localStorage';
import ShowButton from 'container/LogsContextList/ShowButton'; 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 { 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 { Virtuoso } from 'react-virtuoso';
import { ILog } from 'types/api/logs/log'; import { ILog } from 'types/api/logs/log';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData'; import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { useContextLogData } from './useContextLogData'; import { useContextLogData } from './useContextLogData';
@ -22,6 +27,20 @@ function ContextLogRenderer({
const [afterLogPage, setAfterLogPage] = useState<number>(1); const [afterLogPage, setAfterLogPage] = useState<number>(1);
const [logs, setLogs] = useState<ILog[]>([log]); 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 { const {
logs: previousLogs, logs: previousLogs,
isFetching: isPreviousLogsFetching, isFetching: isPreviousLogsFetching,
@ -34,6 +53,7 @@ function ContextLogRenderer({
order: ORDERBY_FILTERS.ASC, order: ORDERBY_FILTERS.ASC,
page: prevLogPage, page: prevLogPage,
setPage: setPrevLogPage, setPage: setPrevLogPage,
fontSize: options.fontSize,
}); });
const { const {
@ -48,6 +68,7 @@ function ContextLogRenderer({
order: ORDERBY_FILTERS.DESC, order: ORDERBY_FILTERS.DESC,
page: afterLogPage, page: afterLogPage,
setPage: setAfterLogPage, setPage: setAfterLogPage,
fontSize: options.fontSize,
}); });
useEffect(() => { useEffect(() => {
@ -65,6 +86,19 @@ function ContextLogRenderer({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [filters]); }, [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( const getItemContent = useCallback(
(_: number, logTorender: ILog): JSX.Element => ( (_: number, logTorender: ILog): JSX.Element => (
<RawLogView <RawLogView
@ -74,9 +108,10 @@ function ContextLogRenderer({
key={logTorender.id} key={logTorender.id}
data={logTorender} data={logTorender}
linesPerRow={1} linesPerRow={1}
fontSize={options.fontSize}
/> />
), ),
[log.id], [log.id, options.fontSize],
); );
return ( return (
@ -101,7 +136,7 @@ function ContextLogRenderer({
initialTopMostItemIndex={0} initialTopMostItemIndex={0}
data={logs} data={logs}
itemContent={getItemContent} itemContent={getItemContent}
style={{ height: `calc(${logs.length} * 32px)` }} style={{ height: `calc(${logs.length} * ${lengthMultipier}px)` }}
/> />
</OverlayScrollbar> </OverlayScrollbar>
{isAfterLogsFetching && ( {isAfterLogsFetching && (

View File

@ -4,9 +4,11 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import { import {
getOrderByTimestamp, getOrderByTimestamp,
INITIAL_PAGE_SIZE, INITIAL_PAGE_SIZE,
INITIAL_PAGE_SIZE_SMALL_FONT,
LOGS_MORE_PAGE_SIZE, LOGS_MORE_PAGE_SIZE,
} from 'container/LogsContextList/configs'; } from 'container/LogsContextList/configs';
import { getRequestData } from 'container/LogsContextList/utils'; import { getRequestData } from 'container/LogsContextList/utils';
import { FontSize } from 'container/OptionsMenu/types';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config'; import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange'; import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
import { import {
@ -30,6 +32,7 @@ export const useContextLogData = ({
filters, filters,
page, page,
setPage, setPage,
fontSize,
}: { }: {
log: ILog; log: ILog;
query: Query; query: Query;
@ -38,6 +41,7 @@ export const useContextLogData = ({
filters: TagFilter | null; filters: TagFilter | null;
page: number; page: number;
setPage: Dispatch<SetStateAction<number>>; setPage: Dispatch<SetStateAction<number>>;
fontSize?: FontSize;
}): { }): {
logs: ILog[]; logs: ILog[];
handleShowNextLines: () => void; handleShowNextLines: () => void;
@ -54,9 +58,14 @@ export const useContextLogData = ({
const logsMorePageSize = useMemo(() => (page - 1) * LOGS_MORE_PAGE_SIZE, [ const logsMorePageSize = useMemo(() => (page - 1) * LOGS_MORE_PAGE_SIZE, [
page, page,
]); ]);
const initialPageSize =
fontSize && fontSize === FontSize.SMALL
? INITIAL_PAGE_SIZE_SMALL_FONT
: INITIAL_PAGE_SIZE;
const pageSize = useMemo( const pageSize = useMemo(
() => (page <= 1 ? INITIAL_PAGE_SIZE : logsMorePageSize + INITIAL_PAGE_SIZE), () => (page <= 1 ? initialPageSize : logsMorePageSize + initialPageSize),
[page, logsMorePageSize], [page, initialPageSize, logsMorePageSize],
); );
const isDisabledFetch = useMemo(() => logs.length < pageSize, [ const isDisabledFetch = useMemo(() => logs.length < pageSize, [
logs.length, logs.length,
@ -77,8 +86,16 @@ export const useContextLogData = ({
log: lastLog, log: lastLog,
orderByTimestamp, orderByTimestamp,
page, page,
pageSize: initialPageSize,
}), }),
[currentStagedQueryData, query, lastLog, orderByTimestamp, page], [
currentStagedQueryData,
query,
lastLog,
orderByTimestamp,
page,
initialPageSize,
],
); );
const [requestData, setRequestData] = useState<Query | null>( const [requestData, setRequestData] = useState<Query | null>(

View File

@ -18,6 +18,7 @@
.tags { .tags {
display: flex; display: flex;
gap: 8; flex-wrap: wrap;
gap: 8px;
} }
} }

View File

@ -2,6 +2,7 @@ import './LogContext.styles.scss';
import RawLogView from 'components/Logs/RawLogView'; import RawLogView from 'components/Logs/RawLogView';
import LogsContextList from 'container/LogsContextList'; import LogsContextList from 'container/LogsContextList';
import { FontSize } from 'container/OptionsMenu/types';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config'; import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { ILog } from 'types/api/logs/log'; import { ILog } from 'types/api/logs/log';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData'; import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
@ -37,6 +38,7 @@ function LogContext({
isTextOverflowEllipsisDisabled={false} isTextOverflowEllipsisDisabled={false}
data={log} data={log}
linesPerRow={1} linesPerRow={1}
fontSize={FontSize.SMALL}
/> />
<LogsContextList <LogsContextList
order={ORDERBY_FILTERS.DESC} order={ORDERBY_FILTERS.DESC}

View File

@ -18,15 +18,22 @@ import { ChevronDown, ChevronRight, Search } from 'lucide-react';
import { ReactNode, useState } from 'react'; import { ReactNode, useState } from 'react';
import { IField } from 'types/api/logs/fields'; import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log'; import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { ActionItemProps } from './ActionItem'; import { ActionItemProps } from './ActionItem';
import TableView from './TableView'; import TableView from './TableView';
import { removeEscapeCharacters } from './utils';
interface OverviewProps { interface OverviewProps {
logData: ILog; logData: ILog;
isListViewPanel?: boolean; isListViewPanel?: boolean;
selectedOptions: OptionsQuery; selectedOptions: OptionsQuery;
listViewPanelSelectedFields?: IField[] | null; listViewPanelSelectedFields?: IField[] | null;
onGroupByAttribute?: (
fieldKey: string,
isJSON?: boolean,
dataType?: DataTypes,
) => Promise<void>;
} }
type Props = OverviewProps & type Props = OverviewProps &
@ -39,6 +46,7 @@ function Overview({
onClickActionItem, onClickActionItem,
isListViewPanel = false, isListViewPanel = false,
selectedOptions, selectedOptions,
onGroupByAttribute,
listViewPanelSelectedFields, listViewPanelSelectedFields,
}: Props): JSX.Element { }: Props): JSX.Element {
const [isWrapWord, setIsWrapWord] = useState<boolean>(true); const [isWrapWord, setIsWrapWord] = useState<boolean>(true);
@ -117,7 +125,7 @@ function Overview({
children: ( children: (
<div className="logs-body-content"> <div className="logs-body-content">
<MEditor <MEditor
value={logData.body} value={removeEscapeCharacters(logData.body)}
language="json" language="json"
options={options} options={options}
onChange={(): void => {}} onChange={(): void => {}}
@ -204,6 +212,7 @@ function Overview({
logData={logData} logData={logData}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
fieldSearchInput={fieldSearchInput} fieldSearchInput={fieldSearchInput}
onGroupByAttribute={onGroupByAttribute}
onClickActionItem={onClickActionItem} onClickActionItem={onClickActionItem}
isListViewPanel={isListViewPanel} isListViewPanel={isListViewPanel}
selectedOptions={selectedOptions} selectedOptions={selectedOptions}
@ -222,6 +231,7 @@ function Overview({
Overview.defaultProps = { Overview.defaultProps = {
isListViewPanel: false, isListViewPanel: false,
listViewPanelSelectedFields: null, listViewPanelSelectedFields: null,
onGroupByAttribute: undefined,
}; };
export default Overview; export default Overview;

View File

@ -11,7 +11,7 @@
top: 50%; top: 50%;
right: 16px; right: 16px;
transform: translateY(-50%); transform: translateY(-50%);
gap: 8px; gap: 4px;
} }
} }
} }
@ -76,8 +76,10 @@
box-shadow: none; box-shadow: none;
border-radius: 2px; border-radius: 2px;
background: var(--bg-slate-400); background: var(--bg-slate-400);
padding: 2px 3px;
height: 24px; gap: 3px;
height: 18px;
width: 20px;
} }
} }
} }

View File

@ -4,23 +4,21 @@ import './TableView.styles.scss';
import { LinkOutlined } from '@ant-design/icons'; import { LinkOutlined } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens'; 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 { ColumnsType } from 'antd/es/table';
import cx from 'classnames'; import cx from 'classnames';
import AddToQueryHOC, { import AddToQueryHOC, {
AddToQueryHOCProps, AddToQueryHOCProps,
} from 'components/Logs/AddToQueryHOC'; } from 'components/Logs/AddToQueryHOC';
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
import { ResizeTable } from 'components/ResizeTable'; import { ResizeTable } from 'components/ResizeTable';
import { OPERATORS } from 'constants/queryBuilder'; import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { OptionsQuery } from 'container/OptionsMenu/types'; import { FontSize, OptionsQuery } from 'container/OptionsMenu/types';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import history from 'lib/history'; import history from 'lib/history';
import { fieldSearchFilter } from 'lib/logs/fieldSearch'; import { fieldSearchFilter } from 'lib/logs/fieldSearch';
import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes'; import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes';
import { isEmpty } from 'lodash-es'; import { Pin } from 'lucide-react';
import { ArrowDownToDot, ArrowUpFromDot, Pin } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { generatePath } from 'react-router-dom'; 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 { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { IField } from 'types/api/logs/fields'; import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log'; import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { ActionItemProps } from './ActionItem'; import { ActionItemProps } from './ActionItem';
import FieldRenderer from './FieldRenderer'; import FieldRenderer from './FieldRenderer';
import { import { TableViewActions } from './TableView/TableViewActions';
filterKeyForField, import { filterKeyForField, findKeyPath, flattenObject } from './utils';
findKeyPath,
flattenObject,
jsonToDataNodes,
recursiveParseJSON,
removeEscapeCharacters,
} from './utils';
// Fields which should be restricted from adding it to query // Fields which should be restricted from adding it to query
const RESTRICTED_FIELDS = ['timestamp']; const RESTRICTED_FIELDS = ['timestamp'];
@ -50,6 +43,11 @@ interface TableViewProps {
selectedOptions: OptionsQuery; selectedOptions: OptionsQuery;
isListViewPanel?: boolean; isListViewPanel?: boolean;
listViewPanelSelectedFields?: IField[] | null; listViewPanelSelectedFields?: IField[] | null;
onGroupByAttribute?: (
fieldKey: string,
isJSON?: boolean,
dataType?: DataTypes,
) => Promise<void>;
} }
type Props = TableViewProps & type Props = TableViewProps &
@ -63,6 +61,7 @@ function TableView({
onClickActionItem, onClickActionItem,
isListViewPanel = false, isListViewPanel = false,
selectedOptions, selectedOptions,
onGroupByAttribute,
listViewPanelSelectedFields, listViewPanelSelectedFields,
}: Props): JSX.Element | null { }: Props): JSX.Element | null {
const dispatch = useDispatch<Dispatch<AppActions>>(); const dispatch = useDispatch<Dispatch<AppActions>>();
@ -256,6 +255,7 @@ function TableView({
fieldKey={fieldFilterKey} fieldKey={fieldFilterKey}
fieldValue={flattenLogData[field]} fieldValue={flattenLogData[field]}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
fontSize={FontSize.SMALL}
> >
{renderedField} {renderedField}
</AddToQueryHOC> </AddToQueryHOC>
@ -270,75 +270,17 @@ function TableView({
width: 70, width: 70,
ellipsis: false, ellipsis: false,
className: 'value-field-container attribute-value', className: 'value-field-container attribute-value',
render: (fieldData: Record<string, string>, record): JSX.Element => { render: (fieldData: Record<string, string>, record): JSX.Element => (
const textToCopy = fieldData.value.slice(1, -1); <TableViewActions
fieldData={fieldData}
if (record.field === 'body') { record={record}
const parsedBody = recursiveParseJSON(fieldData.value); isListViewPanel={isListViewPanel}
if (!isEmpty(parsedBody)) { isfilterInLoading={isfilterInLoading}
return ( isfilterOutLoading={isfilterOutLoading}
<Tree defaultExpandAll showLine treeData={jsonToDataNodes(parsedBody)} /> onClickHandler={onClickHandler}
); onGroupByAttribute={onGroupByAttribute}
}
}
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>
);
},
}, },
]; ];
function sortPinnedAttributes( function sortPinnedAttributes(
@ -379,9 +321,10 @@ function TableView({
TableView.defaultProps = { TableView.defaultProps = {
isListViewPanel: false, isListViewPanel: false,
listViewPanelSelectedFields: null, listViewPanelSelectedFields: null,
onGroupByAttribute: undefined,
}; };
interface DataType { export interface DataType {
key: string; key: string;
field: string; field: string;
value: 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,185 @@
import './TableViewActions.styles.scss';
import { Color } from '@signozhq/design-tokens';
import Convert from 'ansi-to-html';
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 dompurify from 'dompurify';
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 { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
import { DataType } from '../TableView';
import {
filterKeyForField,
jsonToDataNodes,
recursiveParseJSON,
removeEscapeCharacters,
unescapeString,
} 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;
}
const convert = new Convert();
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;
if (record.field === 'body') {
const parsedBody = recursiveParseJSON(fieldData.value);
if (!isEmpty(parsedBody)) {
return (
<Tree defaultExpandAll showLine treeData={jsonToDataNodes(parsedBody)} />
);
}
}
const bodyHtml =
record.field === 'body'
? {
__html: convert.toHtml(
dompurify.sanitize(unescapeString(record.value), {
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
}),
),
}
: { __html: '' };
const fieldFilterKey = filterKeyForField(fieldData.field);
return (
<div className={cx('value-field', isOpen ? 'open-popover' : '')}>
{record.field === 'body' ? (
<CopyClipboardHOC entityKey={fieldFilterKey} textToCopy={textToCopy}>
<span
style={{
color: Color.BG_SIENNA_400,
whiteSpace: 'pre-wrap',
tabSize: 4,
}}
dangerouslySetInnerHTML={bodyHtml}
/>
</CopyClipboardHOC>
) : (
<CopyClipboardHOC entityKey={fieldFilterKey} 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

@ -250,19 +250,37 @@ export const getDataTypes = (value: unknown): DataTypes => {
return determineType(value); return determineType(value);
}; };
// now we do not want to render colors everywhere like in tooltip and monaco editor hence we remove such codes to make
// the log line readable
export const removeEscapeCharacters = (str: string): string => export const removeEscapeCharacters = (str: string): string =>
str.replace(/\\([ntfr'"\\])/g, (_: string, char: string) => { str
const escapeMap: Record<string, string> = { .replace(/\\x1[bB][[0-9;]*m/g, '')
n: '\n', .replace(/\\u001[bB][[0-9;]*m/g, '')
t: '\t', .replace(/\\x[0-9A-Fa-f]{2}/g, '')
f: '\f', .replace(/\\u[0-9A-Fa-f]{4}/g, '')
r: '\r', .replace(/\\[btnfrv0'"\\]/g, '');
"'": "'",
'"': '"', // we need to remove the escape from the escaped characters as some recievers like file log escape the unicode escape characters.
'\\': '\\', // example: Log [\u001B[32;1mThis is bright green\u001B[0m] is being sent as [\\u001B[32;1mThis is bright green\\u001B[0m]
}; //
return escapeMap[char as keyof typeof escapeMap]; // so we need to remove this escapes to render the color properly
}); export const unescapeString = (str: string): string =>
str
.replace(/\\n/g, '\n') // Replaces escaped newlines
.replace(/\\r/g, '\r') // Replaces escaped carriage returns
.replace(/\\t/g, '\t') // Replaces escaped tabs
.replace(/\\b/g, '\b') // Replaces escaped backspaces
.replace(/\\f/g, '\f') // Replaces escaped form feeds
.replace(/\\v/g, '\v') // Replaces escaped vertical tabs
.replace(/\\'/g, "'") // Replaces escaped single quotes
.replace(/\\"/g, '"') // Replaces escaped double quotes
.replace(/\\\\/g, '\\') // Replaces escaped backslashes
.replace(/\\x([0-9A-Fa-f]{2})/g, (_, hex) =>
String.fromCharCode(parseInt(hex, 16)),
) // Replaces hexadecimal escape sequences
.replace(/\\u([0-9A-Fa-f]{4})/g, (_, hex) =>
String.fromCharCode(parseInt(hex, 16)),
); // Replaces Unicode escape sequences
export function removeExtraSpaces(input: string): string { export function removeExtraSpaces(input: string): string {
return input.replace(/\s+/g, ' ').trim(); return input.replace(/\s+/g, ' ').trim();

View File

@ -1,6 +1,7 @@
import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData'; import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData';
export const INITIAL_PAGE_SIZE = 10; export const INITIAL_PAGE_SIZE = 10;
export const INITIAL_PAGE_SIZE_SMALL_FONT = 12;
export const LOGS_MORE_PAGE_SIZE = 10; export const LOGS_MORE_PAGE_SIZE = 10;
export const getOrderByTimestamp = (order: string): OrderByPayload => ({ export const getOrderByTimestamp = (order: string): OrderByPayload => ({

View File

@ -5,6 +5,7 @@ import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { DEFAULT_ENTITY_VERSION } from 'constants/app'; import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import { FontSize } from 'container/OptionsMenu/types';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config'; import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange'; import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
@ -167,6 +168,7 @@ function LogsContextList({
key={log.id} key={log.id}
data={log} data={log}
linesPerRow={1} linesPerRow={1}
fontSize={FontSize.SMALL}
/> />
), ),
[], [],

View File

@ -2,6 +2,7 @@ import { EditFilled } from '@ant-design/icons';
import { Modal, Typography } from 'antd'; import { Modal, Typography } from 'antd';
import RawLogView from 'components/Logs/RawLogView'; import RawLogView from 'components/Logs/RawLogView';
import LogsContextList from 'container/LogsContextList'; import LogsContextList from 'container/LogsContextList';
import { FontSize } from 'container/OptionsMenu/types';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config'; import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch'; import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
@ -99,6 +100,7 @@ function LogsExplorerContext({
isTextOverflowEllipsisDisabled isTextOverflowEllipsisDisabled
data={log} data={log}
linesPerRow={1} linesPerRow={1}
fontSize={FontSize.SMALL}
/> />
</LogContainer> </LogContainer>
<LogsContextList <LogsContextList

View File

@ -3,6 +3,7 @@ import './TableRow.styles.scss';
import { ColumnsType } from 'antd/es/table'; import { ColumnsType } from 'antd/es/table';
import LogLinesActionButtons from 'components/Logs/LogLinesActionButtons/LogLinesActionButtons'; import LogLinesActionButtons from 'components/Logs/LogLinesActionButtons/LogLinesActionButtons';
import { ColumnTypeRender } from 'components/Logs/TableView/types'; import { ColumnTypeRender } from 'components/Logs/TableView/types';
import { FontSize } from 'container/OptionsMenu/types';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink'; import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { import {
@ -24,6 +25,7 @@ interface TableRowProps {
handleSetActiveContextLog: (log: ILog) => void; handleSetActiveContextLog: (log: ILog) => void;
logs: ILog[]; logs: ILog[];
hasActions: boolean; hasActions: boolean;
fontSize: FontSize;
} }
export default function TableRow({ export default function TableRow({
@ -33,6 +35,7 @@ export default function TableRow({
handleSetActiveContextLog, handleSetActiveContextLog,
logs, logs,
hasActions, hasActions,
fontSize,
}: TableRowProps): JSX.Element { }: TableRowProps): JSX.Element {
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
@ -78,6 +81,7 @@ export default function TableRow({
$isDragColumn={false} $isDragColumn={false}
$isDarkMode={isDarkMode} $isDarkMode={isDarkMode}
key={column.key} key={column.key}
fontSize={fontSize}
> >
{cloneElement(children, props)} {cloneElement(children, props)}
</TableCellStyled> </TableCellStyled>

View File

@ -1,3 +1,5 @@
/* eslint-disable no-nested-ternary */
import { FontSize } from 'container/OptionsMenu/types';
import { CSSProperties } from 'react'; import { CSSProperties } from 'react';
export const infinityDefaultStyles: CSSProperties = { export const infinityDefaultStyles: CSSProperties = {
@ -5,3 +7,16 @@ export const infinityDefaultStyles: CSSProperties = {
overflowX: 'scroll', overflowX: 'scroll',
marginTop: '15px', 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'; } from 'react-virtuoso';
import { ILog } from 'types/api/logs/log'; import { ILog } from 'types/api/logs/log';
import { infinityDefaultStyles } from './config'; import { getInfinityDefaultStyles } from './config';
import { LogsCustomTable } from './LogsCustomTable'; import { LogsCustomTable } from './LogsCustomTable';
import { TableHeaderCellStyled, TableRowStyled } from './styles'; import { TableHeaderCellStyled, TableRowStyled } from './styles';
import TableRow from './TableRow'; import TableRow from './TableRow';
@ -59,6 +59,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
onSetActiveLog, onSetActiveLog,
onClearActiveLog, onClearActiveLog,
onAddToQuery, onAddToQuery,
onGroupByAttribute,
} = useActiveLog(); } = useActiveLog();
const { dataSource, columns } = useTableView({ const { dataSource, columns } = useTableView({
@ -95,9 +96,15 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
handleSetActiveContextLog={handleSetActiveContextLog} handleSetActiveContextLog={handleSetActiveContextLog}
logs={tableViewProps.logs} logs={tableViewProps.logs}
hasActions hasActions
fontSize={tableViewProps.fontSize}
/> />
), ),
[handleSetActiveContextLog, tableColumns, tableViewProps.logs], [
handleSetActiveContextLog,
tableColumns,
tableViewProps.fontSize,
tableViewProps.logs,
],
); );
const tableHeader = useCallback( const tableHeader = useCallback(
@ -112,6 +119,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
$isDarkMode={isDarkMode} $isDarkMode={isDarkMode}
$isDragColumn={isDragColumn} $isDragColumn={isDragColumn}
key={column.key} key={column.key}
fontSize={tableViewProps?.fontSize}
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
{...(isDragColumn && { className: 'dragHandler' })} {...(isDragColumn && { className: 'dragHandler' })}
> >
@ -121,7 +129,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
})} })}
</tr> </tr>
), ),
[tableColumns, isDarkMode], [tableColumns, isDarkMode, tableViewProps?.fontSize],
); );
const handleClickExpand = (index: number): void => { const handleClickExpand = (index: number): void => {
@ -137,7 +145,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
initialTopMostItemIndex={ initialTopMostItemIndex={
tableViewProps.activeLogIndex !== -1 ? tableViewProps.activeLogIndex : 0 tableViewProps.activeLogIndex !== -1 ? tableViewProps.activeLogIndex : 0
} }
style={infinityDefaultStyles} style={getInfinityDefaultStyles(tableViewProps.fontSize)}
data={dataSource} data={dataSource}
components={{ components={{
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
@ -165,6 +173,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
onClose={handleClearActiveContextLog} onClose={handleClearActiveContextLog}
onAddToQuery={handleAddToQuery} onAddToQuery={handleAddToQuery}
selectedTab={VIEW_TYPES.CONTEXT} selectedTab={VIEW_TYPES.CONTEXT}
onGroupByAttribute={onGroupByAttribute}
/> />
)} )}
<LogDetail <LogDetail
@ -173,6 +182,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
onClose={onClearActiveLog} onClose={onClearActiveLog}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery} onClickActionItem={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
/> />
</> </>
); );

View File

@ -1,5 +1,7 @@
/* eslint-disable no-nested-ternary */
import { Color } from '@signozhq/design-tokens'; import { Color } from '@signozhq/design-tokens';
import { themeColors } from 'constants/theme'; import { themeColors } from 'constants/theme';
import { FontSize } from 'container/OptionsMenu/types';
import styled from 'styled-components'; import styled from 'styled-components';
import { getActiveLogBackground } from 'utils/logs'; import { getActiveLogBackground } from 'utils/logs';
@ -7,6 +9,7 @@ interface TableHeaderCellStyledProps {
$isDragColumn: boolean; $isDragColumn: boolean;
$isDarkMode: boolean; $isDarkMode: boolean;
$isTimestamp?: boolean; $isTimestamp?: boolean;
fontSize?: FontSize;
} }
export const TableStyled = styled.table` export const TableStyled = styled.table`
@ -15,6 +18,14 @@ export const TableStyled = styled.table`
export const TableCellStyled = styled.td<TableHeaderCellStyledProps>` export const TableCellStyled = styled.td<TableHeaderCellStyledProps>`
padding: 0.5rem; 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 => background-color: ${(props): string =>
props.$isDarkMode ? 'inherit' : themeColors.whiteCream}; props.$isDarkMode ? 'inherit' : themeColors.whiteCream};
@ -33,7 +44,7 @@ export const TableRowStyled = styled.tr<{
? `background-color: ${ ? `background-color: ${
$isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300 $isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300
} !important` } !important`
: ''} : ''};
} }
cursor: pointer; cursor: pointer;
@ -66,9 +77,17 @@ export const TableHeaderCellStyled = styled.th<TableHeaderCellStyledProps>`
line-height: 18px; line-height: 18px;
letter-spacing: -0.07px; letter-spacing: -0.07px;
background: ${(props): string => (props.$isDarkMode ? '#0b0c0d' : '#fdfdfd')}; background: ${(props): string => (props.$isDarkMode ? '#0b0c0d' : '#fdfdfd')};
${({ $isTimestamp }): string => ($isTimestamp ? 'padding-left: 24px;' : '')}
${({ $isDragColumn }): string => ($isDragColumn ? 'cursor: col-resize;' : '')} ${({ $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 => color: ${(props): string =>
props.$isDarkMode ? 'var(--bg-vanilla-100, #fff)' : themeColors.bckgGrey}; 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 LogsError from 'container/LogsError/LogsError';
import { LogsLoading } from 'container/LogsLoading/LogsLoading'; import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { useOptionsMenu } from 'container/OptionsMenu'; import { useOptionsMenu } from 'container/OptionsMenu';
import { FontSize } from 'container/OptionsMenu/types';
import { useActiveLog } from 'hooks/logs/useActiveLog'; import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink'; import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
@ -50,6 +51,7 @@ function LogsExplorerList({
activeLog, activeLog,
onClearActiveLog, onClearActiveLog,
onAddToQuery, onAddToQuery,
onGroupByAttribute,
onSetActiveLog, onSetActiveLog,
} = useActiveLog(); } = useActiveLog();
@ -79,6 +81,7 @@ function LogsExplorerList({
data={log} data={log}
linesPerRow={options.maxLines} linesPerRow={options.maxLines}
selectedFields={selectedFields} selectedFields={selectedFields}
fontSize={options.fontSize}
/> />
); );
} }
@ -91,6 +94,7 @@ function LogsExplorerList({
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
onSetActiveLog={onSetActiveLog} onSetActiveLog={onSetActiveLog}
activeLog={activeLog} activeLog={activeLog}
fontSize={options.fontSize}
linesPerRow={options.maxLines} linesPerRow={options.maxLines}
/> />
); );
@ -99,6 +103,7 @@ function LogsExplorerList({
activeLog, activeLog,
onAddToQuery, onAddToQuery,
onSetActiveLog, onSetActiveLog,
options.fontSize,
options.format, options.format,
options.maxLines, options.maxLines,
selectedFields, selectedFields,
@ -121,6 +126,7 @@ function LogsExplorerList({
logs, logs,
fields: selectedFields, fields: selectedFields,
linesPerRow: options.maxLines, linesPerRow: options.maxLines,
fontSize: options.fontSize,
appendTo: 'end', appendTo: 'end',
activeLogIndex, activeLogIndex,
}} }}
@ -129,13 +135,27 @@ 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 ( return (
<Card <Card
style={{ width: '100%', marginTop: '20px' }} style={{ width: '100%', marginTop: getMarginTop() }}
bodyStyle={CARD_BODY_STYLE} bodyStyle={CARD_BODY_STYLE}
> >
<OverlayScrollbar isVirtuoso> <OverlayScrollbar isVirtuoso>
<Virtuoso <Virtuoso
key={activeLogIndex || 'logs-virtuoso'}
ref={ref} ref={ref}
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0} initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
data={logs} data={logs}
@ -151,6 +171,7 @@ function LogsExplorerList({
isLoading, isLoading,
options.format, options.format,
options.maxLines, options.maxLines,
options.fontSize,
activeLogIndex, activeLogIndex,
logs, logs,
onEndReached, onEndReached,
@ -189,6 +210,7 @@ function LogsExplorerList({
log={activeLog} log={activeLog}
onClose={onClearActiveLog} onClose={onClearActiveLog}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
onClickActionItem={onAddToQuery} onClickActionItem={onAddToQuery}
/> />
</> </>

View File

@ -80,6 +80,36 @@
position: relative; 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 { .logs-actions-container {
@ -149,6 +179,15 @@
background: var(--bg-robin-400); 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 */ /* eslint-disable sonarjs/cognitive-complexity */
import './LogsExplorerViews.styles.scss'; 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 logEvent from 'api/common/logEvent';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu'; import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
import { DEFAULT_ENTITY_VERSION } from 'constants/app'; import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { LOCALSTORAGE } from 'constants/localStorage'; import { LOCALSTORAGE } from 'constants/localStorage';
@ -48,7 +50,15 @@ import {
} from 'lodash-es'; } from 'lodash-es';
import { Sliders } from 'lucide-react'; import { Sliders } from 'lucide-react';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils'; 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 { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
@ -69,12 +79,20 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink'; import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import QueryStatus from './QueryStatus';
function LogsExplorerViews({ function LogsExplorerViews({
selectedView, selectedView,
showFrequencyChart, showFrequencyChart,
setIsLoadingQueries,
listQueryKeyRef,
chartQueryKeyRef,
}: { }: {
selectedView: SELECTED_VIEWS; selectedView: SELECTED_VIEWS;
showFrequencyChart: boolean; showFrequencyChart: boolean;
setIsLoadingQueries: React.Dispatch<React.SetStateAction<boolean>>;
listQueryKeyRef: MutableRefObject<any>;
chartQueryKeyRef: MutableRefObject<any>;
}): JSX.Element { }): JSX.Element {
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const history = useHistory(); const history = useHistory();
@ -82,14 +100,14 @@ function LogsExplorerViews({
// this is to respect the panel type present in the URL rather than defaulting it to list always. // this is to respect the panel type present in the URL rather than defaulting it to list always.
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST); const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
const { activeLogId, timeRange, onTimeRangeChange } = useCopyLogLink(); const { activeLogId, onTimeRangeChange } = useCopyLogLink();
const { queryData: pageSize } = useUrlQueryData( const { queryData: pageSize } = useUrlQueryData(
QueryParams.pageSize, QueryParams.pageSize,
DEFAULT_PER_PAGE_VALUE, DEFAULT_PER_PAGE_VALUE,
); );
const { minTime } = useSelector<AppState, GlobalReducer>( const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime, (state) => state.globalTime,
); );
@ -116,6 +134,8 @@ function LogsExplorerViews({
const [logs, setLogs] = useState<ILog[]>([]); const [logs, setLogs] = useState<ILog[]>([]);
const [requestData, setRequestData] = useState<Query | null>(null); const [requestData, setRequestData] = useState<Query | null>(null);
const [showFormatMenuItems, setShowFormatMenuItems] = useState(false); const [showFormatMenuItems, setShowFormatMenuItems] = useState(false);
const [queryId, setQueryId] = useState<string>(v4());
const [queryStats, setQueryStats] = useState<WsDataEvent>();
const handleAxisError = useAxiosError(); const handleAxisError = useAxiosError();
@ -214,9 +234,18 @@ function LogsExplorerViews({
{ {
enabled: !!listChartQuery && panelType === PANEL_TYPES.LIST, enabled: !!listChartQuery && panelType === PANEL_TYPES.LIST,
}, },
{},
undefined,
chartQueryKeyRef,
); );
const { data, isLoading, isFetching, isError } = useGetExplorerQueryRange( const {
data,
isLoading,
isFetching,
isError,
isSuccess,
} = useGetExplorerQueryRange(
requestData, requestData,
panelType, panelType,
DEFAULT_ENTITY_VERSION, DEFAULT_ENTITY_VERSION,
@ -225,13 +254,18 @@ function LogsExplorerViews({
enabled: !isLimit && !!requestData, enabled: !isLimit && !!requestData,
}, },
{ {
...(timeRange && ...(activeLogId &&
activeLogId &&
!logs.length && { !logs.length && {
start: timeRange.start, start: minTime,
end: timeRange.end, end: maxTime,
}), }),
}, },
undefined,
listQueryKeyRef,
{
...(!isEmpty(queryId) &&
selectedPanelType !== PANEL_TYPES.LIST && { 'X-SIGNOZ-QUERY-ID': queryId }),
},
); );
const getRequestData = useCallback( const getRequestData = useCallback(
@ -318,6 +352,23 @@ function LogsExplorerViews({
], ],
); );
useEffect(() => {
setQueryId(v4());
}, [data]);
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); const logEventCalledRef = useRef(false);
useEffect(() => { useEffect(() => {
if (!logEventCalledRef.current && !isUndefined(data?.payload)) { if (!logEventCalledRef.current && !isUndefined(data?.payload)) {
@ -469,7 +520,7 @@ function LogsExplorerViews({
setLogs(newLogs); setLogs(newLogs);
onTimeRangeChange({ onTimeRangeChange({
start: currentParams?.start, start: currentParams?.start,
end: timeRange?.end || currentParams?.end, end: currentParams?.end,
pageSize: newLogs.length, pageSize: newLogs.length,
}); });
} }
@ -486,8 +537,7 @@ function LogsExplorerViews({
filters: listQuery?.filters || initialFilters, filters: listQuery?.filters || initialFilters,
page: 1, page: 1,
log: null, log: null,
pageSize: pageSize,
timeRange?.pageSize && activeLogId ? timeRange?.pageSize : pageSize,
}); });
setLogs([]); setLogs([]);
@ -502,7 +552,6 @@ function LogsExplorerViews({
listQuery, listQuery,
pageSize, pageSize,
minTime, minTime,
timeRange,
activeLogId, activeLogId,
onTimeRangeChange, onTimeRangeChange,
panelType, panelType,
@ -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( const flattenLogData = useMemo(
() => () =>
logs.map((log) => { logs.map((log) => {
@ -665,6 +733,30 @@ function LogsExplorerViews({
</div> </div>
</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>
</div> </div>

View File

@ -46,6 +46,10 @@ jest.mock(
}, },
); );
jest.mock('api/common/getQueryStats', () => ({
getQueryStats: jest.fn(),
}));
jest.mock('constants/panelTypes', () => ({ jest.mock('constants/panelTypes', () => ({
AVAILABLE_EXPORT_PANEL_TYPES: ['graph', 'table'], AVAILABLE_EXPORT_PANEL_TYPES: ['graph', 'table'],
})); }));
@ -79,6 +83,9 @@ const renderer = (): RenderResult =>
<LogsExplorerViews <LogsExplorerViews
selectedView={SELECTED_VIEWS.SEARCH} selectedView={SELECTED_VIEWS.SEARCH}
showFrequencyChart showFrequencyChart
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
/> />
</VirtuosoMockContext.Provider> </VirtuosoMockContext.Provider>
</QueryBuilderProvider> </QueryBuilderProvider>

View File

@ -108,6 +108,7 @@ function LogsPanelComponent({
onSetActiveLog, onSetActiveLog,
onClearActiveLog, onClearActiveLog,
onAddToQuery, onAddToQuery,
onGroupByAttribute,
} = useActiveLog(); } = useActiveLog();
const handleRow = useCallback( const handleRow = useCallback(
@ -244,6 +245,7 @@ function LogsPanelComponent({
onClose={onClearActiveLog} onClose={onClearActiveLog}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery} onClickActionItem={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
isListViewPanel isListViewPanel
listViewPanelSelectedFields={widget?.selectedLogFields} listViewPanelSelectedFields={widget?.selectedLogFields}
/> />

View File

@ -10,11 +10,14 @@ import LogsTableView from 'components/Logs/TableView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { CARD_BODY_STYLE } from 'constants/card'; import { CARD_BODY_STYLE } from 'constants/card';
import { LOCALSTORAGE } from 'constants/localStorage';
import { useOptionsMenu } from 'container/OptionsMenu';
import { useActiveLog } from 'hooks/logs/useActiveLog'; import { useActiveLog } from 'hooks/logs/useActiveLog';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { Virtuoso } from 'react-virtuoso'; import { Virtuoso } from 'react-virtuoso';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
// interfaces // interfaces
import { ILogsReducer } from 'types/reducer/logs'; import { ILogsReducer } from 'types/reducer/logs';
@ -35,6 +38,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
activeLog, activeLog,
onClearActiveLog, onClearActiveLog,
onAddToQuery, onAddToQuery,
onGroupByAttribute,
onSetActiveLog, onSetActiveLog,
} = useActiveLog(); } = useActiveLog();
@ -55,6 +59,14 @@ function LogsTable(props: LogsTableProps): JSX.Element {
liveTail, liveTail,
]); ]);
const { options } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
// this component will alwyays be called on old logs explorer page itself!
dataSource: DataSource.LOGS,
// and we do not have table / timeseries aggregated views in the old logs explorer!
aggregateOperator: StringOperators.NOOP,
});
const getItemContent = useCallback( const getItemContent = useCallback(
(index: number): JSX.Element => { (index: number): JSX.Element => {
const log = logs[index]; const log = logs[index];
@ -66,6 +78,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
data={log} data={log}
linesPerRow={linesPerRow} linesPerRow={linesPerRow}
selectedFields={selected} selectedFields={selected}
fontSize={options.fontSize}
/> />
); );
} }
@ -78,10 +91,19 @@ function LogsTable(props: LogsTableProps): JSX.Element {
linesPerRow={linesPerRow} linesPerRow={linesPerRow}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
onSetActiveLog={onSetActiveLog} onSetActiveLog={onSetActiveLog}
fontSize={options.fontSize}
/> />
); );
}, },
[logs, viewMode, selected, onAddToQuery, onSetActiveLog, linesPerRow], [
logs,
viewMode,
selected,
linesPerRow,
onAddToQuery,
onSetActiveLog,
options.fontSize,
],
); );
const renderContent = useMemo(() => { const renderContent = useMemo(() => {
@ -92,6 +114,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
logs={logs} logs={logs}
fields={selected} fields={selected}
linesPerRow={linesPerRow} linesPerRow={linesPerRow}
fontSize={options.fontSize}
/> />
); );
} }
@ -103,7 +126,15 @@ function LogsTable(props: LogsTableProps): JSX.Element {
</OverlayScrollbar> </OverlayScrollbar>
</Card> </Card>
); );
}, [getItemContent, linesPerRow, logs, onSetActiveLog, selected, viewMode]); }, [
getItemContent,
linesPerRow,
logs,
onSetActiveLog,
options.fontSize,
selected,
viewMode,
]);
if (isLoading) { if (isLoading) {
return <Spinner height={20} tip="Getting Logs" />; return <Spinner height={20} tip="Getting Logs" />;
@ -126,6 +157,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
selectedTab={VIEW_TYPES.OVERVIEW} selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog} log={activeLog}
onClose={onClearActiveLog} onClose={onClearActiveLog}
onGroupByAttribute={onGroupByAttribute}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery} onClickActionItem={onAddToQuery}
/> />

View File

@ -1,6 +1,7 @@
import { Col } from 'antd'; import { Col } from 'antd';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V4 } from 'constants/app'; import { ENTITY_VERSION_V4 } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import Graph from 'container/GridCardLayout/GridCard'; import Graph from 'container/GridCardLayout/GridCard';
import { import {
@ -12,8 +13,12 @@ import {
convertRawQueriesToTraceSelectedTags, convertRawQueriesToTraceSelectedTags,
resourceAttributesToTagFilterItems, resourceAttributesToTagFilterItems,
} from 'hooks/useResourceAttribute/utils'; } from 'hooks/useResourceAttribute/utils';
import { useEffect, useMemo, useRef, useState } from 'react'; import useUrlQuery from 'hooks/useUrlQuery';
import { useParams } from 'react-router-dom'; 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 { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
@ -37,6 +42,26 @@ function DBCall(): JSX.Element {
const servicename = decodeURIComponent(encodedServiceName); const servicename = decodeURIComponent(encodedServiceName);
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0); const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
const { queries } = useResourceAttribute(); 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( const tagFilterItems: TagFilterItem[] = useMemo(
() => () =>
@ -150,6 +175,7 @@ function DBCall(): JSX.Element {
'database_call_rps', 'database_call_rps',
); );
}} }}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4} version={ENTITY_VERSION_V4}
/> />
</GraphContainer> </GraphContainer>
@ -185,6 +211,7 @@ function DBCall(): JSX.Element {
'database_call_avg_duration', 'database_call_avg_duration',
); );
}} }}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4} version={ENTITY_VERSION_V4}
/> />
</GraphContainer> </GraphContainer>

View File

@ -1,6 +1,7 @@
import { Col } from 'antd'; import { Col } from 'antd';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V4 } from 'constants/app'; import { ENTITY_VERSION_V4 } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import Graph from 'container/GridCardLayout/GridCard'; import Graph from 'container/GridCardLayout/GridCard';
import { import {
@ -14,8 +15,12 @@ import {
convertRawQueriesToTraceSelectedTags, convertRawQueriesToTraceSelectedTags,
resourceAttributesToTagFilterItems, resourceAttributesToTagFilterItems,
} from 'hooks/useResourceAttribute/utils'; } from 'hooks/useResourceAttribute/utils';
import { useEffect, useMemo, useRef, useState } from 'react'; import useUrlQuery from 'hooks/useUrlQuery';
import { useParams } from 'react-router-dom'; 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 { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
@ -40,6 +45,27 @@ function External(): JSX.Element {
const servicename = decodeURIComponent(encodedServiceName); const servicename = decodeURIComponent(encodedServiceName);
const { queries } = useResourceAttribute(); 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( const tagFilterItems = useMemo(
() => () =>
handleNonInQueryRange(resourceAttributesToTagFilterItems(queries)) || [], handleNonInQueryRange(resourceAttributesToTagFilterItems(queries)) || [],
@ -214,6 +240,7 @@ function External(): JSX.Element {
'external_call_error_percentage', 'external_call_error_percentage',
); );
}} }}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4} version={ENTITY_VERSION_V4}
/> />
</GraphContainer> </GraphContainer>
@ -249,6 +276,7 @@ function External(): JSX.Element {
'external_call_duration', 'external_call_duration',
); );
}} }}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4} version={ENTITY_VERSION_V4}
/> />
</GraphContainer> </GraphContainer>
@ -285,6 +313,7 @@ function External(): JSX.Element {
'external_call_rps_by_address', 'external_call_rps_by_address',
) )
} }
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4} version={ENTITY_VERSION_V4}
/> />
</GraphContainer> </GraphContainer>
@ -320,6 +349,7 @@ function External(): JSX.Element {
'external_call_duration_by_address', 'external_call_duration_by_address',
); );
}} }}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4} version={ENTITY_VERSION_V4}
/> />
</GraphContainer> </GraphContainer>

View File

@ -185,7 +185,7 @@ function Application(): JSX.Element {
urlQuery.set(QueryParams.startTime, startTimestamp.toString()); urlQuery.set(QueryParams.startTime, startTimestamp.toString());
urlQuery.set(QueryParams.endTime, endTimestamp.toString()); urlQuery.set(QueryParams.endTime, endTimestamp.toString());
const generatedUrl = `${pathname}?${urlQuery.toString()}`; const generatedUrl = `${pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl); history.push(generatedUrl);
if (startTimestamp !== endTimestamp) { if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp])); dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));

View File

@ -26,7 +26,9 @@ function MySettings(): JSX.Element {
label: ( label: (
<div className="theme-option"> <div className="theme-option">
<Sun size={12} data-testid="light-theme-icon" /> Light{' '} <Sun size={12} data-testid="light-theme-icon" /> Light{' '}
<Tag color="magenta">Beta</Tag> <Tag bordered={false} color="geekblue">
Beta
</Tag>
</div> </div>
), ),
value: 'light', value: 'light',

View File

@ -247,8 +247,7 @@ export default function Onboarding(): JSX.Element {
const handleNext = (): void => { const handleNext = (): void => {
if (activeStep <= 3) { if (activeStep <= 3) {
handleNextStep(); history.push(moduleRouteMap[selectedModule.id as ModulesMap]);
history.replace(moduleRouteMap[selectedModule.id as ModulesMap]);
} }
}; };
@ -258,6 +257,13 @@ export default function Onboarding(): JSX.Element {
updateSelectedDataSource(null); updateSelectedDataSource(null);
}; };
const handleBackNavigation = (): void => {
setCurrent(0);
setActiveStep(1);
setSelectedModule(useCases.APM);
resetProgress();
};
useEffect(() => { useEffect(() => {
const { pathname } = location; const { pathname } = location;
@ -277,9 +283,11 @@ export default function Onboarding(): JSX.Element {
} else if (pathname === ROUTES.GET_STARTED_AZURE_MONITORING) { } else if (pathname === ROUTES.GET_STARTED_AZURE_MONITORING) {
handleModuleSelect(useCases.AzureMonitoring); handleModuleSelect(useCases.AzureMonitoring);
handleNextStep(); handleNextStep();
} else {
handleBackNavigation();
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, [location.pathname]);
const [form] = Form.useForm<InviteMemberFormValues>(); const [form] = Form.useForm<InviteMemberFormValues>();
const [ const [

View File

@ -131,6 +131,11 @@ export default function DataSource(): JSX.Element {
}; };
const goToIntegrationsPage = (): void => { const goToIntegrationsPage = (): void => {
logEvent('Onboarding V2: Go to integrations', {
module: selectedModule?.id,
dataSource: selectedDataSource?.name,
framework: selectedFramework,
});
history.push(ROUTES.INTEGRATIONS); history.push(ROUTES.INTEGRATIONS);
}; };

View File

@ -11,13 +11,15 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Button, Space, Steps, Typography } from 'antd'; import { Button, Space, Steps, Typography } from 'antd';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { onboardingHelpMessage } from 'components/LaunchChatSupport/util';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { stepsMap } from 'container/OnboardingContainer/constants/stepsConfig'; import { stepsMap } from 'container/OnboardingContainer/constants/stepsConfig';
import { DataSourceType } from 'container/OnboardingContainer/Steps/DataSource/DataSource'; import { DataSourceType } from 'container/OnboardingContainer/Steps/DataSource/DataSource';
import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUtils'; import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUtils';
import history from 'lib/history'; import history from 'lib/history';
import { isEmpty, isNull } from 'lodash-es'; import { isEmpty, isNull } from 'lodash-es';
import { HelpCircle, UserPlus } from 'lucide-react'; import { UserPlus } from 'lucide-react';
import { SetStateAction, useState } from 'react'; import { SetStateAction, useState } from 'react';
import { useOnboardingContext } from '../../context/OnboardingContext'; import { useOnboardingContext } from '../../context/OnboardingContext';
@ -381,31 +383,6 @@ export default function ModuleStepsContainer({
history.push('/'); 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 ( return (
<div className="onboarding-module-steps"> <div className="onboarding-module-steps">
<div className="steps-container"> <div className="steps-container">
@ -493,19 +470,26 @@ Thanks
> >
Back Back
</Button> </Button>
<Button onClick={handleNext} type="primary" icon={<ArrowRightOutlined />}> <Button onClick={handleNext} type="primary" icon={<ArrowRightOutlined />}>
{current < lastStepIndex ? 'Continue to next step' : 'Done'} {current < lastStepIndex ? 'Continue to next step' : 'Done'}
</Button> </Button>
<LaunchChatSupport
<Button attributes={{
className="periscope-btn" dataSource: selectedDataSource?.id,
onClick={handleFacingIssuesClick} framework: selectedFramework,
danger environment: selectedEnvironment,
icon={<HelpCircle size={14} />} module: activeStep?.module?.id,
> step: activeStep?.step?.id,
Facing issues sending data to SigNoz? screen: 'Onboarding',
</Button> }}
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> </div>
</div> </div>

View File

@ -1,6 +1,6 @@
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { OptionsQuery } from './types'; import { FontSize, OptionsQuery } from './types';
export const URL_OPTIONS = 'options'; export const URL_OPTIONS = 'options';
@ -8,6 +8,7 @@ export const defaultOptionsQuery: OptionsQuery = {
selectColumns: [], selectColumns: [],
maxLines: 2, maxLines: 2,
format: 'list', format: 'list',
fontSize: FontSize.SMALL,
}; };
export const defaultTraceSelectedColumns = [ export const defaultTraceSelectedColumns = [
@ -18,6 +19,7 @@ export const defaultTraceSelectedColumns = [
isColumn: true, isColumn: true,
isJSON: false, isJSON: false,
id: 'serviceName--string--tag--true', id: 'serviceName--string--tag--true',
isIndexed: false,
}, },
{ {
key: 'name', key: 'name',
@ -26,6 +28,7 @@ export const defaultTraceSelectedColumns = [
isColumn: true, isColumn: true,
isJSON: false, isJSON: false,
id: 'name--string--tag--true', id: 'name--string--tag--true',
isIndexed: false,
}, },
{ {
key: 'durationNano', key: 'durationNano',
@ -34,6 +37,7 @@ export const defaultTraceSelectedColumns = [
isColumn: true, isColumn: true,
isJSON: false, isJSON: false,
id: 'durationNano--float64--tag--true', id: 'durationNano--float64--tag--true',
isIndexed: false,
}, },
{ {
key: 'httpMethod', key: 'httpMethod',
@ -42,6 +46,7 @@ export const defaultTraceSelectedColumns = [
isColumn: true, isColumn: true,
isJSON: false, isJSON: false,
id: 'httpMethod--string--tag--true', id: 'httpMethod--string--tag--true',
isIndexed: false,
}, },
{ {
key: 'responseStatusCode', key: 'responseStatusCode',
@ -50,5 +55,6 @@ export const defaultTraceSelectedColumns = [
isColumn: true, isColumn: true,
isJSON: false, isJSON: false,
id: 'responseStatusCode--string--tag--true', 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 { LogViewMode } from 'container/LogsTable';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; 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 { export interface OptionsQuery {
selectColumns: BaseAutocompleteData[]; selectColumns: BaseAutocompleteData[];
maxLines: number; maxLines: number;
format: LogViewMode; format: LogViewMode;
fontSize: FontSize;
} }
export interface InitialOptions export interface InitialOptions
@ -18,6 +29,7 @@ export type OptionsMenuConfig = {
onChange: (value: LogViewMode) => void; onChange: (value: LogViewMode) => void;
}; };
maxLines?: Pick<InputNumberProps, 'value' | 'onChange'>; maxLines?: Pick<InputNumberProps, 'value' | 'onChange'>;
fontSize?: FontSizeProps;
addColumn?: Pick< addColumn?: Pick<
SelectProps, SelectProps,
'options' | 'onSelect' | 'onFocus' | 'onSearch' | 'onBlur' 'options' | 'onSelect' | 'onFocus' | 'onSearch' | 'onBlur'

View File

@ -7,6 +7,10 @@ import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import useDebounce from 'hooks/useDebounce'; import useDebounce from 'hooks/useDebounce';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import useUrlQueryData from 'hooks/useUrlQueryData'; import useUrlQueryData from 'hooks/useUrlQueryData';
import {
AllTraceFilterKeys,
AllTraceFilterKeyValue,
} from 'pages/TracesExplorer/Filter/filterUtils';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueries } from 'react-query'; import { useQueries } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
@ -21,7 +25,12 @@ import {
defaultTraceSelectedColumns, defaultTraceSelectedColumns,
URL_OPTIONS, URL_OPTIONS,
} from './constants'; } from './constants';
import { InitialOptions, OptionsMenuConfig, OptionsQuery } from './types'; import {
FontSize,
InitialOptions,
OptionsMenuConfig,
OptionsQuery,
} from './types';
import { getOptionsFromKeys } from './utils'; import { getOptionsFromKeys } from './utils';
interface UseOptionsMenuProps { interface UseOptionsMenuProps {
@ -106,15 +115,40 @@ const useOptionsMenu = ({
[] as BaseAutocompleteData[], [] as BaseAutocompleteData[],
); );
return ( let initialSelected = initialOptions.selectColumns
(initialOptions.selectColumns
?.map((column) => attributesData.find(({ key }) => key === column)) ?.map((column) => attributesData.find(({ key }) => key === column))
.filter(Boolean) as BaseAutocompleteData[]) || [] .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, isFetchedInitialAttributes,
initialOptions?.selectColumns, initialOptions?.selectColumns,
initialAttributesResult, initialAttributesResult,
dataSource,
]); ]);
const { const {
@ -248,6 +282,17 @@ const useOptionsMenu = ({
}, },
[handleRedirectWithOptionsData, optionsQueryData], [handleRedirectWithOptionsData, optionsQueryData],
); );
const handleFontSizeChange = useCallback(
(value: FontSize) => {
const optionsData: OptionsQuery = {
...optionsQueryData,
fontSize: value,
};
handleRedirectWithOptionsData(optionsData);
},
[handleRedirectWithOptionsData, optionsQueryData],
);
const handleSearchAttribute = useCallback((value: string) => { const handleSearchAttribute = useCallback((value: string) => {
setSearchText(value); setSearchText(value);
@ -282,18 +327,24 @@ const useOptionsMenu = ({
value: optionsQueryData.maxLines || defaultOptionsQuery.maxLines, value: optionsQueryData.maxLines || defaultOptionsQuery.maxLines,
onChange: handleMaxLinesChange, onChange: handleMaxLinesChange,
}, },
fontSize: {
value: optionsQueryData?.fontSize || defaultOptionsQuery.fontSize,
onChange: handleFontSizeChange,
},
}), }),
[ [
optionsFromAttributeKeys,
optionsQueryData?.maxLines,
optionsQueryData?.format,
optionsQueryData?.selectColumns,
isSearchedAttributesFetching, isSearchedAttributesFetching,
handleSearchAttribute, optionsQueryData?.selectColumns,
optionsQueryData.format,
optionsQueryData.maxLines,
optionsQueryData?.fontSize,
optionsFromAttributeKeys,
handleSelectColumns, handleSelectColumns,
handleRemoveSelectedColumn, handleRemoveSelectedColumn,
handleSearchAttribute,
handleFormatChange, handleFormatChange,
handleMaxLinesChange, handleMaxLinesChange,
handleFontSizeChange,
], ],
); );

View File

@ -15,6 +15,7 @@ function PanelWrapper({
onDragSelect, onDragSelect,
selectedGraph, selectedGraph,
tableProcessedDataRef, tableProcessedDataRef,
customTooltipElement,
}: PanelWrapperProps): JSX.Element { }: PanelWrapperProps): JSX.Element {
const Component = PanelTypeVsPanelWrapper[ const Component = PanelTypeVsPanelWrapper[
selectedGraph || widget.panelTypes selectedGraph || widget.panelTypes
@ -37,6 +38,7 @@ function PanelWrapper({
onDragSelect={onDragSelect} onDragSelect={onDragSelect}
selectedGraph={selectedGraph} selectedGraph={selectedGraph}
tableProcessedDataRef={tableProcessedDataRef} tableProcessedDataRef={tableProcessedDataRef}
customTooltipElement={customTooltipElement}
/> />
); );
} }

View File

@ -30,6 +30,7 @@ function UplotPanelWrapper({
onClickHandler, onClickHandler,
onDragSelect, onDragSelect,
selectedGraph, selectedGraph,
customTooltipElement,
}: PanelWrapperProps): JSX.Element { }: PanelWrapperProps): JSX.Element {
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard(); const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
@ -126,6 +127,7 @@ function UplotPanelWrapper({
stackBarChart: widget?.stackedBarChart, stackBarChart: widget?.stackedBarChart,
hiddenGraph, hiddenGraph,
setHiddenGraph, setHiddenGraph,
customTooltipElement,
}), }),
[ [
widget?.id, widget?.id,
@ -147,6 +149,7 @@ function UplotPanelWrapper({
selectedGraph, selectedGraph,
currentQuery, currentQuery,
hiddenGraph, hiddenGraph,
customTooltipElement,
], ],
); );

Some files were not shown because too many files have changed in this diff Show More