Compare commits

...

45 Commits

Author SHA1 Message Date
Vikrant Gupta
706f967246 chore: add extra safety nets in case of malformed URL (#5767) 2024-08-23 22:29:07 +05:30
Shivanshu Raj Shrivastava
1685f0e74f Merge pull request #5766 from shivanshuraj1333/patch-for-ee
patch for ee
2024-08-23 21:57:09 +05:30
shivanshu
74162456e5 chore: patch for ee 2024-08-23 21:45:09 +05:30
Srikanth Chekuri
b798518aa9 chore: add total count and state filter (#5745) 2024-08-23 21:13:00 +05:30
thesnallygaster
d7fd1d032b feat: Add option to change max_execution_time setting for clickhouse … (#5683) 2024-08-23 20:11:49 +05:30
Vikrant Gupta
a2ac49bfc2 fix: remove same origin check and return proper errors from upgrader function (#5724)
* chore: test websockets

* chore: test websockets

* chore: test websockets

* chore: test websockets

* chore: cleanup PR

* chore: added back check origin function and loggings to check why the mismatch

* chore: the uuid should update on every new request

* chore: experiment with delaying the query deletion

* chore: experiment with delaying the query deletion

* chore: experiment with delaying the query deletion

* chore: upgrade nginx conf

* chore: upgrade nginx conf

* chore: upgrade nginx conf

* chore: experiment with delaying the query deletion

* chore: cleanup PR

* chore: remove print statements
2024-08-23 17:37:56 +05:30
Sudeep MP
33541a2ac0 feat: add view templates option to dashboard menu (#5696)
* feat: add view templates option to dashboard menu

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

---------

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

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

Add `render` package

#### Related Issues / PR's

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

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

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

* fix: build issues and logs context

* chore: fix build issues

* feat: scale all the spaces

* chore: handle light mode designs

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

Add a web package for serving frontend

#### Related Issues / PR's

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

Add errors package

#### Related Issues / PR's

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

Add packages for Registry and HTTP

#### Related Issues / PR's

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

* feat: added trace filter test cases - initial render

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

* feat: deleted mock-data files

* feat: added test cases of undefined filters and items

* feat: deleted tsconfig

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

* feat: added collapse and uncollapse test for traces filters

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

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

#### Related Issues / PR's

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

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

* feat: setup the framework and necessary states needed

* feat: cover the happy path of selects

* chore: forward typing flow handled

* chore: add antd select

* chore: add antd select

* chore: handle forward and backward flows

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

* feat: handle tag on blur and body contains changes

* feat: handle tag deselect

* feat: multi tag handling

* feat: multi tag handling

* fix: jest test cases

* chore: update the key

* chore: add edit tag not working as expected

* feat: handle cases for exists and nexists

* fix: handle has / nhas operators

* chore: fix usability issues

* chore: remove the usage for the new bar

* fix: flaky build issues

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

* feat: query search new ui

* feat: suggestions changes in v2

* feat: dropdown and tags ui touch up

* feat: added missing keyboard shortcuts

* fix: race condition issues

* feat: remove usage

* fix: operator select fix

* fix: handle example queries click changes

* chore: design sync

* chore: handle boolean selects

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

* fix: handle local env better

* chore: update the websocket endpoint

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

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

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

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

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

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

This reverts commit 5bb837ec27.

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

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

* chore: remove unwanted files

* fix: work on random id rather than hash

* fix: improve the icons and design

* feat: webpack and docker file changes

* fix: test cases

* chore: format the units

* chore: address review comments

* chore: update the id to uuid package

* fix: build issues

* chore: remove docker file changes

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

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

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

* chore: added the handlers for example queries

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

* feat: handle changes for cancel query

* chore: remove stupid concept of option group

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

* chore: minor css changes and remove transitions

* feat: integrate suggestions api and control re-renders

* feat: added keyboard shortcuts for the dropdown

* fix: design cleanups and touchups

* fix: build issues and tests

* chore: extra safety check for base64 and fix tests

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

* chore: some code comments

* chore: some code comments

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

* chore: address minor designs

* chore: update the keyboard shortcut to cmd+/

* feat: correct the option render for logs for tooltip

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

* fix: update the spacing and icon for search bar

* chore: address review comments
2024-08-16 13:11:39 +05:30
Yunus M
1308f0f15f feat: move chat support behind paywall (#5673)
* feat: move chat support behind paywall

* feat: wire up chat support paywall

* feat: move chat support code from app layout to separate component

* feat: add log events
2024-08-14 20:50:35 +05:30
Raj Kamal Singh
6c634b99d0 Feat: QS: query range progress api (#5671)
* feat: get query progress tracker started

* feat: flesh out query progress test some more and get first few assertions passing

* chore: flesh out query tracker tests and impl some more

* feat: add impl for QueryTracker.Subscribe

* feat: send latest update if available on subscription

* feat: broadcast query progress to all subscribers on update

* feat: finish plumbing query tracker happy path

* feat: finish with v0 impl for query progress tracker

* chore: some cleanup of query progress tracker

* feat: hook up query progress tracking for queryRangeV3

* feat: impl for query progress websocket API handler

* feat: implement Hijacker iface for loggingResponseWriter for websocket upgrades

* chore: some cleanup to query progress websocket API handler

* chore: some cleanup

* chore: move query progress impl into its own subpackage

* chore: separate in-memory tracker impl from interface

* chore: some more cleanup of in memory tracker

* chore: some more cleanup of query progress tracker

* chore: some final cleanups
2024-08-14 19:53:36 +05:30
Yunus M
9856335840 fix: hide beta icon in sidebar collapsed view (#5693) 2024-08-14 18:01:29 +05:30
Yunus M
e85b405396 fix: dashboards listing and details page css fixes (#5672)
* fix: dashboards listing and details page css fixes

* chore: remove commented code
2024-08-14 12:22:15 +05:30
Vishal Sharma
e2e965bc7f chore: zeus features (#5686)
* chore: zeus features

* chore: add tests and improve logging
2024-08-14 10:10:33 +05:30
Srikanth Chekuri
7811fdd17a fix: nil pointer dereference for empty payload (#5680) 2024-08-12 21:43:38 +05:30
Yunus M
0dca1237b9 feat: add beta tag for service map and light mode (#5674)
* feat: add beta tag for service map and light mode

* chore: update test case
2024-08-12 16:41:47 +05:30
Vikrant Gupta
f3d73f6d44 feat: use selected columns as pinned attributes (#5601)
* feat: use selected columns as pinned attributes

* chore: handle nested json structs

* chore: refactor and fix build issues

* feat: handle changes for dashboard list panel

* chore: remove console logs
2024-08-12 16:34:43 +05:30
Vikrant Gupta
187927403a fix: clean out the panel type change attribute dependency (#5648)
* fix: clean out the panel type change attribute dependency

* fix: clean out the updater function as well

* fix: issue with rendering list panel as first and then moving around
2024-08-11 16:46:18 +05:30
Srikanth Chekuri
0157b47424 chore: add empty labels to response (#5668) 2024-08-09 18:19:15 +05:30
Srikanth Chekuri
156905afc7 fix: send alert default annotations for missing data alert (#5315) 2024-08-09 15:31:39 +05:30
221 changed files with 9928 additions and 1199 deletions

6
.github/CODEOWNERS vendored
View File

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

3
.gitignore vendored
View File

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

View File

@@ -1,3 +1,8 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 3301;
server_name _;
@@ -42,6 +47,14 @@ server {
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
#
error_page 500 502 503 504 /50x.html;

View File

@@ -1,17 +1,48 @@
package api
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
"go.signoz.io/signoz/ee/query-service/constants"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.uber.org/zap"
)
func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
featureSet, err := ah.FF().GetFeatureFlags()
if err != nil {
ah.HandleError(w, err, http.StatusInternalServerError)
return
}
if constants.FetchFeatures == "true" {
zap.L().Debug("fetching license")
license, err := ah.LM().GetRepo().GetActiveLicense(ctx)
if err != nil {
zap.L().Error("failed to fetch license", zap.Error(err))
} else if license == nil {
zap.L().Debug("no active license found")
} else {
licenseKey := license.Key
zap.L().Debug("fetching zeus features")
zeusFeatures, err := fetchZeusFeatures(constants.ZeusFeaturesURL, licenseKey)
if err == nil {
zap.L().Debug("fetched zeus features", zap.Any("features", zeusFeatures))
// merge featureSet and zeusFeatures in featureSet with higher priority to zeusFeatures
featureSet = MergeFeatureSets(zeusFeatures, featureSet)
} else {
zap.L().Error("failed to fetch zeus features", zap.Error(err))
}
}
}
if ah.opts.PreferSpanMetrics {
for idx := range featureSet {
feature := &featureSet[idx]
@@ -20,5 +51,96 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
}
}
}
ah.Respond(w, featureSet)
}
// fetchZeusFeatures makes an HTTP GET request to the /zeusFeatures endpoint
// and returns the FeatureSet.
func fetchZeusFeatures(url, licenseKey string) (basemodel.FeatureSet, error) {
// Check if the URL is empty
if url == "" {
return nil, fmt.Errorf("url is empty")
}
// Check if the licenseKey is empty
if licenseKey == "" {
return nil, fmt.Errorf("licenseKey is empty")
}
// Creating an HTTP client with a timeout for better control
client := &http.Client{
Timeout: 10 * time.Second,
}
// Creating a new GET request
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Setting the custom header
req.Header.Set("X-Signoz-Cloud-Api-Key", licenseKey)
// Making the GET request
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to make GET request: %w", err)
}
defer func() {
if resp != nil {
resp.Body.Close()
}
}()
// Check for non-OK status code
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d %s", errors.New("received non-OK HTTP status code"), resp.StatusCode, http.StatusText(resp.StatusCode))
}
// Reading and decoding the response body
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
var zeusResponse ZeusFeaturesResponse
if err := json.Unmarshal(body, &zeusResponse); err != nil {
return nil, fmt.Errorf("%w: %v", errors.New("failed to decode response body"), err)
}
if zeusResponse.Status != "success" {
return nil, fmt.Errorf("%w: %s", errors.New("failed to fetch zeus features"), zeusResponse.Status)
}
return zeusResponse.Data, nil
}
type ZeusFeaturesResponse struct {
Status string `json:"status"`
Data basemodel.FeatureSet `json:"data"`
}
// MergeFeatureSets merges two FeatureSet arrays with precedence to zeusFeatures.
func MergeFeatureSets(zeusFeatures, internalFeatures basemodel.FeatureSet) basemodel.FeatureSet {
// Create a map to store the merged features
featureMap := make(map[string]basemodel.Feature)
// Add all features from the otherFeatures set to the map
for _, feature := range internalFeatures {
featureMap[feature.Name] = feature
}
// Add all features from the zeusFeatures set to the map
// If a feature already exists (i.e., same name), the zeusFeature will overwrite it
for _, feature := range zeusFeatures {
featureMap[feature.Name] = feature
}
// Convert the map back to a FeatureSet slice
var mergedFeatures basemodel.FeatureSet
for _, feature := range featureMap {
mergedFeatures = append(mergedFeatures, feature)
}
return mergedFeatures
}

View File

@@ -0,0 +1,88 @@
package api
import (
"testing"
"github.com/stretchr/testify/assert"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
)
func TestMergeFeatureSets(t *testing.T) {
tests := []struct {
name string
zeusFeatures basemodel.FeatureSet
internalFeatures basemodel.FeatureSet
expected basemodel.FeatureSet
}{
{
name: "empty zeusFeatures and internalFeatures",
zeusFeatures: basemodel.FeatureSet{},
internalFeatures: basemodel.FeatureSet{},
expected: basemodel.FeatureSet{},
},
{
name: "non-empty zeusFeatures and empty internalFeatures",
zeusFeatures: basemodel.FeatureSet{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
internalFeatures: basemodel.FeatureSet{},
expected: basemodel.FeatureSet{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
},
{
name: "empty zeusFeatures and non-empty internalFeatures",
zeusFeatures: basemodel.FeatureSet{},
internalFeatures: basemodel.FeatureSet{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
expected: basemodel.FeatureSet{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
},
{
name: "non-empty zeusFeatures and non-empty internalFeatures with no conflicts",
zeusFeatures: basemodel.FeatureSet{
{Name: "Feature1", Active: true},
{Name: "Feature3", Active: false},
},
internalFeatures: basemodel.FeatureSet{
{Name: "Feature2", Active: true},
{Name: "Feature4", Active: false},
},
expected: basemodel.FeatureSet{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: true},
{Name: "Feature3", Active: false},
{Name: "Feature4", Active: false},
},
},
{
name: "non-empty zeusFeatures and non-empty internalFeatures with conflicts",
zeusFeatures: basemodel.FeatureSet{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
internalFeatures: basemodel.FeatureSet{
{Name: "Feature1", Active: false},
{Name: "Feature3", Active: true},
},
expected: basemodel.FeatureSet{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
{Name: "Feature3", Active: true},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual := MergeFeatureSets(test.zeusFeatures, test.internalFeatures)
assert.ElementsMatch(t, test.expected, actual)
})
}
}

View File

@@ -1,6 +1,7 @@
package app
import (
"bufio"
"bytes"
"context"
"encoding/json"
@@ -317,7 +318,7 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
// ip here for alert manager
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "DELETE", "POST", "PUT", "PATCH"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "SIGNOZ-API-KEY"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "SIGNOZ-API-KEY", "X-SIGNOZ-QUERY-ID", "Sec-WebSocket-Protocol"},
})
handler := c.Handler(r)
@@ -358,11 +359,13 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
apiHandler.RegisterIntegrationRoutes(r, am)
apiHandler.RegisterQueryRangeV3Routes(r, am)
apiHandler.RegisterQueryRangeV4Routes(r, am)
apiHandler.RegisterWebSocketPaths(r, am)
apiHandler.RegisterMessagingQueuesRoutes(r, am)
c := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "DELETE", "POST", "PUT", "PATCH", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "cache-control"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "cache-control", "X-SIGNOZ-QUERY-ID", "Sec-WebSocket-Protocol"},
})
handler := c.Handler(r)
@@ -374,6 +377,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
}, nil
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
// loggingMiddleware is used for logging public api calls
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -385,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
// from internal services like alert manager
func loggingMiddlewarePrivate(next http.Handler) http.Handler {
@@ -397,27 +402,41 @@ func loggingMiddlewarePrivate(next http.Handler) http.Handler {
})
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
type loggingResponseWriter struct {
http.ResponseWriter
statusCode int
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter {
// WriteHeader(int) is not called if our response implicitly returns 200 OK, so
// we default to that status code.
return &loggingResponseWriter{w, http.StatusOK}
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
func (lrw *loggingResponseWriter) WriteHeader(code int) {
lrw.statusCode = code
lrw.ResponseWriter.WriteHeader(code)
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
// Flush implements the http.Flush interface.
func (lrw *loggingResponseWriter) Flush() {
lrw.ResponseWriter.(http.Flusher).Flush()
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
// Support websockets
func (lrw *loggingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
h, ok := lrw.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, errors.New("hijack not supported")
}
return h.Hijack()
}
func extractQueryRangeData(path string, r *http.Request) (map[string]interface{}, bool) {
pathToExtractBodyFromV3 := "/api/v3/query_range"
pathToExtractBodyFromV4 := "/api/v4/query_range"
@@ -554,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 {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

View File

@@ -13,6 +13,8 @@ var LicenseAPIKey = GetOrDefaultEnv("SIGNOZ_LICENSE_API_KEY", "")
var SaasSegmentKey = GetOrDefaultEnv("SIGNOZ_SAAS_SEGMENT_KEY", "")
var SpanRenderLimitStr = GetOrDefaultEnv("SPAN_RENDER_LIMIT", "2500")
var MaxSpansInTraceStr = GetOrDefaultEnv("MAX_SPANS_IN_TRACE", "250000")
var FetchFeatures = GetOrDefaultEnv("FETCH_FEATURES", "false")
var ZeusFeaturesURL = GetOrDefaultEnv("ZEUS_FEATURES_URL", "ZeusFeaturesURL")
func GetOrDefaultEnv(key string, fallback string) string {
v := os.Getenv(key)

View File

@@ -147,7 +147,7 @@ func (lm *Manager) GetLicenses(ctx context.Context) (response []model.License, a
for _, l := range licenses {
l.ParsePlan()
if l.Key == lm.activeLicense.Key {
if lm.activeLicense != nil && l.Key == lm.activeLicense.Key {
l.IsCurrent = true
}

View File

@@ -12,6 +12,7 @@ const DisableUpsell = "DISABLE_UPSELL"
const Onboarding = "ONBOARDING"
const ChatSupport = "CHAT_SUPPORT"
const Gateway = "GATEWAY"
const PremiumSupport = "PREMIUM_SUPPORT"
var BasicPlan = basemodel.FeatureSet{
basemodel.Feature{
@@ -119,6 +120,13 @@ var BasicPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: PremiumSupport,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}
var ProPlan = basemodel.FeatureSet{
@@ -220,6 +228,13 @@ var ProPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: PremiumSupport,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}
var EnterprisePlan = basemodel.FeatureSet{
@@ -335,4 +350,11 @@ var EnterprisePlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: PremiumSupport,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}

View File

@@ -0,0 +1 @@
<svg width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#prefix__clip0_4344_1236)" stroke="#C0C1C3" stroke-width="1.167" stroke-linecap="round" stroke-linejoin="round"><path d="M4.667 1.167H2.333c-.644 0-1.166.522-1.166 1.166v2.334c0 .644.522 1.166 1.166 1.166h2.334c.644 0 1.166-.522 1.166-1.166V2.333c0-.644-.522-1.166-1.166-1.166zM8.167 1.167a1.17 1.17 0 011.166 1.166v2.334a1.17 1.17 0 01-1.166 1.166M11.667 1.167a1.17 1.17 0 011.166 1.166v2.334a1.17 1.17 0 01-1.166 1.166M5.833 10.5H2.917c-.992 0-1.75-.758-1.75-1.75v-.583"/><path d="M4.083 12.25l1.75-1.75-1.75-1.75M11.667 8.167H9.333c-.644 0-1.166.522-1.166 1.166v2.334c0 .644.522 1.166 1.166 1.166h2.334c.644 0 1.166-.522 1.166-1.166V9.333c0-.644-.522-1.166-1.166-1.166z"/></g><defs><clipPath id="prefix__clip0_4344_1236"><path fill="#fff" d="M0 0h14v14H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 878 B

View File

@@ -0,0 +1 @@
<svg width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#prefix__clip0_4062_7291)" stroke-width="1.167" stroke-linecap="round" stroke-linejoin="round"><path d="M7 12.833A5.833 5.833 0 107 1.167a5.833 5.833 0 000 11.666z" fill="#E5484D" stroke="#E5484D"/><path d="M8.75 5.25l-3.5 3.5M5.25 5.25l3.5 3.5" stroke="#121317"/></g><defs><clipPath id="prefix__clip0_4062_7291"><path fill="#fff" d="M0 0h14v14H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 467 B

View File

@@ -1,6 +1,7 @@
{
"create_dashboard": "Create Dashboard",
"import_json": "Import Dashboard JSON",
"view_template": "View templates",
"import_grafana_json": "Import Grafana JSON",
"copy_to_clipboard": "Copy To ClipBoard",
"download_json": "Download JSON",

View File

@@ -66,6 +66,14 @@ function App(): JSX.Element {
allFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)?.active ||
false;
const isPremiumSupportEnabled =
allFlags.find((flag) => flag.name === FeatureKeys.PREMIUM_SUPPORT)?.active ||
false;
const showAddCreditCardModal =
!isPremiumSupportEnabled &&
!licenseData?.payload?.trialConvertedToSubscription;
dispatch({
type: UPDATE_FEATURE_FLAG_RESPONSE,
payload: {
@@ -82,7 +90,7 @@ function App(): JSX.Element {
setRoutes(newRoutes);
}
if (isLoggedInState && isChatSupportEnabled) {
if (isLoggedInState && isChatSupportEnabled && !showAddCreditCardModal) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.Intercom('boot', {

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 { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
import store from 'store';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
Props,
@@ -11,7 +13,26 @@ const dashboardVariablesQuery = async (
props: Props,
): Promise<SuccessResponse<VariableResponseProps> | ErrorResponse> => {
try {
const response = await axios.post(`/variables/query`, props);
const { globalTime } = store.getState();
const { start, end } = getStartEndRangeTime({
type: 'GLOBAL_TIME',
interval: globalTime.selectedTime,
});
const timeVariables: Record<string, number> = {
start_timestamp_ms: parseInt(start, 10) * 1e3,
end_timestamp_ms: parseInt(end, 10) * 1e3,
start_timestamp_nano: parseInt(start, 10) * 1e9,
end_timestamp_nano: parseInt(end, 10) * 1e9,
start_timestamp: parseInt(start, 10),
end_timestamp: parseInt(end, 10),
};
const payload = { ...props };
payload.variables = { ...payload.variables, ...timeVariables };
const response = await axios.post(`/variables/query`, payload);
return {
statusCode: 200,

View File

@@ -12,10 +12,13 @@ export const getMetricsQueryRange = async (
props: QueryRangePayload,
version: string,
signal: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => {
try {
if (version && version === ENTITY_VERSION_V4) {
const response = await ApiV4Instance.post('/query_range', props, { signal });
const response = await ApiV4Instance.post('/query_range', props, {
signal,
});
return {
statusCode: 200,
@@ -26,7 +29,10 @@ export const getMetricsQueryRange = async (
};
}
const response = await ApiV3Instance.post('/query_range', props, { signal });
const response = await ApiV3Instance.post('/query_range', props, {
signal,
headers,
});
return {
statusCode: 200,

View File

@@ -0,0 +1,63 @@
import { ApiV3Instance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError, AxiosResponse } from 'axios';
import { baseAutoCompleteIdKeysOrder } from 'constants/queryBuilder';
import { encode } from 'js-base64';
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
import createQueryParams from 'lib/createQueryParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
IGetAttributeSuggestionsPayload,
IGetAttributeSuggestionsSuccessResponse,
} from 'types/api/queryBuilder/getAttributeSuggestions';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
export const getAttributeSuggestions = async ({
searchText,
dataSource,
filters,
}: IGetAttributeSuggestionsPayload): Promise<
SuccessResponse<IGetAttributeSuggestionsSuccessResponse> | ErrorResponse
> => {
try {
let base64EncodedFiltersString;
try {
// the replace function is to remove the padding at the end of base64 encoded string which is auto added to make it a multiple of 4
// why ? because the current working of qs doesn't work well with padding
base64EncodedFiltersString = encode(JSON.stringify(filters)).replace(
/=+$/,
'',
);
} catch {
// default base64 encoded string for empty filters object
base64EncodedFiltersString = 'eyJpdGVtcyI6W10sIm9wIjoiQU5EIn0';
}
const response: AxiosResponse<{
data: IGetAttributeSuggestionsSuccessResponse;
}> = await ApiV3Instance.get(
`/filter_suggestions?${createQueryParams({
searchText,
dataSource,
existingFilter: base64EncodedFiltersString,
})}`,
);
const payload: BaseAutocompleteData[] =
response.data.data.attributes?.map(({ id: _, ...item }) => ({
...item,
id: createIdFromObjectFields(item, baseAutoCompleteIdKeysOrder),
})) || [];
return {
statusCode: 200,
error: null,
message: response.statusText,
payload: {
attributes: payload,
example_queries: response.data.data.example_queries,
},
};
} catch (e) {
return ErrorResponseHandler(e as AxiosError);
}
};

View File

@@ -0,0 +1,27 @@
import { Color } from '@signozhq/design-tokens';
import { useIsDarkMode } from 'hooks/useDarkMode';
function GroupByIcon(): JSX.Element {
const isDarkMode = useIsDarkMode();
return (
<svg width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g
clipPath="url(#prefix__clip0_4344_1236)"
stroke={isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500}
strokeWidth="1.167"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M4.667 1.167H2.333c-.644 0-1.166.522-1.166 1.166v2.334c0 .644.522 1.166 1.166 1.166h2.334c.644 0 1.166-.522 1.166-1.166V2.333c0-.644-.522-1.166-1.166-1.166zM8.167 1.167a1.17 1.17 0 011.166 1.166v2.334a1.17 1.17 0 01-1.166 1.166M11.667 1.167a1.17 1.17 0 011.166 1.166v2.334a1.17 1.17 0 01-1.166 1.166M5.833 10.5H2.917c-.992 0-1.75-.758-1.75-1.75v-.583" />
<path d="M4.083 12.25l1.75-1.75-1.75-1.75M11.667 8.167H9.333c-.644 0-1.166.522-1.166 1.166v2.334c0 .644.522 1.166 1.166 1.166h2.334c.644 0 1.166-.522 1.166-1.166V9.333c0-.644-.522-1.166-1.166-1.166z" />
</g>
<defs>
<clipPath id="prefix__clip0_4344_1236">
<path fill="#fff" d="M0 0h14v14H0z" />
</clipPath>
</defs>
</svg>
);
}
export default GroupByIcon;

View File

@@ -0,0 +1,137 @@
import { Button, Modal, Typography } from 'antd';
import updateCreditCardApi from 'api/billing/checkout';
import logEvent from 'api/common/logEvent';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import { CreditCard, X } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useMutation } from 'react-query';
import { useLocation } from 'react-router-dom';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { License } from 'types/api/licenses/def';
export default function ChatSupportGateway(): JSX.Element {
const { notifications } = useNotifications();
const [activeLicense, setActiveLicense] = useState<License | null>(null);
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] = useState(
false,
);
const { data: licenseData, isFetching } = useLicense();
useEffect(() => {
const activeValidLicense =
licenseData?.payload?.licenses?.find(
(license) => license.isCurrent === true,
) || null;
setActiveLicense(activeValidLicense);
}, [licenseData, isFetching]);
const handleBillingOnSuccess = (
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
): void => {
if (data?.payload?.redirectURL) {
const newTab = document.createElement('a');
newTab.href = data.payload.redirectURL;
newTab.target = '_blank';
newTab.rel = 'noopener noreferrer';
newTab.click();
}
};
const handleBillingOnError = (): void => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
};
const { mutate: updateCreditCard, isLoading: isLoadingBilling } = useMutation(
updateCreditCardApi,
{
onSuccess: (data) => {
handleBillingOnSuccess(data);
},
onError: handleBillingOnError,
},
);
const { pathname } = useLocation();
const handleAddCreditCard = (): void => {
logEvent('Add Credit card modal: Clicked', {
source: `intercom icon`,
page: pathname,
});
updateCreditCard({
licenseKey: activeLicense?.key || '',
successURL: window.location.href,
cancelURL: window.location.href,
});
};
return (
<>
<div className="chat-support-gateway">
<Button
className="chat-support-gateway-btn"
onClick={(): void => {
logEvent('Disabled Chat Support: Clicked', {
source: `intercom icon`,
page: pathname,
});
setIsAddCreditCardModalOpen(true);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 28 32"
className="chat-support-gateway-btn-icon"
>
<path d="M28 32s-4.714-1.855-8.527-3.34H3.437C1.54 28.66 0 27.026 0 25.013V3.644C0 1.633 1.54 0 3.437 0h21.125c1.898 0 3.437 1.632 3.437 3.645v18.404H28V32zm-4.139-11.982a.88.88 0 00-1.292-.105c-.03.026-3.015 2.681-8.57 2.681-5.486 0-8.517-2.636-8.571-2.684a.88.88 0 00-1.29.107 1.01 1.01 0 00-.219.708.992.992 0 00.318.664c.142.128 3.537 3.15 9.762 3.15 6.226 0 9.621-3.022 9.763-3.15a.992.992 0 00.317-.664 1.01 1.01 0 00-.218-.707z" />
</svg>
</Button>
</div>
{/* Add Credit Card Modal */}
<Modal
className="add-credit-card-modal"
title={<span className="title">Add Credit Card for Chat Support</span>}
open={isAddCreditCardModalOpen}
closable
onCancel={(): void => setIsAddCreditCardModalOpen(false)}
destroyOnClose
footer={[
<Button
key="cancel"
onClick={(): void => setIsAddCreditCardModalOpen(false)}
className="cancel-btn"
icon={<X size={16} />}
>
Cancel
</Button>,
<Button
key="submit"
type="primary"
icon={<CreditCard size={16} />}
size="middle"
loading={isLoadingBilling}
disabled={isLoadingBilling}
onClick={handleAddCreditCard}
className="add-credit-card-btn"
>
Add Credit Card
</Button>,
]}
>
<Typography.Text className="add-credit-card-text">
You&apos;re currently on <span className="highlight-text">Trial plan</span>
. Add a credit card to access SigNoz chat support to your workspace.
</Typography.Text>
</Modal>
</>
);
}

View File

@@ -0,0 +1,191 @@
import './LaunchChatSupport.styles.scss';
import { Button, Modal, Tooltip, Typography } from 'antd';
import updateCreditCardApi from 'api/billing/checkout';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { FeatureKeys } from 'constants/features';
import useFeatureFlags from 'hooks/useFeatureFlag';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import { defaultTo } from 'lodash-es';
import { CreditCard, HelpCircle, X } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useMutation } from 'react-query';
import { useLocation } from 'react-router-dom';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { License } from 'types/api/licenses/def';
import { isCloudUser } from 'utils/app';
export interface LaunchChatSupportProps {
eventName: string;
attributes: Record<string, unknown>;
message?: string;
buttonText?: string;
className?: string;
onHoverText?: string;
intercomMessageDisabled?: boolean;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
function LaunchChatSupport({
attributes,
eventName,
message = '',
buttonText = '',
className = '',
onHoverText = '',
intercomMessageDisabled = false,
}: LaunchChatSupportProps): JSX.Element | null {
const isChatSupportEnabled = useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active;
const isCloudUserVal = isCloudUser();
const { notifications } = useNotifications();
const { data: licenseData, isFetching } = useLicense();
const [activeLicense, setActiveLicense] = useState<License | null>(null);
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] = useState(
false,
);
const { pathname } = useLocation();
const isPremiumChatSupportEnabled =
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
const showAddCreditCardModal =
!isPremiumChatSupportEnabled &&
!licenseData?.payload?.trialConvertedToSubscription;
useEffect(() => {
const activeValidLicense =
licenseData?.payload?.licenses?.find(
(license) => license.isCurrent === true,
) || null;
setActiveLicense(activeValidLicense);
}, [licenseData, isFetching]);
const handleFacingIssuesClick = (): void => {
if (showAddCreditCardModal) {
logEvent('Disabled Chat Support: Clicked', {
source: `facing issues button`,
page: pathname,
...attributes,
});
setIsAddCreditCardModalOpen(true);
} else {
logEvent(eventName, attributes);
if (window.Intercom && !intercomMessageDisabled) {
window.Intercom('showNewMessage', defaultTo(message, ''));
}
}
};
const handleBillingOnSuccess = (
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
): void => {
if (data?.payload?.redirectURL) {
const newTab = document.createElement('a');
newTab.href = data.payload.redirectURL;
newTab.target = '_blank';
newTab.rel = 'noopener noreferrer';
newTab.click();
}
};
const handleBillingOnError = (): void => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
};
const { mutate: updateCreditCard, isLoading: isLoadingBilling } = useMutation(
updateCreditCardApi,
{
onSuccess: (data) => {
handleBillingOnSuccess(data);
},
onError: handleBillingOnError,
},
);
const handleAddCreditCard = (): void => {
logEvent('Add Credit card modal: Clicked', {
source: `facing issues button`,
page: pathname,
...attributes,
});
updateCreditCard({
licenseKey: activeLicense?.key || '',
successURL: window.location.href,
cancelURL: window.location.href,
});
};
return isCloudUserVal && isChatSupportEnabled ? ( // Note: we would need to move this condition to license based in future
<div className="facing-issue-button">
<Tooltip
title={onHoverText}
autoAdjustOverflow
style={{ padding: 8 }}
overlayClassName="tooltip-overlay"
>
<Button
className={cx('periscope-btn', 'facing-issue-button', className)}
onClick={handleFacingIssuesClick}
icon={<HelpCircle size={14} />}
>
{buttonText || 'Facing issues?'}
</Button>
</Tooltip>
{/* Add Credit Card Modal */}
<Modal
className="add-credit-card-modal"
title={<span className="title">Add Credit Card for Chat Support</span>}
open={isAddCreditCardModalOpen}
closable
onCancel={(): void => setIsAddCreditCardModalOpen(false)}
destroyOnClose
footer={[
<Button
key="cancel"
onClick={(): void => setIsAddCreditCardModalOpen(false)}
className="cancel-btn"
icon={<X size={16} />}
>
Cancel
</Button>,
<Button
key="submit"
type="primary"
icon={<CreditCard size={16} />}
size="middle"
loading={isLoadingBilling}
disabled={isLoadingBilling}
onClick={handleAddCreditCard}
className="add-credit-card-btn"
>
Add Credit Card
</Button>,
]}
>
<Typography.Text className="add-credit-card-text">
You&apos;re currently on <span className="highlight-text">Trial plan</span>
. Add a credit card to access SigNoz chat support to your workspace.
</Typography.Text>
</Modal>
</div>
) : null;
}
LaunchChatSupport.defaultProps = {
message: '',
buttonText: '',
className: '',
onHoverText: '',
intercomMessageDisabled: false,
};
export default LaunchChatSupport;

View File

@@ -41,6 +41,21 @@ I need help with managing alerts.
Thanks`;
export const onboardingHelpMessage = (
dataSourceName: string,
moduleId: string,
): string => `Hi Team,
I am facing issues sending data to SigNoz. Here are my application details
Data Source: ${dataSourceName}
Framework:
Environment:
Module: ${moduleId}
Thanks
`;
export const alertHelpMessage = (
alertDef: AlertDef,
ruleId: number,

View File

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

View File

@@ -6,10 +6,13 @@ import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import cx from 'classnames';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
import { LOCALSTORAGE } from 'constants/localStorage';
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
import JSONView from 'container/LogDetailedView/JsonView';
import Overview from 'container/LogDetailedView/Overview';
import { aggregateAttributesResourcesToString } from 'container/LogDetailedView/utils';
import { useOptionsMenu } from 'container/OptionsMenu';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import {
@@ -21,9 +24,10 @@ import {
TextSelect,
X,
} from 'lucide-react';
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { VIEW_TYPES, VIEWS } from './constants';
import { LogDetailProps } from './LogDetail.interfaces';
@@ -33,9 +37,11 @@ function LogDetail({
log,
onClose,
onAddToQuery,
onGroupByAttribute,
onClickActionItem,
selectedTab,
isListViewPanel = false,
listViewPanelSelectedFields,
}: LogDetailProps): JSX.Element {
const [, copyToClipboard] = useCopyToClipboard();
const [selectedView, setSelectedView] = useState<VIEWS>(selectedTab);
@@ -45,6 +51,19 @@ function LogDetail({
const [contextQuery, setContextQuery] = useState<Query | undefined>();
const [filters, setFilters] = useState<TagFilter | null>(null);
const [isEdit, setIsEdit] = useState<boolean>(false);
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.LOGS,
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
});
const isDarkMode = useIsDarkMode();
@@ -191,7 +210,10 @@ function LogDetail({
logData={log}
onAddToQuery={onAddToQuery}
onClickActionItem={onClickActionItem}
onGroupByAttribute={onGroupByAttribute}
isListViewPanel={isListViewPanel}
selectedOptions={options}
listViewPanelSelectedFields={listViewPanelSelectedFields}
/>
)}
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}

View File

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

View File

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

View File

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

View File

@@ -3,8 +3,10 @@ import './ListLogView.styles.scss';
import { blue } from '@ant-design/colors';
import Convert from 'ansi-to-html';
import { Typography } from 'antd';
import cx from 'classnames';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { FontSize } from 'container/OptionsMenu/types';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
import { useActiveLog } from 'hooks/logs/useActiveLog';
@@ -39,6 +41,7 @@ interface LogFieldProps {
fieldKey: string;
fieldValue: string;
linesPerRow?: number;
fontSize: FontSize;
}
type LogSelectedFieldProps = Omit<LogFieldProps, 'linesPerRow'> &
@@ -48,6 +51,7 @@ function LogGeneralField({
fieldKey,
fieldValue,
linesPerRow = 1,
fontSize,
}: LogFieldProps): JSX.Element {
const html = useMemo(
() => ({
@@ -62,12 +66,12 @@ function LogGeneralField({
return (
<TextContainer>
<Text ellipsis type="secondary" className="log-field-key">
<Text ellipsis type="secondary" className={cx('log-field-key', fontSize)}>
{`${fieldKey} : `}
</Text>
<LogText
dangerouslySetInnerHTML={html}
className="log-value"
className={cx('log-value', fontSize)}
linesPerRow={linesPerRow > 1 ? linesPerRow : undefined}
/>
</TextContainer>
@@ -78,6 +82,7 @@ function LogSelectedField({
fieldKey = '',
fieldValue = '',
onAddToQuery,
fontSize,
}: LogSelectedFieldProps): JSX.Element {
return (
<div className="log-selected-fields">
@@ -85,16 +90,22 @@ function LogSelectedField({
fieldKey={fieldKey}
fieldValue={fieldValue}
onAddToQuery={onAddToQuery}
fontSize={fontSize}
>
<Typography.Text>
<span style={{ color: blue[4] }} className="selected-log-field-key">
<span
style={{ color: blue[4] }}
className={cx('selected-log-field-key', fontSize)}
>
{fieldKey}
</span>
</Typography.Text>
</AddToQueryHOC>
<Typography.Text ellipsis className="selected-log-kv">
<span className="selected-log-field-key">{': '}</span>
<span className="selected-log-value">{fieldValue || "''"}</span>
<Typography.Text ellipsis className={cx('selected-log-kv', fontSize)}>
<span className={cx('selected-log-field-key', fontSize)}>{': '}</span>
<span className={cx('selected-log-value', fontSize)}>
{fieldValue || "''"}
</span>
</Typography.Text>
</div>
);
@@ -107,6 +118,7 @@ type ListLogViewProps = {
onAddToQuery: AddToQueryHOCProps['onAddToQuery'];
activeLog?: ILog | null;
linesPerRow: number;
fontSize: FontSize;
};
function ListLogView({
@@ -116,6 +128,7 @@ function ListLogView({
onAddToQuery,
activeLog,
linesPerRow,
fontSize,
}: ListLogViewProps): JSX.Element {
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
@@ -128,6 +141,7 @@ function ListLogView({
onAddToQuery: handleAddToQuery,
onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog,
onGroupByAttribute,
} = useActiveLog();
const isDarkMode = useIsDarkMode();
@@ -185,6 +199,7 @@ function ListLogView({
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleDetailedView}
fontSize={fontSize}
>
<div className="log-line">
<LogStateIndicator
@@ -192,18 +207,28 @@ function ListLogView({
isActive={
activeLog?.id === logData.id || activeContextLog?.id === logData.id
}
fontSize={fontSize}
/>
<div>
<LogContainer>
<LogContainer fontSize={fontSize}>
<LogGeneralField
fieldKey="Log"
fieldValue={flattenLogData.body}
linesPerRow={linesPerRow}
fontSize={fontSize}
/>
{flattenLogData.stream && (
<LogGeneralField fieldKey="Stream" fieldValue={flattenLogData.stream} />
<LogGeneralField
fieldKey="Stream"
fieldValue={flattenLogData.stream}
fontSize={fontSize}
/>
)}
<LogGeneralField fieldKey="Timestamp" fieldValue={timestampValue} />
<LogGeneralField
fieldKey="Timestamp"
fieldValue={timestampValue}
fontSize={fontSize}
/>
{updatedSelecedFields.map((field) =>
isValidLogField(flattenLogData[field.name] as never) ? (
@@ -212,6 +237,7 @@ function ListLogView({
fieldKey={field.name}
fieldValue={flattenLogData[field.name] as never}
onAddToQuery={onAddToQuery}
fontSize={fontSize}
/>
) : null,
)}
@@ -232,6 +258,7 @@ function ListLogView({
onAddToQuery={handleAddToQuery}
selectedTab={VIEW_TYPES.CONTEXT}
onClose={handlerClearActiveContextLog}
onGroupByAttribute={onGroupByAttribute}
/>
)}
</>

View File

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

View File

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

View File

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

View File

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

View File

@@ -39,6 +39,7 @@ function RawLogView({
linesPerRow,
isTextOverflowEllipsisDisabled,
selectedFields = [],
fontSize,
}: RawLogViewProps): JSX.Element {
const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink(
data.id,
@@ -54,6 +55,7 @@ function RawLogView({
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
onGroupByAttribute,
} = useActiveLog();
const [hasActionButtons, setHasActionButtons] = useState<boolean>(false);
@@ -160,6 +162,7 @@ function RawLogView({
$isActiveLog={isActiveLog}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
fontSize={fontSize}
>
<LogStateIndicator
type={logType}
@@ -168,6 +171,7 @@ function RawLogView({
activeContextLog?.id === data.id ||
isActiveLog
}
fontSize={fontSize}
/>
<RawLogContent
@@ -176,6 +180,7 @@ function RawLogView({
$isDarkMode={isDarkMode}
$isTextOverflowEllipsisDisabled={isTextOverflowEllipsisDisabled}
linesPerRow={linesPerRow}
fontSize={fontSize}
dangerouslySetInnerHTML={html}
/>
@@ -199,6 +204,7 @@ function RawLogView({
onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
/>
)}
</RawLogViewContainer>

View File

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

View File

@@ -1,3 +1,4 @@
import { FontSize } from 'container/OptionsMenu/types';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
@@ -7,11 +8,13 @@ export interface RawLogViewProps {
isTextOverflowEllipsisDisabled?: boolean;
data: ILog;
linesPerRow: number;
fontSize: FontSize;
selectedFields?: IField[];
}
export interface RawLogContentProps {
linesPerRow: number;
fontSize: FontSize;
$isReadOnly?: boolean;
$isActiveLog?: boolean;
$isDarkMode?: boolean;

View File

@@ -1,7 +1,10 @@
/* eslint-disable no-nested-ternary */
import { FontSize } from 'container/OptionsMenu/types';
import styled from 'styled-components';
interface TableBodyContentProps {
linesPerRow: number;
fontSize: FontSize;
isDarkMode?: boolean;
}
@@ -20,4 +23,10 @@ export const TableBodyContent = styled.div<TableBodyContentProps>`
-webkit-line-clamp: ${(props): number => props.linesPerRow};
line-clamp: ${(props): number => props.linesPerRow};
-webkit-box-orient: vertical;
${({ fontSize }): string =>
fontSize === FontSize.SMALL
? `font-size:11px; line-height:16px;`
: fontSize === FontSize.MEDIUM
? `font-size:13px; line-height:20px;`
: `font-size:14px; line-height:24px;`}
`;

View File

@@ -1,4 +1,5 @@
import { ColumnsType, ColumnType } from 'antd/es/table';
import { FontSize } from 'container/OptionsMenu/types';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
@@ -10,6 +11,7 @@ export type LogsTableViewProps = {
logs: ILog[];
fields: IField[];
linesPerRow: number;
fontSize: FontSize;
onClickExpand?: (log: ILog) => void;
};

View File

@@ -5,6 +5,21 @@
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
&.small {
font-size: 11px;
line-height: 16px;
}
&.medium {
font-size: 13px;
line-height: 20px;
}
&.large {
font-size: 14px;
line-height: 24px;
}
}
.table-timestamp {
@@ -25,3 +40,21 @@
color: var(--bg-slate-400);
}
}
.paragraph {
padding: 0px !important;
&.small {
font-size: 11px !important;
line-height: 16px !important;
}
&.medium {
font-size: 13px !important;
line-height: 20px !important;
}
&.large {
font-size: 14px !important;
line-height: 24px !important;
}
}

View File

@@ -3,6 +3,7 @@ import './useTableView.styles.scss';
import Convert from 'ansi-to-html';
import { Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import cx from 'classnames';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -31,6 +32,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
logs,
fields,
linesPerRow,
fontSize,
appendTo = 'center',
activeContextLog,
activeLog,
@@ -57,7 +59,10 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
: getDefaultCellStyle(isDarkMode),
},
children: (
<Typography.Paragraph ellipsis={{ rows: linesPerRow }}>
<Typography.Paragraph
ellipsis={{ rows: linesPerRow }}
className={cx('paragraph', fontSize)}
>
{field}
</Typography.Paragraph>
),
@@ -87,8 +92,9 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
isActive={
activeLog?.id === item.id || activeContextLog?.id === item.id
}
fontSize={fontSize}
/>
<Typography.Paragraph ellipsis className="text">
<Typography.Paragraph ellipsis className={cx('text', fontSize)}>
{date}
</Typography.Paragraph>
</div>
@@ -114,6 +120,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
}),
),
}}
fontSize={fontSize}
linesPerRow={linesPerRow}
isDarkMode={isDarkMode}
/>
@@ -130,6 +137,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
linesPerRow,
activeLog?.id,
activeContextLog?.id,
fontSize,
]);
return { columns, dataSource: flattenLogData };

View File

@@ -17,17 +17,126 @@
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
.font-size-dropdown {
display: flex;
flex-direction: column;
.back-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 12px;
border: none !important;
box-shadow: none !important;
.icon {
flex-shrink: 0;
}
.text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: 0.14px;
}
}
.back-btn:hover {
background-color: unset !important;
}
.content {
display: flex;
flex-direction: column;
.option-btn {
display: flex;
align-items: center;
padding: 12px;
border: none !important;
box-shadow: none !important;
justify-content: space-between;
.icon {
flex-shrink: 0;
}
.text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: normal; /* 142.857% */
letter-spacing: 0.14px;
text-transform: capitalize;
}
.text:hover {
color: var(--bg-vanilla-300);
}
}
.option-btn:hover {
background-color: unset !important;
}
}
}
.font-size-container {
padding: 12px;
display: flex;
flex-direction: column;
gap: 12px;
.title {
color: var(--bg-slate-50);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 163.636% */
letter-spacing: 0.88px;
text-transform: uppercase;
}
.value {
display: flex;
height: 20px;
padding: 4px 0px;
justify-content: space-between;
align-items: center;
border: none !important;
.font-value {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: 0.14px;
text-transform: capitalize;
}
.icon {
}
}
.value:hover {
background-color: unset !important;
}
}
.menu-container {
padding: 12px;
.title {
font-family: Inter;
font-size: 11px;
font-weight: 600;
font-weight: 500;
line-height: 18px;
letter-spacing: 0.08em;
text-align: left;
color: #52575c;
color: var(--bg-slate-50);
}
.menu-items {
@@ -65,11 +174,11 @@
padding: 12px;
.title {
color: #52575c;
color: var(--bg-slate-50);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 600;
font-weight: 500;
line-height: 18px; /* 163.636% */
letter-spacing: 0.88px;
text-transform: uppercase;
@@ -149,11 +258,11 @@
}
.title {
color: #52575c;
color: var(--bg-slate-50);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 600;
font-weight: 500;
line-height: 18px; /* 163.636% */
letter-spacing: 0.88px;
text-transform: uppercase;
@@ -299,6 +408,38 @@
box-shadow: 4px 10px 16px 2px rgba(255, 255, 255, 0.2);
.font-size-dropdown {
.back-btn {
.text {
color: var(--bg-ink-400);
}
}
.content {
.option-btn {
.text {
color: var(--bg-ink-400);
}
.text:hover {
color: var(--bg-ink-300);
}
}
}
}
.font-size-container {
.title {
color: var(--bg-ink-100);
}
.value {
.font-value {
color: var(--bg-ink-400);
}
}
}
.horizontal-line {
background: var(--bg-vanilla-300);
}

View File

@@ -3,12 +3,12 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './LogsFormatOptionsMenu.styles.scss';
import { Divider, Input, InputNumber, Tooltip } from 'antd';
import { Button, Divider, Input, InputNumber, Tooltip, Typography } from 'antd';
import cx from 'classnames';
import { LogViewMode } from 'container/LogsTable';
import { OptionsMenuConfig } from 'container/OptionsMenu/types';
import { FontSize, OptionsMenuConfig } from 'container/OptionsMenu/types';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { Check, Minus, Plus, X } from 'lucide-react';
import { Check, ChevronLeft, ChevronRight, Minus, Plus, X } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
interface LogsFormatOptionsMenuProps {
@@ -24,10 +24,16 @@ export default function LogsFormatOptionsMenu({
selectedOptionFormat,
config,
}: LogsFormatOptionsMenuProps): JSX.Element {
const { maxLines, format, addColumn } = config;
const { maxLines, format, addColumn, fontSize } = config;
const [selectedItem, setSelectedItem] = useState(selectedOptionFormat);
const maxLinesNumber = (maxLines?.value as number) || 1;
const [maxLinesPerRow, setMaxLinesPerRow] = useState<number>(maxLinesNumber);
const [fontSizeValue, setFontSizeValue] = useState<FontSize>(
fontSize?.value || FontSize.SMALL,
);
const [isFontSizeOptionsOpen, setIsFontSizeOptionsOpen] = useState<boolean>(
false,
);
const [addNewColumn, setAddNewColumn] = useState(false);
@@ -88,6 +94,12 @@ export default function LogsFormatOptionsMenu({
}
}, [maxLinesPerRow]);
useEffect(() => {
if (fontSizeValue && config && config.fontSize?.onChange) {
config.fontSize.onChange(fontSizeValue);
}
}, [fontSizeValue]);
return (
<div
className={cx('nested-menu-container', addNewColumn ? 'active' : '')}
@@ -96,145 +108,213 @@ export default function LogsFormatOptionsMenu({
event.stopPropagation();
}}
>
<div className="menu-container">
<div className="title"> {title} </div>
<div className="menu-items">
{items.map(
(item: any): JSX.Element => (
<div
className="item"
key={item.label}
onClick={(): void => handleMenuItemClick(item.key)}
>
<div className={cx('item-label')}>
{item.label}
{selectedItem === item.key && <Check size={12} />}
</div>
</div>
),
)}
{isFontSizeOptionsOpen ? (
<div className="font-size-dropdown">
<Button
onClick={(): void => setIsFontSizeOptionsOpen(false)}
className="back-btn"
type="text"
>
<ChevronLeft size={14} className="icon" />
<Typography.Text className="text">Select font size</Typography.Text>
</Button>
<div className="horizontal-line" />
<div className="content">
<Button
onClick={(): void => {
setFontSizeValue(FontSize.SMALL);
}}
className="option-btn"
type="text"
>
<Typography.Text className="text">{FontSize.SMALL}</Typography.Text>
{fontSizeValue === FontSize.SMALL && (
<Check size={14} className="icon" />
)}
</Button>
<Button
onClick={(): void => {
setFontSizeValue(FontSize.MEDIUM);
}}
className="option-btn"
type="text"
>
<Typography.Text className="text">{FontSize.MEDIUM}</Typography.Text>
{fontSizeValue === FontSize.MEDIUM && (
<Check size={14} className="icon" />
)}
</Button>
<Button
onClick={(): void => {
setFontSizeValue(FontSize.LARGE);
}}
className="option-btn"
type="text"
>
<Typography.Text className="text">{FontSize.LARGE}</Typography.Text>
{fontSizeValue === FontSize.LARGE && (
<Check size={14} className="icon" />
)}
</Button>
</div>
</div>
</div>
{selectedItem && (
) : (
<>
<>
<div className="horizontal-line" />
<div className="max-lines-per-row">
<div className="title"> max lines per row </div>
<div className="raw-format max-lines-per-row-input">
<button
type="button"
className="periscope-btn"
onClick={decrementMaxLinesPerRow}
>
{' '}
<Minus size={12} />{' '}
</button>
<InputNumber
min={1}
max={10}
value={maxLinesPerRow}
onChange={handleLinesPerRowChange}
/>
<button
type="button"
className="periscope-btn"
onClick={incrementMaxLinesPerRow}
>
{' '}
<Plus size={12} />{' '}
</button>
</div>
</div>
</>
<div className="font-size-container">
<div className="title">Font Size</div>
<Button
className="value"
type="text"
onClick={(): void => {
setIsFontSizeOptionsOpen(true);
}}
>
<Typography.Text className="font-value">{fontSizeValue}</Typography.Text>
<ChevronRight size={14} className="icon" />
</Button>
</div>
<div className="horizontal-line" />
<div className="menu-container">
<div className="title"> {title} </div>
<div className="selected-item-content-container active">
{!addNewColumn && <div className="horizontal-line" />}
<div className="menu-items">
{items.map(
(item: any): JSX.Element => (
<div
className="item"
key={item.label}
onClick={(): void => handleMenuItemClick(item.key)}
>
<div className={cx('item-label')}>
{item.label}
{addNewColumn && (
<div className="add-new-column-header">
<div className="title">
{' '}
columns
<X size={14} onClick={handleToggleAddNewColumn} />{' '}
</div>
<Input
tabIndex={0}
type="text"
autoFocus
onFocus={addColumn?.onFocus}
onChange={handleSearchValueChange}
placeholder="Search..."
/>
</div>
)}
<div className="item-content">
{!addNewColumn && (
<div className="title">
columns
<Plus size={14} onClick={handleToggleAddNewColumn} />{' '}
</div>
)}
<div className="column-format">
{addColumn?.value?.map(({ key, id }) => (
<div className="column-name" key={id}>
<div className="name">
<Tooltip placement="left" title={key}>
{key}
</Tooltip>
{selectedItem === item.key && <Check size={12} />}
</div>
<X
className="delete-btn"
size={14}
onClick={(): void => addColumn.onRemove(id as string)}
/>
</div>
))}
</div>
{addColumn?.isFetching && (
<div className="loading-container"> Loading ... </div>
)}
{addNewColumn &&
addColumn &&
addColumn.value.length > 0 &&
addColumn.options &&
addColumn?.options?.length > 0 && (
<Divider className="column-divider" />
)}
{addNewColumn && (
<div className="column-format-new-options">
{addColumn?.options?.map(({ label, value }) => (
<div
className="column-name"
key={value}
onClick={(eve): void => {
eve.stopPropagation();
if (addColumn && addColumn?.onSelect) {
addColumn?.onSelect(value, { label, disabled: false });
}
}}
>
<div className="name">
<Tooltip placement="left" title={label}>
{label}
</Tooltip>
</div>
</div>
))}
</div>
),
)}
</div>
</div>
{selectedItem && (
<>
<>
<div className="horizontal-line" />
<div className="max-lines-per-row">
<div className="title"> max lines per row </div>
<div className="raw-format max-lines-per-row-input">
<button
type="button"
className="periscope-btn"
onClick={decrementMaxLinesPerRow}
>
{' '}
<Minus size={12} />{' '}
</button>
<InputNumber
min={1}
max={10}
value={maxLinesPerRow}
onChange={handleLinesPerRowChange}
/>
<button
type="button"
className="periscope-btn"
onClick={incrementMaxLinesPerRow}
>
{' '}
<Plus size={12} />{' '}
</button>
</div>
</div>
</>
<div className="selected-item-content-container active">
{!addNewColumn && <div className="horizontal-line" />}
{addNewColumn && (
<div className="add-new-column-header">
<div className="title">
{' '}
columns
<X size={14} onClick={handleToggleAddNewColumn} />{' '}
</div>
<Input
tabIndex={0}
type="text"
autoFocus
onFocus={addColumn?.onFocus}
onChange={handleSearchValueChange}
placeholder="Search..."
/>
</div>
)}
<div className="item-content">
{!addNewColumn && (
<div className="title">
columns
<Plus size={14} onClick={handleToggleAddNewColumn} />{' '}
</div>
)}
<div className="column-format">
{addColumn?.value?.map(({ key, id }) => (
<div className="column-name" key={id}>
<div className="name">
<Tooltip placement="left" title={key}>
{key}
</Tooltip>
</div>
<X
className="delete-btn"
size={14}
onClick={(): void => addColumn.onRemove(id as string)}
/>
</div>
))}
</div>
{addColumn?.isFetching && (
<div className="loading-container"> Loading ... </div>
)}
{addNewColumn &&
addColumn &&
addColumn.value.length > 0 &&
addColumn.options &&
addColumn?.options?.length > 0 && (
<Divider className="column-divider" />
)}
{addNewColumn && (
<div className="column-format-new-options">
{addColumn?.options?.map(({ label, value }) => (
<div
className="column-name"
key={value}
onClick={(eve): void => {
eve.stopPropagation();
if (addColumn && addColumn?.onSelect) {
addColumn?.onSelect(value, { label, disabled: false });
}
}}
>
<div className="name">
<Tooltip placement="left" title={label}>
{label}
</Tooltip>
</div>
</div>
))}
</div>
)}
</div>
</div>
</>
)}
</>
)}
</div>

View File

@@ -5,7 +5,7 @@ import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd';
import { ColumnGroupType, ColumnType } from 'antd/es/table';
import { ColumnsType } from 'antd/lib/table';
import logEvent from 'api/common/logEvent';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { SlidersHorizontal } from 'lucide-react';
import { memo, useEffect, useState } from 'react';
import { popupContainer } from 'utils/selectPopupContainer';
@@ -96,7 +96,7 @@ function DynamicColumnTable({
return (
<div className="DynamicColumnTable">
<Flex justify="flex-end" align="center" gap={8}>
{facingIssueBtn && <FacingIssueBtn {...facingIssueBtn} />}
{facingIssueBtn && <LaunchChatSupport {...facingIssueBtn} />}
{dynamicColumns && (
<Dropdown
getPopupContainer={popupContainer}

View File

@@ -2,7 +2,7 @@
import { TableProps } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { ColumnGroupType, ColumnType } from 'antd/lib/table';
import { FacingIssueBtnProps } from 'components/facingIssueBtn/FacingIssueBtn';
import { LaunchChatSupportProps } from 'components/LaunchChatSupport/LaunchChatSupport';
import { TableDataSource } from './contants';
@@ -13,7 +13,7 @@ export interface DynamicColumnTableProps extends TableProps<any> {
tablesource: typeof TableDataSource[keyof typeof TableDataSource];
dynamicColumns: TableProps<any>['columns'];
onDragColumn?: (fromIndex: number, toIndex: number) => void;
facingIssueBtn?: FacingIssueBtnProps;
facingIssueBtn?: LaunchChatSupportProps;
shouldSendAlertsLogEvent?: boolean;
}

View File

@@ -1,70 +0,0 @@
import './FacingIssueBtn.style.scss';
import { Button, Tooltip } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { FeatureKeys } from 'constants/features';
import useFeatureFlags from 'hooks/useFeatureFlag';
import { defaultTo } from 'lodash-es';
import { HelpCircle } from 'lucide-react';
import { isCloudUser } from 'utils/app';
export interface FacingIssueBtnProps {
eventName: string;
attributes: Record<string, unknown>;
message?: string;
buttonText?: string;
className?: string;
onHoverText?: string;
intercomMessageDisabled?: boolean;
}
function FacingIssueBtn({
attributes,
eventName,
message = '',
buttonText = '',
className = '',
onHoverText = '',
intercomMessageDisabled = false,
}: FacingIssueBtnProps): JSX.Element | null {
const handleFacingIssuesClick = (): void => {
logEvent(eventName, attributes);
if (window.Intercom && !intercomMessageDisabled) {
window.Intercom('showNewMessage', defaultTo(message, ''));
}
};
const isChatSupportEnabled = useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active;
const isCloudUserVal = isCloudUser();
return isCloudUserVal && isChatSupportEnabled ? ( // Note: we would need to move this condition to license based in future
<div className="facing-issue-button">
<Tooltip
title={onHoverText}
autoAdjustOverflow
style={{ padding: 8 }}
overlayClassName="tooltip-overlay"
>
<Button
className={cx('periscope-btn', 'facing-issue-button', className)}
onClick={handleFacingIssuesClick}
icon={<HelpCircle size={14} />}
>
{buttonText || 'Facing issues?'}
</Button>
</Tooltip>
</div>
) : null;
}
FacingIssueBtn.defaultProps = {
message: '',
buttonText: '',
className: '',
onHoverText: '',
intercomMessageDisabled: false,
};
export default FacingIssueBtn;

View File

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

View File

@@ -20,4 +20,5 @@ export enum FeatureKeys {
ONBOARDING = 'ONBOARDING',
CHAT_SUPPORT = 'CHAT_SUPPORT',
GATEWAY = 'GATEWAY',
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
}

View File

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

View File

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

View File

@@ -24,6 +24,71 @@
}
}
.chat-support-gateway {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
.chat-support-gateway-btn {
max-width: 48px;
width: 48px;
max-height: 48px;
height: 48px;
padding: 12px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
background-color: #f25733;
border: none;
&:hover {
color: white !important;
border-color: white !important;
}
.chat-support-gateway-btn-icon {
fill: white;
}
}
}
.add-credit-card-btn,
.cancel-btn {
display: inline-flex;
justify-content: center;
align-items: center;
}
.highlight-text {
border-radius: 2px;
background: rgba(78, 116, 248, 0.1);
padding-right: 4px;
font-family: 'Geist Mono';
color: var(--bg-robin-500);
}
.add-credit-card-modal {
.ant-modal-footer {
display: flex;
justify-content: flex-end;
}
.cancel-btn {
border-radius: 2px;
border: none;
background: var(--bg-slate-500, #161922);
box-shadow: none;
}
.add-credit-card-btn {
box-shadow: none;
}
}
.isDarkMode {
.app-layout {
.app-content {

View File

@@ -9,12 +9,15 @@ import getLocalStorageKey from 'api/browser/localstorage/get';
import getUserLatestVersion from 'api/user/getLatestVersion';
import getUserVersion from 'api/user/getVersion';
import cx from 'classnames';
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
import { FeatureKeys } from 'constants/features';
import ROUTES from 'constants/routes';
import SideNav from 'container/SideNav';
import TopNav from 'container/TopNav';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useFeatureFlags from 'hooks/useFeatureFlag';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
@@ -49,6 +52,7 @@ import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
import { ChildrenContainer, Layout, LayoutContent } from './styles';
import { getRouteKey } from './utils';
// eslint-disable-next-line sonarjs/cognitive-complexity
function AppLayout(props: AppLayoutProps): JSX.Element {
const { isLoggedIn, user, role } = useSelector<AppState, AppReducer>(
(state) => state.app,
@@ -58,10 +62,19 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true',
);
const { notifications } = useNotifications();
const isDarkMode = useIsDarkMode();
const { data: licenseData, isFetching } = useLicense();
const isPremiumChatSupportEnabled =
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
const showAddCreditCardModal =
!isPremiumChatSupportEnabled &&
!licenseData?.payload?.trialConvertedToSubscription;
const { pathname } = useLocation();
const { t } = useTranslation(['titles']);
@@ -95,8 +108,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const latestCurrentCounter = useRef(0);
const latestVersionCounter = useRef(0);
const { notifications } = useNotifications();
const onCollapse = useCallback(() => {
setCollapsed((collapsed) => !collapsed);
}, []);
@@ -331,6 +342,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
</Sentry.ErrorBoundary>
</div>
</Flex>
{showAddCreditCardModal && <ChatSupportGateway />}
</Layout>
);
}

View File

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

View File

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

View File

@@ -13,8 +13,8 @@ import {
import saveAlertApi from 'api/alerts/save';
import testAlertApi from 'api/alerts/testAlert';
import logEvent from 'api/common/logEvent';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import { alertHelpMessage } from 'components/facingIssueBtn/util';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { alertHelpMessage } from 'components/LaunchChatSupport/util';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
@@ -712,7 +712,7 @@ function FormAlertRules({
>
Check an example alert
</Button>
<FacingIssueBtn
<LaunchChatSupport
attributes={{
alert: alertDef?.alert,
alertType: alertDef?.alertType,

View File

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

View File

@@ -5,7 +5,7 @@ import type { ColumnsType } from 'antd/es/table/interface';
import saveAlertApi from 'api/alerts/save';
import logEvent from 'api/common/logEvent';
import DropDown from 'components/DropDown/DropDown';
import { listAlertMessage } from 'components/facingIssueBtn/util';
import { listAlertMessage } from 'components/LaunchChatSupport/util';
import {
DynamicColumnsKey,
TableDataSource,

View File

@@ -51,6 +51,7 @@
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
min-height: 24px;
@@ -59,6 +60,14 @@
align-items: center;
gap: 6px;
line-height: 20px;
width: 60%;
.dashboard-icon {
display: inline-block;
margin-top: 4px;
margin-right: 4px;
line-height: 20px;
}
.dot {
min-height: 6px;
@@ -66,6 +75,20 @@
border-radius: 50%;
}
.title {
color: var(--bg-vanilla-100);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: 20px;
letter-spacing: -0.07px;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.ant-typography {
color: var(--bg-vanilla-100);
font-size: var(--font-size-sm);
@@ -86,6 +109,45 @@
display: none;
}
}
.tags-with-actions {
display: flex;
align-items: center;
width: 40%;
justify-content: flex-end;
.dashboard-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-end;
.tag {
display: flex;
padding: 4px 8px;
justify-content: center;
align-items: center;
gap: 4px;
height: 28px;
border-radius: 20px;
border: 1px solid rgba(173, 127, 88, 0.2);
background: rgba(173, 127, 88, 0.1);
color: var(--bg-sienna-400);
text-align: center;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
margin-inline-end: 0px !important;
}
}
}
}
.dashboard-action-icon {
margin-left: 16px;
}
.dashboard-details {
@@ -525,38 +587,11 @@
cursor: pointer;
}
}
.tags-with-actions {
display: flex;
align-items: center;
.dashboard-tags {
display: flex;
.tag {
display: flex;
padding: 4px 8px;
justify-content: center;
align-items: center;
gap: 4px;
height: 28px;
border-radius: 20px;
border: 1px solid rgba(173, 127, 88, 0.2);
background: rgba(173, 127, 88, 0.1);
color: var(--bg-sienna-400);
text-align: center;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
}
}
.new-dashboard-menu {
width: 200px;
.create-dashboard-menu-item {
display: flex;
align-items: center;
@@ -681,13 +716,13 @@
.action-btn {
display: flex;
padding: 14px;
padding: 8px;
height: unset;
align-items: center;
gap: 6px;
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: normal;
@@ -706,12 +741,12 @@
.ant-typography {
display: flex;
padding: 14px;
padding: 12px 8px;
align-items: center;
gap: 6px;
color: var(--bg-cherry-400) !important;
font-family: Inter;
font-size: 14px;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: normal;
@@ -1034,7 +1069,7 @@
color: var(--bg-ink-500);
}
.subtitle {
color: var(--bg-vanilla-400);
color: var(--bg-ink-300);
}
.ant-table-row {
@@ -1054,6 +1089,10 @@
.dashboard-title {
color: var(--bg-slate-300);
.title {
color: var(--bg-ink-500);
}
}
.title-with-action {
@@ -1321,3 +1360,12 @@
}
}
}
.title-toolip {
.ant-tooltip-content {
.ant-tooltip-inner {
height: 400px;
overflow: auto;
}
}
}

View File

@@ -25,8 +25,8 @@ import logEvent from 'api/common/logEvent';
import createDashboard from 'api/dashboard/create';
import { AxiosError } from 'axios';
import cx from 'classnames';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import { dashboardListMessage } from 'components/facingIssueBtn/util';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { dashboardListMessage } from 'components/LaunchChatSupport/util';
import { ENTITY_VERSION_V4 } from 'constants/app';
import ROUTES from 'constants/routes';
import { Base64Icons } from 'container/NewDashboard/DashboardSettings/General/utils';
@@ -45,6 +45,8 @@ import {
Ellipsis,
EllipsisVertical,
Expand,
ExternalLink,
Github,
HdmiPort,
LayoutGrid,
Link2,
@@ -53,6 +55,8 @@ import {
RotateCw,
Search,
} from 'lucide-react';
// #TODO: lucide will be removing brand icons like Github in future, in that case we can use simple icons
// see more: https://github.com/lucide-icons/lucide/issues/94
import { handleContactSupport } from 'pages/Integrations/utils';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
@@ -449,79 +453,94 @@ function DashboardsList(): JSX.Element {
<div className="dashboard-list-item" onClick={onClickHandler}>
<div className="title-with-action">
<div className="dashboard-title">
<img
src={dashboard?.image || Base64Icons[0]}
style={{ height: '14px', width: '14px' }}
alt="dashboard-image"
/>
<Typography.Text data-testid={`dashboard-title-${index}`}>
<Link to={getLink()} className="dashboard-title">
{dashboard.name}
</Link>
</Typography.Text>
<Tooltip
title={dashboard?.name?.length > 50 ? dashboard?.name : ''}
placement="left"
overlayClassName="title-toolip"
>
<Typography.Text data-testid={`dashboard-title-${index}`}>
<Link to={getLink()} className="title">
<img
src={dashboard?.image || Base64Icons[0]}
style={{ height: '14px', width: '14px' }}
alt="dashboard-image"
className="dashboard-icon"
/>
{dashboard.name}
</Link>
</Typography.Text>
</Tooltip>
</div>
<div className="tags-with-actions">
{dashboard?.tags && dashboard.tags.length > 0 && (
<div className="dashboard-tags">
{dashboard.tags.map((tag) => (
{dashboard.tags.slice(0, 3).map((tag) => (
<Tag className="tag" key={tag}>
{tag}
</Tag>
))}
{dashboard.tags.length > 3 && (
<Tag className="tag" key={dashboard.tags[3]}>
+ <span> {dashboard.tags.length - 3} </span>
</Tag>
)}
</div>
)}
{action && (
<Popover
trigger="click"
content={
<div className="dashboard-action-content">
<section className="section-1">
<Button
type="text"
className="action-btn"
icon={<Expand size={14} />}
onClick={onClickHandler}
>
View
</Button>
<Button
type="text"
className="action-btn"
icon={<Link2 size={14} />}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
setCopy(`${window.location.origin}${getLink()}`);
}}
>
Copy Link
</Button>
</section>
<section className="section-2">
<DeleteButton
name={dashboard.name}
id={dashboard.id}
isLocked={dashboard.isLocked}
createdBy={dashboard.createdBy}
/>
</section>
</div>
}
placement="bottomRight"
arrow={false}
rootClassName="dashboard-actions"
>
<EllipsisVertical
size={14}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
}}
/>
</Popover>
)}
</div>
{action && (
<Popover
trigger="click"
content={
<div className="dashboard-action-content">
<section className="section-1">
<Button
type="text"
className="action-btn"
icon={<Expand size={12} />}
onClick={onClickHandler}
>
View
</Button>
<Button
type="text"
className="action-btn"
icon={<Link2 size={12} />}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
setCopy(`${window.location.origin}${getLink()}`);
}}
>
Copy Link
</Button>
</section>
<section className="section-2">
<DeleteButton
name={dashboard.name}
id={dashboard.id}
isLocked={dashboard.isLocked}
createdBy={dashboard.createdBy}
/>
</section>
</div>
}
placement="bottomRight"
arrow={false}
rootClassName="dashboard-actions"
>
<EllipsisVertical
className="dashboard-action-icon"
size={14}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
}}
/>
</Popover>
)}
</div>
<div className="dashboard-details">
<div className="dashboard-created-at">
@@ -585,6 +604,28 @@ function DashboardsList(): JSX.Element {
),
key: '1',
},
{
label: (
<a
href="https://github.com/SigNoz/dashboards"
target="_blank"
rel="noopener noreferrer"
>
<Flex
justify="space-between"
align="center"
style={{ width: '100%' }}
gap="small"
>
<div className="create-dashboard-menu-item">
<Github size={14} /> View templates
</div>
<ExternalLink size={14} />
</Flex>
</a>
),
key: '2',
},
];
if (createNewDashboard) {
@@ -649,7 +690,7 @@ function DashboardsList(): JSX.Element {
<Typography.Text className="subtitle">
Create and manage dashboards for your workspace.
</Typography.Text>
<FacingIssueBtn
<LaunchChatSupport
attributes={{
screen: 'Dashboard list page',
}}

View File

@@ -4,7 +4,15 @@ import { red } from '@ant-design/colors';
import { ExclamationCircleTwoTone } from '@ant-design/icons';
import MEditor, { Monaco } from '@monaco-editor/react';
import { Color } from '@signozhq/design-tokens';
import { Button, Modal, Space, Typography, Upload, UploadProps } from 'antd';
import {
Button,
Flex,
Modal,
Space,
Typography,
Upload,
UploadProps,
} from 'antd';
import logEvent from 'api/common/logEvent';
import createDashboard from 'api/dashboard/create';
import ROUTES from 'constants/routes';
@@ -13,7 +21,9 @@ import { MESSAGE } from 'hooks/useFeatureFlag';
import { useNotifications } from 'hooks/useNotifications';
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
import history from 'lib/history';
import { MonitorDot, MoveRight, X } from 'lucide-react';
import { ExternalLink, Github, MonitorDot, MoveRight, X } from 'lucide-react';
// #TODO: Lucide will be removing brand icons like GitHub in the future. In that case, we can use Simple Icons. https://simpleicons.org/
// See more: https://github.com/lucide-icons/lucide/issues/94
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom';
@@ -174,27 +184,43 @@ function ImportJSON({
)}
<div className="action-btns-container">
<Upload
accept=".json"
showUploadList={false}
multiple={false}
onChange={onChangeHandler}
beforeUpload={(): boolean => false}
action="none"
data={jsonData}
>
<Button
type="default"
className="periscope-btn"
icon={<MonitorDot size={14} />}
onClick={(): void => {
logEvent('Dashboard List: Upload JSON file clicked', {});
}}
<Flex gap="small">
<Upload
accept=".json"
showUploadList={false}
multiple={false}
onChange={onChangeHandler}
beforeUpload={(): boolean => false}
action="none"
data={jsonData}
>
{' '}
{t('upload_json_file')}
</Button>
</Upload>
<Button
type="default"
className="periscope-btn"
icon={<MonitorDot size={14} />}
onClick={(): void => {
logEvent('Dashboard List: Upload JSON file clicked', {});
}}
>
{' '}
{t('upload_json_file')}
</Button>
</Upload>
<a
href="https://github.com/SigNoz/dashboards"
target="_blank"
rel="noopener noreferrer"
>
<Button
type="default"
className="periscope-btn"
icon={<Github size={14} />}
>
{t('view_template')}&nbsp;
<ExternalLink size={14} />
</Button>
</a>
</Flex>
<Button
// disabled={editorValue.length === 0}

View File

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

View File

@@ -3,12 +3,17 @@ import './ContextLogRenderer.styles.scss';
import { Skeleton } from 'antd';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { LOCALSTORAGE } from 'constants/localStorage';
import ShowButton from 'container/LogsContextList/ShowButton';
import { useOptionsMenu } from 'container/OptionsMenu';
import { FontSize } from 'container/OptionsMenu/types';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { useCallback, useEffect, useState } from 'react';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Virtuoso } from 'react-virtuoso';
import { ILog } from 'types/api/logs/log';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { useContextLogData } from './useContextLogData';
@@ -22,6 +27,20 @@ function ContextLogRenderer({
const [afterLogPage, setAfterLogPage] = useState<number>(1);
const [logs, setLogs] = useState<ILog[]>([log]);
const { initialDataSource, stagedQuery } = useQueryBuilder();
const listQuery = useMemo(() => {
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
return stagedQuery.builder.queryData.find((item) => !item.disabled) || null;
}, [stagedQuery]);
const { options } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: initialDataSource || DataSource.METRICS,
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
});
const {
logs: previousLogs,
isFetching: isPreviousLogsFetching,
@@ -34,6 +53,7 @@ function ContextLogRenderer({
order: ORDERBY_FILTERS.ASC,
page: prevLogPage,
setPage: setPrevLogPage,
fontSize: options.fontSize,
});
const {
@@ -48,6 +68,7 @@ function ContextLogRenderer({
order: ORDERBY_FILTERS.DESC,
page: afterLogPage,
setPage: setAfterLogPage,
fontSize: options.fontSize,
});
useEffect(() => {
@@ -65,6 +86,19 @@ function ContextLogRenderer({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filters]);
const lengthMultipier = useMemo(() => {
switch (options.fontSize) {
case FontSize.SMALL:
return 24;
case FontSize.MEDIUM:
return 28;
case FontSize.LARGE:
return 32;
default:
return 32;
}
}, [options.fontSize]);
const getItemContent = useCallback(
(_: number, logTorender: ILog): JSX.Element => (
<RawLogView
@@ -74,9 +108,10 @@ function ContextLogRenderer({
key={logTorender.id}
data={logTorender}
linesPerRow={1}
fontSize={options.fontSize}
/>
),
[log.id],
[log.id, options.fontSize],
);
return (
@@ -101,7 +136,7 @@ function ContextLogRenderer({
initialTopMostItemIndex={0}
data={logs}
itemContent={getItemContent}
style={{ height: `calc(${logs.length} * 32px)` }}
style={{ height: `calc(${logs.length} * ${lengthMultipier}px)` }}
/>
</OverlayScrollbar>
{isAfterLogsFetching && (

View File

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

View File

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

View File

@@ -12,10 +12,13 @@ import {
Typography,
} from 'antd';
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { OptionsQuery } from 'container/OptionsMenu/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { ChevronDown, ChevronRight, Search } from 'lucide-react';
import { ReactNode, useState } from 'react';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { ActionItemProps } from './ActionItem';
import TableView from './TableView';
@@ -23,6 +26,13 @@ import TableView from './TableView';
interface OverviewProps {
logData: ILog;
isListViewPanel?: boolean;
selectedOptions: OptionsQuery;
listViewPanelSelectedFields?: IField[] | null;
onGroupByAttribute?: (
fieldKey: string,
isJSON?: boolean,
dataType?: DataTypes,
) => Promise<void>;
}
type Props = OverviewProps &
@@ -34,6 +44,9 @@ function Overview({
onAddToQuery,
onClickActionItem,
isListViewPanel = false,
selectedOptions,
onGroupByAttribute,
listViewPanelSelectedFields,
}: Props): JSX.Element {
const [isWrapWord, setIsWrapWord] = useState<boolean>(true);
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(false);
@@ -198,8 +211,11 @@ function Overview({
logData={logData}
onAddToQuery={onAddToQuery}
fieldSearchInput={fieldSearchInput}
onGroupByAttribute={onGroupByAttribute}
onClickActionItem={onClickActionItem}
isListViewPanel={isListViewPanel}
selectedOptions={selectedOptions}
listViewPanelSelectedFields={listViewPanelSelectedFields}
/>
</>
),
@@ -213,6 +229,8 @@ function Overview({
Overview.defaultProps = {
isListViewPanel: false,
listViewPanelSelectedFields: null,
onGroupByAttribute: undefined,
};
export default Overview;

View File

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

View File

@@ -4,42 +4,35 @@ import './TableView.styles.scss';
import { LinkOutlined } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Button, Space, Spin, Tooltip, Tree, Typography } from 'antd';
import { Button, Space, Tooltip, Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import cx from 'classnames';
import AddToQueryHOC, {
AddToQueryHOCProps,
} from 'components/Logs/AddToQueryHOC';
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
import { ResizeTable } from 'components/ResizeTable';
import { LOCALSTORAGE } from 'constants/localStorage';
import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { FontSize, OptionsQuery } from 'container/OptionsMenu/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import history from 'lib/history';
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes';
import { isEmpty } from 'lodash-es';
import { ArrowDownToDot, ArrowUpFromDot, Pin } from 'lucide-react';
import { Pin } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { useDispatch } from 'react-redux';
import { generatePath } from 'react-router-dom';
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { ActionItemProps } from './ActionItem';
import FieldRenderer from './FieldRenderer';
import {
filterKeyForField,
flattenObject,
jsonToDataNodes,
recursiveParseJSON,
removeEscapeCharacters,
} from './utils';
import { TableViewActions } from './TableView/TableViewActions';
import { filterKeyForField, findKeyPath, flattenObject } from './utils';
// Fields which should be restricted from adding it to query
const RESTRICTED_FIELDS = ['timestamp'];
@@ -47,7 +40,14 @@ const RESTRICTED_FIELDS = ['timestamp'];
interface TableViewProps {
logData: ILog;
fieldSearchInput: string;
selectedOptions: OptionsQuery;
isListViewPanel?: boolean;
listViewPanelSelectedFields?: IField[] | null;
onGroupByAttribute?: (
fieldKey: string,
isJSON?: boolean,
dataType?: DataTypes,
) => Promise<void>;
}
type Props = TableViewProps &
@@ -60,6 +60,9 @@ function TableView({
onAddToQuery,
onClickActionItem,
isListViewPanel = false,
selectedOptions,
onGroupByAttribute,
listViewPanelSelectedFields,
}: Props): JSX.Element | null {
const dispatch = useDispatch<Dispatch<AppActions>>();
const [isfilterInLoading, setIsFilterInLoading] = useState<boolean>(false);
@@ -71,21 +74,31 @@ function TableView({
>({});
useEffect(() => {
const pinnedAttributesFromLocalStorage = getLocalStorageApi(
LOCALSTORAGE.PINNED_ATTRIBUTES,
);
const pinnedAttributes: Record<string, boolean> = {};
if (pinnedAttributesFromLocalStorage) {
try {
const parsedPinnedAttributes = JSON.parse(pinnedAttributesFromLocalStorage);
setPinnedAttributes(parsedPinnedAttributes);
} catch (e) {
console.error('Error parsing pinned attributes from local storgage');
}
if (isListViewPanel) {
listViewPanelSelectedFields?.forEach((val) => {
const path = findKeyPath(logData, val.name, '');
if (path) {
pinnedAttributes[path] = true;
}
});
} else {
setPinnedAttributes({});
selectedOptions.selectColumns.forEach((val) => {
const path = findKeyPath(logData, val.key, '');
if (path) {
pinnedAttributes[path] = true;
}
});
}
}, []);
setPinnedAttributes(pinnedAttributes);
}, [
logData,
selectedOptions.selectColumns,
listViewPanelSelectedFields,
isListViewPanel,
]);
const flattenLogData: Record<string, string> | null = useMemo(
() => (logData ? flattenObject(logData) : null),
@@ -103,19 +116,6 @@ function TableView({
}
};
const togglePinAttribute = (record: DataType): void => {
if (record) {
const newPinnedAttributes = { ...pinnedAttributes };
newPinnedAttributes[record.key] = !newPinnedAttributes[record.key];
setPinnedAttributes(newPinnedAttributes);
setLocalStorageApi(
LOCALSTORAGE.PINNED_ATTRIBUTES,
JSON.stringify(newPinnedAttributes),
);
}
};
const onClickHandler = (
operator: string,
fieldKey: string,
@@ -201,11 +201,8 @@ function TableView({
'pin-attribute-icon',
pinnedAttributes[record?.key] ? 'pinned' : '',
)}
onClick={(): void => {
togglePinAttribute(record);
}}
>
<Pin size={14} color={pinColor} />
{pinnedAttributes[record?.key] && <Pin size={14} color={pinColor} />}
</div>
</div>
);
@@ -258,6 +255,7 @@ function TableView({
fieldKey={fieldFilterKey}
fieldValue={flattenLogData[field]}
onAddToQuery={onAddToQuery}
fontSize={FontSize.SMALL}
>
{renderedField}
</AddToQueryHOC>
@@ -272,75 +270,17 @@ function TableView({
width: 70,
ellipsis: false,
className: 'value-field-container attribute-value',
render: (fieldData: Record<string, string>, record): JSX.Element => {
const textToCopy = fieldData.value.slice(1, -1);
if (record.field === 'body') {
const parsedBody = recursiveParseJSON(fieldData.value);
if (!isEmpty(parsedBody)) {
return (
<Tree defaultExpandAll showLine treeData={jsonToDataNodes(parsedBody)} />
);
}
}
const fieldFilterKey = filterKeyForField(fieldData.field);
return (
<div className="value-field">
<CopyClipboardHOC textToCopy={textToCopy}>
<span
style={{
color: Color.BG_SIENNA_400,
whiteSpace: 'pre-wrap',
tabSize: 4,
}}
>
{removeEscapeCharacters(fieldData.value)}
</span>
</CopyClipboardHOC>
{!isListViewPanel && (
<span className="action-btn">
<Tooltip title="Filter for value">
<Button
className="filter-btn periscope-btn"
icon={
isfilterInLoading ? (
<Spin size="small" />
) : (
<ArrowDownToDot size={14} style={{ transform: 'rotate(90deg)' }} />
)
}
onClick={onClickHandler(
OPERATORS.IN,
fieldFilterKey,
fieldData.value,
)}
/>
</Tooltip>
<Tooltip title="Filter out value">
<Button
className="filter-btn periscope-btn"
icon={
isfilterOutLoading ? (
<Spin size="small" />
) : (
<ArrowUpFromDot size={14} style={{ transform: 'rotate(90deg)' }} />
)
}
onClick={onClickHandler(
OPERATORS.NIN,
fieldFilterKey,
fieldData.value,
)}
/>
</Tooltip>
</span>
)}
</div>
);
},
render: (fieldData: Record<string, string>, record): JSX.Element => (
<TableViewActions
fieldData={fieldData}
record={record}
isListViewPanel={isListViewPanel}
isfilterInLoading={isfilterInLoading}
isfilterOutLoading={isfilterOutLoading}
onClickHandler={onClickHandler}
onGroupByAttribute={onGroupByAttribute}
/>
),
},
];
function sortPinnedAttributes(
@@ -380,9 +320,11 @@ function TableView({
TableView.defaultProps = {
isListViewPanel: false,
listViewPanelSelectedFields: null,
onGroupByAttribute: undefined,
};
interface DataType {
export interface DataType {
key: string;
field: string;
value: string;

View File

@@ -0,0 +1,61 @@
.open-popover {
&.value-field {
.action-btn {
display: flex !important;
position: absolute !important;
top: 50% !important;
right: 16px !important;
transform: translateY(-50%) !important;
gap: 4px !important;
}
}
}
.table-view-actions-content {
.ant-popover-inner {
border-radius: 4px;
border: 1px solid var(--bg-slate-400);
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
rgba(18, 19, 23, 0.9) 98.68%
);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
padding: 0px;
.group-by-clause {
display: flex;
align-items: center;
gap: 4px;
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: 0.14px;
padding: 12px 18px 12px 14px;
.ant-btn-icon {
margin-inline-end: 0px;
}
}
.group-by-clause:hover {
background-color: unset !important;
}
}
}
.lightMode {
.table-view-actions-content {
.ant-popover-inner {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100) !important;
.group-by-clause {
color: var(--bg-ink-400);
}
}
}
}

View File

@@ -0,0 +1,156 @@
import './TableViewActions.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Popover, Spin, Tooltip, Tree } from 'antd';
import GroupByIcon from 'assets/CustomIcons/GroupByIcon';
import cx from 'classnames';
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { isEmpty } from 'lodash-es';
import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react';
import { useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataType } from '../TableView';
import {
filterKeyForField,
jsonToDataNodes,
recursiveParseJSON,
removeEscapeCharacters,
} from '../utils';
interface ITableViewActionsProps {
fieldData: Record<string, string>;
record: DataType;
isListViewPanel: boolean;
isfilterInLoading: boolean;
isfilterOutLoading: boolean;
onGroupByAttribute?: (
fieldKey: string,
isJSON?: boolean,
dataType?: DataTypes,
) => Promise<void>;
onClickHandler: (
operator: string,
fieldKey: string,
fieldValue: string,
) => () => void;
}
export function TableViewActions(
props: ITableViewActionsProps,
): React.ReactElement {
const {
fieldData,
record,
isListViewPanel,
isfilterInLoading,
isfilterOutLoading,
onClickHandler,
onGroupByAttribute,
} = props;
const { pathname } = useLocation();
// there is no option for where clause in old logs explorer and live logs page
const isOldLogsExplorerOrLiveLogsPage = useMemo(
() => pathname === ROUTES.OLD_LOGS_EXPLORER || pathname === ROUTES.LIVE_LOGS,
[pathname],
);
const [isOpen, setIsOpen] = useState<boolean>(false);
const textToCopy = fieldData.value.slice(1, -1);
if (record.field === 'body') {
const parsedBody = recursiveParseJSON(fieldData.value);
if (!isEmpty(parsedBody)) {
return (
<Tree defaultExpandAll showLine treeData={jsonToDataNodes(parsedBody)} />
);
}
}
const fieldFilterKey = filterKeyForField(fieldData.field);
return (
<div className={cx('value-field', isOpen ? 'open-popover' : '')}>
<CopyClipboardHOC textToCopy={textToCopy}>
<span
style={{
color: Color.BG_SIENNA_400,
whiteSpace: 'pre-wrap',
tabSize: 4,
}}
>
{removeEscapeCharacters(fieldData.value)}
</span>
</CopyClipboardHOC>
{!isListViewPanel && (
<span className="action-btn">
<Tooltip title="Filter for value">
<Button
className="filter-btn periscope-btn"
icon={
isfilterInLoading ? (
<Spin size="small" />
) : (
<ArrowDownToDot size={14} style={{ transform: 'rotate(90deg)' }} />
)
}
onClick={onClickHandler(OPERATORS.IN, fieldFilterKey, fieldData.value)}
/>
</Tooltip>
<Tooltip title="Filter out value">
<Button
className="filter-btn periscope-btn"
icon={
isfilterOutLoading ? (
<Spin size="small" />
) : (
<ArrowUpFromDot size={14} style={{ transform: 'rotate(90deg)' }} />
)
}
onClick={onClickHandler(OPERATORS.NIN, fieldFilterKey, fieldData.value)}
/>
</Tooltip>
{!isOldLogsExplorerOrLiveLogsPage && (
<Popover
open={isOpen}
onOpenChange={setIsOpen}
arrow={false}
content={
<div>
<Button
className="group-by-clause"
type="text"
icon={<GroupByIcon />}
onClick={(): Promise<void> | void =>
onGroupByAttribute?.(fieldFilterKey)
}
>
Group By Attribute
</Button>
</div>
}
rootClassName="table-view-actions-content"
trigger="hover"
placement="bottomLeft"
>
<Button
icon={<Ellipsis size={14} />}
className="filter-btn periscope-btn"
/>
</Popover>
)}
</span>
)}
</div>
);
}
TableViewActions.defaultProps = {
onGroupByAttribute: undefined,
};

View File

@@ -267,3 +267,27 @@ export const removeEscapeCharacters = (str: string): string =>
export function removeExtraSpaces(input: string): string {
return input.replace(/\s+/g, ' ').trim();
}
export function findKeyPath(
obj: AnyObject,
targetKey: string,
currentPath = '',
): string | null {
let finalPath = null;
Object.keys(obj).forEach((key) => {
const value = obj[key];
const newPath = currentPath ? `${currentPath}.${key}` : key;
if (key === targetKey) {
finalPath = newPath;
}
if (typeof value === 'object' && value !== null) {
const result = findKeyPath(value, targetKey, newPath);
if (result) {
finalPath = result;
}
}
});
return finalPath;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
/* eslint-disable no-nested-ternary */
import { FontSize } from 'container/OptionsMenu/types';
import { CSSProperties } from 'react';
export const infinityDefaultStyles: CSSProperties = {
@@ -5,3 +7,16 @@ export const infinityDefaultStyles: CSSProperties = {
overflowX: 'scroll',
marginTop: '15px',
};
export function getInfinityDefaultStyles(fontSize: FontSize): CSSProperties {
return {
width: '100%',
overflowX: 'scroll',
marginTop:
fontSize === FontSize.SMALL
? '10px'
: fontSize === FontSize.MEDIUM
? '12px'
: '15px',
};
}

View File

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

View File

@@ -1,5 +1,7 @@
/* eslint-disable no-nested-ternary */
import { Color } from '@signozhq/design-tokens';
import { themeColors } from 'constants/theme';
import { FontSize } from 'container/OptionsMenu/types';
import styled from 'styled-components';
import { getActiveLogBackground } from 'utils/logs';
@@ -7,6 +9,7 @@ interface TableHeaderCellStyledProps {
$isDragColumn: boolean;
$isDarkMode: boolean;
$isTimestamp?: boolean;
fontSize?: FontSize;
}
export const TableStyled = styled.table`
@@ -15,6 +18,14 @@ export const TableStyled = styled.table`
export const TableCellStyled = styled.td<TableHeaderCellStyledProps>`
padding: 0.5rem;
${({ fontSize }): string =>
fontSize === FontSize.SMALL
? `padding:0.3rem;`
: fontSize === FontSize.MEDIUM
? `padding:0.4rem;`
: fontSize === FontSize.LARGE
? `padding:0.5rem;`
: ``}
background-color: ${(props): string =>
props.$isDarkMode ? 'inherit' : themeColors.whiteCream};
@@ -33,7 +44,7 @@ export const TableRowStyled = styled.tr<{
? `background-color: ${
$isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300
} !important`
: ''}
: ''};
}
cursor: pointer;
@@ -66,9 +77,17 @@ export const TableHeaderCellStyled = styled.th<TableHeaderCellStyledProps>`
line-height: 18px;
letter-spacing: -0.07px;
background: ${(props): string => (props.$isDarkMode ? '#0b0c0d' : '#fdfdfd')};
${({ $isTimestamp }): string => ($isTimestamp ? 'padding-left: 24px;' : '')}
${({ $isDragColumn }): string => ($isDragColumn ? 'cursor: col-resize;' : '')}
${({ fontSize }): string =>
fontSize === FontSize.SMALL
? `font-size:11px; line-height:16px; padding: 0.1rem;`
: fontSize === FontSize.MEDIUM
? `font-size:13px; line-height:20px; padding:0.3rem;`
: fontSize === FontSize.LARGE
? `font-size:14px; line-height:24px; padding: 0.5rem;`
: ``};
${({ $isTimestamp }): string => ($isTimestamp ? 'padding-left: 24px;' : '')}
color: ${(props): string =>
props.$isDarkMode ? 'var(--bg-vanilla-100, #fff)' : themeColors.bckgGrey};
`;

View File

@@ -14,6 +14,7 @@ import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
import LogsError from 'container/LogsError/LogsError';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { useOptionsMenu } from 'container/OptionsMenu';
import { FontSize } from 'container/OptionsMenu/types';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
@@ -50,6 +51,7 @@ function LogsExplorerList({
activeLog,
onClearActiveLog,
onAddToQuery,
onGroupByAttribute,
onSetActiveLog,
} = useActiveLog();
@@ -79,6 +81,7 @@ function LogsExplorerList({
data={log}
linesPerRow={options.maxLines}
selectedFields={selectedFields}
fontSize={options.fontSize}
/>
);
}
@@ -91,6 +94,7 @@ function LogsExplorerList({
onAddToQuery={onAddToQuery}
onSetActiveLog={onSetActiveLog}
activeLog={activeLog}
fontSize={options.fontSize}
linesPerRow={options.maxLines}
/>
);
@@ -99,6 +103,7 @@ function LogsExplorerList({
activeLog,
onAddToQuery,
onSetActiveLog,
options.fontSize,
options.format,
options.maxLines,
selectedFields,
@@ -121,6 +126,7 @@ function LogsExplorerList({
logs,
fields: selectedFields,
linesPerRow: options.maxLines,
fontSize: options.fontSize,
appendTo: 'end',
activeLogIndex,
}}
@@ -129,9 +135,22 @@ function LogsExplorerList({
);
}
function getMarginTop(): string {
switch (options.fontSize) {
case FontSize.SMALL:
return '10px';
case FontSize.MEDIUM:
return '12px';
case FontSize.LARGE:
return '15px';
default:
return '15px';
}
}
return (
<Card
style={{ width: '100%', marginTop: '20px' }}
style={{ width: '100%', marginTop: getMarginTop() }}
bodyStyle={CARD_BODY_STYLE}
>
<OverlayScrollbar isVirtuoso>
@@ -151,6 +170,7 @@ function LogsExplorerList({
isLoading,
options.format,
options.maxLines,
options.fontSize,
activeLogIndex,
logs,
onEndReached,
@@ -189,6 +209,7 @@ function LogsExplorerList({
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
onClickActionItem={onAddToQuery}
/>
</>

View File

@@ -80,6 +80,36 @@
position: relative;
}
}
.query-stats {
display: flex;
align-items: center;
gap: 12px;
.rows {
color: var(--bg-vanilla-400);
font-family: 'Geist Mono';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
letter-spacing: 0.36px;
}
.divider {
width: 1px;
height: 14px;
background: #242834;
}
.time {
color: var(--bg-vanilla-400);
font-family: 'Geist Mono';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
letter-spacing: 0.36px;
}
}
}
.logs-actions-container {
@@ -149,6 +179,15 @@
background: var(--bg-robin-400);
}
}
.query-stats {
.rows {
color: var(--bg-ink-400);
}
.time {
color: var(--bg-ink-400);
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,42 @@
import './QueryStatus.styles.scss';
import { LoadingOutlined } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Spin } from 'antd';
import { CircleCheck } from 'lucide-react';
import React, { useMemo } from 'react';
interface IQueryStatusProps {
loading: boolean;
error: boolean;
success: boolean;
}
export default function QueryStatus(
props: IQueryStatusProps,
): React.ReactElement {
const { loading, error, success } = props;
const content = useMemo((): React.ReactElement => {
if (loading) {
return <Spin spinning size="small" indicator={<LoadingOutlined spin />} />;
}
if (error) {
return (
<img
src="/Icons/solid-x-circle.svg"
alt="header"
className="error"
style={{ height: '14px', width: '14px' }}
/>
);
}
if (success) {
return (
<CircleCheck className="success" size={14} fill={Color.BG_ROBIN_500} />
);
}
return <div />;
}, [error, loading, success]);
return <div className="query-status">{content}</div>;
}

View File

@@ -1,8 +1,10 @@
/* eslint-disable sonarjs/cognitive-complexity */
import './LogsExplorerViews.styles.scss';
import { Button } from 'antd';
import { Button, Typography } from 'antd';
import { getQueryStats, WsDataEvent } from 'api/common/getQueryStats';
import logEvent from 'api/common/logEvent';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { LOCALSTORAGE } from 'constants/localStorage';
@@ -48,7 +50,15 @@ import {
} from 'lodash-es';
import { Sliders } from 'lucide-react';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
memo,
MutableRefObject,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { AppState } from 'store/reducers';
@@ -69,12 +79,20 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
import { v4 } from 'uuid';
import QueryStatus from './QueryStatus';
function LogsExplorerViews({
selectedView,
showFrequencyChart,
setIsLoadingQueries,
listQueryKeyRef,
chartQueryKeyRef,
}: {
selectedView: SELECTED_VIEWS;
showFrequencyChart: boolean;
setIsLoadingQueries: React.Dispatch<React.SetStateAction<boolean>>;
listQueryKeyRef: MutableRefObject<any>;
chartQueryKeyRef: MutableRefObject<any>;
}): JSX.Element {
const { notifications } = useNotifications();
const history = useHistory();
@@ -116,6 +134,8 @@ function LogsExplorerViews({
const [logs, setLogs] = useState<ILog[]>([]);
const [requestData, setRequestData] = useState<Query | null>(null);
const [showFormatMenuItems, setShowFormatMenuItems] = useState(false);
const [queryId, setQueryId] = useState<string>(v4());
const [queryStats, setQueryStats] = useState<WsDataEvent>();
const handleAxisError = useAxiosError();
@@ -214,9 +234,18 @@ function LogsExplorerViews({
{
enabled: !!listChartQuery && panelType === PANEL_TYPES.LIST,
},
{},
undefined,
chartQueryKeyRef,
);
const { data, isLoading, isFetching, isError } = useGetExplorerQueryRange(
const {
data,
isLoading,
isFetching,
isError,
isSuccess,
} = useGetExplorerQueryRange(
requestData,
panelType,
DEFAULT_ENTITY_VERSION,
@@ -232,6 +261,12 @@ function LogsExplorerViews({
end: timeRange.end,
}),
},
undefined,
listQueryKeyRef,
{
...(!isEmpty(queryId) &&
selectedPanelType !== PANEL_TYPES.LIST && { 'X-SIGNOZ-QUERY-ID': queryId }),
},
);
const getRequestData = useCallback(
@@ -318,6 +353,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);
useEffect(() => {
if (!logEventCalledRef.current && !isUndefined(data?.payload)) {
@@ -569,6 +621,25 @@ function LogsExplorerViews({
},
});
useEffect(() => {
if (
isLoading ||
isFetching ||
isLoadingListChartData ||
isFetchingListChartData
) {
setIsLoadingQueries(true);
} else {
setIsLoadingQueries(false);
}
}, [
isLoading,
isFetching,
isFetchingListChartData,
isLoadingListChartData,
setIsLoadingQueries,
]);
const flattenLogData = useMemo(
() =>
logs.map((log) => {
@@ -665,6 +736,30 @@ function LogsExplorerViews({
</div>
</div>
)}
{(selectedPanelType === PANEL_TYPES.TIME_SERIES ||
selectedPanelType === PANEL_TYPES.TABLE) && (
<div className="query-stats">
<QueryStatus
loading={isLoading || isFetching}
error={isError}
success={isSuccess}
/>
{queryStats?.read_rows && (
<Typography.Text className="rows">
{getYAxisFormattedValue(queryStats.read_rows?.toString(), 'short')}{' '}
rows
</Typography.Text>
)}
{queryStats?.elapsed_ms && (
<>
<div className="divider" />
<Typography.Text className="time">
{getYAxisFormattedValue(queryStats?.elapsed_ms?.toString(), 'ms')}
</Typography.Text>
</>
)}
</div>
)}
</div>
</div>

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ import LogsTableView from 'components/Logs/TableView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import Spinner from 'components/Spinner';
import { CARD_BODY_STYLE } from 'constants/card';
import { FontSize } from 'container/OptionsMenu/types';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { memo, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
@@ -35,6 +36,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
activeLog,
onClearActiveLog,
onAddToQuery,
onGroupByAttribute,
onSetActiveLog,
} = useActiveLog();
@@ -66,6 +68,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
data={log}
linesPerRow={linesPerRow}
selectedFields={selected}
fontSize={FontSize.SMALL}
/>
);
}
@@ -78,6 +81,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
linesPerRow={linesPerRow}
onAddToQuery={onAddToQuery}
onSetActiveLog={onSetActiveLog}
fontSize={FontSize.SMALL}
/>
);
},
@@ -92,6 +96,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
logs={logs}
fields={selected}
linesPerRow={linesPerRow}
fontSize={FontSize.SMALL}
/>
);
}
@@ -126,6 +131,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={onClearActiveLog}
onGroupByAttribute={onGroupByAttribute}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
/>

View File

@@ -1,6 +1,7 @@
import { Col } from 'antd';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import Graph from 'container/GridCardLayout/GridCard';
import {
@@ -12,8 +13,12 @@ import {
convertRawQueriesToTraceSelectedTags,
resourceAttributesToTagFilterItems,
} from 'hooks/useResourceAttribute/utils';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useLocation, useParams } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
@@ -37,6 +42,26 @@ function DBCall(): JSX.Element {
const servicename = decodeURIComponent(encodedServiceName);
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
const { queries } = useResourceAttribute();
const urlQuery = useUrlQuery();
const { pathname } = useLocation();
const dispatch = useDispatch();
const onDragSelect = useCallback(
(start: number, end: number) => {
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
},
[dispatch, pathname, urlQuery],
);
const tagFilterItems: TagFilterItem[] = useMemo(
() =>
@@ -150,6 +175,7 @@ function DBCall(): JSX.Element {
'database_call_rps',
);
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
/>
</GraphContainer>
@@ -185,6 +211,7 @@ function DBCall(): JSX.Element {
'database_call_avg_duration',
);
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
/>
</GraphContainer>

View File

@@ -1,6 +1,7 @@
import { Col } from 'antd';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import Graph from 'container/GridCardLayout/GridCard';
import {
@@ -14,8 +15,12 @@ import {
convertRawQueriesToTraceSelectedTags,
resourceAttributesToTagFilterItems,
} from 'hooks/useResourceAttribute/utils';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useLocation, useParams } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
@@ -40,6 +45,27 @@ function External(): JSX.Element {
const servicename = decodeURIComponent(encodedServiceName);
const { queries } = useResourceAttribute();
const urlQuery = useUrlQuery();
const { pathname } = useLocation();
const dispatch = useDispatch();
const onDragSelect = useCallback(
(start: number, end: number) => {
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
},
[dispatch, pathname, urlQuery],
);
const tagFilterItems = useMemo(
() =>
handleNonInQueryRange(resourceAttributesToTagFilterItems(queries)) || [],
@@ -214,6 +240,7 @@ function External(): JSX.Element {
'external_call_error_percentage',
);
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
/>
</GraphContainer>
@@ -249,6 +276,7 @@ function External(): JSX.Element {
'external_call_duration',
);
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
/>
</GraphContainer>
@@ -285,6 +313,7 @@ function External(): JSX.Element {
'external_call_rps_by_address',
)
}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
/>
</GraphContainer>
@@ -320,6 +349,7 @@ function External(): JSX.Element {
'external_call_duration_by_address',
);
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
/>
</GraphContainer>

View File

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

View File

@@ -27,7 +27,7 @@ jest.mock('hooks/useNotifications', () => ({
enum ThemeOptions {
Dark = 'Dark',
Light = 'Light',
Light = 'Light Beta',
}
describe('MySettings Flows', () => {

View File

@@ -1,6 +1,6 @@
import './MySettings.styles.scss';
import { Button, Radio, RadioChangeEvent, Space, Typography } from 'antd';
import { Button, Radio, RadioChangeEvent, Space, Tag, Typography } from 'antd';
import { Logout } from 'api/utils';
import useThemeMode, { useIsDarkMode } from 'hooks/useDarkMode';
import { LogOut, Moon, Sun } from 'lucide-react';
@@ -26,6 +26,7 @@ function MySettings(): JSX.Element {
label: (
<div className="theme-option">
<Sun size={12} data-testid="light-theme-icon" /> Light{' '}
<Tag color="magenta">Beta</Tag>
</div>
),
value: 'light',

View File

@@ -59,13 +59,16 @@
justify-content: space-between;
align-items: center;
margin-right: 16px;
box-sizing: border-box;
.dashboard-breadcrumbs {
width: 100%;
height: 48px;
padding: 16px;
display: flex;
gap: 6px;
align-items: center;
max-width: 100%;
.dashboard-btn {
display: flex;
@@ -99,6 +102,14 @@
line-height: 20px; /* 142.857% */
height: 20px;
max-width: calc(100% - 120px);
span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ant-btn-icon {
margin-inline-end: 4px;
}
@@ -110,14 +121,20 @@
}
}
.dashbord-details {
.dashboard-details {
display: flex;
justify-content: space-between;
gap: 8px;
padding: 16px 16px 0px 16px;
align-items: flex-start;
.left-section {
display: flex;
padding: 10px 0px 0px 16px;
flex-wrap: wrap;
align-items: center;
gap: 8px;
width: 45%;
.dashboard-title {
color: #fff;
@@ -128,13 +145,23 @@
line-height: 24px; /* 150% */
letter-spacing: -0.08px;
flex-shrink: 0;
flex: 1;
min-width: fit-content;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
.right-section {
display: flex;
width: 55%;
justify-content: flex-end;
flex-wrap: wrap;
align-items: center;
padding: 10px 16px 0px 0px;
gap: 14px;
.icons {
@@ -199,6 +226,8 @@
display: flex;
gap: 6px;
padding: 16px 16px 0px 16px;
flex-wrap: wrap;
.tag {
display: flex;
padding: 4px 8px;
@@ -219,7 +248,6 @@
}
}
.dashboard-description-section {
max-width: 957px;
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
@@ -578,7 +606,7 @@
}
}
.dashbord-details {
.dashboard-details {
.left-section {
.dashboard-title {
color: var(--bg-ink-300);

View File

@@ -12,8 +12,8 @@ import {
Typography,
} from 'antd';
import logEvent from 'api/common/logEvent';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import { dashboardHelpMessage } from 'components/facingIssueBtn/util';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { dashboardHelpMessage } from 'components/LaunchChatSupport/util';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
@@ -294,36 +294,35 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
>
Dashboard /
</Button>
<Button
type="text"
className="id-btn"
icon={
// eslint-disable-next-line jsx-a11y/img-redundant-alt
<img
src={image}
alt="dashboard-image"
style={{ height: '14px', width: '14px' }}
/>
}
>
<Button type="text" className="id-btn dashboard-name-btn">
<img
src={image}
alt="dashboard-icon"
style={{ height: '14px', width: '14px' }}
/>
{title}
</Button>
</section>
</div>
<section className="dashbord-details">
<section className="dashboard-details">
<div className="left-section">
<img
src={image}
alt="dashboard-img"
style={{ width: '16px', height: '16px' }}
/>
<Typography.Text className="dashboard-title" data-testid="dashboard-title">
{title}
</Typography.Text>
<Tooltip title={title.length > 30 ? title : ''}>
<Typography.Text
className="dashboard-title"
data-testid="dashboard-title"
>
<img
src={image}
alt="dashboard-img"
style={{ width: '16px', height: '16px' }}
/>{' '}
{title}
</Typography.Text>
</Tooltip>
{isDashboardLocked && <LockKeyhole size={14} />}
</div>
<div className="right-section">
<FacingIssueBtn
<LaunchChatSupport
attributes={{
uuid: selectedDashboard?.uuid,
title: updatedTitle,

View File

@@ -4,7 +4,7 @@ import { Color } from '@signozhq/design-tokens';
import { Button, Tabs, Tooltip, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import PromQLIcon from 'assets/Dashboard/PromQl';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import TextToolTip from 'components/TextToolTip';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
@@ -237,7 +237,7 @@ function QuerySection({
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<FacingIssueBtn
<LaunchChatSupport
attributes={{
uuid: selectedDashboard?.uuid,
title: selectedDashboard?.data.title,

View File

@@ -8,7 +8,7 @@ import {
listViewInitialTraceQuery,
PANEL_TYPES_INITIAL_QUERY,
} from 'container/NewDashboard/ComponentsSlider/constants';
import { isEqual, set, unset } from 'lodash-es';
import { cloneDeep, isEqual, set, unset } from 'lodash-es';
import { Widgets } from 'types/api/dashboard/getAll';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
@@ -43,54 +43,59 @@ export const panelTypeDataSourceFormValuesMap: Record<
[DataSource.LOGS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'filters',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'queryName',
'expression',
'stepInterval',
'disabled',
'queryName',
'legend',
'expression',
],
},
},
[DataSource.METRICS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'timeAggregation',
'filters',
'spaceAggregation',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'spaceAggregation',
'queryName',
'expression',
'disabled',
'stepInterval',
'legend',
'queryName',
'disabled',
'functions',
'expression',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'filters',
'groupBy',
'limit',
'having',
'orderBy',
'queryName',
'expression',
'functions',
'stepInterval',
'disabled',
'queryName',
'legend',
'expression',
],
},
},
@@ -99,54 +104,59 @@ export const panelTypeDataSourceFormValuesMap: Record<
[DataSource.LOGS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'filters',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'queryName',
'expression',
'stepInterval',
'disabled',
'queryName',
'legend',
'expression',
],
},
},
[DataSource.METRICS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'timeAggregation',
'filters',
'spaceAggregation',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'spaceAggregation',
'queryName',
'expression',
'disabled',
'stepInterval',
'legend',
'queryName',
'disabled',
'functions',
'expression',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'filters',
'groupBy',
'limit',
'having',
'orderBy',
'queryName',
'expression',
'functions',
'stepInterval',
'disabled',
'queryName',
'legend',
'expression',
],
},
},
@@ -155,48 +165,59 @@ export const panelTypeDataSourceFormValuesMap: Record<
[DataSource.LOGS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'filters',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'stepInterval',
'disabled',
'queryName',
'legend',
'expression',
],
},
},
[DataSource.METRICS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'timeAggregation',
'filters',
'spaceAggregation',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'spaceAggregation',
'disabled',
'stepInterval',
'legend',
'queryName',
'disabled',
'functions',
'expression',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'filters',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'stepInterval',
'disabled',
'queryName',
'legend',
'expression',
],
},
},
@@ -205,18 +226,18 @@ export const panelTypeDataSourceFormValuesMap: Record<
[DataSource.LOGS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'filters',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'stepInterval',
'disabled',
'queryName',
'expression',
'disabled',
'reduceTo',
'legend',
],
},
@@ -224,37 +245,40 @@ export const panelTypeDataSourceFormValuesMap: Record<
[DataSource.METRICS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'timeAggregation',
'filters',
'spaceAggregation',
'groupBy',
'reduceTo',
'limit',
'having',
'orderBy',
'functions',
'spaceAggregation',
'stepInterval',
'legend',
'queryName',
'expression',
'disabled',
'reduceTo',
'legend',
'functions',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'filters',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'stepInterval',
'disabled',
'queryName',
'expression',
'disabled',
'reduceTo',
'legend',
],
},
@@ -264,18 +288,18 @@ export const panelTypeDataSourceFormValuesMap: Record<
[DataSource.LOGS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'filters',
'groupBy',
'reduceTo',
'limit',
'having',
'orderBy',
'functions',
'stepInterval',
'disabled',
'queryName',
'expression',
'disabled',
'legend',
],
},
@@ -283,37 +307,40 @@ export const panelTypeDataSourceFormValuesMap: Record<
[DataSource.METRICS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'timeAggregation',
'filters',
'spaceAggregation',
'groupBy',
'reduceTo',
'limit',
'having',
'orderBy',
'functions',
'spaceAggregation',
'stepInterval',
'legend',
'queryName',
'expression',
'disabled',
'legend',
'functions',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'filters',
'groupBy',
'reduceTo',
'limit',
'having',
'orderBy',
'functions',
'stepInterval',
'disabled',
'queryName',
'expression',
'disabled',
'legend',
],
},
@@ -322,7 +349,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
[PANEL_TYPES.LIST]: {
[DataSource.LOGS]: {
builder: {
queryData: ['filters', 'limit', 'orderBy'],
queryData: ['filters', 'limit', 'orderBy', 'functions'],
},
},
[DataSource.METRICS]: {
@@ -332,7 +359,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
},
[DataSource.TRACES]: {
builder: {
queryData: ['filters', 'limit', 'orderBy'],
queryData: ['filters', 'limit', 'orderBy', 'functions'],
},
},
},
@@ -340,12 +367,13 @@ export const panelTypeDataSourceFormValuesMap: Record<
[DataSource.LOGS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'filters',
'reduceTo',
'having',
'functions',
'stepInterval',
'queryName',
'expression',
'disabled',
@@ -356,30 +384,32 @@ export const panelTypeDataSourceFormValuesMap: Record<
[DataSource.METRICS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'timeAggregation',
'filters',
'spaceAggregation',
'having',
'reduceTo',
'functions',
'spaceAggregation',
'stepInterval',
'legend',
'queryName',
'expression',
'disabled',
'legend',
'functions',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'groupBy',
'limit',
'aggregateOperator',
'filters',
'reduceTo',
'having',
'orderBy',
'functions',
'stepInterval',
'queryName',
'expression',
'disabled',
@@ -399,12 +429,8 @@ export function handleQueryChange(
builder: {
...supersetQuery.builder,
queryData: supersetQuery.builder.queryData.map((query, index) => {
const { dataSource, expression, queryName } = query;
const tempQuery = {
...initialQueryBuilderFormValuesMap[dataSource],
expression,
queryName,
};
const { dataSource } = query;
const tempQuery = cloneDeep(initialQueryBuilderFormValuesMap[dataSource]);
const fieldsToSelect =
panelTypeDataSourceFormValuesMap[newPanelType][dataSource].builder
@@ -419,6 +445,8 @@ export function handleQueryChange(
set(tempQuery, 'offset', 0);
set(tempQuery, 'pageSize', 10);
} else if (tempQuery.aggregateOperator === 'noop') {
// this condition takes care of the part where we start with the list panel type and then shift to other panels
// because in other cases we never set list operator and other fields in superset query rather just update in the current / staged query
set(tempQuery, 'aggregateOperator', 'count');
unset(tempQuery, 'offset');
unset(tempQuery, 'pageSize');

View File

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

View File

@@ -11,13 +11,15 @@ import {
} from '@ant-design/icons';
import { Button, Space, Steps, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { onboardingHelpMessage } from 'components/LaunchChatSupport/util';
import ROUTES from 'constants/routes';
import { stepsMap } from 'container/OnboardingContainer/constants/stepsConfig';
import { DataSourceType } from 'container/OnboardingContainer/Steps/DataSource/DataSource';
import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUtils';
import history from 'lib/history';
import { isEmpty, isNull } from 'lodash-es';
import { HelpCircle, UserPlus } from 'lucide-react';
import { UserPlus } from 'lucide-react';
import { SetStateAction, useState } from 'react';
import { useOnboardingContext } from '../../context/OnboardingContext';
@@ -381,31 +383,6 @@ export default function ModuleStepsContainer({
history.push('/');
};
const handleFacingIssuesClick = (): void => {
logEvent('Onboarding V2: Facing Issues Sending Data to SigNoz', {
dataSource: selectedDataSource?.id,
framework: selectedFramework,
environment: selectedEnvironment,
module: activeStep?.module?.id,
step: activeStep?.step?.id,
});
const message = `Hi Team,
I am facing issues sending data to SigNoz. Here are my application details
Data Source: ${selectedDataSource?.name}
Framework:
Environment:
Module: ${activeStep?.module?.id}
Thanks
`;
if (window.Intercom) {
window.Intercom('showNewMessage', message);
}
};
return (
<div className="onboarding-module-steps">
<div className="steps-container">
@@ -493,19 +470,26 @@ Thanks
>
Back
</Button>
<Button onClick={handleNext} type="primary" icon={<ArrowRightOutlined />}>
{current < lastStepIndex ? 'Continue to next step' : 'Done'}
</Button>
<Button
className="periscope-btn"
onClick={handleFacingIssuesClick}
danger
icon={<HelpCircle size={14} />}
>
Facing issues sending data to SigNoz?
</Button>
<LaunchChatSupport
attributes={{
dataSource: selectedDataSource?.id,
framework: selectedFramework,
environment: selectedEnvironment,
module: activeStep?.module?.id,
step: activeStep?.step?.id,
screen: 'Onboarding',
}}
eventName="Onboarding V2: Facing Issues Sending Data to SigNoz"
message={onboardingHelpMessage(
selectedDataSource?.name || '',
activeStep?.module?.id,
)}
buttonText="Facing issues sending data to SigNoz?"
onHoverText="Click here to get help with sending data to SigNoz"
/>
</div>
</div>
</div>

View File

@@ -1,6 +1,6 @@
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { OptionsQuery } from './types';
import { FontSize, OptionsQuery } from './types';
export const URL_OPTIONS = 'options';
@@ -8,6 +8,7 @@ export const defaultOptionsQuery: OptionsQuery = {
selectColumns: [],
maxLines: 2,
format: 'list',
fontSize: FontSize.SMALL,
};
export const defaultTraceSelectedColumns = [
@@ -18,6 +19,7 @@ export const defaultTraceSelectedColumns = [
isColumn: true,
isJSON: false,
id: 'serviceName--string--tag--true',
isIndexed: false,
},
{
key: 'name',
@@ -26,6 +28,7 @@ export const defaultTraceSelectedColumns = [
isColumn: true,
isJSON: false,
id: 'name--string--tag--true',
isIndexed: false,
},
{
key: 'durationNano',
@@ -34,6 +37,7 @@ export const defaultTraceSelectedColumns = [
isColumn: true,
isJSON: false,
id: 'durationNano--float64--tag--true',
isIndexed: false,
},
{
key: 'httpMethod',
@@ -42,6 +46,7 @@ export const defaultTraceSelectedColumns = [
isColumn: true,
isJSON: false,
id: 'httpMethod--string--tag--true',
isIndexed: false,
},
{
key: 'responseStatusCode',
@@ -50,5 +55,6 @@ export const defaultTraceSelectedColumns = [
isColumn: true,
isJSON: false,
id: 'responseStatusCode--string--tag--true',
isIndexed: false,
},
];

View File

@@ -2,10 +2,21 @@ import { InputNumberProps, RadioProps, SelectProps } from 'antd';
import { LogViewMode } from 'container/LogsTable';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
export enum FontSize {
SMALL = 'small',
MEDIUM = 'medium',
LARGE = 'large',
}
interface FontSizeProps {
value: FontSize;
onChange: (val: FontSize) => void;
}
export interface OptionsQuery {
selectColumns: BaseAutocompleteData[];
maxLines: number;
format: LogViewMode;
fontSize: FontSize;
}
export interface InitialOptions
@@ -18,6 +29,7 @@ export type OptionsMenuConfig = {
onChange: (value: LogViewMode) => void;
};
maxLines?: Pick<InputNumberProps, 'value' | 'onChange'>;
fontSize?: FontSizeProps;
addColumn?: Pick<
SelectProps,
'options' | 'onSelect' | 'onFocus' | 'onSearch' | 'onBlur'

View File

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

View File

@@ -13,6 +13,7 @@ function LogsList({ logs }: LogsListProps): JSX.Element {
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
onGroupByAttribute,
} = useActiveLog();
const makeLogDetailsHandler = (log: ILog) => (): void => onSetActiveLog(log);
@@ -42,6 +43,7 @@ function LogsList({ logs }: LogsListProps): JSX.Element {
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
/>
</div>
);

View File

@@ -3,18 +3,27 @@ import './ToolbarActions.styles.scss';
import { Button } from 'antd';
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { Play } from 'lucide-react';
import { useEffect } from 'react';
import { Play, X } from 'lucide-react';
import { MutableRefObject, useEffect } from 'react';
import { useQueryClient } from 'react-query';
interface RightToolbarActionsProps {
onStageRunQuery: () => void;
isLoadingQueries?: boolean;
listQueryKeyRef?: MutableRefObject<any>;
chartQueryKeyRef?: MutableRefObject<any>;
}
export default function RightToolbarActions({
onStageRunQuery,
isLoadingQueries,
listQueryKeyRef,
chartQueryKeyRef,
}: RightToolbarActionsProps): JSX.Element {
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
const queryClient = useQueryClient();
useEffect(() => {
registerShortcut(LogsExplorerShortcuts.StageAndRunQuery, onStageRunQuery);
@@ -25,14 +34,41 @@ export default function RightToolbarActions({
}, [onStageRunQuery]);
return (
<div>
<Button
type="primary"
className="right-toolbar"
onClick={onStageRunQuery}
icon={<Play size={14} />}
>
Stage & Run Query
</Button>
{isLoadingQueries ? (
<div className="loading-container">
<Button className="loading-btn" loading={isLoadingQueries} />
<Button
icon={<X size={14} />}
className="cancel-run"
onClick={(): void => {
if (listQueryKeyRef?.current) {
queryClient.cancelQueries(listQueryKeyRef.current);
}
if (chartQueryKeyRef?.current) {
queryClient.cancelQueries(chartQueryKeyRef.current);
}
}}
>
Cancel Run
</Button>
</div>
) : (
<Button
type="primary"
className="right-toolbar"
disabled={isLoadingQueries}
onClick={onStageRunQuery}
icon={<Play size={14} />}
>
Stage & Run Query
</Button>
)}
</div>
);
}
RightToolbarActions.defaultProps = {
isLoadingQueries: false,
listQueryKeyRef: null,
chartQueryKeyRef: null,
};

View File

@@ -5,8 +5,8 @@
.left-toolbar-query-actions {
display: flex;
border-radius: 2px;
border: 1px solid var(--bg-slate-400, #1d212d);
background: var(--bg-ink-300, #16181d);
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
flex-direction: row;
.prom-ql-icon {
@@ -24,7 +24,7 @@
border-radius: 0;
&.active-tab {
background-color: #1d212d;
background-color: var(--bg-slate-400);
}
&:disabled {
@@ -33,7 +33,7 @@
}
}
.action-btn + .action-btn {
border-left: 1px solid var(--bg-slate-400, #1d212d);
border-left: 1px solid var(--bg-slate-400);
}
}
@@ -51,6 +51,50 @@
background-color: var(--bg-robin-600);
}
.right-actions {
display: flex;
align-items: center;
}
.loading-container {
display: flex;
gap: 8px;
align-items: center;
.loading-btn {
display: flex;
width: 32px;
height: 33px;
padding: 4px 10px;
justify-content: center;
align-items: center;
gap: 6px;
flex-shrink: 0;
border-radius: 2px;
background: var(--bg-slate-300);
box-shadow: none;
border: none;
}
.cancel-run {
display: flex;
height: 33px;
padding: 4px 10px;
justify-content: center;
align-items: center;
gap: 6px;
flex: 1 0 0;
border-radius: 2px;
background: var(--bg-cherry-500);
border-color: none;
}
.cancel-run:hover {
background-color: #ff7875 !important;
color: var(--bg-vanilla-100) !important;
border: none;
}
}
.lightMode {
.left-toolbar {
.left-toolbar-query-actions {
@@ -68,4 +112,17 @@
}
}
}
.loading-container {
.loading-btn {
background: var(--bg-vanilla-300);
}
.cancel-run {
color: var(--bg-vanilla-100);
}
.cancel-run:hover {
background-color: #ff7875;
}
}
}

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