Compare commits
118 Commits
v0.51.0-ag
...
feat/make-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0d8898ea7 | ||
|
|
381a4de88a | ||
|
|
10ebd0cad6 | ||
|
|
6e7f04b492 | ||
|
|
20ac75e3d2 | ||
|
|
d6b75d76ca | ||
|
|
41d3342a42 | ||
|
|
f3cb3b9840 | ||
|
|
4799d3147b | ||
|
|
b60b26189f | ||
|
|
c79520c874 | ||
|
|
2cc2a43e17 | ||
|
|
47d42e6a57 | ||
|
|
573d369d4b | ||
|
|
3c151e3adb | ||
|
|
ee1e2b824f | ||
|
|
6f0cf03371 | ||
|
|
b8d228a339 | ||
|
|
c6ba2b4598 | ||
|
|
36adc17a34 | ||
|
|
3e32dabf46 | ||
|
|
74c994fbab | ||
|
|
7844522691 | ||
|
|
12f2f80958 | ||
|
|
7b5ff54f47 | ||
|
|
afc97511af | ||
|
|
ae857d3fcd | ||
|
|
0db2784d6b | ||
|
|
47d1caf078 | ||
|
|
292b3f418e | ||
|
|
4eb533fff8 | ||
|
|
7a10fe2b8c | ||
|
|
4214e36d22 | ||
|
|
b9ab6d3fd4 | ||
|
|
23704b00ce | ||
|
|
266894b0f8 | ||
|
|
4a9847abdd | ||
|
|
ba95ca682b | ||
|
|
5942c758f0 | ||
|
|
e97d0ea51c | ||
|
|
6019b38da5 | ||
|
|
3544ffdcc6 | ||
|
|
be7a687088 | ||
|
|
1066b217cb | ||
|
|
709c286086 | ||
|
|
e4753e6b44 | ||
|
|
6f7999acb2 | ||
|
|
16738ea7e3 | ||
|
|
6b096576ee | ||
|
|
5dc5b2e366 | ||
|
|
363fb7bc34 | ||
|
|
dde4485839 | ||
|
|
44598e304d | ||
|
|
4295a2756a | ||
|
|
2f0d98ae51 | ||
|
|
fb92ddc822 | ||
|
|
15b0569b56 | ||
|
|
140533b790 | ||
|
|
532f274bd6 | ||
|
|
3200fd054e | ||
|
|
8468cc863e | ||
|
|
71911687bf | ||
|
|
9644297d28 | ||
|
|
faa6fdfcde | ||
|
|
aabf364cc6 | ||
|
|
4b861b2169 | ||
|
|
8d655bf419 | ||
|
|
90cb8ba9a1 | ||
|
|
f508ee7521 | ||
|
|
413caad0d8 | ||
|
|
666f601ecd | ||
|
|
5cdcbef00c | ||
|
|
c2f607ab6b | ||
|
|
2ca10bb87c | ||
|
|
6fb2a6d4c9 | ||
|
|
464589e0ca | ||
|
|
3b94dab3ce | ||
|
|
9f481aacff | ||
|
|
22f2e68db2 | ||
|
|
706f967246 | ||
|
|
1685f0e74f | ||
|
|
74162456e5 | ||
|
|
b798518aa9 | ||
|
|
d7fd1d032b | ||
|
|
a2ac49bfc2 | ||
|
|
33541a2ac0 | ||
|
|
947b5bdefb | ||
|
|
bd7d14b1ca | ||
|
|
43ed49f9d9 | ||
|
|
758b10f1bf | ||
|
|
ab1caf13fc | ||
|
|
96b81817e0 | ||
|
|
bfeceb0ed2 | ||
|
|
c322fc72d9 | ||
|
|
e7b5410c5b | ||
|
|
072693d57d | ||
|
|
a20794040a | ||
|
|
ab4a8dfbea | ||
|
|
fa0a065b95 | ||
|
|
abc8096a39 | ||
|
|
7cff07333f | ||
|
|
5796d6cb8c | ||
|
|
98367fd054 | ||
|
|
ff8df5dc36 | ||
|
|
f0c9f12897 | ||
|
|
79e96e544f | ||
|
|
871e5ada9e | ||
|
|
0401c27dbc | ||
|
|
57c45f22d6 | ||
|
|
29f1883edd | ||
|
|
5d903b5487 | ||
|
|
1b9683d699 | ||
|
|
65280cf4e1 | ||
|
|
1308f0f15f | ||
|
|
6c634b99d0 | ||
|
|
9856335840 | ||
|
|
e85b405396 | ||
|
|
e2e965bc7f |
6
.github/CODEOWNERS
vendored
6
.github/CODEOWNERS
vendored
@@ -5,6 +5,6 @@
|
|||||||
/frontend/ @YounixM
|
/frontend/ @YounixM
|
||||||
/frontend/src/container/MetricsApplication @srikanthccv
|
/frontend/src/container/MetricsApplication @srikanthccv
|
||||||
/frontend/src/container/NewWidget/RightContainer/types.ts @srikanthccv
|
/frontend/src/container/NewWidget/RightContainer/types.ts @srikanthccv
|
||||||
/deploy/ @prashant-shahi
|
/deploy/ @SigNoz/devops
|
||||||
/sample-apps/ @prashant-shahi
|
/sample-apps/ @SigNoz/devops
|
||||||
.github @prashant-shahi
|
.github @SigNoz/devops
|
||||||
|
|||||||
7
.github/workflows/build.yaml
vendored
7
.github/workflows/build.yaml
vendored
@@ -8,6 +8,13 @@ on:
|
|||||||
- release/v*
|
- release/v*
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
check-no-ee-references:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Run check
|
||||||
|
run: make check-no-ee-references
|
||||||
|
|
||||||
build-frontend:
|
build-frontend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -67,3 +67,6 @@ e2e/.auth
|
|||||||
# go
|
# go
|
||||||
vendor/
|
vendor/
|
||||||
**/main/**
|
**/main/**
|
||||||
|
|
||||||
|
# git-town
|
||||||
|
.git-branches.toml
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# It Comments out the Line Query-Service & Frontend Section of deploy/docker/clickhouse-setup/docker-compose.yaml
|
|
||||||
# Update the Line Numbers when deploy/docker/clickhouse-setup/docker-compose.yaml chnages.
|
|
||||||
# Docs Ref.: https://github.com/SigNoz/signoz/blob/main/CONTRIBUTING.md#contribute-to-frontend-with-docker-installation-of-signoz
|
|
||||||
|
|
||||||
sed -i 38,62's/.*/# &/' .././deploy/docker/clickhouse-setup/docker-compose.yaml
|
|
||||||
9
Makefile
9
Makefile
@@ -178,6 +178,15 @@ clear-swarm-ch:
|
|||||||
@docker run --rm -v "$(PWD)/$(SWARM_DIRECTORY)/data:/pwd" busybox \
|
@docker run --rm -v "$(PWD)/$(SWARM_DIRECTORY)/data:/pwd" busybox \
|
||||||
sh -c "cd /pwd && rm -rf clickhouse*/* zookeeper-*/*"
|
sh -c "cd /pwd && rm -rf clickhouse*/* zookeeper-*/*"
|
||||||
|
|
||||||
|
check-no-ee-references:
|
||||||
|
@echo "Checking for 'ee' package references in 'pkg' directory..."
|
||||||
|
@if grep -R --include="*.go" '.*/ee/.*' pkg/; then \
|
||||||
|
echo "Error: Found references to 'ee' packages in 'pkg' directory"; \
|
||||||
|
exit 1; \
|
||||||
|
else \
|
||||||
|
echo "No references to 'ee' packages found in 'pkg' directory"; \
|
||||||
|
fi
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go test ./pkg/query-service/app/metrics/...
|
go test ./pkg/query-service/app/metrics/...
|
||||||
go test ./pkg/query-service/cache/...
|
go test ./pkg/query-service/cache/...
|
||||||
|
|||||||
@@ -23,6 +23,9 @@
|
|||||||
[1]: https://github.com/pocoproject/poco/blob/poco-1.9.4-release/Foundation/include/Poco/Logger.h#L105-L114
|
[1]: https://github.com/pocoproject/poco/blob/poco-1.9.4-release/Foundation/include/Poco/Logger.h#L105-L114
|
||||||
-->
|
-->
|
||||||
<level>information</level>
|
<level>information</level>
|
||||||
|
<formatting>
|
||||||
|
<type>json</type>
|
||||||
|
</formatting>
|
||||||
<log>/var/log/clickhouse-server/clickhouse-server.log</log>
|
<log>/var/log/clickhouse-server/clickhouse-server.log</log>
|
||||||
<errorlog>/var/log/clickhouse-server/clickhouse-server.err.log</errorlog>
|
<errorlog>/var/log/clickhouse-server/clickhouse-server.err.log</errorlog>
|
||||||
<!-- Rotation policy
|
<!-- Rotation policy
|
||||||
@@ -649,12 +652,12 @@
|
|||||||
|
|
||||||
See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/#creating-replicated-tables
|
See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/#creating-replicated-tables
|
||||||
-->
|
-->
|
||||||
<!--
|
|
||||||
<macros>
|
<macros>
|
||||||
<shard>01</shard>
|
<shard>01</shard>
|
||||||
<replica>example01-01-1</replica>
|
<replica>example01-01-1</replica>
|
||||||
</macros>
|
</macros>
|
||||||
-->
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Reloading interval for embedded dictionaries, in seconds. Default: 3600. -->
|
<!-- Reloading interval for embedded dictionaries, in seconds. Default: 3600. -->
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ extensions:
|
|||||||
|
|
||||||
service:
|
service:
|
||||||
telemetry:
|
telemetry:
|
||||||
|
logs:
|
||||||
|
encoding: json
|
||||||
metrics:
|
metrics:
|
||||||
address: 0.0.0.0:8888
|
address: 0.0.0.0:8888
|
||||||
extensions: [health_check, zpages, pprof]
|
extensions: [health_check, zpages, pprof]
|
||||||
|
|||||||
@@ -23,6 +23,9 @@
|
|||||||
[1]: https://github.com/pocoproject/poco/blob/poco-1.9.4-release/Foundation/include/Poco/Logger.h#L105-L114
|
[1]: https://github.com/pocoproject/poco/blob/poco-1.9.4-release/Foundation/include/Poco/Logger.h#L105-L114
|
||||||
-->
|
-->
|
||||||
<level>information</level>
|
<level>information</level>
|
||||||
|
<formatting>
|
||||||
|
<type>json</type>
|
||||||
|
</formatting>
|
||||||
<log>/var/log/clickhouse-server/clickhouse-server.log</log>
|
<log>/var/log/clickhouse-server/clickhouse-server.log</log>
|
||||||
<errorlog>/var/log/clickhouse-server/clickhouse-server.err.log</errorlog>
|
<errorlog>/var/log/clickhouse-server/clickhouse-server.err.log</errorlog>
|
||||||
<!-- Rotation policy
|
<!-- Rotation policy
|
||||||
@@ -649,12 +652,12 @@
|
|||||||
|
|
||||||
See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/#creating-replicated-tables
|
See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/#creating-replicated-tables
|
||||||
-->
|
-->
|
||||||
<!--
|
|
||||||
<macros>
|
<macros>
|
||||||
<shard>01</shard>
|
<shard>01</shard>
|
||||||
<replica>example01-01-1</replica>
|
<replica>example01-01-1</replica>
|
||||||
</macros>
|
</macros>
|
||||||
-->
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Reloading interval for embedded dictionaries, in seconds. Default: 3600. -->
|
<!-- Reloading interval for embedded dictionaries, in seconds. Default: 3600. -->
|
||||||
|
|||||||
@@ -158,6 +158,8 @@ exporters:
|
|||||||
|
|
||||||
service:
|
service:
|
||||||
telemetry:
|
telemetry:
|
||||||
|
logs:
|
||||||
|
encoding: json
|
||||||
metrics:
|
metrics:
|
||||||
address: 0.0.0.0:8888
|
address: 0.0.0.0:8888
|
||||||
extensions:
|
extensions:
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 3301;
|
listen 3301;
|
||||||
server_name _;
|
server_name _;
|
||||||
@@ -42,6 +47,14 @@ server {
|
|||||||
proxy_read_timeout 600s;
|
proxy_read_timeout 600s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /ws {
|
||||||
|
proxy_pass http://query-service:8080/ws;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade "websocket";
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_read_timeout 86400;
|
||||||
|
}
|
||||||
|
|
||||||
# redirect server error pages to the static page /50x.html
|
# redirect server error pages to the static page /50x.html
|
||||||
#
|
#
|
||||||
error_page 500 502 503 504 /50x.html;
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ type APIHandlerOptions struct {
|
|||||||
Gateway *httputil.ReverseProxy
|
Gateway *httputil.ReverseProxy
|
||||||
// Querier Influx Interval
|
// Querier Influx Interval
|
||||||
FluxInterval time.Duration
|
FluxInterval time.Duration
|
||||||
|
|
||||||
|
UseLogsNewSchema bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type APIHandler struct {
|
type APIHandler struct {
|
||||||
@@ -63,6 +65,7 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
|
|||||||
LogsParsingPipelineController: opts.LogsParsingPipelineController,
|
LogsParsingPipelineController: opts.LogsParsingPipelineController,
|
||||||
Cache: opts.Cache,
|
Cache: opts.Cache,
|
||||||
FluxInterval: opts.FluxInterval,
|
FluxInterval: opts.FluxInterval,
|
||||||
|
UseLogsNewSchema: opts.UseLogsNewSchema,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,17 +1,48 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.signoz.io/signoz/ee/query-service/constants"
|
||||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
featureSet, err := ah.FF().GetFeatureFlags()
|
featureSet, err := ah.FF().GetFeatureFlags()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ah.HandleError(w, err, http.StatusInternalServerError)
|
ah.HandleError(w, err, http.StatusInternalServerError)
|
||||||
return
|
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 {
|
if ah.opts.PreferSpanMetrics {
|
||||||
for idx := range featureSet {
|
for idx := range featureSet {
|
||||||
feature := &featureSet[idx]
|
feature := &featureSet[idx]
|
||||||
@@ -20,5 +51,96 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ah.Respond(w, featureSet)
|
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
|
||||||
|
}
|
||||||
|
|||||||
88
ee/query-service/app/api/featureFlags_test.go
Normal file
88
ee/query-service/app/api/featureFlags_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,8 +25,9 @@ func NewDataConnector(
|
|||||||
maxOpenConns int,
|
maxOpenConns int,
|
||||||
dialTimeout time.Duration,
|
dialTimeout time.Duration,
|
||||||
cluster string,
|
cluster string,
|
||||||
|
useLogsNewSchema bool,
|
||||||
) *ClickhouseReader {
|
) *ClickhouseReader {
|
||||||
ch := basechr.NewReader(localDB, promConfigPath, lm, maxIdleConns, maxOpenConns, dialTimeout, cluster)
|
ch := basechr.NewReader(localDB, promConfigPath, lm, maxIdleConns, maxOpenConns, dialTimeout, cluster, useLogsNewSchema)
|
||||||
return &ClickhouseReader{
|
return &ClickhouseReader{
|
||||||
conn: ch.GetConn(),
|
conn: ch.GetConn(),
|
||||||
appdb: localDB,
|
appdb: localDB,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -27,6 +28,7 @@ import (
|
|||||||
"go.signoz.io/signoz/ee/query-service/dao"
|
"go.signoz.io/signoz/ee/query-service/dao"
|
||||||
"go.signoz.io/signoz/ee/query-service/integrations/gateway"
|
"go.signoz.io/signoz/ee/query-service/integrations/gateway"
|
||||||
"go.signoz.io/signoz/ee/query-service/interfaces"
|
"go.signoz.io/signoz/ee/query-service/interfaces"
|
||||||
|
"go.signoz.io/signoz/ee/query-service/rules"
|
||||||
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
|
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
|
||||||
"go.signoz.io/signoz/pkg/query-service/migrate"
|
"go.signoz.io/signoz/pkg/query-service/migrate"
|
||||||
"go.signoz.io/signoz/pkg/query-service/model"
|
"go.signoz.io/signoz/pkg/query-service/model"
|
||||||
@@ -51,7 +53,7 @@ import (
|
|||||||
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
|
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
|
||||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||||
pqle "go.signoz.io/signoz/pkg/query-service/pqlEngine"
|
pqle "go.signoz.io/signoz/pkg/query-service/pqlEngine"
|
||||||
rules "go.signoz.io/signoz/pkg/query-service/rules"
|
baserules "go.signoz.io/signoz/pkg/query-service/rules"
|
||||||
"go.signoz.io/signoz/pkg/query-service/telemetry"
|
"go.signoz.io/signoz/pkg/query-service/telemetry"
|
||||||
"go.signoz.io/signoz/pkg/query-service/utils"
|
"go.signoz.io/signoz/pkg/query-service/utils"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -75,12 +77,13 @@ type ServerOptions struct {
|
|||||||
FluxInterval string
|
FluxInterval string
|
||||||
Cluster string
|
Cluster string
|
||||||
GatewayUrl string
|
GatewayUrl string
|
||||||
|
UseLogsNewSchema bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server runs HTTP api service
|
// Server runs HTTP api service
|
||||||
type Server struct {
|
type Server struct {
|
||||||
serverOptions *ServerOptions
|
serverOptions *ServerOptions
|
||||||
ruleManager *rules.Manager
|
ruleManager *baserules.Manager
|
||||||
|
|
||||||
// public http router
|
// public http router
|
||||||
httpConn net.Listener
|
httpConn net.Listener
|
||||||
@@ -152,6 +155,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
serverOptions.MaxOpenConns,
|
serverOptions.MaxOpenConns,
|
||||||
serverOptions.DialTimeout,
|
serverOptions.DialTimeout,
|
||||||
serverOptions.Cluster,
|
serverOptions.Cluster,
|
||||||
|
serverOptions.UseLogsNewSchema,
|
||||||
)
|
)
|
||||||
go qb.Start(readerReady)
|
go qb.Start(readerReady)
|
||||||
reader = qb
|
reader = qb
|
||||||
@@ -174,7 +178,9 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
localDB,
|
localDB,
|
||||||
reader,
|
reader,
|
||||||
serverOptions.DisableRules,
|
serverOptions.DisableRules,
|
||||||
lm)
|
lm,
|
||||||
|
serverOptions.UseLogsNewSchema,
|
||||||
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -263,6 +269,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
Cache: c,
|
Cache: c,
|
||||||
FluxInterval: fluxInterval,
|
FluxInterval: fluxInterval,
|
||||||
Gateway: gatewayProxy,
|
Gateway: gatewayProxy,
|
||||||
|
UseLogsNewSchema: serverOptions.UseLogsNewSchema,
|
||||||
}
|
}
|
||||||
|
|
||||||
apiHandler, err := api.NewAPIHandler(apiOpts)
|
apiHandler, err := api.NewAPIHandler(apiOpts)
|
||||||
@@ -317,7 +324,7 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
|
|||||||
// ip here for alert manager
|
// ip here for alert manager
|
||||||
AllowedOrigins: []string{"*"},
|
AllowedOrigins: []string{"*"},
|
||||||
AllowedMethods: []string{"GET", "DELETE", "POST", "PUT", "PATCH"},
|
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)
|
handler := c.Handler(r)
|
||||||
@@ -358,11 +365,13 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
|
|||||||
apiHandler.RegisterIntegrationRoutes(r, am)
|
apiHandler.RegisterIntegrationRoutes(r, am)
|
||||||
apiHandler.RegisterQueryRangeV3Routes(r, am)
|
apiHandler.RegisterQueryRangeV3Routes(r, am)
|
||||||
apiHandler.RegisterQueryRangeV4Routes(r, am)
|
apiHandler.RegisterQueryRangeV4Routes(r, am)
|
||||||
|
apiHandler.RegisterWebSocketPaths(r, am)
|
||||||
|
apiHandler.RegisterMessagingQueuesRoutes(r, am)
|
||||||
|
|
||||||
c := cors.New(cors.Options{
|
c := cors.New(cors.Options{
|
||||||
AllowedOrigins: []string{"*"},
|
AllowedOrigins: []string{"*"},
|
||||||
AllowedMethods: []string{"GET", "DELETE", "POST", "PUT", "PATCH", "OPTIONS"},
|
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)
|
handler := c.Handler(r)
|
||||||
@@ -374,6 +383,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(remove): Implemented at pkg/http/middleware/logging.go
|
||||||
// loggingMiddleware is used for logging public api calls
|
// loggingMiddleware is used for logging public api calls
|
||||||
func loggingMiddleware(next http.Handler) http.Handler {
|
func loggingMiddleware(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -385,6 +395,7 @@ func loggingMiddleware(next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(remove): Implemented at pkg/http/middleware/logging.go
|
||||||
// loggingMiddlewarePrivate is used for logging private api calls
|
// loggingMiddlewarePrivate is used for logging private api calls
|
||||||
// from internal services like alert manager
|
// from internal services like alert manager
|
||||||
func loggingMiddlewarePrivate(next http.Handler) http.Handler {
|
func loggingMiddlewarePrivate(next http.Handler) http.Handler {
|
||||||
@@ -397,27 +408,41 @@ func loggingMiddlewarePrivate(next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(remove): Implemented at pkg/http/middleware/logging.go
|
||||||
type loggingResponseWriter struct {
|
type loggingResponseWriter struct {
|
||||||
http.ResponseWriter
|
http.ResponseWriter
|
||||||
statusCode int
|
statusCode int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(remove): Implemented at pkg/http/middleware/logging.go
|
||||||
func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter {
|
func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter {
|
||||||
// WriteHeader(int) is not called if our response implicitly returns 200 OK, so
|
// WriteHeader(int) is not called if our response implicitly returns 200 OK, so
|
||||||
// we default to that status code.
|
// we default to that status code.
|
||||||
return &loggingResponseWriter{w, http.StatusOK}
|
return &loggingResponseWriter{w, http.StatusOK}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(remove): Implemented at pkg/http/middleware/logging.go
|
||||||
func (lrw *loggingResponseWriter) WriteHeader(code int) {
|
func (lrw *loggingResponseWriter) WriteHeader(code int) {
|
||||||
lrw.statusCode = code
|
lrw.statusCode = code
|
||||||
lrw.ResponseWriter.WriteHeader(code)
|
lrw.ResponseWriter.WriteHeader(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(remove): Implemented at pkg/http/middleware/logging.go
|
||||||
// Flush implements the http.Flush interface.
|
// Flush implements the http.Flush interface.
|
||||||
func (lrw *loggingResponseWriter) Flush() {
|
func (lrw *loggingResponseWriter) Flush() {
|
||||||
lrw.ResponseWriter.(http.Flusher).Flush()
|
lrw.ResponseWriter.(http.Flusher).Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(remove): Implemented at pkg/http/middleware/logging.go
|
||||||
|
// Support websockets
|
||||||
|
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) {
|
func extractQueryRangeData(path string, r *http.Request) (map[string]interface{}, bool) {
|
||||||
pathToExtractBodyFromV3 := "/api/v3/query_range"
|
pathToExtractBodyFromV3 := "/api/v3/query_range"
|
||||||
pathToExtractBodyFromV4 := "/api/v4/query_range"
|
pathToExtractBodyFromV4 := "/api/v4/query_range"
|
||||||
@@ -554,6 +579,7 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(remove): Implemented at pkg/http/middleware/timeout.go
|
||||||
func setTimeoutMiddleware(next http.Handler) http.Handler {
|
func setTimeoutMiddleware(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
@@ -707,7 +733,8 @@ func makeRulesManager(
|
|||||||
db *sqlx.DB,
|
db *sqlx.DB,
|
||||||
ch baseint.Reader,
|
ch baseint.Reader,
|
||||||
disableRules bool,
|
disableRules bool,
|
||||||
fm baseint.FeatureLookup) (*rules.Manager, error) {
|
fm baseint.FeatureLookup,
|
||||||
|
useLogsNewSchema bool) (*baserules.Manager, error) {
|
||||||
|
|
||||||
// create engine
|
// create engine
|
||||||
pqle, err := pqle.FromConfigPath(promConfigPath)
|
pqle, err := pqle.FromConfigPath(promConfigPath)
|
||||||
@@ -723,12 +750,9 @@ func makeRulesManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create manager opts
|
// create manager opts
|
||||||
managerOpts := &rules.ManagerOptions{
|
managerOpts := &baserules.ManagerOptions{
|
||||||
NotifierOpts: notifierOpts,
|
NotifierOpts: notifierOpts,
|
||||||
Queriers: &rules.Queriers{
|
PqlEngine: pqle,
|
||||||
PqlEngine: pqle,
|
|
||||||
Ch: ch.GetConn(),
|
|
||||||
},
|
|
||||||
RepoURL: ruleRepoURL,
|
RepoURL: ruleRepoURL,
|
||||||
DBConn: db,
|
DBConn: db,
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
@@ -737,10 +761,13 @@ func makeRulesManager(
|
|||||||
FeatureFlags: fm,
|
FeatureFlags: fm,
|
||||||
Reader: ch,
|
Reader: ch,
|
||||||
EvalDelay: baseconst.GetEvalDelay(),
|
EvalDelay: baseconst.GetEvalDelay(),
|
||||||
|
|
||||||
|
PrepareTaskFunc: rules.PrepareTaskFunc,
|
||||||
|
UseLogsNewSchema: useLogsNewSchema,
|
||||||
}
|
}
|
||||||
|
|
||||||
// create Manager
|
// create Manager
|
||||||
manager, err := rules.NewManager(managerOpts)
|
manager, err := baserules.NewManager(managerOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("rule manager error: %v", err)
|
return nil, fmt.Errorf("rule manager error: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ const (
|
|||||||
var LicenseSignozIo = "https://license.signoz.io/api/v1"
|
var LicenseSignozIo = "https://license.signoz.io/api/v1"
|
||||||
var LicenseAPIKey = GetOrDefaultEnv("SIGNOZ_LICENSE_API_KEY", "")
|
var LicenseAPIKey = GetOrDefaultEnv("SIGNOZ_LICENSE_API_KEY", "")
|
||||||
var SaasSegmentKey = GetOrDefaultEnv("SIGNOZ_SAAS_SEGMENT_KEY", "")
|
var SaasSegmentKey = GetOrDefaultEnv("SIGNOZ_SAAS_SEGMENT_KEY", "")
|
||||||
var SpanRenderLimitStr = GetOrDefaultEnv("SPAN_RENDER_LIMIT", "2500")
|
var FetchFeatures = GetOrDefaultEnv("FETCH_FEATURES", "false")
|
||||||
var MaxSpansInTraceStr = GetOrDefaultEnv("MAX_SPANS_IN_TRACE", "250000")
|
var ZeusFeaturesURL = GetOrDefaultEnv("ZEUS_FEATURES_URL", "ZeusFeaturesURL")
|
||||||
|
|
||||||
func GetOrDefaultEnv(key string, fallback string) string {
|
func GetOrDefaultEnv(key string, fallback string) string {
|
||||||
v := os.Getenv(key)
|
v := os.Getenv(key)
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ func (lm *Manager) GetLicenses(ctx context.Context) (response []model.License, a
|
|||||||
for _, l := range licenses {
|
for _, l := range licenses {
|
||||||
l.ParsePlan()
|
l.ParsePlan()
|
||||||
|
|
||||||
if l.Key == lm.activeLicense.Key {
|
if lm.activeLicense != nil && l.Key == lm.activeLicense.Key {
|
||||||
l.IsCurrent = true
|
l.IsCurrent = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ func main() {
|
|||||||
var ruleRepoURL string
|
var ruleRepoURL string
|
||||||
var cluster string
|
var cluster string
|
||||||
|
|
||||||
|
var useLogsNewSchema bool
|
||||||
var cacheConfigPath, fluxInterval string
|
var cacheConfigPath, fluxInterval string
|
||||||
var enableQueryServiceLogOTLPExport bool
|
var enableQueryServiceLogOTLPExport bool
|
||||||
var preferSpanMetrics bool
|
var preferSpanMetrics bool
|
||||||
@@ -96,6 +97,7 @@ func main() {
|
|||||||
var dialTimeout time.Duration
|
var dialTimeout time.Duration
|
||||||
var gatewayUrl string
|
var gatewayUrl string
|
||||||
|
|
||||||
|
flag.BoolVar(&useLogsNewSchema, "use-logs-new-schema", false, "use logs_v2 schema for logs")
|
||||||
flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)")
|
flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)")
|
||||||
flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)")
|
flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)")
|
||||||
flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)")
|
flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)")
|
||||||
@@ -134,6 +136,7 @@ func main() {
|
|||||||
FluxInterval: fluxInterval,
|
FluxInterval: fluxInterval,
|
||||||
Cluster: cluster,
|
Cluster: cluster,
|
||||||
GatewayUrl: gatewayUrl,
|
GatewayUrl: gatewayUrl,
|
||||||
|
UseLogsNewSchema: useLogsNewSchema,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the jwt secret key
|
// Read the jwt secret key
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ const DisableUpsell = "DISABLE_UPSELL"
|
|||||||
const Onboarding = "ONBOARDING"
|
const Onboarding = "ONBOARDING"
|
||||||
const ChatSupport = "CHAT_SUPPORT"
|
const ChatSupport = "CHAT_SUPPORT"
|
||||||
const Gateway = "GATEWAY"
|
const Gateway = "GATEWAY"
|
||||||
|
const PremiumSupport = "PREMIUM_SUPPORT"
|
||||||
|
const QueryBuilderSearchV2 = "QUERY_BUILDER_SEARCH_V2"
|
||||||
|
|
||||||
var BasicPlan = basemodel.FeatureSet{
|
var BasicPlan = basemodel.FeatureSet{
|
||||||
basemodel.Feature{
|
basemodel.Feature{
|
||||||
@@ -119,6 +121,20 @@ var BasicPlan = basemodel.FeatureSet{
|
|||||||
UsageLimit: -1,
|
UsageLimit: -1,
|
||||||
Route: "",
|
Route: "",
|
||||||
},
|
},
|
||||||
|
basemodel.Feature{
|
||||||
|
Name: PremiumSupport,
|
||||||
|
Active: false,
|
||||||
|
Usage: 0,
|
||||||
|
UsageLimit: -1,
|
||||||
|
Route: "",
|
||||||
|
},
|
||||||
|
basemodel.Feature{
|
||||||
|
Name: QueryBuilderSearchV2,
|
||||||
|
Active: false,
|
||||||
|
Usage: 0,
|
||||||
|
UsageLimit: -1,
|
||||||
|
Route: "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var ProPlan = basemodel.FeatureSet{
|
var ProPlan = basemodel.FeatureSet{
|
||||||
@@ -220,6 +236,20 @@ var ProPlan = basemodel.FeatureSet{
|
|||||||
UsageLimit: -1,
|
UsageLimit: -1,
|
||||||
Route: "",
|
Route: "",
|
||||||
},
|
},
|
||||||
|
basemodel.Feature{
|
||||||
|
Name: PremiumSupport,
|
||||||
|
Active: true,
|
||||||
|
Usage: 0,
|
||||||
|
UsageLimit: -1,
|
||||||
|
Route: "",
|
||||||
|
},
|
||||||
|
basemodel.Feature{
|
||||||
|
Name: QueryBuilderSearchV2,
|
||||||
|
Active: false,
|
||||||
|
Usage: 0,
|
||||||
|
UsageLimit: -1,
|
||||||
|
Route: "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var EnterprisePlan = basemodel.FeatureSet{
|
var EnterprisePlan = basemodel.FeatureSet{
|
||||||
@@ -335,4 +365,18 @@ var EnterprisePlan = basemodel.FeatureSet{
|
|||||||
UsageLimit: -1,
|
UsageLimit: -1,
|
||||||
Route: "",
|
Route: "",
|
||||||
},
|
},
|
||||||
|
basemodel.Feature{
|
||||||
|
Name: PremiumSupport,
|
||||||
|
Active: true,
|
||||||
|
Usage: 0,
|
||||||
|
UsageLimit: -1,
|
||||||
|
Route: "",
|
||||||
|
},
|
||||||
|
basemodel.Feature{
|
||||||
|
Name: QueryBuilderSearchV2,
|
||||||
|
Active: false,
|
||||||
|
Usage: 0,
|
||||||
|
UsageLimit: -1,
|
||||||
|
Route: "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
70
ee/query-service/rules/manager.go
Normal file
70
ee/query-service/rules/manager.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package rules
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
baserules "go.signoz.io/signoz/pkg/query-service/rules"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error) {
|
||||||
|
|
||||||
|
rules := make([]baserules.Rule, 0)
|
||||||
|
var task baserules.Task
|
||||||
|
|
||||||
|
ruleId := baserules.RuleIdFromTaskName(opts.TaskName)
|
||||||
|
if opts.Rule.RuleType == baserules.RuleTypeThreshold {
|
||||||
|
// create a threshold rule
|
||||||
|
tr, err := baserules.NewThresholdRule(
|
||||||
|
ruleId,
|
||||||
|
opts.Rule,
|
||||||
|
opts.FF,
|
||||||
|
opts.Reader,
|
||||||
|
opts.UseLogsNewSchema,
|
||||||
|
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return task, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rules = append(rules, tr)
|
||||||
|
|
||||||
|
// create ch rule task for evalution
|
||||||
|
task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB)
|
||||||
|
|
||||||
|
} else if opts.Rule.RuleType == baserules.RuleTypeProm {
|
||||||
|
|
||||||
|
// create promql rule
|
||||||
|
pr, err := baserules.NewPromRule(
|
||||||
|
ruleId,
|
||||||
|
opts.Rule,
|
||||||
|
opts.Logger,
|
||||||
|
opts.Reader,
|
||||||
|
opts.ManagerOpts.PqlEngine,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return task, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rules = append(rules, pr)
|
||||||
|
|
||||||
|
// create promql rule task for evalution
|
||||||
|
task = newTask(baserules.TaskTypeProm, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("unsupported rule type. Supported types: %s, %s", baserules.RuleTypeProm, baserules.RuleTypeThreshold)
|
||||||
|
}
|
||||||
|
|
||||||
|
return task, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// newTask returns an appropriate group for
|
||||||
|
// rule type
|
||||||
|
func newTask(taskType baserules.TaskType, name string, frequency time.Duration, rules []baserules.Rule, opts *baserules.ManagerOptions, notify baserules.NotifyFunc, ruleDB baserules.RuleDB) baserules.Task {
|
||||||
|
if taskType == baserules.TaskTypeCh {
|
||||||
|
return baserules.NewRuleTask(name, "", frequency, rules, opts, notify, ruleDB)
|
||||||
|
}
|
||||||
|
return baserules.NewPromRuleTask(name, "", frequency, rules, opts, notify, ruleDB)
|
||||||
|
}
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
"ansi-to-html": "0.7.2",
|
"ansi-to-html": "0.7.2",
|
||||||
"antd": "5.11.0",
|
"antd": "5.11.0",
|
||||||
"antd-table-saveas-excel": "2.2.1",
|
"antd-table-saveas-excel": "2.2.1",
|
||||||
"axios": "1.6.4",
|
"axios": "1.7.4",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-jest": "^29.6.4",
|
"babel-jest": "^29.6.4",
|
||||||
"babel-loader": "9.1.3",
|
"babel-loader": "9.1.3",
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
"lucide-react": "0.379.0",
|
"lucide-react": "0.379.0",
|
||||||
"mini-css-extract-plugin": "2.4.5",
|
"mini-css-extract-plugin": "2.4.5",
|
||||||
"papaparse": "5.4.1",
|
"papaparse": "5.4.1",
|
||||||
"posthog-js": "1.142.1",
|
"posthog-js": "1.160.3",
|
||||||
"rc-tween-one": "3.0.6",
|
"rc-tween-one": "3.0.6",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-addons-update": "15.6.3",
|
"react-addons-update": "15.6.3",
|
||||||
|
|||||||
1
frontend/public/Icons/groupBy.svg
Normal file
1
frontend/public/Icons/groupBy.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#prefix__clip0_4344_1236)" stroke="#C0C1C3" stroke-width="1.167" stroke-linecap="round" stroke-linejoin="round"><path d="M4.667 1.167H2.333c-.644 0-1.166.522-1.166 1.166v2.334c0 .644.522 1.166 1.166 1.166h2.334c.644 0 1.166-.522 1.166-1.166V2.333c0-.644-.522-1.166-1.166-1.166zM8.167 1.167a1.17 1.17 0 011.166 1.166v2.334a1.17 1.17 0 01-1.166 1.166M11.667 1.167a1.17 1.17 0 011.166 1.166v2.334a1.17 1.17 0 01-1.166 1.166M5.833 10.5H2.917c-.992 0-1.75-.758-1.75-1.75v-.583"/><path d="M4.083 12.25l1.75-1.75-1.75-1.75M11.667 8.167H9.333c-.644 0-1.166.522-1.166 1.166v2.334c0 .644.522 1.166 1.166 1.166h2.334c.644 0 1.166-.522 1.166-1.166V9.333c0-.644-.522-1.166-1.166-1.166z"/></g><defs><clipPath id="prefix__clip0_4344_1236"><path fill="#fff" d="M0 0h14v14H0z"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 878 B |
1
frontend/public/Icons/solid-x-circle.svg
Normal file
1
frontend/public/Icons/solid-x-circle.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#prefix__clip0_4062_7291)" stroke-width="1.167" stroke-linecap="round" stroke-linejoin="round"><path d="M7 12.833A5.833 5.833 0 107 1.167a5.833 5.833 0 000 11.666z" fill="#E5484D" stroke="#E5484D"/><path d="M8.75 5.25l-3.5 3.5M5.25 5.25l3.5 3.5" stroke="#121317"/></g><defs><clipPath id="prefix__clip0_4062_7291"><path fill="#fff" d="M0 0h14v14H0z"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 467 B |
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"breadcrumb": "Messaging Queues",
|
||||||
|
"header": "Kafka / Overview",
|
||||||
|
"overview": {
|
||||||
|
"title": "Start sending data in as little as 20 minutes",
|
||||||
|
"subtitle": "Connect and Monitor Your Data Streams"
|
||||||
|
},
|
||||||
|
"configureConsumer": {
|
||||||
|
"title": "Configure Consumer",
|
||||||
|
"description": "Add consumer data sources to gain insights and enhance monitoring.",
|
||||||
|
"button": "Get Started"
|
||||||
|
},
|
||||||
|
"configureProducer": {
|
||||||
|
"title": "Configure Producer",
|
||||||
|
"description": "Add producer data sources to gain insights and enhance monitoring.",
|
||||||
|
"button": "Get Started"
|
||||||
|
},
|
||||||
|
"monitorKafka": {
|
||||||
|
"title": "Monitor kafka",
|
||||||
|
"description": "Add your Kafka source to gain insights and enhance activity tracking.",
|
||||||
|
"button": "Get Started"
|
||||||
|
},
|
||||||
|
"summarySection": {
|
||||||
|
"viewDetailsButton": "View Details"
|
||||||
|
},
|
||||||
|
"confirmModal": {
|
||||||
|
"content": "Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.",
|
||||||
|
"okText": "Proceed"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,5 +38,7 @@
|
|||||||
"LIST_LICENSES": "SigNoz | List of Licenses",
|
"LIST_LICENSES": "SigNoz | List of Licenses",
|
||||||
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
|
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
|
||||||
"SUPPORT": "SigNoz | Support",
|
"SUPPORT": "SigNoz | Support",
|
||||||
"DEFAULT": "Open source Observability Platform | SigNoz"
|
"DEFAULT": "Open source Observability Platform | SigNoz",
|
||||||
|
"ALERT_HISTORY": "SigNoz | Alert Rule History",
|
||||||
|
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview"
|
||||||
}
|
}
|
||||||
|
|||||||
22
frontend/public/locales/en-GB/workspaceLocked.json
Normal file
22
frontend/public/locales/en-GB/workspaceLocked.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"trialPlanExpired": "Trial Plan Expired",
|
||||||
|
"gotQuestions": "Got Questions?",
|
||||||
|
"contactUs": "Contact Us",
|
||||||
|
"upgradeToContinue": "Upgrade to Continue",
|
||||||
|
"upgradeNow": "Upgrade now to keep enjoying all the great features you’ve been using.",
|
||||||
|
"yourDataIsSafe": "Your data is safe with us until",
|
||||||
|
"actNow": "Act now to avoid any disruptions and continue where you left off.",
|
||||||
|
"contactAdmin": "Contact your admin to proceed with the upgrade.",
|
||||||
|
"continueMyJourney": "Continue My Journey",
|
||||||
|
"needMoreTime": "Need More Time?",
|
||||||
|
"extendTrial": "Extend Trial",
|
||||||
|
"extendTrialMsgPart1": "If you have a specific reason why you were not able to finish your PoC in the trial period, please write to us on",
|
||||||
|
"extendTrialMsgPart2": "with the reason. Sometimes we can extend trial by a few days on a case by case basis",
|
||||||
|
"whyChooseSignoz": "Why choose Signoz",
|
||||||
|
"enterpriseGradeObservability": "Enterprise-grade Observability",
|
||||||
|
"observabilityDescription": "Get access to observability at any scale with advanced security and compliance.",
|
||||||
|
"continueToUpgrade": "Continue to Upgrade",
|
||||||
|
"youAreInGoodCompany": "You are in good company",
|
||||||
|
"faqs": "FAQs",
|
||||||
|
"somethingWentWrong": "Something went wrong"
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"create_dashboard": "Create Dashboard",
|
"create_dashboard": "Create Dashboard",
|
||||||
"import_json": "Import Dashboard JSON",
|
"import_json": "Import Dashboard JSON",
|
||||||
|
"view_template": "View templates",
|
||||||
"import_grafana_json": "Import Grafana JSON",
|
"import_grafana_json": "Import Grafana JSON",
|
||||||
"copy_to_clipboard": "Copy To ClipBoard",
|
"copy_to_clipboard": "Copy To ClipBoard",
|
||||||
"download_json": "Download JSON",
|
"download_json": "Download JSON",
|
||||||
|
|||||||
30
frontend/public/locales/en/messagingQueuesKafkaOverview.json
Normal file
30
frontend/public/locales/en/messagingQueuesKafkaOverview.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"breadcrumb": "Messaging Queues",
|
||||||
|
"header": "Kafka / Overview",
|
||||||
|
"overview": {
|
||||||
|
"title": "Start sending data in as little as 20 minutes",
|
||||||
|
"subtitle": "Connect and Monitor Your Data Streams"
|
||||||
|
},
|
||||||
|
"configureConsumer": {
|
||||||
|
"title": "Configure Consumer",
|
||||||
|
"description": "Add consumer data sources to gain insights and enhance monitoring.",
|
||||||
|
"button": "Get Started"
|
||||||
|
},
|
||||||
|
"configureProducer": {
|
||||||
|
"title": "Configure Producer",
|
||||||
|
"description": "Add producer data sources to gain insights and enhance monitoring.",
|
||||||
|
"button": "Get Started"
|
||||||
|
},
|
||||||
|
"monitorKafka": {
|
||||||
|
"title": "Monitor kafka",
|
||||||
|
"description": "Add your Kafka source to gain insights and enhance activity tracking.",
|
||||||
|
"button": "Get Started"
|
||||||
|
},
|
||||||
|
"summarySection": {
|
||||||
|
"viewDetailsButton": "View Details"
|
||||||
|
},
|
||||||
|
"confirmModal": {
|
||||||
|
"content": "Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.",
|
||||||
|
"okText": "Proceed"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,5 +49,8 @@
|
|||||||
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
|
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
|
||||||
"DEFAULT": "Open source Observability Platform | SigNoz",
|
"DEFAULT": "Open source Observability Platform | SigNoz",
|
||||||
"SHORTCUTS": "SigNoz | Shortcuts",
|
"SHORTCUTS": "SigNoz | Shortcuts",
|
||||||
"INTEGRATIONS": "SigNoz | Integrations"
|
"INTEGRATIONS": "SigNoz | Integrations",
|
||||||
|
"ALERT_HISTORY": "SigNoz | Alert Rule History",
|
||||||
|
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
|
||||||
|
"MESSAGING_QUEUES": "SigNoz | Messaging Queues"
|
||||||
}
|
}
|
||||||
|
|||||||
22
frontend/public/locales/en/workspaceLocked.json
Normal file
22
frontend/public/locales/en/workspaceLocked.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"trialPlanExpired": "Trial Plan Expired",
|
||||||
|
"gotQuestions": "Got Questions?",
|
||||||
|
"contactUs": "Contact Us",
|
||||||
|
"upgradeToContinue": "Upgrade to Continue",
|
||||||
|
"upgradeNow": "Upgrade now to keep enjoying all the great features you’ve been using.",
|
||||||
|
"yourDataIsSafe": "Your data is safe with us until",
|
||||||
|
"actNow": "Act now to avoid any disruptions and continue where you left off.",
|
||||||
|
"contactAdmin": "Contact your admin to proceed with the upgrade.",
|
||||||
|
"continueMyJourney": "Continue My Journey",
|
||||||
|
"needMoreTime": "Need More Time?",
|
||||||
|
"extendTrial": "Extend Trial",
|
||||||
|
"extendTrialMsgPart1": "If you have a specific reason why you were not able to finish your PoC in the trial period, please write to us on",
|
||||||
|
"extendTrialMsgPart2": "with the reason. Sometimes we can extend trial by a few days on a case by case basis",
|
||||||
|
"whyChooseSignoz": "Why choose Signoz",
|
||||||
|
"enterpriseGradeObservability": "Enterprise-grade Observability",
|
||||||
|
"observabilityDescription": "Get access to observability at any scale with advanced security and compliance.",
|
||||||
|
"continueToUpgrade": "Continue to Upgrade",
|
||||||
|
"youAreInGoodCompany": "You are in good company",
|
||||||
|
"faqs": "FAQs",
|
||||||
|
"somethingWentWrong": "Something went wrong"
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import { ResourceProvider } from 'hooks/useResourceAttribute';
|
|||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { identity, pick, pickBy } from 'lodash-es';
|
import { identity, pick, pickBy } from 'lodash-es';
|
||||||
import posthog from 'posthog-js';
|
import posthog from 'posthog-js';
|
||||||
|
import AlertRuleProvider from 'providers/Alert';
|
||||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||||
import { Suspense, useEffect, useState } from 'react';
|
import { Suspense, useEffect, useState } from 'react';
|
||||||
@@ -66,6 +67,14 @@ function App(): JSX.Element {
|
|||||||
allFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)?.active ||
|
allFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)?.active ||
|
||||||
false;
|
false;
|
||||||
|
|
||||||
|
const isPremiumSupportEnabled =
|
||||||
|
allFlags.find((flag) => flag.name === FeatureKeys.PREMIUM_SUPPORT)?.active ||
|
||||||
|
false;
|
||||||
|
|
||||||
|
const showAddCreditCardModal =
|
||||||
|
!isPremiumSupportEnabled &&
|
||||||
|
!licenseData?.payload?.trialConvertedToSubscription;
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: UPDATE_FEATURE_FLAG_RESPONSE,
|
type: UPDATE_FEATURE_FLAG_RESPONSE,
|
||||||
payload: {
|
payload: {
|
||||||
@@ -82,7 +91,7 @@ function App(): JSX.Element {
|
|||||||
setRoutes(newRoutes);
|
setRoutes(newRoutes);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoggedInState && isChatSupportEnabled) {
|
if (isLoggedInState && isChatSupportEnabled && !showAddCreditCardModal) {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.Intercom('boot', {
|
window.Intercom('boot', {
|
||||||
@@ -228,22 +237,24 @@ function App(): JSX.Element {
|
|||||||
<QueryBuilderProvider>
|
<QueryBuilderProvider>
|
||||||
<DashboardProvider>
|
<DashboardProvider>
|
||||||
<KeyboardHotkeysProvider>
|
<KeyboardHotkeysProvider>
|
||||||
<AppLayout>
|
<AlertRuleProvider>
|
||||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
<AppLayout>
|
||||||
<Switch>
|
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||||
{routes.map(({ path, component, exact }) => (
|
<Switch>
|
||||||
<Route
|
{routes.map(({ path, component, exact }) => (
|
||||||
key={`${path}`}
|
<Route
|
||||||
exact={exact}
|
key={`${path}`}
|
||||||
path={path}
|
exact={exact}
|
||||||
component={component}
|
path={path}
|
||||||
/>
|
component={component}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
<Route path="*" component={NotFound} />
|
<Route path="*" component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
|
</AlertRuleProvider>
|
||||||
</KeyboardHotkeysProvider>
|
</KeyboardHotkeysProvider>
|
||||||
</DashboardProvider>
|
</DashboardProvider>
|
||||||
</QueryBuilderProvider>
|
</QueryBuilderProvider>
|
||||||
|
|||||||
@@ -92,6 +92,14 @@ export const CreateNewAlerts = Loadable(
|
|||||||
() => import(/* webpackChunkName: "Create Alerts" */ 'pages/CreateAlert'),
|
() => import(/* webpackChunkName: "Create Alerts" */ 'pages/CreateAlert'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const AlertHistory = Loadable(
|
||||||
|
() => import(/* webpackChunkName: "Alert History" */ 'pages/AlertList'),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AlertOverview = Loadable(
|
||||||
|
() => import(/* webpackChunkName: "Alert Overview" */ 'pages/AlertList'),
|
||||||
|
);
|
||||||
|
|
||||||
export const CreateAlertChannelAlerts = Loadable(
|
export const CreateAlertChannelAlerts = Loadable(
|
||||||
() =>
|
() =>
|
||||||
import(/* webpackChunkName: "Create Channels" */ 'pages/AlertChannelCreate'),
|
import(/* webpackChunkName: "Create Channels" */ 'pages/AlertChannelCreate'),
|
||||||
@@ -204,3 +212,15 @@ export const InstalledIntegrations = Loadable(
|
|||||||
/* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage'
|
/* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage'
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const MessagingQueues = Loadable(
|
||||||
|
() =>
|
||||||
|
import(/* webpackChunkName: "MessagingQueues" */ 'pages/MessagingQueues'),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const MQDetailPage = Loadable(
|
||||||
|
() =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "MQDetailPage" */ 'pages/MessagingQueues/MQDetailPage'
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import ROUTES from 'constants/routes';
|
|||||||
import { RouteProps } from 'react-router-dom';
|
import { RouteProps } from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AlertHistory,
|
||||||
|
AlertOverview,
|
||||||
AllAlertChannels,
|
AllAlertChannels,
|
||||||
AllErrors,
|
AllErrors,
|
||||||
APIKeys,
|
APIKeys,
|
||||||
@@ -23,6 +25,8 @@ import {
|
|||||||
LogsExplorer,
|
LogsExplorer,
|
||||||
LogsIndexToFields,
|
LogsIndexToFields,
|
||||||
LogsSaveViews,
|
LogsSaveViews,
|
||||||
|
MessagingQueues,
|
||||||
|
MQDetailPage,
|
||||||
MySettings,
|
MySettings,
|
||||||
NewDashboardPage,
|
NewDashboardPage,
|
||||||
OldLogsExplorer,
|
OldLogsExplorer,
|
||||||
@@ -169,6 +173,20 @@ const routes: AppRoutes[] = [
|
|||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
key: 'ALERTS_NEW',
|
key: 'ALERTS_NEW',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: ROUTES.ALERT_HISTORY,
|
||||||
|
exact: true,
|
||||||
|
component: AlertHistory,
|
||||||
|
isPrivate: true,
|
||||||
|
key: 'ALERT_HISTORY',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ROUTES.ALERT_OVERVIEW,
|
||||||
|
exact: true,
|
||||||
|
component: AlertOverview,
|
||||||
|
isPrivate: true,
|
||||||
|
key: 'ALERT_OVERVIEW',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: ROUTES.TRACE,
|
path: ROUTES.TRACE,
|
||||||
exact: true,
|
exact: true,
|
||||||
@@ -351,6 +369,20 @@ const routes: AppRoutes[] = [
|
|||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
key: 'INTEGRATIONS',
|
key: 'INTEGRATIONS',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: ROUTES.MESSAGING_QUEUES,
|
||||||
|
exact: true,
|
||||||
|
component: MessagingQueues,
|
||||||
|
key: 'MESSAGING_QUEUES',
|
||||||
|
isPrivate: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ROUTES.MESSAGING_QUEUES_DETAIL,
|
||||||
|
exact: true,
|
||||||
|
component: MQDetailPage,
|
||||||
|
key: 'MESSAGING_QUEUES_DETAIL',
|
||||||
|
isPrivate: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const SUPPORT_ROUTE: AppRoutes = {
|
export const SUPPORT_ROUTE: AppRoutes = {
|
||||||
|
|||||||
@@ -1,26 +1,20 @@
|
|||||||
import axios from 'api';
|
import axios from 'api';
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { PayloadProps, Props } from 'types/api/alerts/create';
|
import { PayloadProps, Props } from 'types/api/alerts/create';
|
||||||
|
|
||||||
const create = async (
|
const create = async (
|
||||||
props: Props,
|
props: Props,
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
try {
|
const response = await axios.post('/rules', {
|
||||||
const response = await axios.post('/rules', {
|
...props.data,
|
||||||
...props.data,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
error: null,
|
error: null,
|
||||||
message: response.data.status,
|
message: response.data.status,
|
||||||
payload: response.data.data,
|
payload: response.data.data,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default create;
|
export default create;
|
||||||
|
|||||||
@@ -1,24 +1,18 @@
|
|||||||
import axios from 'api';
|
import axios from 'api';
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { PayloadProps, Props } from 'types/api/alerts/delete';
|
import { PayloadProps, Props } from 'types/api/alerts/delete';
|
||||||
|
|
||||||
const deleteAlerts = async (
|
const deleteAlerts = async (
|
||||||
props: Props,
|
props: Props,
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
try {
|
const response = await axios.delete(`/rules/${props.id}`);
|
||||||
const response = await axios.delete(`/rules/${props.id}`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
error: null,
|
error: null,
|
||||||
message: response.data.status,
|
message: response.data.status,
|
||||||
payload: response.data.data.rules,
|
payload: response.data.data.rules,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default deleteAlerts;
|
export default deleteAlerts;
|
||||||
|
|||||||
@@ -1,24 +1,16 @@
|
|||||||
import axios from 'api';
|
import axios from 'api';
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { PayloadProps, Props } from 'types/api/alerts/get';
|
import { PayloadProps, Props } from 'types/api/alerts/get';
|
||||||
|
|
||||||
const get = async (
|
const get = async (
|
||||||
props: Props,
|
props: Props,
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
try {
|
const response = await axios.get(`/rules/${props.id}`);
|
||||||
const response = await axios.get(`/rules/${props.id}`);
|
return {
|
||||||
|
statusCode: 200,
|
||||||
return {
|
error: null,
|
||||||
statusCode: 200,
|
message: response.data.status,
|
||||||
error: null,
|
payload: response.data,
|
||||||
message: response.data.status,
|
};
|
||||||
payload: response.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default get;
|
export default get;
|
||||||
|
|||||||
@@ -1,26 +1,20 @@
|
|||||||
import axios from 'api';
|
import axios from 'api';
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { PayloadProps, Props } from 'types/api/alerts/patch';
|
import { PayloadProps, Props } from 'types/api/alerts/patch';
|
||||||
|
|
||||||
const patch = async (
|
const patch = async (
|
||||||
props: Props,
|
props: Props,
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
try {
|
const response = await axios.patch(`/rules/${props.id}`, {
|
||||||
const response = await axios.patch(`/rules/${props.id}`, {
|
...props.data,
|
||||||
...props.data,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
error: null,
|
error: null,
|
||||||
message: response.data.status,
|
message: response.data.status,
|
||||||
payload: response.data.data,
|
payload: response.data.data,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default patch;
|
export default patch;
|
||||||
|
|||||||
@@ -1,26 +1,20 @@
|
|||||||
import axios from 'api';
|
import axios from 'api';
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { PayloadProps, Props } from 'types/api/alerts/save';
|
import { PayloadProps, Props } from 'types/api/alerts/save';
|
||||||
|
|
||||||
const put = async (
|
const put = async (
|
||||||
props: Props,
|
props: Props,
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
try {
|
const response = await axios.put(`/rules/${props.id}`, {
|
||||||
const response = await axios.put(`/rules/${props.id}`, {
|
...props.data,
|
||||||
...props.data,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
error: null,
|
error: null,
|
||||||
message: response.data.status,
|
message: response.data.status,
|
||||||
payload: response.data.data,
|
payload: response.data.data,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default put;
|
export default put;
|
||||||
|
|||||||
28
frontend/src/api/alerts/ruleStats.ts
Normal file
28
frontend/src/api/alerts/ruleStats.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { AlertRuleStatsPayload } from 'types/api/alerts/def';
|
||||||
|
import { RuleStatsProps } from 'types/api/alerts/ruleStats';
|
||||||
|
|
||||||
|
const ruleStats = async (
|
||||||
|
props: RuleStatsProps,
|
||||||
|
): Promise<SuccessResponse<AlertRuleStatsPayload> | ErrorResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`/rules/${props.id}/history/stats`, {
|
||||||
|
start: props.start,
|
||||||
|
end: props.end,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ruleStats;
|
||||||
33
frontend/src/api/alerts/timelineGraph.ts
Normal file
33
frontend/src/api/alerts/timelineGraph.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { AlertRuleTimelineGraphResponsePayload } from 'types/api/alerts/def';
|
||||||
|
import { GetTimelineGraphRequestProps } from 'types/api/alerts/timelineGraph';
|
||||||
|
|
||||||
|
const timelineGraph = async (
|
||||||
|
props: GetTimelineGraphRequestProps,
|
||||||
|
): Promise<
|
||||||
|
SuccessResponse<AlertRuleTimelineGraphResponsePayload> | ErrorResponse
|
||||||
|
> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`/rules/${props.id}/history/overall_status`,
|
||||||
|
{
|
||||||
|
start: props.start,
|
||||||
|
end: props.end,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default timelineGraph;
|
||||||
36
frontend/src/api/alerts/timelineTable.ts
Normal file
36
frontend/src/api/alerts/timelineTable.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { AlertRuleTimelineTableResponsePayload } from 'types/api/alerts/def';
|
||||||
|
import { GetTimelineTableRequestProps } from 'types/api/alerts/timelineTable';
|
||||||
|
|
||||||
|
const timelineTable = async (
|
||||||
|
props: GetTimelineTableRequestProps,
|
||||||
|
): Promise<
|
||||||
|
SuccessResponse<AlertRuleTimelineTableResponsePayload> | ErrorResponse
|
||||||
|
> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`/rules/${props.id}/history/timeline`, {
|
||||||
|
start: props.start,
|
||||||
|
end: props.end,
|
||||||
|
offset: props.offset,
|
||||||
|
limit: props.limit,
|
||||||
|
order: props.order,
|
||||||
|
state: props.state,
|
||||||
|
// TODO(shaheer): implement filters
|
||||||
|
filters: props.filters,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default timelineTable;
|
||||||
33
frontend/src/api/alerts/topContributors.ts
Normal file
33
frontend/src/api/alerts/topContributors.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { AlertRuleTopContributorsPayload } from 'types/api/alerts/def';
|
||||||
|
import { TopContributorsProps } from 'types/api/alerts/topContributors';
|
||||||
|
|
||||||
|
const topContributors = async (
|
||||||
|
props: TopContributorsProps,
|
||||||
|
): Promise<
|
||||||
|
SuccessResponse<AlertRuleTopContributorsPayload> | ErrorResponse
|
||||||
|
> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`/rules/${props.id}/history/top_contributors`,
|
||||||
|
{
|
||||||
|
start: props.start,
|
||||||
|
end: props.end,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default topContributors;
|
||||||
62
frontend/src/api/common/getQueryStats.ts
Normal file
62
frontend/src/api/common/getQueryStats.ts
Normal 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.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { ApiV2Instance as axios } from 'api';
|
import { ApiV2Instance as axios } from 'api';
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
|
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||||
|
import store from 'store';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import {
|
import {
|
||||||
Props,
|
Props,
|
||||||
@@ -11,7 +13,26 @@ const dashboardVariablesQuery = async (
|
|||||||
props: Props,
|
props: Props,
|
||||||
): Promise<SuccessResponse<VariableResponseProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<VariableResponseProps> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(`/variables/query`, props);
|
const { globalTime } = store.getState();
|
||||||
|
const { start, end } = getStartEndRangeTime({
|
||||||
|
type: 'GLOBAL_TIME',
|
||||||
|
interval: globalTime.selectedTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
const timeVariables: Record<string, number> = {
|
||||||
|
start_timestamp_ms: parseInt(start, 10) * 1e3,
|
||||||
|
end_timestamp_ms: parseInt(end, 10) * 1e3,
|
||||||
|
start_timestamp_nano: parseInt(start, 10) * 1e9,
|
||||||
|
end_timestamp_nano: parseInt(end, 10) * 1e9,
|
||||||
|
start_timestamp: parseInt(start, 10),
|
||||||
|
end_timestamp: parseInt(end, 10),
|
||||||
|
};
|
||||||
|
|
||||||
|
const payload = { ...props };
|
||||||
|
|
||||||
|
payload.variables = { ...payload.variables, ...timeVariables };
|
||||||
|
|
||||||
|
const response = await axios.post(`/variables/query`, payload);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
|
|||||||
@@ -12,10 +12,13 @@ export const getMetricsQueryRange = async (
|
|||||||
props: QueryRangePayload,
|
props: QueryRangePayload,
|
||||||
version: string,
|
version: string,
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
|
headers?: Record<string, string>,
|
||||||
): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => {
|
): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
if (version && version === ENTITY_VERSION_V4) {
|
if (version && version === ENTITY_VERSION_V4) {
|
||||||
const response = await ApiV4Instance.post('/query_range', props, { signal });
|
const response = await ApiV4Instance.post('/query_range', props, {
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
@@ -26,7 +29,10 @@ export const getMetricsQueryRange = async (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await ApiV3Instance.post('/query_range', props, { signal });
|
const response = await ApiV3Instance.post('/query_range', props, {
|
||||||
|
signal,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
|
|||||||
63
frontend/src/api/queryBuilder/getAttributeSuggestions.ts
Normal file
63
frontend/src/api/queryBuilder/getAttributeSuggestions.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { ApiV3Instance } from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError, AxiosResponse } from 'axios';
|
||||||
|
import { baseAutoCompleteIdKeysOrder } from 'constants/queryBuilder';
|
||||||
|
import { encode } from 'js-base64';
|
||||||
|
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
||||||
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import {
|
||||||
|
IGetAttributeSuggestionsPayload,
|
||||||
|
IGetAttributeSuggestionsSuccessResponse,
|
||||||
|
} from 'types/api/queryBuilder/getAttributeSuggestions';
|
||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
|
||||||
|
export const getAttributeSuggestions = async ({
|
||||||
|
searchText,
|
||||||
|
dataSource,
|
||||||
|
filters,
|
||||||
|
}: IGetAttributeSuggestionsPayload): Promise<
|
||||||
|
SuccessResponse<IGetAttributeSuggestionsSuccessResponse> | ErrorResponse
|
||||||
|
> => {
|
||||||
|
try {
|
||||||
|
let base64EncodedFiltersString;
|
||||||
|
try {
|
||||||
|
// the replace function is to remove the padding at the end of base64 encoded string which is auto added to make it a multiple of 4
|
||||||
|
// why ? because the current working of qs doesn't work well with padding
|
||||||
|
base64EncodedFiltersString = encode(JSON.stringify(filters)).replace(
|
||||||
|
/=+$/,
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// default base64 encoded string for empty filters object
|
||||||
|
base64EncodedFiltersString = 'eyJpdGVtcyI6W10sIm9wIjoiQU5EIn0';
|
||||||
|
}
|
||||||
|
const response: AxiosResponse<{
|
||||||
|
data: IGetAttributeSuggestionsSuccessResponse;
|
||||||
|
}> = await ApiV3Instance.get(
|
||||||
|
`/filter_suggestions?${createQueryParams({
|
||||||
|
searchText,
|
||||||
|
dataSource,
|
||||||
|
existingFilter: base64EncodedFiltersString,
|
||||||
|
})}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const payload: BaseAutocompleteData[] =
|
||||||
|
response.data.data.attributes?.map(({ id: _, ...item }) => ({
|
||||||
|
...item,
|
||||||
|
id: createIdFromObjectFields(item, baseAutoCompleteIdKeysOrder),
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.statusText,
|
||||||
|
payload: {
|
||||||
|
attributes: payload,
|
||||||
|
example_queries: response.data.data.example_queries,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return ErrorResponseHandler(e as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
41
frontend/src/assets/AlertHistory/ConfigureIcon.tsx
Normal file
41
frontend/src/assets/AlertHistory/ConfigureIcon.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
interface ConfigureIconProps {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
fill?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConfigureIcon({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fill,
|
||||||
|
}: ConfigureIconProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
fill={fill}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="#C0C1C3"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="1.333"
|
||||||
|
d="M9.71 4.745a.576.576 0 000 .806l.922.922a.576.576 0 00.806 0l2.171-2.171a3.455 3.455 0 01-4.572 4.572l-3.98 3.98a1.222 1.222 0 11-1.727-1.728l3.98-3.98a3.455 3.455 0 014.572-4.572L9.717 4.739l-.006.006z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
stroke="#C0C1C3"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeWidth="1.333"
|
||||||
|
d="M4 7L2.527 5.566a1.333 1.333 0 01-.013-1.898l.81-.81a1.333 1.333 0 011.991.119L5.333 3m5.417 7.988l1.179 1.178m0 0l-.138.138a.833.833 0 00.387 1.397v0a.833.833 0 00.792-.219l.446-.446a.833.833 0 00.176-.917v0a.833.833 0 00-1.355-.261l-.308.308z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigureIcon.defaultProps = {
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
fill: 'none',
|
||||||
|
};
|
||||||
|
export default ConfigureIcon;
|
||||||
65
frontend/src/assets/AlertHistory/LogsIcon.tsx
Normal file
65
frontend/src/assets/AlertHistory/LogsIcon.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
interface LogsIconProps {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
fill?: string;
|
||||||
|
strokeColor?: string;
|
||||||
|
strokeWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LogsIcon({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fill,
|
||||||
|
strokeColor,
|
||||||
|
strokeWidth,
|
||||||
|
}: LogsIconProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
fill={fill}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke={strokeColor}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
d="M2.917 3.208v7.875"
|
||||||
|
/>
|
||||||
|
<ellipse
|
||||||
|
cx="6.417"
|
||||||
|
cy="3.208"
|
||||||
|
stroke={strokeColor}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
rx="3.5"
|
||||||
|
ry="1.458"
|
||||||
|
/>
|
||||||
|
<ellipse cx="6.417" cy="3.165" fill={strokeColor} rx="0.875" ry="0.365" />
|
||||||
|
<path
|
||||||
|
stroke={strokeColor}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
d="M9.917 11.083c0 .645-1.567 1.167-3.5 1.167s-3.5-.522-3.5-1.167"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
stroke={strokeColor}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
d="M5.25 6.417v1.117c0 .028.02.053.049.057l1.652.276A.058.058 0 017 7.924v1.993"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
stroke={strokeColor}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
d="M9.917 3.208v3.103c0 .046.05.074.089.05L12.182 5a.058.058 0 01.088.035l.264 1.059a.058.058 0 01-.013.053l-2.59 2.877a.058.058 0 00-.014.04v2.018"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LogsIcon.defaultProps = {
|
||||||
|
width: 14,
|
||||||
|
height: 14,
|
||||||
|
fill: 'none',
|
||||||
|
strokeColor: '#C0C1C3',
|
||||||
|
strokeWidth: 1.167,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogsIcon;
|
||||||
39
frontend/src/assets/AlertHistory/SeverityCriticalIcon.tsx
Normal file
39
frontend/src/assets/AlertHistory/SeverityCriticalIcon.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
interface SeverityCriticalIconProps {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
fill?: string;
|
||||||
|
stroke?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SeverityCriticalIcon({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fill,
|
||||||
|
stroke,
|
||||||
|
}: SeverityCriticalIconProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
fill={fill}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M.99707.666056.99707 2.99939M.99707 5.33337H.991237M3.00293.666056 3.00293 2.99939M3.00293 5.33337H2.9971M5.00879.666056V2.99939M5.00879 5.33337H5.00296"
|
||||||
|
stroke={stroke}
|
||||||
|
strokeWidth="1.16667"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SeverityCriticalIcon.defaultProps = {
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
fill: 'none',
|
||||||
|
stroke: '#F56C87',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SeverityCriticalIcon;
|
||||||
42
frontend/src/assets/AlertHistory/SeverityErrorIcon.tsx
Normal file
42
frontend/src/assets/AlertHistory/SeverityErrorIcon.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
interface SeverityErrorIconProps {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
fill?: string;
|
||||||
|
stroke?: string;
|
||||||
|
strokeWidth?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SeverityErrorIcon({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fill,
|
||||||
|
stroke,
|
||||||
|
strokeWidth,
|
||||||
|
}: SeverityErrorIconProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
fill={fill}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M1.00781.957845 1.00781 2.99951M1.00781 5.04175H1.00228"
|
||||||
|
stroke={stroke}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SeverityErrorIcon.defaultProps = {
|
||||||
|
width: 2,
|
||||||
|
height: 6,
|
||||||
|
fill: 'none',
|
||||||
|
stroke: '#F56C87',
|
||||||
|
strokeWidth: '1.02083',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SeverityErrorIcon;
|
||||||
46
frontend/src/assets/AlertHistory/SeverityInfoIcon.tsx
Normal file
46
frontend/src/assets/AlertHistory/SeverityInfoIcon.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
interface SeverityInfoIconProps {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
fill?: string;
|
||||||
|
stroke?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SeverityInfoIcon({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fill,
|
||||||
|
stroke,
|
||||||
|
}: SeverityInfoIconProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
fill={fill}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
rx="3.5"
|
||||||
|
fill={stroke}
|
||||||
|
fillOpacity=".2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M7 9.33346V7.00012M7 4.66675H7.00583"
|
||||||
|
stroke={stroke}
|
||||||
|
strokeWidth="1.16667"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SeverityInfoIcon.defaultProps = {
|
||||||
|
width: 14,
|
||||||
|
height: 14,
|
||||||
|
fill: 'none',
|
||||||
|
stroke: '#7190F9',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SeverityInfoIcon;
|
||||||
42
frontend/src/assets/AlertHistory/SeverityWarningIcon.tsx
Normal file
42
frontend/src/assets/AlertHistory/SeverityWarningIcon.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
interface SeverityWarningIconProps {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
fill?: string;
|
||||||
|
stroke?: string;
|
||||||
|
strokeWidth?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SeverityWarningIcon({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fill,
|
||||||
|
stroke,
|
||||||
|
strokeWidth,
|
||||||
|
}: SeverityWarningIconProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
fill={fill}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M1.00732.957845 1.00732 2.99951M1.00732 5.04175H1.00179"
|
||||||
|
stroke={stroke}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SeverityWarningIcon.defaultProps = {
|
||||||
|
width: 2,
|
||||||
|
height: 6,
|
||||||
|
fill: 'none',
|
||||||
|
stroke: '#FFD778',
|
||||||
|
strokeWidth: '0.978299',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SeverityWarningIcon;
|
||||||
27
frontend/src/assets/CustomIcons/GroupByIcon.tsx
Normal file
27
frontend/src/assets/CustomIcons/GroupByIcon.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
|
||||||
|
function GroupByIcon(): JSX.Element {
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
return (
|
||||||
|
<svg width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g
|
||||||
|
clipPath="url(#prefix__clip0_4344_1236)"
|
||||||
|
stroke={isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500}
|
||||||
|
strokeWidth="1.167"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M4.667 1.167H2.333c-.644 0-1.166.522-1.166 1.166v2.334c0 .644.522 1.166 1.166 1.166h2.334c.644 0 1.166-.522 1.166-1.166V2.333c0-.644-.522-1.166-1.166-1.166zM8.167 1.167a1.17 1.17 0 011.166 1.166v2.334a1.17 1.17 0 01-1.166 1.166M11.667 1.167a1.17 1.17 0 011.166 1.166v2.334a1.17 1.17 0 01-1.166 1.166M5.833 10.5H2.917c-.992 0-1.75-.758-1.75-1.75v-.583" />
|
||||||
|
<path d="M4.083 12.25l1.75-1.75-1.75-1.75M11.667 8.167H9.333c-.644 0-1.166.522-1.166 1.166v2.334c0 .644.522 1.166 1.166 1.166h2.334c.644 0 1.166-.522 1.166-1.166V9.333c0-.644-.522-1.166-1.166-1.166z" />
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="prefix__clip0_4344_1236">
|
||||||
|
<path fill="#fff" d="M0 0h14v14H0z" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GroupByIcon;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
.reset-button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.reset-button {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
frontend/src/components/AlertDetailsFilters/Filters.tsx
Normal file
11
frontend/src/components/AlertDetailsFilters/Filters.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import './Filters.styles.scss';
|
||||||
|
|
||||||
|
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
|
||||||
|
|
||||||
|
export function Filters(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="filters">
|
||||||
|
<DateTimeSelector showAutoRefresh={false} hideShareModal showResetButton />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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'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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
191
frontend/src/components/LaunchChatSupport/LaunchChatSupport.tsx
Normal file
191
frontend/src/components/LaunchChatSupport/LaunchChatSupport.tsx
Normal 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'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;
|
||||||
@@ -41,6 +41,21 @@ I need help with managing alerts.
|
|||||||
|
|
||||||
Thanks`;
|
Thanks`;
|
||||||
|
|
||||||
|
export const onboardingHelpMessage = (
|
||||||
|
dataSourceName: string,
|
||||||
|
moduleId: string,
|
||||||
|
): string => `Hi Team,
|
||||||
|
|
||||||
|
I am facing issues sending data to SigNoz. Here are my application details
|
||||||
|
|
||||||
|
Data Source: ${dataSourceName}
|
||||||
|
Framework:
|
||||||
|
Environment:
|
||||||
|
Module: ${moduleId}
|
||||||
|
|
||||||
|
Thanks
|
||||||
|
`;
|
||||||
|
|
||||||
export const alertHelpMessage = (
|
export const alertHelpMessage = (
|
||||||
alertDef: AlertDef,
|
alertDef: AlertDef,
|
||||||
ruleId: number,
|
ruleId: number,
|
||||||
@@ -3,12 +3,18 @@ import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
|
|||||||
import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
|
import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
|
||||||
import { IField } from 'types/api/logs/fields';
|
import { IField } from 'types/api/logs/fields';
|
||||||
import { ILog } from 'types/api/logs/log';
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
|
||||||
import { VIEWS } from './constants';
|
import { VIEWS } from './constants';
|
||||||
|
|
||||||
export type LogDetailProps = {
|
export type LogDetailProps = {
|
||||||
log: ILog | null;
|
log: ILog | null;
|
||||||
selectedTab: VIEWS;
|
selectedTab: VIEWS;
|
||||||
|
onGroupByAttribute?: (
|
||||||
|
fieldKey: string,
|
||||||
|
isJSON?: boolean,
|
||||||
|
dataType?: DataTypes,
|
||||||
|
) => Promise<void>;
|
||||||
isListViewPanel?: boolean;
|
isListViewPanel?: boolean;
|
||||||
listViewPanelSelectedFields?: IField[] | null;
|
listViewPanelSelectedFields?: IField[] | null;
|
||||||
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
|
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import './LogDetails.styles.scss';
|
import './LogDetails.styles.scss';
|
||||||
|
|
||||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||||
|
import Convert from 'ansi-to-html';
|
||||||
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
|
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
|
||||||
import { RadioChangeEvent } from 'antd/lib';
|
import { RadioChangeEvent } from 'antd/lib';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
@@ -10,8 +11,13 @@ import { LOCALSTORAGE } from 'constants/localStorage';
|
|||||||
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
|
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
|
||||||
import JSONView from 'container/LogDetailedView/JsonView';
|
import JSONView from 'container/LogDetailedView/JsonView';
|
||||||
import Overview from 'container/LogDetailedView/Overview';
|
import Overview from 'container/LogDetailedView/Overview';
|
||||||
import { aggregateAttributesResourcesToString } from 'container/LogDetailedView/utils';
|
import {
|
||||||
|
aggregateAttributesResourcesToString,
|
||||||
|
removeEscapeCharacters,
|
||||||
|
unescapeString,
|
||||||
|
} from 'container/LogDetailedView/utils';
|
||||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||||
|
import dompurify from 'dompurify';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
@@ -28,15 +34,19 @@ import { useMemo, useState } from 'react';
|
|||||||
import { useCopyToClipboard } from 'react-use';
|
import { useCopyToClipboard } from 'react-use';
|
||||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||||
|
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
|
||||||
|
|
||||||
import { VIEW_TYPES, VIEWS } from './constants';
|
import { VIEW_TYPES, VIEWS } from './constants';
|
||||||
import { LogDetailProps } from './LogDetail.interfaces';
|
import { LogDetailProps } from './LogDetail.interfaces';
|
||||||
import QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper';
|
import QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper';
|
||||||
|
|
||||||
|
const convert = new Convert();
|
||||||
|
|
||||||
function LogDetail({
|
function LogDetail({
|
||||||
log,
|
log,
|
||||||
onClose,
|
onClose,
|
||||||
onAddToQuery,
|
onAddToQuery,
|
||||||
|
onGroupByAttribute,
|
||||||
onClickActionItem,
|
onClickActionItem,
|
||||||
selectedTab,
|
selectedTab,
|
||||||
isListViewPanel = false,
|
isListViewPanel = false,
|
||||||
@@ -89,6 +99,17 @@ function LogDetail({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const htmlBody = useMemo(
|
||||||
|
() => ({
|
||||||
|
__html: convert.toHtml(
|
||||||
|
dompurify.sanitize(unescapeString(log?.body || ''), {
|
||||||
|
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
[log?.body],
|
||||||
|
);
|
||||||
|
|
||||||
const handleJSONCopy = (): void => {
|
const handleJSONCopy = (): void => {
|
||||||
copyToClipboard(LogJsonData);
|
copyToClipboard(LogJsonData);
|
||||||
notifications.success({
|
notifications.success({
|
||||||
@@ -126,8 +147,8 @@ function LogDetail({
|
|||||||
>
|
>
|
||||||
<div className="log-detail-drawer__log">
|
<div className="log-detail-drawer__log">
|
||||||
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
|
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
|
||||||
<Tooltip title={log?.body} placement="left">
|
<Tooltip title={removeEscapeCharacters(log?.body)} placement="left">
|
||||||
<Typography.Text className="log-body">{log?.body}</Typography.Text>
|
<div className="log-body" dangerouslySetInnerHTML={htmlBody} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<div className="log-overflow-shadow"> </div>
|
<div className="log-overflow-shadow"> </div>
|
||||||
@@ -209,6 +230,7 @@ function LogDetail({
|
|||||||
logData={log}
|
logData={log}
|
||||||
onAddToQuery={onAddToQuery}
|
onAddToQuery={onAddToQuery}
|
||||||
onClickActionItem={onClickActionItem}
|
onClickActionItem={onClickActionItem}
|
||||||
|
onGroupByAttribute={onGroupByAttribute}
|
||||||
isListViewPanel={isListViewPanel}
|
isListViewPanel={isListViewPanel}
|
||||||
selectedOptions={options}
|
selectedOptions={options}
|
||||||
listViewPanelSelectedFields={listViewPanelSelectedFields}
|
listViewPanelSelectedFields={listViewPanelSelectedFields}
|
||||||
|
|||||||
@@ -1,3 +1,16 @@
|
|||||||
.addToQueryContainer {
|
.addToQueryContainer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
&.small {
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.medium {
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.large {
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import './AddToQueryHOC.styles.scss';
|
import './AddToQueryHOC.styles.scss';
|
||||||
|
|
||||||
import { Popover } from 'antd';
|
import { Popover } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
import { OPERATORS } from 'constants/queryBuilder';
|
import { OPERATORS } from 'constants/queryBuilder';
|
||||||
|
import { FontSize } from 'container/OptionsMenu/types';
|
||||||
import { memo, MouseEvent, ReactNode, useMemo } from 'react';
|
import { memo, MouseEvent, ReactNode, useMemo } from 'react';
|
||||||
|
|
||||||
function AddToQueryHOC({
|
function AddToQueryHOC({
|
||||||
fieldKey,
|
fieldKey,
|
||||||
fieldValue,
|
fieldValue,
|
||||||
onAddToQuery,
|
onAddToQuery,
|
||||||
|
fontSize,
|
||||||
children,
|
children,
|
||||||
}: AddToQueryHOCProps): JSX.Element {
|
}: AddToQueryHOCProps): JSX.Element {
|
||||||
const handleQueryAdd = (event: MouseEvent<HTMLDivElement>): void => {
|
const handleQueryAdd = (event: MouseEvent<HTMLDivElement>): void => {
|
||||||
@@ -21,7 +24,7 @@ function AddToQueryHOC({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||||
<div className="addToQueryContainer" onClick={handleQueryAdd}>
|
<div className={cx('addToQueryContainer', fontSize)} onClick={handleQueryAdd}>
|
||||||
<Popover placement="top" content={popOverContent}>
|
<Popover placement="top" content={popOverContent}>
|
||||||
{children}
|
{children}
|
||||||
</Popover>
|
</Popover>
|
||||||
@@ -33,6 +36,7 @@ export interface AddToQueryHOCProps {
|
|||||||
fieldKey: string;
|
fieldKey: string;
|
||||||
fieldValue: string;
|
fieldValue: string;
|
||||||
onAddToQuery: (fieldKey: string, fieldValue: string, operator: string) => void;
|
onAddToQuery: (fieldKey: string, fieldValue: string, operator: string) => void;
|
||||||
|
fontSize: FontSize;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ReactNode, useCallback, useEffect } from 'react';
|
|||||||
import { useCopyToClipboard } from 'react-use';
|
import { useCopyToClipboard } from 'react-use';
|
||||||
|
|
||||||
function CopyClipboardHOC({
|
function CopyClipboardHOC({
|
||||||
|
entityKey,
|
||||||
textToCopy,
|
textToCopy,
|
||||||
children,
|
children,
|
||||||
}: CopyClipboardHOCProps): JSX.Element {
|
}: CopyClipboardHOCProps): JSX.Element {
|
||||||
@@ -11,11 +12,15 @@ function CopyClipboardHOC({
|
|||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value.value) {
|
if (value.value) {
|
||||||
|
const key = entityKey || '';
|
||||||
|
|
||||||
|
const notificationMessage = `${key} copied to clipboard`;
|
||||||
|
|
||||||
notifications.success({
|
notifications.success({
|
||||||
message: 'Copied to clipboard',
|
message: notificationMessage,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [value, notifications]);
|
}, [value, notifications, entityKey]);
|
||||||
|
|
||||||
const onClick = useCallback((): void => {
|
const onClick = useCallback((): void => {
|
||||||
setCopy(textToCopy);
|
setCopy(textToCopy);
|
||||||
@@ -34,6 +39,7 @@ function CopyClipboardHOC({
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface CopyClipboardHOCProps {
|
interface CopyClipboardHOCProps {
|
||||||
|
entityKey: string | undefined;
|
||||||
textToCopy: string;
|
textToCopy: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,21 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 18px; /* 128.571% */
|
line-height: 18px; /* 128.571% */
|
||||||
letter-spacing: -0.07px;
|
letter-spacing: -0.07px;
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.medium {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.large {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.log-value {
|
.log-value {
|
||||||
color: var(--text-vanilla-400, #c0c1c3);
|
color: var(--text-vanilla-400, #c0c1c3);
|
||||||
@@ -14,6 +29,21 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 18px; /* 128.571% */
|
line-height: 18px; /* 128.571% */
|
||||||
letter-spacing: -0.07px;
|
letter-spacing: -0.07px;
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.medium {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.large {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.log-line {
|
.log-line {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -40,6 +70,20 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 18px; /* 128.571% */
|
line-height: 18px; /* 128.571% */
|
||||||
letter-spacing: -0.07px;
|
letter-spacing: -0.07px;
|
||||||
|
&.small {
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.medium {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.large {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected-log-value {
|
.selected-log-value {
|
||||||
@@ -52,12 +96,37 @@
|
|||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
letter-spacing: -0.07px;
|
letter-spacing: -0.07px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
&.small {
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.medium {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.large {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected-log-kv {
|
.selected-log-kv {
|
||||||
min-height: 24px;
|
min-height: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
&.small {
|
||||||
|
min-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.medium {
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.large {
|
||||||
|
min-height: 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ import './ListLogView.styles.scss';
|
|||||||
import { blue } from '@ant-design/colors';
|
import { blue } from '@ant-design/colors';
|
||||||
import Convert from 'ansi-to-html';
|
import Convert from 'ansi-to-html';
|
||||||
import { Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
import LogDetail from 'components/LogDetail';
|
import LogDetail from 'components/LogDetail';
|
||||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||||
|
import { unescapeString } from 'container/LogDetailedView/utils';
|
||||||
|
import { FontSize } from 'container/OptionsMenu/types';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import dompurify from 'dompurify';
|
import dompurify from 'dompurify';
|
||||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||||
@@ -39,6 +42,7 @@ interface LogFieldProps {
|
|||||||
fieldKey: string;
|
fieldKey: string;
|
||||||
fieldValue: string;
|
fieldValue: string;
|
||||||
linesPerRow?: number;
|
linesPerRow?: number;
|
||||||
|
fontSize: FontSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogSelectedFieldProps = Omit<LogFieldProps, 'linesPerRow'> &
|
type LogSelectedFieldProps = Omit<LogFieldProps, 'linesPerRow'> &
|
||||||
@@ -48,11 +52,12 @@ function LogGeneralField({
|
|||||||
fieldKey,
|
fieldKey,
|
||||||
fieldValue,
|
fieldValue,
|
||||||
linesPerRow = 1,
|
linesPerRow = 1,
|
||||||
|
fontSize,
|
||||||
}: LogFieldProps): JSX.Element {
|
}: LogFieldProps): JSX.Element {
|
||||||
const html = useMemo(
|
const html = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
__html: convert.toHtml(
|
__html: convert.toHtml(
|
||||||
dompurify.sanitize(fieldValue, {
|
dompurify.sanitize(unescapeString(fieldValue), {
|
||||||
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -62,12 +67,12 @@ function LogGeneralField({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TextContainer>
|
<TextContainer>
|
||||||
<Text ellipsis type="secondary" className="log-field-key">
|
<Text ellipsis type="secondary" className={cx('log-field-key', fontSize)}>
|
||||||
{`${fieldKey} : `}
|
{`${fieldKey} : `}
|
||||||
</Text>
|
</Text>
|
||||||
<LogText
|
<LogText
|
||||||
dangerouslySetInnerHTML={html}
|
dangerouslySetInnerHTML={html}
|
||||||
className="log-value"
|
className={cx('log-value', fontSize)}
|
||||||
linesPerRow={linesPerRow > 1 ? linesPerRow : undefined}
|
linesPerRow={linesPerRow > 1 ? linesPerRow : undefined}
|
||||||
/>
|
/>
|
||||||
</TextContainer>
|
</TextContainer>
|
||||||
@@ -78,6 +83,7 @@ function LogSelectedField({
|
|||||||
fieldKey = '',
|
fieldKey = '',
|
||||||
fieldValue = '',
|
fieldValue = '',
|
||||||
onAddToQuery,
|
onAddToQuery,
|
||||||
|
fontSize,
|
||||||
}: LogSelectedFieldProps): JSX.Element {
|
}: LogSelectedFieldProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="log-selected-fields">
|
<div className="log-selected-fields">
|
||||||
@@ -85,16 +91,22 @@ function LogSelectedField({
|
|||||||
fieldKey={fieldKey}
|
fieldKey={fieldKey}
|
||||||
fieldValue={fieldValue}
|
fieldValue={fieldValue}
|
||||||
onAddToQuery={onAddToQuery}
|
onAddToQuery={onAddToQuery}
|
||||||
|
fontSize={fontSize}
|
||||||
>
|
>
|
||||||
<Typography.Text>
|
<Typography.Text>
|
||||||
<span style={{ color: blue[4] }} className="selected-log-field-key">
|
<span
|
||||||
|
style={{ color: blue[4] }}
|
||||||
|
className={cx('selected-log-field-key', fontSize)}
|
||||||
|
>
|
||||||
{fieldKey}
|
{fieldKey}
|
||||||
</span>
|
</span>
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</AddToQueryHOC>
|
</AddToQueryHOC>
|
||||||
<Typography.Text ellipsis className="selected-log-kv">
|
<Typography.Text ellipsis className={cx('selected-log-kv', fontSize)}>
|
||||||
<span className="selected-log-field-key">{': '}</span>
|
<span className={cx('selected-log-field-key', fontSize)}>{': '}</span>
|
||||||
<span className="selected-log-value">{fieldValue || "''"}</span>
|
<span className={cx('selected-log-value', fontSize)}>
|
||||||
|
{fieldValue || "''"}
|
||||||
|
</span>
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -107,6 +119,7 @@ type ListLogViewProps = {
|
|||||||
onAddToQuery: AddToQueryHOCProps['onAddToQuery'];
|
onAddToQuery: AddToQueryHOCProps['onAddToQuery'];
|
||||||
activeLog?: ILog | null;
|
activeLog?: ILog | null;
|
||||||
linesPerRow: number;
|
linesPerRow: number;
|
||||||
|
fontSize: FontSize;
|
||||||
};
|
};
|
||||||
|
|
||||||
function ListLogView({
|
function ListLogView({
|
||||||
@@ -116,6 +129,7 @@ function ListLogView({
|
|||||||
onAddToQuery,
|
onAddToQuery,
|
||||||
activeLog,
|
activeLog,
|
||||||
linesPerRow,
|
linesPerRow,
|
||||||
|
fontSize,
|
||||||
}: ListLogViewProps): JSX.Element {
|
}: ListLogViewProps): JSX.Element {
|
||||||
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
|
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
|
||||||
|
|
||||||
@@ -128,6 +142,7 @@ function ListLogView({
|
|||||||
onAddToQuery: handleAddToQuery,
|
onAddToQuery: handleAddToQuery,
|
||||||
onSetActiveLog: handleSetActiveContextLog,
|
onSetActiveLog: handleSetActiveContextLog,
|
||||||
onClearActiveLog: handleClearActiveContextLog,
|
onClearActiveLog: handleClearActiveContextLog,
|
||||||
|
onGroupByAttribute,
|
||||||
} = useActiveLog();
|
} = useActiveLog();
|
||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
@@ -185,6 +200,7 @@ function ListLogView({
|
|||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
onClick={handleDetailedView}
|
onClick={handleDetailedView}
|
||||||
|
fontSize={fontSize}
|
||||||
>
|
>
|
||||||
<div className="log-line">
|
<div className="log-line">
|
||||||
<LogStateIndicator
|
<LogStateIndicator
|
||||||
@@ -192,18 +208,28 @@ function ListLogView({
|
|||||||
isActive={
|
isActive={
|
||||||
activeLog?.id === logData.id || activeContextLog?.id === logData.id
|
activeLog?.id === logData.id || activeContextLog?.id === logData.id
|
||||||
}
|
}
|
||||||
|
fontSize={fontSize}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<LogContainer>
|
<LogContainer fontSize={fontSize}>
|
||||||
<LogGeneralField
|
<LogGeneralField
|
||||||
fieldKey="Log"
|
fieldKey="Log"
|
||||||
fieldValue={flattenLogData.body}
|
fieldValue={flattenLogData.body}
|
||||||
linesPerRow={linesPerRow}
|
linesPerRow={linesPerRow}
|
||||||
|
fontSize={fontSize}
|
||||||
/>
|
/>
|
||||||
{flattenLogData.stream && (
|
{flattenLogData.stream && (
|
||||||
<LogGeneralField fieldKey="Stream" fieldValue={flattenLogData.stream} />
|
<LogGeneralField
|
||||||
|
fieldKey="Stream"
|
||||||
|
fieldValue={flattenLogData.stream}
|
||||||
|
fontSize={fontSize}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<LogGeneralField fieldKey="Timestamp" fieldValue={timestampValue} />
|
<LogGeneralField
|
||||||
|
fieldKey="Timestamp"
|
||||||
|
fieldValue={timestampValue}
|
||||||
|
fontSize={fontSize}
|
||||||
|
/>
|
||||||
|
|
||||||
{updatedSelecedFields.map((field) =>
|
{updatedSelecedFields.map((field) =>
|
||||||
isValidLogField(flattenLogData[field.name] as never) ? (
|
isValidLogField(flattenLogData[field.name] as never) ? (
|
||||||
@@ -212,6 +238,7 @@ function ListLogView({
|
|||||||
fieldKey={field.name}
|
fieldKey={field.name}
|
||||||
fieldValue={flattenLogData[field.name] as never}
|
fieldValue={flattenLogData[field.name] as never}
|
||||||
onAddToQuery={onAddToQuery}
|
onAddToQuery={onAddToQuery}
|
||||||
|
fontSize={fontSize}
|
||||||
/>
|
/>
|
||||||
) : null,
|
) : null,
|
||||||
)}
|
)}
|
||||||
@@ -232,6 +259,7 @@ function ListLogView({
|
|||||||
onAddToQuery={handleAddToQuery}
|
onAddToQuery={handleAddToQuery}
|
||||||
selectedTab={VIEW_TYPES.CONTEXT}
|
selectedTab={VIEW_TYPES.CONTEXT}
|
||||||
onClose={handlerClearActiveContextLog}
|
onClose={handlerClearActiveContextLog}
|
||||||
|
onGroupByAttribute={onGroupByAttribute}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,21 +1,46 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Card, Typography } from 'antd';
|
import { Card, Typography } from 'antd';
|
||||||
|
import { FontSize } from 'container/OptionsMenu/types';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
interface LogTextProps {
|
interface LogTextProps {
|
||||||
linesPerRow?: number;
|
linesPerRow?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LogContainerProps {
|
||||||
|
fontSize: FontSize;
|
||||||
|
}
|
||||||
|
|
||||||
export const Container = styled(Card)<{
|
export const Container = styled(Card)<{
|
||||||
$isActiveLog: boolean;
|
$isActiveLog: boolean;
|
||||||
$isDarkMode: boolean;
|
$isDarkMode: boolean;
|
||||||
|
fontSize: FontSize;
|
||||||
}>`
|
}>`
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
margin-bottom: 0.3rem;
|
margin-bottom: 0.3rem;
|
||||||
|
|
||||||
|
${({ fontSize }): string =>
|
||||||
|
fontSize === FontSize.SMALL
|
||||||
|
? `margin-bottom:0.1rem;`
|
||||||
|
: fontSize === FontSize.MEDIUM
|
||||||
|
? `margin-bottom: 0.2rem;`
|
||||||
|
: fontSize === FontSize.LARGE
|
||||||
|
? `margin-bottom:0.3rem;`
|
||||||
|
: ``}
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
padding: 0.3rem 0.6rem;
|
padding: 0.3rem 0.6rem;
|
||||||
|
|
||||||
|
${({ fontSize }): string =>
|
||||||
|
fontSize === FontSize.SMALL
|
||||||
|
? `padding:0.1rem 0.6rem;`
|
||||||
|
: fontSize === FontSize.MEDIUM
|
||||||
|
? `padding: 0.2rem 0.6rem;`
|
||||||
|
: fontSize === FontSize.LARGE
|
||||||
|
? `padding:0.3rem 0.6rem;`
|
||||||
|
: ``}
|
||||||
|
|
||||||
${({ $isActiveLog, $isDarkMode }): string =>
|
${({ $isActiveLog, $isDarkMode }): string =>
|
||||||
$isActiveLog
|
$isActiveLog
|
||||||
? `background-color: ${
|
? `background-color: ${
|
||||||
@@ -38,11 +63,17 @@ export const TextContainer = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const LogContainer = styled.div`
|
export const LogContainer = styled.div<LogContainerProps>`
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
${({ fontSize }): string =>
|
||||||
|
fontSize === FontSize.SMALL
|
||||||
|
? `gap: 2px;`
|
||||||
|
: fontSize === FontSize.MEDIUM
|
||||||
|
? ` gap:4px;`
|
||||||
|
: `gap:6px;`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const LogText = styled.div<LogTextProps>`
|
export const LogText = styled.div<LogTextProps>`
|
||||||
|
|||||||
@@ -9,11 +9,24 @@
|
|||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
min-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.medium {
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.large {
|
||||||
|
min-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
&.INFO {
|
&.INFO {
|
||||||
background-color: var(--bg-slate-400);
|
background-color: var(--bg-slate-400);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.WARNING, &.WARN {
|
&.WARNING,
|
||||||
|
&.WARN {
|
||||||
background-color: var(--bg-amber-500);
|
background-color: var(--bg-amber-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
|
import { FontSize } from 'container/OptionsMenu/types';
|
||||||
|
|
||||||
import LogStateIndicator from './LogStateIndicator';
|
import LogStateIndicator from './LogStateIndicator';
|
||||||
|
|
||||||
describe('LogStateIndicator', () => {
|
describe('LogStateIndicator', () => {
|
||||||
it('renders correctly with default props', () => {
|
it('renders correctly with default props', () => {
|
||||||
const { container } = render(<LogStateIndicator type="INFO" />);
|
const { container } = render(
|
||||||
|
<LogStateIndicator type="INFO" fontSize={FontSize.MEDIUM} />,
|
||||||
|
);
|
||||||
const indicator = container.firstChild as HTMLElement;
|
const indicator = container.firstChild as HTMLElement;
|
||||||
expect(indicator.classList.contains('log-state-indicator')).toBe(true);
|
expect(indicator.classList.contains('log-state-indicator')).toBe(true);
|
||||||
expect(indicator.classList.contains('isActive')).toBe(false);
|
expect(indicator.classList.contains('isActive')).toBe(false);
|
||||||
@@ -15,28 +18,30 @@ describe('LogStateIndicator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders correctly when isActive is true', () => {
|
it('renders correctly when isActive is true', () => {
|
||||||
const { container } = render(<LogStateIndicator type="INFO" isActive />);
|
const { container } = render(
|
||||||
|
<LogStateIndicator type="INFO" isActive fontSize={FontSize.MEDIUM} />,
|
||||||
|
);
|
||||||
const indicator = container.firstChild as HTMLElement;
|
const indicator = container.firstChild as HTMLElement;
|
||||||
expect(indicator.classList.contains('isActive')).toBe(true);
|
expect(indicator.classList.contains('isActive')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders correctly with different types', () => {
|
it('renders correctly with different types', () => {
|
||||||
const { container: containerInfo } = render(
|
const { container: containerInfo } = render(
|
||||||
<LogStateIndicator type="INFO" />,
|
<LogStateIndicator type="INFO" fontSize={FontSize.MEDIUM} />,
|
||||||
);
|
);
|
||||||
expect(containerInfo.querySelector('.line')?.classList.contains('INFO')).toBe(
|
expect(containerInfo.querySelector('.line')?.classList.contains('INFO')).toBe(
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { container: containerWarning } = render(
|
const { container: containerWarning } = render(
|
||||||
<LogStateIndicator type="WARNING" />,
|
<LogStateIndicator type="WARNING" fontSize={FontSize.MEDIUM} />,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
containerWarning.querySelector('.line')?.classList.contains('WARNING'),
|
containerWarning.querySelector('.line')?.classList.contains('WARNING'),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
|
|
||||||
const { container: containerError } = render(
|
const { container: containerError } = render(
|
||||||
<LogStateIndicator type="ERROR" />,
|
<LogStateIndicator type="ERROR" fontSize={FontSize.MEDIUM} />,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
containerError.querySelector('.line')?.classList.contains('ERROR'),
|
containerError.querySelector('.line')?.classList.contains('ERROR'),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import './LogStateIndicator.styles.scss';
|
import './LogStateIndicator.styles.scss';
|
||||||
|
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
|
import { FontSize } from 'container/OptionsMenu/types';
|
||||||
|
|
||||||
export const SEVERITY_TEXT_TYPE = {
|
export const SEVERITY_TEXT_TYPE = {
|
||||||
TRACE: 'TRACE',
|
TRACE: 'TRACE',
|
||||||
@@ -44,13 +45,15 @@ export const LogType = {
|
|||||||
function LogStateIndicator({
|
function LogStateIndicator({
|
||||||
type,
|
type,
|
||||||
isActive,
|
isActive,
|
||||||
|
fontSize,
|
||||||
}: {
|
}: {
|
||||||
type: string;
|
type: string;
|
||||||
|
fontSize: FontSize;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className={cx('log-state-indicator', isActive ? 'isActive' : '')}>
|
<div className={cx('log-state-indicator', isActive ? 'isActive' : '')}>
|
||||||
<div className={cx('line', type)}> </div>
|
<div className={cx('line', type, fontSize)}> </div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Convert from 'ansi-to-html';
|
|||||||
import { DrawerProps } from 'antd';
|
import { DrawerProps } from 'antd';
|
||||||
import LogDetail from 'components/LogDetail';
|
import LogDetail from 'components/LogDetail';
|
||||||
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
|
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
|
||||||
|
import { unescapeString } from 'container/LogDetailedView/utils';
|
||||||
import LogsExplorerContext from 'container/LogsExplorerContext';
|
import LogsExplorerContext from 'container/LogsExplorerContext';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import dompurify from 'dompurify';
|
import dompurify from 'dompurify';
|
||||||
@@ -39,6 +40,7 @@ function RawLogView({
|
|||||||
linesPerRow,
|
linesPerRow,
|
||||||
isTextOverflowEllipsisDisabled,
|
isTextOverflowEllipsisDisabled,
|
||||||
selectedFields = [],
|
selectedFields = [],
|
||||||
|
fontSize,
|
||||||
}: RawLogViewProps): JSX.Element {
|
}: RawLogViewProps): JSX.Element {
|
||||||
const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink(
|
const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink(
|
||||||
data.id,
|
data.id,
|
||||||
@@ -54,6 +56,7 @@ function RawLogView({
|
|||||||
onSetActiveLog,
|
onSetActiveLog,
|
||||||
onClearActiveLog,
|
onClearActiveLog,
|
||||||
onAddToQuery,
|
onAddToQuery,
|
||||||
|
onGroupByAttribute,
|
||||||
} = useActiveLog();
|
} = useActiveLog();
|
||||||
|
|
||||||
const [hasActionButtons, setHasActionButtons] = useState<boolean>(false);
|
const [hasActionButtons, setHasActionButtons] = useState<boolean>(false);
|
||||||
@@ -143,7 +146,9 @@ function RawLogView({
|
|||||||
const html = useMemo(
|
const html = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
__html: convert.toHtml(
|
__html: convert.toHtml(
|
||||||
dompurify.sanitize(text, { FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS] }),
|
dompurify.sanitize(unescapeString(text), {
|
||||||
|
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
[text],
|
[text],
|
||||||
@@ -160,6 +165,7 @@ function RawLogView({
|
|||||||
$isActiveLog={isActiveLog}
|
$isActiveLog={isActiveLog}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
|
fontSize={fontSize}
|
||||||
>
|
>
|
||||||
<LogStateIndicator
|
<LogStateIndicator
|
||||||
type={logType}
|
type={logType}
|
||||||
@@ -168,6 +174,7 @@ function RawLogView({
|
|||||||
activeContextLog?.id === data.id ||
|
activeContextLog?.id === data.id ||
|
||||||
isActiveLog
|
isActiveLog
|
||||||
}
|
}
|
||||||
|
fontSize={fontSize}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RawLogContent
|
<RawLogContent
|
||||||
@@ -176,6 +183,7 @@ function RawLogView({
|
|||||||
$isDarkMode={isDarkMode}
|
$isDarkMode={isDarkMode}
|
||||||
$isTextOverflowEllipsisDisabled={isTextOverflowEllipsisDisabled}
|
$isTextOverflowEllipsisDisabled={isTextOverflowEllipsisDisabled}
|
||||||
linesPerRow={linesPerRow}
|
linesPerRow={linesPerRow}
|
||||||
|
fontSize={fontSize}
|
||||||
dangerouslySetInnerHTML={html}
|
dangerouslySetInnerHTML={html}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -199,6 +207,7 @@ function RawLogView({
|
|||||||
onClose={handleCloseLogDetail}
|
onClose={handleCloseLogDetail}
|
||||||
onAddToQuery={onAddToQuery}
|
onAddToQuery={onAddToQuery}
|
||||||
onClickActionItem={onAddToQuery}
|
onClickActionItem={onAddToQuery}
|
||||||
|
onGroupByAttribute={onGroupByAttribute}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</RawLogViewContainer>
|
</RawLogViewContainer>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
import { blue } from '@ant-design/colors';
|
import { blue } from '@ant-design/colors';
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Col, Row, Space } from 'antd';
|
import { Col, Row, Space } from 'antd';
|
||||||
|
import { FontSize } from 'container/OptionsMenu/types';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { getActiveLogBackground, getDefaultLogBackground } from 'utils/logs';
|
import { getActiveLogBackground, getDefaultLogBackground } from 'utils/logs';
|
||||||
|
|
||||||
@@ -11,6 +13,7 @@ export const RawLogViewContainer = styled(Row)<{
|
|||||||
$isReadOnly?: boolean;
|
$isReadOnly?: boolean;
|
||||||
$isActiveLog?: boolean;
|
$isActiveLog?: boolean;
|
||||||
$isHightlightedLog: boolean;
|
$isHightlightedLog: boolean;
|
||||||
|
fontSize: FontSize;
|
||||||
}>`
|
}>`
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -22,6 +25,13 @@ export const RawLogViewContainer = styled(Row)<{
|
|||||||
|
|
||||||
.log-state-indicator {
|
.log-state-indicator {
|
||||||
margin: 4px 0;
|
margin: 4px 0;
|
||||||
|
|
||||||
|
${({ fontSize }): string =>
|
||||||
|
fontSize === FontSize.SMALL
|
||||||
|
? `margin: 1px 0;`
|
||||||
|
: fontSize === FontSize.MEDIUM
|
||||||
|
? `margin: 1px 0;`
|
||||||
|
: `margin: 2px 0;`}
|
||||||
}
|
}
|
||||||
|
|
||||||
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
|
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
|
||||||
@@ -50,8 +60,8 @@ export const RawLogContent = styled.div<RawLogContentProps>`
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
font-family: 'SF Mono', monospace;
|
font-family: 'SF Mono', monospace;
|
||||||
font-family: 'Geist Mono';
|
font-family: 'Geist Mono';
|
||||||
font-size: 13px;
|
letter-spacing: -0.07px;
|
||||||
font-weight: 400;
|
padding: 4px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: ${({ $isDarkMode }): string =>
|
color: ${({ $isDarkMode }): string =>
|
||||||
$isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400};
|
$isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400};
|
||||||
@@ -66,9 +76,15 @@ export const RawLogContent = styled.div<RawLogContentProps>`
|
|||||||
line-clamp: ${linesPerRow};
|
line-clamp: ${linesPerRow};
|
||||||
-webkit-box-orient: vertical;`};
|
-webkit-box-orient: vertical;`};
|
||||||
|
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 400;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
letter-spacing: -0.07px;
|
${({ fontSize }): string =>
|
||||||
padding: 4px;
|
fontSize === FontSize.SMALL
|
||||||
|
? `font-size:11px; line-height:16px; padding:1px;`
|
||||||
|
: fontSize === FontSize.MEDIUM
|
||||||
|
? `font-size:13px; line-height:20px; padding:1px;`
|
||||||
|
: `font-size:14px; line-height:24px; padding:2px;`}
|
||||||
|
|
||||||
cursor: ${({ $isActiveLog, $isReadOnly }): string =>
|
cursor: ${({ $isActiveLog, $isReadOnly }): string =>
|
||||||
$isActiveLog || $isReadOnly ? 'initial' : 'pointer'};
|
$isActiveLog || $isReadOnly ? 'initial' : 'pointer'};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { FontSize } from 'container/OptionsMenu/types';
|
||||||
import { IField } from 'types/api/logs/fields';
|
import { IField } from 'types/api/logs/fields';
|
||||||
import { ILog } from 'types/api/logs/log';
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
|
||||||
@@ -7,11 +8,13 @@ export interface RawLogViewProps {
|
|||||||
isTextOverflowEllipsisDisabled?: boolean;
|
isTextOverflowEllipsisDisabled?: boolean;
|
||||||
data: ILog;
|
data: ILog;
|
||||||
linesPerRow: number;
|
linesPerRow: number;
|
||||||
|
fontSize: FontSize;
|
||||||
selectedFields?: IField[];
|
selectedFields?: IField[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RawLogContentProps {
|
export interface RawLogContentProps {
|
||||||
linesPerRow: number;
|
linesPerRow: number;
|
||||||
|
fontSize: FontSize;
|
||||||
$isReadOnly?: boolean;
|
$isReadOnly?: boolean;
|
||||||
$isActiveLog?: boolean;
|
$isActiveLog?: boolean;
|
||||||
$isDarkMode?: boolean;
|
$isDarkMode?: boolean;
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
|
import { FontSize } from 'container/OptionsMenu/types';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
interface TableBodyContentProps {
|
interface TableBodyContentProps {
|
||||||
linesPerRow: number;
|
linesPerRow: number;
|
||||||
|
fontSize: FontSize;
|
||||||
isDarkMode?: boolean;
|
isDarkMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,4 +23,10 @@ export const TableBodyContent = styled.div<TableBodyContentProps>`
|
|||||||
-webkit-line-clamp: ${(props): number => props.linesPerRow};
|
-webkit-line-clamp: ${(props): number => props.linesPerRow};
|
||||||
line-clamp: ${(props): number => props.linesPerRow};
|
line-clamp: ${(props): number => props.linesPerRow};
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
|
${({ fontSize }): string =>
|
||||||
|
fontSize === FontSize.SMALL
|
||||||
|
? `font-size:11px; line-height:16px;`
|
||||||
|
: fontSize === FontSize.MEDIUM
|
||||||
|
? `font-size:13px; line-height:20px;`
|
||||||
|
: `font-size:14px; line-height:24px;`}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ColumnsType, ColumnType } from 'antd/es/table';
|
import { ColumnsType, ColumnType } from 'antd/es/table';
|
||||||
|
import { FontSize } from 'container/OptionsMenu/types';
|
||||||
import { IField } from 'types/api/logs/fields';
|
import { IField } from 'types/api/logs/fields';
|
||||||
import { ILog } from 'types/api/logs/log';
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ export type LogsTableViewProps = {
|
|||||||
logs: ILog[];
|
logs: ILog[];
|
||||||
fields: IField[];
|
fields: IField[];
|
||||||
linesPerRow: number;
|
linesPerRow: number;
|
||||||
|
fontSize: FontSize;
|
||||||
onClickExpand?: (log: ILog) => void;
|
onClickExpand?: (log: ILog) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,21 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 18px; /* 128.571% */
|
line-height: 18px; /* 128.571% */
|
||||||
letter-spacing: -0.07px;
|
letter-spacing: -0.07px;
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.medium {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.large {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-timestamp {
|
.table-timestamp {
|
||||||
@@ -25,3 +40,21 @@
|
|||||||
color: var(--bg-slate-400);
|
color: var(--bg-slate-400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.paragraph {
|
||||||
|
padding: 0px !important;
|
||||||
|
&.small {
|
||||||
|
font-size: 11px !important;
|
||||||
|
line-height: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.medium {
|
||||||
|
font-size: 13px !important;
|
||||||
|
line-height: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.large {
|
||||||
|
font-size: 14px !important;
|
||||||
|
line-height: 24px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import './useTableView.styles.scss';
|
|||||||
import Convert from 'ansi-to-html';
|
import Convert from 'ansi-to-html';
|
||||||
import { Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
import { ColumnsType } from 'antd/es/table';
|
import { ColumnsType } from 'antd/es/table';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import { unescapeString } from 'container/LogDetailedView/utils';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import dompurify from 'dompurify';
|
import dompurify from 'dompurify';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
@@ -31,6 +33,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
logs,
|
logs,
|
||||||
fields,
|
fields,
|
||||||
linesPerRow,
|
linesPerRow,
|
||||||
|
fontSize,
|
||||||
appendTo = 'center',
|
appendTo = 'center',
|
||||||
activeContextLog,
|
activeContextLog,
|
||||||
activeLog,
|
activeLog,
|
||||||
@@ -57,7 +60,10 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
: getDefaultCellStyle(isDarkMode),
|
: getDefaultCellStyle(isDarkMode),
|
||||||
},
|
},
|
||||||
children: (
|
children: (
|
||||||
<Typography.Paragraph ellipsis={{ rows: linesPerRow }}>
|
<Typography.Paragraph
|
||||||
|
ellipsis={{ rows: linesPerRow }}
|
||||||
|
className={cx('paragraph', fontSize)}
|
||||||
|
>
|
||||||
{field}
|
{field}
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
),
|
),
|
||||||
@@ -87,8 +93,9 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
isActive={
|
isActive={
|
||||||
activeLog?.id === item.id || activeContextLog?.id === item.id
|
activeLog?.id === item.id || activeContextLog?.id === item.id
|
||||||
}
|
}
|
||||||
|
fontSize={fontSize}
|
||||||
/>
|
/>
|
||||||
<Typography.Paragraph ellipsis className="text">
|
<Typography.Paragraph ellipsis className={cx('text', fontSize)}>
|
||||||
{date}
|
{date}
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,11 +116,12 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
<TableBodyContent
|
<TableBodyContent
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: convert.toHtml(
|
__html: convert.toHtml(
|
||||||
dompurify.sanitize(field, {
|
dompurify.sanitize(unescapeString(field), {
|
||||||
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
fontSize={fontSize}
|
||||||
linesPerRow={linesPerRow}
|
linesPerRow={linesPerRow}
|
||||||
isDarkMode={isDarkMode}
|
isDarkMode={isDarkMode}
|
||||||
/>
|
/>
|
||||||
@@ -130,6 +138,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
linesPerRow,
|
linesPerRow,
|
||||||
activeLog?.id,
|
activeLog?.id,
|
||||||
activeContextLog?.id,
|
activeContextLog?.id,
|
||||||
|
fontSize,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { columns, dataSource: flattenLogData };
|
return { columns, dataSource: flattenLogData };
|
||||||
|
|||||||
@@ -17,17 +17,126 @@
|
|||||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
|
|
||||||
|
.font-size-dropdown {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.text {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: 0.14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn:hover {
|
||||||
|
background-color: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.option-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.text {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: normal; /* 142.857% */
|
||||||
|
letter-spacing: 0.14px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text:hover {
|
||||||
|
color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-btn:hover {
|
||||||
|
background-color: unset !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-size-container {
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: var(--bg-slate-50);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 18px; /* 163.636% */
|
||||||
|
letter-spacing: 0.88px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
display: flex;
|
||||||
|
height: 20px;
|
||||||
|
padding: 4px 0px;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border: none !important;
|
||||||
|
.font-value {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: normal;
|
||||||
|
letter-spacing: 0.14px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.value:hover {
|
||||||
|
background-color: unset !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.menu-container {
|
.menu-container {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-family: Inter;
|
font-family: Inter;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: #52575c;
|
color: var(--bg-slate-50);
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-items {
|
.menu-items {
|
||||||
@@ -65,11 +174,11 @@
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
color: #52575c;
|
color: var(--bg-slate-50);
|
||||||
font-family: Inter;
|
font-family: Inter;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
line-height: 18px; /* 163.636% */
|
line-height: 18px; /* 163.636% */
|
||||||
letter-spacing: 0.88px;
|
letter-spacing: 0.88px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -149,11 +258,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
color: #52575c;
|
color: var(--bg-slate-50);
|
||||||
font-family: Inter;
|
font-family: Inter;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
line-height: 18px; /* 163.636% */
|
line-height: 18px; /* 163.636% */
|
||||||
letter-spacing: 0.88px;
|
letter-spacing: 0.88px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -299,6 +408,38 @@
|
|||||||
|
|
||||||
box-shadow: 4px 10px 16px 2px rgba(255, 255, 255, 0.2);
|
box-shadow: 4px 10px 16px 2px rgba(255, 255, 255, 0.2);
|
||||||
|
|
||||||
|
.font-size-dropdown {
|
||||||
|
.back-btn {
|
||||||
|
.text {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
.option-btn {
|
||||||
|
.text {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text:hover {
|
||||||
|
color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-size-container {
|
||||||
|
.title {
|
||||||
|
color: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
.font-value {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.horizontal-line {
|
.horizontal-line {
|
||||||
background: var(--bg-vanilla-300);
|
background: var(--bg-vanilla-300);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
import './LogsFormatOptionsMenu.styles.scss';
|
import './LogsFormatOptionsMenu.styles.scss';
|
||||||
|
|
||||||
import { Divider, Input, InputNumber, Tooltip } from 'antd';
|
import { Button, Divider, Input, InputNumber, Tooltip, Typography } from 'antd';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { LogViewMode } from 'container/LogsTable';
|
import { LogViewMode } from 'container/LogsTable';
|
||||||
import { OptionsMenuConfig } from 'container/OptionsMenu/types';
|
import { FontSize, OptionsMenuConfig } from 'container/OptionsMenu/types';
|
||||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||||
import { Check, Minus, Plus, X } from 'lucide-react';
|
import { Check, ChevronLeft, ChevronRight, Minus, Plus, X } from 'lucide-react';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
interface LogsFormatOptionsMenuProps {
|
interface LogsFormatOptionsMenuProps {
|
||||||
@@ -24,10 +24,16 @@ export default function LogsFormatOptionsMenu({
|
|||||||
selectedOptionFormat,
|
selectedOptionFormat,
|
||||||
config,
|
config,
|
||||||
}: LogsFormatOptionsMenuProps): JSX.Element {
|
}: LogsFormatOptionsMenuProps): JSX.Element {
|
||||||
const { maxLines, format, addColumn } = config;
|
const { maxLines, format, addColumn, fontSize } = config;
|
||||||
const [selectedItem, setSelectedItem] = useState(selectedOptionFormat);
|
const [selectedItem, setSelectedItem] = useState(selectedOptionFormat);
|
||||||
const maxLinesNumber = (maxLines?.value as number) || 1;
|
const maxLinesNumber = (maxLines?.value as number) || 1;
|
||||||
const [maxLinesPerRow, setMaxLinesPerRow] = useState<number>(maxLinesNumber);
|
const [maxLinesPerRow, setMaxLinesPerRow] = useState<number>(maxLinesNumber);
|
||||||
|
const [fontSizeValue, setFontSizeValue] = useState<FontSize>(
|
||||||
|
fontSize?.value || FontSize.SMALL,
|
||||||
|
);
|
||||||
|
const [isFontSizeOptionsOpen, setIsFontSizeOptionsOpen] = useState<boolean>(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
const [addNewColumn, setAddNewColumn] = useState(false);
|
const [addNewColumn, setAddNewColumn] = useState(false);
|
||||||
|
|
||||||
@@ -88,6 +94,12 @@ export default function LogsFormatOptionsMenu({
|
|||||||
}
|
}
|
||||||
}, [maxLinesPerRow]);
|
}, [maxLinesPerRow]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fontSizeValue && config && config.fontSize?.onChange) {
|
||||||
|
config.fontSize.onChange(fontSizeValue);
|
||||||
|
}
|
||||||
|
}, [fontSizeValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cx('nested-menu-container', addNewColumn ? 'active' : '')}
|
className={cx('nested-menu-container', addNewColumn ? 'active' : '')}
|
||||||
@@ -96,145 +108,213 @@ export default function LogsFormatOptionsMenu({
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="menu-container">
|
{isFontSizeOptionsOpen ? (
|
||||||
<div className="title"> {title} </div>
|
<div className="font-size-dropdown">
|
||||||
|
<Button
|
||||||
<div className="menu-items">
|
onClick={(): void => setIsFontSizeOptionsOpen(false)}
|
||||||
{items.map(
|
className="back-btn"
|
||||||
(item: any): JSX.Element => (
|
type="text"
|
||||||
<div
|
>
|
||||||
className="item"
|
<ChevronLeft size={14} className="icon" />
|
||||||
key={item.label}
|
<Typography.Text className="text">Select font size</Typography.Text>
|
||||||
onClick={(): void => handleMenuItemClick(item.key)}
|
</Button>
|
||||||
>
|
<div className="horizontal-line" />
|
||||||
<div className={cx('item-label')}>
|
<div className="content">
|
||||||
{item.label}
|
<Button
|
||||||
|
onClick={(): void => {
|
||||||
{selectedItem === item.key && <Check size={12} />}
|
setFontSizeValue(FontSize.SMALL);
|
||||||
</div>
|
}}
|
||||||
</div>
|
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>
|
||||||
</div>
|
) : (
|
||||||
|
|
||||||
{selectedItem && (
|
|
||||||
<>
|
<>
|
||||||
<>
|
<div className="font-size-container">
|
||||||
<div className="horizontal-line" />
|
<div className="title">Font Size</div>
|
||||||
<div className="max-lines-per-row">
|
<Button
|
||||||
<div className="title"> max lines per row </div>
|
className="value"
|
||||||
<div className="raw-format max-lines-per-row-input">
|
type="text"
|
||||||
<button
|
onClick={(): void => {
|
||||||
type="button"
|
setIsFontSizeOptionsOpen(true);
|
||||||
className="periscope-btn"
|
}}
|
||||||
onClick={decrementMaxLinesPerRow}
|
>
|
||||||
>
|
<Typography.Text className="font-value">{fontSizeValue}</Typography.Text>
|
||||||
{' '}
|
<ChevronRight size={14} className="icon" />
|
||||||
<Minus size={12} />{' '}
|
</Button>
|
||||||
</button>
|
</div>
|
||||||
<InputNumber
|
<div className="horizontal-line" />
|
||||||
min={1}
|
<div className="menu-container">
|
||||||
max={10}
|
<div className="title"> {title} </div>
|
||||||
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">
|
<div className="menu-items">
|
||||||
{!addNewColumn && <div className="horizontal-line" />}
|
{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 && (
|
{selectedItem === item.key && <Check size={12} />}
|
||||||
<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>
|
</div>
|
||||||
<X
|
|
||||||
className="delete-btn"
|
|
||||||
size={14}
|
|
||||||
onClick={(): void => addColumn.onRemove(id as string)}
|
|
||||||
/>
|
|
||||||
</div>
|
</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>
|
</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>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
.checkbox-filter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 12px;
|
||||||
|
gap: 12px;
|
||||||
|
border-bottom: 1px solid var(--bg-slate-400);
|
||||||
|
.filter-header-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.left-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px;
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.clear-all {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--bg-robin-500);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.values {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.checkbox-value-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.filter-disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
.value-string {
|
||||||
|
color: var(--bg-slate-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.only-btn {
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: var(--bg-slate-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn {
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: var(--bg-slate-200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-string {
|
||||||
|
}
|
||||||
|
|
||||||
|
.only-btn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.toggle-btn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn:hover {
|
||||||
|
background-color: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.only-btn:hover {
|
||||||
|
background-color: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-value-section:hover {
|
||||||
|
.toggle-btn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.only-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 21px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.value:hover {
|
||||||
|
.toggle-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 21px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-more {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.show-more-text {
|
||||||
|
color: var(--bg-robin-500);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.checkbox-filter {
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
|
.filter-header-checkbox {
|
||||||
|
.left-action {
|
||||||
|
.title {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,510 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
|
/* eslint-disable sonarjs/no-identical-functions */
|
||||||
|
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||||
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
|
import './Checkbox.styles.scss';
|
||||||
|
|
||||||
|
import { Button, Checkbox, Input, Skeleton, Typography } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import { IQuickFiltersConfig } from 'components/QuickFilters/QuickFilters';
|
||||||
|
import { OPERATORS } from 'constants/queryBuilder';
|
||||||
|
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||||
|
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { cloneDeep, isArray, isEmpty, isEqual } from 'lodash-es';
|
||||||
|
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
const SELECTED_OPERATORS = [OPERATORS['='], 'in'];
|
||||||
|
const NON_SELECTED_OPERATORS = [OPERATORS['!='], 'nin'];
|
||||||
|
|
||||||
|
function setDefaultValues(
|
||||||
|
values: string[],
|
||||||
|
trueOrFalse: boolean,
|
||||||
|
): Record<string, boolean> {
|
||||||
|
const defaultState: Record<string, boolean> = {};
|
||||||
|
values.forEach((val) => {
|
||||||
|
defaultState[val] = trueOrFalse;
|
||||||
|
});
|
||||||
|
return defaultState;
|
||||||
|
}
|
||||||
|
interface ICheckboxProps {
|
||||||
|
filter: IQuickFiltersConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||||
|
const { filter } = props;
|
||||||
|
const [searchText, setSearchText] = useState<string>('');
|
||||||
|
const [isOpen, setIsOpen] = useState<boolean>(filter.defaultOpen);
|
||||||
|
const [visibleItemsCount, setVisibleItemsCount] = useState<number>(10);
|
||||||
|
|
||||||
|
const {
|
||||||
|
lastUsedQuery,
|
||||||
|
currentQuery,
|
||||||
|
redirectWithQueryBuilderData,
|
||||||
|
} = useQueryBuilder();
|
||||||
|
|
||||||
|
const { data, isLoading } = useGetAggregateValues(
|
||||||
|
{
|
||||||
|
aggregateOperator: 'noop',
|
||||||
|
dataSource: DataSource.LOGS,
|
||||||
|
aggregateAttribute: '',
|
||||||
|
attributeKey: filter.attributeKey.key,
|
||||||
|
filterAttributeKeyDataType: filter.attributeKey.dataType || DataTypes.EMPTY,
|
||||||
|
tagType: filter.attributeKey.type || '',
|
||||||
|
searchText: searchText ?? '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: isOpen,
|
||||||
|
keepPreviousData: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const attributeValues: string[] = useMemo(
|
||||||
|
() =>
|
||||||
|
((Object.values(data?.payload || {}).find((el) => !!el) ||
|
||||||
|
[]) as string[]).filter((val) => !isEmpty(val)),
|
||||||
|
[data?.payload],
|
||||||
|
);
|
||||||
|
const currentAttributeKeys = attributeValues.slice(0, visibleItemsCount);
|
||||||
|
|
||||||
|
// derive the state of each filter key here in the renderer itself and keep it in sync with staged query
|
||||||
|
// also we need to keep a note of last focussed query.
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
const currentFilterState = useMemo(() => {
|
||||||
|
let filterState: Record<string, boolean> = setDefaultValues(
|
||||||
|
attributeValues,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const filterSync = currentQuery?.builder.queryData?.[
|
||||||
|
lastUsedQuery || 0
|
||||||
|
]?.filters?.items.find((item) =>
|
||||||
|
isEqual(item.key?.key, filter.attributeKey.key),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filterSync) {
|
||||||
|
if (SELECTED_OPERATORS.includes(filterSync.op)) {
|
||||||
|
if (isArray(filterSync.value)) {
|
||||||
|
filterSync.value.forEach((val) => {
|
||||||
|
filterState[val] = true;
|
||||||
|
});
|
||||||
|
} else if (typeof filterSync.value === 'string') {
|
||||||
|
filterState[filterSync.value] = true;
|
||||||
|
} else if (typeof filterSync.value === 'boolean') {
|
||||||
|
filterState[String(filterSync.value)] = true;
|
||||||
|
} else if (typeof filterSync.value === 'number') {
|
||||||
|
filterState[String(filterSync.value)] = true;
|
||||||
|
}
|
||||||
|
} else if (NON_SELECTED_OPERATORS.includes(filterSync.op)) {
|
||||||
|
filterState = setDefaultValues(attributeValues, true);
|
||||||
|
if (isArray(filterSync.value)) {
|
||||||
|
filterSync.value.forEach((val) => {
|
||||||
|
filterState[val] = false;
|
||||||
|
});
|
||||||
|
} else if (typeof filterSync.value === 'string') {
|
||||||
|
filterState[filterSync.value] = false;
|
||||||
|
} else if (typeof filterSync.value === 'boolean') {
|
||||||
|
filterState[String(filterSync.value)] = false;
|
||||||
|
} else if (typeof filterSync.value === 'number') {
|
||||||
|
filterState[String(filterSync.value)] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filterState = setDefaultValues(attributeValues, true);
|
||||||
|
}
|
||||||
|
return filterState;
|
||||||
|
}, [
|
||||||
|
attributeValues,
|
||||||
|
currentQuery?.builder.queryData,
|
||||||
|
filter.attributeKey,
|
||||||
|
lastUsedQuery,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// disable the filter when there are multiple entries of the same attribute key present in the filter bar
|
||||||
|
const isFilterDisabled = useMemo(
|
||||||
|
() =>
|
||||||
|
(currentQuery?.builder?.queryData?.[
|
||||||
|
lastUsedQuery || 0
|
||||||
|
]?.filters?.items?.filter((item) =>
|
||||||
|
isEqual(item.key?.key, filter.attributeKey.key),
|
||||||
|
)?.length || 0) > 1,
|
||||||
|
|
||||||
|
[currentQuery?.builder?.queryData, lastUsedQuery, filter.attributeKey],
|
||||||
|
);
|
||||||
|
|
||||||
|
// variable to check if the current filter has multiple values to its name in the key op value section
|
||||||
|
const isMultipleValuesTrueForTheKey =
|
||||||
|
Object.values(currentFilterState).filter((val) => val).length > 1;
|
||||||
|
|
||||||
|
const handleClearFilterAttribute = (): void => {
|
||||||
|
const preparedQuery: Query = {
|
||||||
|
...currentQuery,
|
||||||
|
builder: {
|
||||||
|
...currentQuery.builder,
|
||||||
|
queryData: currentQuery.builder.queryData.map((item, idx) => ({
|
||||||
|
...item,
|
||||||
|
filters: {
|
||||||
|
...item.filters,
|
||||||
|
items:
|
||||||
|
idx === lastUsedQuery
|
||||||
|
? item.filters.items.filter(
|
||||||
|
(fil) => !isEqual(fil.key?.key, filter.attributeKey.key),
|
||||||
|
)
|
||||||
|
: [...item.filters.items],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
redirectWithQueryBuilderData(preparedQuery);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSomeFilterPresentForCurrentAttribute = currentQuery.builder.queryData?.[
|
||||||
|
lastUsedQuery || 0
|
||||||
|
]?.filters?.items?.some((item) =>
|
||||||
|
isEqual(item.key?.key, filter.attributeKey.key),
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChange = (
|
||||||
|
value: string,
|
||||||
|
checked: boolean,
|
||||||
|
isOnlyOrAllClicked: boolean,
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
): void => {
|
||||||
|
const query = cloneDeep(currentQuery.builder.queryData?.[lastUsedQuery || 0]);
|
||||||
|
|
||||||
|
// if only or all are clicked we do not need to worry about anything just override whatever we have
|
||||||
|
// by either adding a new IN operator value clause in case of ONLY or remove everything we have for ALL.
|
||||||
|
if (isOnlyOrAllClicked && query?.filters?.items) {
|
||||||
|
const isOnlyOrAll = isSomeFilterPresentForCurrentAttribute
|
||||||
|
? currentFilterState[value] && !isMultipleValuesTrueForTheKey
|
||||||
|
? 'All'
|
||||||
|
: 'Only'
|
||||||
|
: 'Only';
|
||||||
|
query.filters.items = query.filters.items.filter(
|
||||||
|
(q) => !isEqual(q.key?.key, filter.attributeKey.key),
|
||||||
|
);
|
||||||
|
if (isOnlyOrAll === 'Only') {
|
||||||
|
const newFilterItem: TagFilterItem = {
|
||||||
|
id: uuid(),
|
||||||
|
op: getOperatorValue(OPERATORS.IN),
|
||||||
|
key: filter.attributeKey,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
query.filters.items = [...query.filters.items, newFilterItem];
|
||||||
|
}
|
||||||
|
} else if (query?.filters?.items) {
|
||||||
|
if (
|
||||||
|
query.filters?.items?.some((item) =>
|
||||||
|
isEqual(item.key?.key, filter.attributeKey.key),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// if there is already a running filter for the current attribute key then
|
||||||
|
// we split the cases by which particular operator is present right now!
|
||||||
|
const currentFilter = query.filters?.items?.find((q) =>
|
||||||
|
isEqual(q.key?.key, filter.attributeKey.key),
|
||||||
|
);
|
||||||
|
if (currentFilter) {
|
||||||
|
const runningOperator = currentFilter?.op;
|
||||||
|
switch (runningOperator) {
|
||||||
|
case 'in':
|
||||||
|
if (checked) {
|
||||||
|
// if it's an IN operator then if we are checking another value it get's added to the
|
||||||
|
// filter clause. example - key IN [value1, currentSelectedValue]
|
||||||
|
if (isArray(currentFilter.value)) {
|
||||||
|
const newFilter = {
|
||||||
|
...currentFilter,
|
||||||
|
value: [...currentFilter.value, value],
|
||||||
|
};
|
||||||
|
query.filters.items = query.filters.items.map((item) => {
|
||||||
|
if (isEqual(item.key?.key, filter.attributeKey.key)) {
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// if the current state wasn't an array we make it one and add our value
|
||||||
|
const newFilter = {
|
||||||
|
...currentFilter,
|
||||||
|
value: [currentFilter.value as string, value],
|
||||||
|
};
|
||||||
|
query.filters.items = query.filters.items.map((item) => {
|
||||||
|
if (isEqual(item.key?.key, filter.attributeKey.key)) {
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (!checked) {
|
||||||
|
// if we are removing some value when the running operator is IN we filter.
|
||||||
|
// example - key IN [value1,currentSelectedValue] becomes key IN [value1] in case of array
|
||||||
|
if (isArray(currentFilter.value)) {
|
||||||
|
const newFilter = {
|
||||||
|
...currentFilter,
|
||||||
|
value: currentFilter.value.filter((val) => val !== value),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (newFilter.value.length === 0) {
|
||||||
|
query.filters.items = query.filters.items.filter(
|
||||||
|
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
query.filters.items = query.filters.items.map((item) => {
|
||||||
|
if (isEqual(item.key?.key, filter.attributeKey.key)) {
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if not an array remove the whole thing altogether!
|
||||||
|
query.filters.items = query.filters.items.filter(
|
||||||
|
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'nin':
|
||||||
|
// if the current running operator is NIN then when unchecking the value it gets
|
||||||
|
// added to the clause like key NIN [value1 , currentUnselectedValue]
|
||||||
|
if (!checked) {
|
||||||
|
// in case of array add the currentUnselectedValue to the list.
|
||||||
|
if (isArray(currentFilter.value)) {
|
||||||
|
const newFilter = {
|
||||||
|
...currentFilter,
|
||||||
|
value: [...currentFilter.value, value],
|
||||||
|
};
|
||||||
|
query.filters.items = query.filters.items.map((item) => {
|
||||||
|
if (isEqual(item.key?.key, filter.attributeKey.key)) {
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// in case of not an array make it one!
|
||||||
|
const newFilter = {
|
||||||
|
...currentFilter,
|
||||||
|
value: [currentFilter.value as string, value],
|
||||||
|
};
|
||||||
|
query.filters.items = query.filters.items.map((item) => {
|
||||||
|
if (isEqual(item.key?.key, filter.attributeKey.key)) {
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (checked) {
|
||||||
|
// opposite of above!
|
||||||
|
if (isArray(currentFilter.value)) {
|
||||||
|
const newFilter = {
|
||||||
|
...currentFilter,
|
||||||
|
value: currentFilter.value.filter((val) => val !== value),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (newFilter.value.length === 0) {
|
||||||
|
query.filters.items = query.filters.items.filter(
|
||||||
|
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
query.filters.items = query.filters.items.map((item) => {
|
||||||
|
if (isEqual(item.key?.key, filter.attributeKey.key)) {
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
query.filters.items = query.filters.items.filter(
|
||||||
|
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '=':
|
||||||
|
if (checked) {
|
||||||
|
const newFilter = {
|
||||||
|
...currentFilter,
|
||||||
|
op: getOperatorValue(OPERATORS.IN),
|
||||||
|
value: [currentFilter.value as string, value],
|
||||||
|
};
|
||||||
|
query.filters.items = query.filters.items.map((item) => {
|
||||||
|
if (isEqual(item.key?.key, filter.attributeKey.key)) {
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
} else if (!checked) {
|
||||||
|
query.filters.items = query.filters.items.filter(
|
||||||
|
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '!=':
|
||||||
|
if (!checked) {
|
||||||
|
const newFilter = {
|
||||||
|
...currentFilter,
|
||||||
|
op: getOperatorValue(OPERATORS.NIN),
|
||||||
|
value: [currentFilter.value as string, value],
|
||||||
|
};
|
||||||
|
query.filters.items = query.filters.items.map((item) => {
|
||||||
|
if (isEqual(item.key?.key, filter.attributeKey.key)) {
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
} else if (checked) {
|
||||||
|
query.filters.items = query.filters.items.filter(
|
||||||
|
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// case - when there is no filter for the current key that means all are selected right now.
|
||||||
|
const newFilterItem: TagFilterItem = {
|
||||||
|
id: uuid(),
|
||||||
|
op: getOperatorValue(OPERATORS.NIN),
|
||||||
|
key: filter.attributeKey,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
query.filters.items = [...query.filters.items, newFilterItem];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const finalQuery = {
|
||||||
|
...currentQuery,
|
||||||
|
builder: {
|
||||||
|
...currentQuery.builder,
|
||||||
|
queryData: [
|
||||||
|
...currentQuery.builder.queryData.map((q, idx) => {
|
||||||
|
if (idx === lastUsedQuery) {
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
return q;
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
redirectWithQueryBuilderData(finalQuery);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="checkbox-filter">
|
||||||
|
<section className="filter-header-checkbox">
|
||||||
|
<section className="left-action">
|
||||||
|
{isOpen ? (
|
||||||
|
<ChevronDown
|
||||||
|
size={13}
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={(): void => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setVisibleItemsCount(10);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ChevronRight
|
||||||
|
size={13}
|
||||||
|
onClick={(): void => setIsOpen(true)}
|
||||||
|
cursor="pointer"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Typography.Text className="title">{filter.title}</Typography.Text>
|
||||||
|
</section>
|
||||||
|
<section className="right-action">
|
||||||
|
{isOpen && (
|
||||||
|
<Typography.Text
|
||||||
|
className="clear-all"
|
||||||
|
onClick={handleClearFilterAttribute}
|
||||||
|
>
|
||||||
|
Clear All
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
{isOpen && isLoading && !attributeValues.length && (
|
||||||
|
<section className="loading">
|
||||||
|
<Skeleton paragraph={{ rows: 4 }} />
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
{isOpen && !isLoading && (
|
||||||
|
<>
|
||||||
|
<section className="search">
|
||||||
|
<Input
|
||||||
|
placeholder="Filter values"
|
||||||
|
onChange={(e): void => setSearchText(e.target.value)}
|
||||||
|
disabled={isFilterDisabled}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
{attributeValues.length > 0 ? (
|
||||||
|
<section className="values">
|
||||||
|
{currentAttributeKeys.map((value: string) => (
|
||||||
|
<div key={value} className="value">
|
||||||
|
<Checkbox
|
||||||
|
onChange={(e): void => onChange(value, e.target.checked, false)}
|
||||||
|
checked={currentFilterState[value]}
|
||||||
|
disabled={isFilterDisabled}
|
||||||
|
rootClassName="check-box"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
'checkbox-value-section',
|
||||||
|
isFilterDisabled ? 'filter-disabled' : '',
|
||||||
|
)}
|
||||||
|
onClick={(): void => {
|
||||||
|
if (isFilterDisabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onChange(value, currentFilterState[value], true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{filter.customRendererForValue ? (
|
||||||
|
filter.customRendererForValue(value)
|
||||||
|
) : (
|
||||||
|
<Typography.Text
|
||||||
|
className="value-string"
|
||||||
|
ellipsis={{ tooltip: { placement: 'right' } }}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
<Button type="text" className="only-btn">
|
||||||
|
{isSomeFilterPresentForCurrentAttribute
|
||||||
|
? currentFilterState[value] && !isMultipleValuesTrueForTheKey
|
||||||
|
? 'All'
|
||||||
|
: 'Only'
|
||||||
|
: 'Only'}
|
||||||
|
</Button>
|
||||||
|
<Button type="text" className="toggle-btn">
|
||||||
|
Toggle
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
) : (
|
||||||
|
<section className="no-data">
|
||||||
|
<Typography.Text>No values found</Typography.Text>{' '}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
{visibleItemsCount < attributeValues?.length && (
|
||||||
|
<section className="show-more">
|
||||||
|
<Typography.Text
|
||||||
|
className="show-more-text"
|
||||||
|
onClick={(): void => setVisibleItemsCount((prev) => prev + 10)}
|
||||||
|
>
|
||||||
|
Show More...
|
||||||
|
</Typography.Text>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import './Slider.styles.scss';
|
||||||
|
|
||||||
|
import { IQuickFiltersConfig } from 'components/QuickFilters/QuickFilters';
|
||||||
|
|
||||||
|
interface ISliderProps {
|
||||||
|
filter: IQuickFiltersConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// not needed for now build when required
|
||||||
|
export default function Slider(props: ISliderProps): JSX.Element {
|
||||||
|
const { filter } = props;
|
||||||
|
console.log(filter);
|
||||||
|
return <div>Slider</div>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
.quick-filters {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
border-right: 1px solid var(--bg-slate-400);
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10.5px;
|
||||||
|
border-bottom: 1px solid var(--bg-slate-400);
|
||||||
|
|
||||||
|
.left-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px;
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-tag {
|
||||||
|
display: flex;
|
||||||
|
padding: 5px 9px;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid rgba(78, 116, 248, 0.2);
|
||||||
|
background: rgba(78, 116, 248, 0.1);
|
||||||
|
color: var(--bg-robin-500);
|
||||||
|
font-family: 'Geist Mono';
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.divider-filter {
|
||||||
|
width: 1px;
|
||||||
|
height: 14px;
|
||||||
|
background: #161922;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-icon {
|
||||||
|
background-color: var(--bg-ink-500);
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.quick-filters {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
border-right: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.header {
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.left-actions {
|
||||||
|
.text {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-icon {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.right-actions {
|
||||||
|
.sync-icon {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
124
frontend/src/components/QuickFilters/QuickFilters.tsx
Normal file
124
frontend/src/components/QuickFilters/QuickFilters.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import './QuickFilters.styles.scss';
|
||||||
|
|
||||||
|
import {
|
||||||
|
FilterOutlined,
|
||||||
|
SyncOutlined,
|
||||||
|
VerticalAlignTopOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { Tooltip, Typography } from 'antd';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
import Checkbox from './FilterRenderers/Checkbox/Checkbox';
|
||||||
|
import Slider from './FilterRenderers/Slider/Slider';
|
||||||
|
|
||||||
|
export enum FiltersType {
|
||||||
|
SLIDER = 'SLIDER',
|
||||||
|
CHECKBOX = 'CHECKBOX',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MinMax {
|
||||||
|
MIN = 'MIN',
|
||||||
|
MAX = 'MAX',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SpecficFilterOperations {
|
||||||
|
ALL = 'ALL',
|
||||||
|
ONLY = 'ONLY',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IQuickFiltersConfig {
|
||||||
|
type: FiltersType;
|
||||||
|
title: string;
|
||||||
|
attributeKey: BaseAutocompleteData;
|
||||||
|
customRendererForValue?: (value: string) => JSX.Element;
|
||||||
|
defaultOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IQuickFiltersProps {
|
||||||
|
config: IQuickFiltersConfig[];
|
||||||
|
handleFilterVisibilityChange: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||||
|
const { config, handleFilterVisibilityChange } = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
currentQuery,
|
||||||
|
lastUsedQuery,
|
||||||
|
redirectWithQueryBuilderData,
|
||||||
|
} = useQueryBuilder();
|
||||||
|
|
||||||
|
// clear all the filters for the query which is in sync with filters
|
||||||
|
const handleReset = (): void => {
|
||||||
|
const updatedQuery = cloneDeep(
|
||||||
|
currentQuery?.builder.queryData?.[lastUsedQuery || 0],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updatedQuery) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedQuery?.filters?.items) {
|
||||||
|
updatedQuery.filters.items = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const preparedQuery: Query = {
|
||||||
|
...currentQuery,
|
||||||
|
builder: {
|
||||||
|
...currentQuery.builder,
|
||||||
|
queryData: currentQuery.builder.queryData.map((item, idx) => ({
|
||||||
|
...item,
|
||||||
|
filters: {
|
||||||
|
...item.filters,
|
||||||
|
items: idx === lastUsedQuery ? [] : [...item.filters.items],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
redirectWithQueryBuilderData(preparedQuery);
|
||||||
|
};
|
||||||
|
|
||||||
|
const lastQueryName =
|
||||||
|
currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName;
|
||||||
|
return (
|
||||||
|
<div className="quick-filters">
|
||||||
|
<section className="header">
|
||||||
|
<section className="left-actions">
|
||||||
|
<FilterOutlined />
|
||||||
|
<Typography.Text className="text">Filters for</Typography.Text>
|
||||||
|
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
|
||||||
|
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
|
||||||
|
</Tooltip>
|
||||||
|
</section>
|
||||||
|
<section className="right-actions">
|
||||||
|
<Tooltip title="Reset All">
|
||||||
|
<SyncOutlined className="sync-icon" onClick={handleReset} />
|
||||||
|
</Tooltip>
|
||||||
|
<div className="divider-filter" />
|
||||||
|
<Tooltip title="Collapse Filters">
|
||||||
|
<VerticalAlignTopOutlined
|
||||||
|
rotate={270}
|
||||||
|
onClick={handleFilterVisibilityChange}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="filters">
|
||||||
|
{config.map((filter) => {
|
||||||
|
switch (filter.type) {
|
||||||
|
case FiltersType.CHECKBOX:
|
||||||
|
return <Checkbox filter={filter} />;
|
||||||
|
case FiltersType.SLIDER:
|
||||||
|
return <Slider filter={filter} />;
|
||||||
|
default:
|
||||||
|
return <Checkbox filter={filter} />;
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd';
|
|||||||
import { ColumnGroupType, ColumnType } from 'antd/es/table';
|
import { ColumnGroupType, ColumnType } from 'antd/es/table';
|
||||||
import { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
|
||||||
import { SlidersHorizontal } from 'lucide-react';
|
import { SlidersHorizontal } from 'lucide-react';
|
||||||
import { memo, useEffect, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
@@ -96,7 +96,7 @@ function DynamicColumnTable({
|
|||||||
return (
|
return (
|
||||||
<div className="DynamicColumnTable">
|
<div className="DynamicColumnTable">
|
||||||
<Flex justify="flex-end" align="center" gap={8}>
|
<Flex justify="flex-end" align="center" gap={8}>
|
||||||
{facingIssueBtn && <FacingIssueBtn {...facingIssueBtn} />}
|
{facingIssueBtn && <LaunchChatSupport {...facingIssueBtn} />}
|
||||||
{dynamicColumns && (
|
{dynamicColumns && (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
getPopupContainer={popupContainer}
|
getPopupContainer={popupContainer}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { TableProps } from 'antd';
|
import { TableProps } from 'antd';
|
||||||
import { ColumnsType } from 'antd/es/table';
|
import { ColumnsType } from 'antd/es/table';
|
||||||
import { ColumnGroupType, ColumnType } from 'antd/lib/table';
|
import { ColumnGroupType, ColumnType } from 'antd/lib/table';
|
||||||
import { FacingIssueBtnProps } from 'components/facingIssueBtn/FacingIssueBtn';
|
import { LaunchChatSupportProps } from 'components/LaunchChatSupport/LaunchChatSupport';
|
||||||
|
|
||||||
import { TableDataSource } from './contants';
|
import { TableDataSource } from './contants';
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ export interface DynamicColumnTableProps extends TableProps<any> {
|
|||||||
tablesource: typeof TableDataSource[keyof typeof TableDataSource];
|
tablesource: typeof TableDataSource[keyof typeof TableDataSource];
|
||||||
dynamicColumns: TableProps<any>['columns'];
|
dynamicColumns: TableProps<any>['columns'];
|
||||||
onDragColumn?: (fromIndex: number, toIndex: number) => void;
|
onDragColumn?: (fromIndex: number, toIndex: number) => void;
|
||||||
facingIssueBtn?: FacingIssueBtnProps;
|
facingIssueBtn?: LaunchChatSupportProps;
|
||||||
shouldSendAlertsLogEvent?: boolean;
|
shouldSendAlertsLogEvent?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.tab-title {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
41
frontend/src/components/TabsAndFilters/Tabs/Tabs.tsx
Normal file
41
frontend/src/components/TabsAndFilters/Tabs/Tabs.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import './Tabs.styles.scss';
|
||||||
|
|
||||||
|
import { Radio } from 'antd';
|
||||||
|
import { RadioChangeEvent } from 'antd/lib';
|
||||||
|
import { History, Table } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { ALERT_TABS } from '../constants';
|
||||||
|
|
||||||
|
export function Tabs(): JSX.Element {
|
||||||
|
const [selectedTab, setSelectedTab] = useState('overview');
|
||||||
|
|
||||||
|
const handleTabChange = (e: RadioChangeEvent): void => {
|
||||||
|
setSelectedTab(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Radio.Group className="tabs" onChange={handleTabChange} value={selectedTab}>
|
||||||
|
<Radio.Button
|
||||||
|
className={
|
||||||
|
selectedTab === ALERT_TABS.OVERVIEW ? 'selected_view tab' : 'tab'
|
||||||
|
}
|
||||||
|
value={ALERT_TABS.OVERVIEW}
|
||||||
|
>
|
||||||
|
<div className="tab-title">
|
||||||
|
<Table size={14} />
|
||||||
|
Overview
|
||||||
|
</div>
|
||||||
|
</Radio.Button>
|
||||||
|
<Radio.Button
|
||||||
|
className={selectedTab === ALERT_TABS.HISTORY ? 'selected_view tab' : 'tab'}
|
||||||
|
value={ALERT_TABS.HISTORY}
|
||||||
|
>
|
||||||
|
<div className="tab-title">
|
||||||
|
<History size={14} />
|
||||||
|
History
|
||||||
|
</div>
|
||||||
|
</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
@mixin flex-center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-and-filters {
|
||||||
|
@include flex-center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
.filters {
|
||||||
|
@include flex-center;
|
||||||
|
gap: 16px;
|
||||||
|
.reset-button {
|
||||||
|
@include flex-center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
frontend/src/components/TabsAndFilters/TabsAndFilters.tsx
Normal file
16
frontend/src/components/TabsAndFilters/TabsAndFilters.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import './TabsAndFilters.styles.scss';
|
||||||
|
|
||||||
|
import { Filters } from 'components/AlertDetailsFilters/Filters';
|
||||||
|
|
||||||
|
import { Tabs } from './Tabs/Tabs';
|
||||||
|
|
||||||
|
function TabsAndFilters(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="tabs-and-filters">
|
||||||
|
<Tabs />
|
||||||
|
<Filters />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TabsAndFilters;
|
||||||
5
frontend/src/components/TabsAndFilters/constants.ts
Normal file
5
frontend/src/components/TabsAndFilters/constants.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const ALERT_TABS = {
|
||||||
|
OVERVIEW: 'OVERVIEW',
|
||||||
|
HISTORY: 'HISTORY',
|
||||||
|
ACTIVITY: 'ACTIVITY',
|
||||||
|
} as const;
|
||||||
@@ -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;
|
|
||||||
@@ -3,4 +3,5 @@ export const ENVIRONMENT = {
|
|||||||
process?.env?.FRONTEND_API_ENDPOINT ||
|
process?.env?.FRONTEND_API_ENDPOINT ||
|
||||||
process?.env?.GITPOD_WORKSPACE_URL?.replace('://', '://8080-') ||
|
process?.env?.GITPOD_WORKSPACE_URL?.replace('://', '://8080-') ||
|
||||||
'',
|
'',
|
||||||
|
wsURL: process?.env?.WEBSOCKET_API_ENDPOINT || '',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,4 +20,6 @@ export enum FeatureKeys {
|
|||||||
ONBOARDING = 'ONBOARDING',
|
ONBOARDING = 'ONBOARDING',
|
||||||
CHAT_SUPPORT = 'CHAT_SUPPORT',
|
CHAT_SUPPORT = 'CHAT_SUPPORT',
|
||||||
GATEWAY = 'GATEWAY',
|
GATEWAY = 'GATEWAY',
|
||||||
|
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
|
||||||
|
QUERY_BUILDER_SEARCH_V2 = 'QUERY_BUILDER_SEARCH_V2',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,17 @@
|
|||||||
|
import { ManipulateType } from 'dayjs';
|
||||||
|
|
||||||
const MAX_RPS_LIMIT = 100;
|
const MAX_RPS_LIMIT = 100;
|
||||||
export { MAX_RPS_LIMIT };
|
export { MAX_RPS_LIMIT };
|
||||||
|
|
||||||
export const LEGEND = 'legend';
|
export const LEGEND = 'legend';
|
||||||
|
|
||||||
|
export const DAYJS_MANIPULATE_TYPES: { [key: string]: ManipulateType } = {
|
||||||
|
DAY: 'day',
|
||||||
|
WEEK: 'week',
|
||||||
|
MONTH: 'month',
|
||||||
|
YEAR: 'year',
|
||||||
|
HOUR: 'hour',
|
||||||
|
MINUTE: 'minute',
|
||||||
|
SECOND: 'second',
|
||||||
|
MILLISECOND: 'millisecond',
|
||||||
|
};
|
||||||
|
|||||||
@@ -19,4 +19,6 @@ export enum LOCALSTORAGE {
|
|||||||
SHOW_EXPLORER_TOOLBAR = 'SHOW_EXPLORER_TOOLBAR',
|
SHOW_EXPLORER_TOOLBAR = 'SHOW_EXPLORER_TOOLBAR',
|
||||||
PINNED_ATTRIBUTES = 'PINNED_ATTRIBUTES',
|
PINNED_ATTRIBUTES = 'PINNED_ATTRIBUTES',
|
||||||
THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1',
|
THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1',
|
||||||
|
LAST_USED_SAVED_VIEWS = 'LAST_USED_SAVED_VIEWS',
|
||||||
|
SHOW_LOGS_QUICK_FILTERS = 'SHOW_LOGS_QUICK_FILTERS',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,4 +32,8 @@ export enum QueryParams {
|
|||||||
relativeTime = 'relativeTime',
|
relativeTime = 'relativeTime',
|
||||||
alertType = 'alertType',
|
alertType = 'alertType',
|
||||||
ruleId = 'ruleId',
|
ruleId = 'ruleId',
|
||||||
|
consumerGrp = 'consumerGrp',
|
||||||
|
topic = 'topic',
|
||||||
|
partition = 'partition',
|
||||||
|
selectedTimelineQuery = 'selectedTimelineQuery',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const selectValueDivider = '__';
|
|||||||
|
|
||||||
export const baseAutoCompleteIdKeysOrder: (keyof Omit<
|
export const baseAutoCompleteIdKeysOrder: (keyof Omit<
|
||||||
BaseAutocompleteData,
|
BaseAutocompleteData,
|
||||||
'id' | 'isJSON'
|
'id' | 'isJSON' | 'isIndexed'
|
||||||
>)[] = ['key', 'dataType', 'type', 'isColumn'];
|
>)[] = ['key', 'dataType', 'type', 'isColumn'];
|
||||||
|
|
||||||
export const autocompleteType: Record<AutocompleteType, AutocompleteType> = {
|
export const autocompleteType: Record<AutocompleteType, AutocompleteType> = {
|
||||||
@@ -71,6 +71,7 @@ export const alphabet: string[] = alpha.map((str) => String.fromCharCode(str));
|
|||||||
export enum QueryBuilderKeys {
|
export enum QueryBuilderKeys {
|
||||||
GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE',
|
GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE',
|
||||||
GET_AGGREGATE_KEYS = 'GET_AGGREGATE_KEYS',
|
GET_AGGREGATE_KEYS = 'GET_AGGREGATE_KEYS',
|
||||||
|
GET_ATTRIBUTE_SUGGESTIONS = 'GET_ATTRIBUTE_SUGGESTIONS',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mapOfOperators = {
|
export const mapOfOperators = {
|
||||||
|
|||||||
@@ -8,4 +8,14 @@ export const REACT_QUERY_KEY = {
|
|||||||
GET_FEATURES_FLAGS: 'GET_FEATURES_FLAGS',
|
GET_FEATURES_FLAGS: 'GET_FEATURES_FLAGS',
|
||||||
DELETE_DASHBOARD: 'DELETE_DASHBOARD',
|
DELETE_DASHBOARD: 'DELETE_DASHBOARD',
|
||||||
LOGS_PIPELINE_PREVIEW: 'LOGS_PIPELINE_PREVIEW',
|
LOGS_PIPELINE_PREVIEW: 'LOGS_PIPELINE_PREVIEW',
|
||||||
|
ALERT_RULE_DETAILS: 'ALERT_RULE_DETAILS',
|
||||||
|
ALERT_RULE_STATS: 'ALERT_RULE_STATS',
|
||||||
|
ALERT_RULE_TOP_CONTRIBUTORS: 'ALERT_RULE_TOP_CONTRIBUTORS',
|
||||||
|
ALERT_RULE_TIMELINE_TABLE: 'ALERT_RULE_TIMELINE_TABLE',
|
||||||
|
ALERT_RULE_TIMELINE_GRAPH: 'ALERT_RULE_TIMELINE_GRAPH',
|
||||||
|
GET_CONSUMER_LAG_DETAILS: 'GET_CONSUMER_LAG_DETAILS',
|
||||||
|
TOGGLE_ALERT_STATE: 'TOGGLE_ALERT_STATE',
|
||||||
|
GET_ALL_ALLERTS: 'GET_ALL_ALLERTS',
|
||||||
|
REMOVE_ALERT_RULE: 'REMOVE_ALERT_RULE',
|
||||||
|
DUPLICATE_ALERT_RULE: 'DUPLICATE_ALERT_RULE',
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user