Compare commits
98 Commits
v0.49.0-cl
...
v0.51.0-cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb8ca5a7ca | ||
|
|
80133240ca | ||
|
|
7d7d112f40 | ||
|
|
add2d19614 | ||
|
|
adfe20e88a | ||
|
|
d3b83f5a41 | ||
|
|
77eba9a558 | ||
|
|
43e73e06fe | ||
|
|
840d8b2e49 | ||
|
|
df751c7f38 | ||
|
|
cd07c743b6 | ||
|
|
46e6c34e51 | ||
|
|
42f7905b3b | ||
|
|
a6e68c6519 | ||
|
|
c7e3e6dc4e | ||
|
|
9194ab08b6 | ||
|
|
3ecb2e35ef | ||
|
|
9844dcdfb7 | ||
|
|
ddf5569ce9 | ||
|
|
83455e614e | ||
|
|
831de18464 | ||
|
|
3b2a811f7b | ||
|
|
2c7a5126fd | ||
|
|
87f1597d4e | ||
|
|
916663b4d5 | ||
|
|
b0e355eb64 | ||
|
|
69a39531f0 | ||
|
|
9c9ed741b2 | ||
|
|
e6eaaa660a | ||
|
|
79eef5bb91 | ||
|
|
4d64f1dede | ||
|
|
bf177882e6 | ||
|
|
f6b29999c9 | ||
|
|
75815897b0 | ||
|
|
c9309eecaa | ||
|
|
4264fc0f3a | ||
|
|
ef854910db | ||
|
|
6b8b2ae761 | ||
|
|
a48340a2ea | ||
|
|
e542d2ee09 | ||
|
|
08431131a9 | ||
|
|
1b0ec8ac43 | ||
|
|
2e0ddc7c7f | ||
|
|
858a0cb0de | ||
|
|
216ad36234 | ||
|
|
6628abd435 | ||
|
|
7c81270ed9 | ||
|
|
81c3e6fa65 | ||
|
|
d215ce09b0 | ||
|
|
161a69fbe9 | ||
|
|
3ee51770fd | ||
|
|
932b7ddc69 | ||
|
|
6e466df89d | ||
|
|
326dec21fd | ||
|
|
b0b69c83db | ||
|
|
02106277a6 | ||
|
|
b34509215e | ||
|
|
fd603b8fdf | ||
|
|
c5d23336a7 | ||
|
|
53c6288025 | ||
|
|
4f2c314f39 | ||
|
|
1ad61615c6 | ||
|
|
7ddfadfb18 | ||
|
|
a7e02af8b0 | ||
|
|
da3f6fd7fd | ||
|
|
a453471b51 | ||
|
|
13df87ed69 | ||
|
|
f23ceea54e | ||
|
|
46b4c8a004 | ||
|
|
580198ca7a | ||
|
|
2fb5b16840 | ||
|
|
de571aa69a | ||
|
|
daa5a05677 | ||
|
|
4f69996b9d | ||
|
|
6c402d9e46 | ||
|
|
c6e9eeeee6 | ||
|
|
97b66741a7 | ||
|
|
6b234da969 | ||
|
|
51032f6caa | ||
|
|
41f91db622 | ||
|
|
52e0303997 | ||
|
|
5df25e83d1 | ||
|
|
873280abea | ||
|
|
8ccdc71eaf | ||
|
|
d5f156a6e9 | ||
|
|
cc7559ddee | ||
|
|
415057c260 | ||
|
|
89b67b8880 | ||
|
|
878cb7c0a6 | ||
|
|
0375fc47a7 | ||
|
|
a7a160df76 | ||
|
|
9dbef080c6 | ||
|
|
6c192f1242 | ||
|
|
537641000d | ||
|
|
4916cf5083 | ||
|
|
b57a24a177 | ||
|
|
a6e005e3a2 | ||
|
|
4d375e7cc3 |
1
.github/workflows/push.yaml
vendored
1
.github/workflows/push.yaml
vendored
@@ -158,6 +158,7 @@ jobs:
|
|||||||
echo 'SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> frontend/.env
|
echo 'SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> frontend/.env
|
||||||
echo 'TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> frontend/.env
|
echo 'TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> frontend/.env
|
||||||
echo 'TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> frontend/.env
|
echo 'TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> frontend/.env
|
||||||
|
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> frontend/.env
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
run: yarn install
|
run: yarn install
|
||||||
|
|||||||
@@ -347,7 +347,7 @@ curl -sL https://github.com/SigNoz/signoz/raw/develop/sample-apps/hotrod/hotrod-
|
|||||||
```bash
|
```bash
|
||||||
kubectl -n sample-application run strzal --image=djbingham/curl \
|
kubectl -n sample-application run strzal --image=djbingham/curl \
|
||||||
--restart='OnFailure' -i --tty --rm --command -- curl -X POST -F \
|
--restart='OnFailure' -i --tty --rm --command -- curl -X POST -F \
|
||||||
'locust_count=6' -F 'hatch_rate=2' http://locust-master:8089/swarm
|
'user_count=6' -F 'spawn_rate=2' http://locust-master:8089/swarm
|
||||||
```
|
```
|
||||||
|
|
||||||
**5.1.3 To stop the load generation:**
|
**5.1.3 To stop the load generation:**
|
||||||
|
|||||||
1
Makefile
1
Makefile
@@ -188,3 +188,4 @@ test:
|
|||||||
go test ./pkg/query-service/tests/integration/...
|
go test ./pkg/query-service/tests/integration/...
|
||||||
go test ./pkg/query-service/rules/...
|
go test ./pkg/query-service/rules/...
|
||||||
go test ./pkg/query-service/collectorsimulator/...
|
go test ./pkg/query-service/collectorsimulator/...
|
||||||
|
go test ./pkg/query-service/postprocess/...
|
||||||
|
|||||||
@@ -198,14 +198,14 @@ Not sure how to get started? Just ping us on `#contributing` in our [slack commu
|
|||||||
|
|
||||||
#### Frontend
|
#### Frontend
|
||||||
|
|
||||||
- [Palash Gupta](https://github.com/palashgdev)
|
|
||||||
- [Yunus M](https://github.com/YounixM)
|
- [Yunus M](https://github.com/YounixM)
|
||||||
- [Rajat Dabade](https://github.com/Rajat-Dabade)
|
- [Vikrant Gupta](https://github.com/vikrantgupta25)
|
||||||
|
- [Sagar Rajput](https://github.com/SagarRajput-7)
|
||||||
|
|
||||||
#### DevOps
|
#### DevOps
|
||||||
|
|
||||||
- [Prashant Shahi](https://github.com/prashant-shahi)
|
- [Prashant Shahi](https://github.com/prashant-shahi)
|
||||||
- [Dhawal Sanghvi](https://github.com/dhawal1248)
|
- [Vibhu Pandey](https://github.com/grandwizard28)
|
||||||
|
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ services:
|
|||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:0.46.0
|
image: signoz/query-service:0.49.1
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"-config=/root/config/prometheus.yml",
|
"-config=/root/config/prometheus.yml",
|
||||||
@@ -186,7 +186,7 @@ services:
|
|||||||
<<: *db-depend
|
<<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:0.46.0
|
image: signoz/frontend:0.48.0
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
@@ -199,7 +199,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:0.88.24
|
image: signoz/signoz-otel-collector:0.102.2
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"--config=/etc/otel-collector-config.yaml",
|
"--config=/etc/otel-collector-config.yaml",
|
||||||
@@ -237,7 +237,7 @@ services:
|
|||||||
- query-service
|
- query-service
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:0.88.24
|
image: signoz/signoz-schema-migrator:0.102.2
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ services:
|
|||||||
- --storage.path=/data
|
- --storage.path=/data
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.24}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
|
||||||
container_name: otel-migrator
|
container_name: otel-migrator
|
||||||
command:
|
command:
|
||||||
- "--dsn=tcp://clickhouse:9000"
|
- "--dsn=tcp://clickhouse:9000"
|
||||||
@@ -81,7 +81,7 @@ services:
|
|||||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||||
otel-collector:
|
otel-collector:
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
image: signoz/signoz-otel-collector:0.88.24
|
image: signoz/signoz-otel-collector:0.102.2
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"--config=/etc/otel-collector-config.yaml",
|
"--config=/etc/otel-collector-config.yaml",
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ services:
|
|||||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:${DOCKER_TAG:-0.46.0}
|
image: signoz/query-service:${DOCKER_TAG:-0.49.1}
|
||||||
container_name: signoz-query-service
|
container_name: signoz-query-service
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
@@ -204,7 +204,7 @@ services:
|
|||||||
<<: *db-depend
|
<<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:${DOCKER_TAG:-0.46.0}
|
image: signoz/frontend:${DOCKER_TAG:-0.49.1}
|
||||||
container_name: signoz-frontend
|
container_name: signoz-frontend
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -216,7 +216,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.24}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
|
||||||
container_name: otel-migrator
|
container_name: otel-migrator
|
||||||
command:
|
command:
|
||||||
- "--dsn=tcp://clickhouse:9000"
|
- "--dsn=tcp://clickhouse:9000"
|
||||||
@@ -230,7 +230,7 @@ services:
|
|||||||
|
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.24}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.2}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ services:
|
|||||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:${DOCKER_TAG:-0.46.0}
|
image: signoz/query-service:${DOCKER_TAG:-0.49.1}
|
||||||
container_name: signoz-query-service
|
container_name: signoz-query-service
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
@@ -203,7 +203,7 @@ services:
|
|||||||
<<: *db-depend
|
<<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:${DOCKER_TAG:-0.46.0}
|
image: signoz/frontend:${DOCKER_TAG:-0.49.1}
|
||||||
container_name: signoz-frontend
|
container_name: signoz-frontend
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -215,7 +215,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.24}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
|
||||||
container_name: otel-migrator
|
container_name: otel-migrator
|
||||||
command:
|
command:
|
||||||
- "--dsn=tcp://clickhouse:9000"
|
- "--dsn=tcp://clickhouse:9000"
|
||||||
@@ -229,7 +229,7 @@ services:
|
|||||||
|
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.24}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.2}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ trap bye EXIT
|
|||||||
|
|
||||||
URL="https://api.segment.io/v1/track"
|
URL="https://api.segment.io/v1/track"
|
||||||
HEADER_1="Content-Type: application/json"
|
HEADER_1="Content-Type: application/json"
|
||||||
HEADER_2="Authorization: Basic NEdtb2E0aXhKQVVIeDJCcEp4c2p3QTFiRWZud0VlUno6"
|
HEADER_2="Authorization: Basic OWtScko3b1BDR1BFSkxGNlFqTVBMdDVibGpGaFJRQnI="
|
||||||
|
|
||||||
send_event() {
|
send_event() {
|
||||||
error=""
|
error=""
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import (
|
|||||||
type APIHandlerOptions struct {
|
type APIHandlerOptions struct {
|
||||||
DataConnector interfaces.DataConnector
|
DataConnector interfaces.DataConnector
|
||||||
SkipConfig *basemodel.SkipConfig
|
SkipConfig *basemodel.SkipConfig
|
||||||
PreferDelta bool
|
|
||||||
PreferSpanMetrics bool
|
PreferSpanMetrics bool
|
||||||
MaxIdleConns int
|
MaxIdleConns int
|
||||||
MaxOpenConns int
|
MaxOpenConns int
|
||||||
@@ -53,7 +52,6 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
|
|||||||
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
|
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
|
||||||
Reader: opts.DataConnector,
|
Reader: opts.DataConnector,
|
||||||
SkipConfig: opts.SkipConfig,
|
SkipConfig: opts.SkipConfig,
|
||||||
PerferDelta: opts.PreferDelta,
|
|
||||||
PreferSpanMetrics: opts.PreferSpanMetrics,
|
PreferSpanMetrics: opts.PreferSpanMetrics,
|
||||||
MaxIdleConns: opts.MaxIdleConns,
|
MaxIdleConns: opts.MaxIdleConns,
|
||||||
MaxOpenConns: opts.MaxOpenConns,
|
MaxOpenConns: opts.MaxOpenConns,
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ type ServerOptions struct {
|
|||||||
// alert specific params
|
// alert specific params
|
||||||
DisableRules bool
|
DisableRules bool
|
||||||
RuleRepoURL string
|
RuleRepoURL string
|
||||||
PreferDelta bool
|
|
||||||
PreferSpanMetrics bool
|
PreferSpanMetrics bool
|
||||||
MaxIdleConns int
|
MaxIdleConns int
|
||||||
MaxOpenConns int
|
MaxOpenConns int
|
||||||
@@ -256,7 +255,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
apiOpts := api.APIHandlerOptions{
|
apiOpts := api.APIHandlerOptions{
|
||||||
DataConnector: reader,
|
DataConnector: reader,
|
||||||
SkipConfig: skipConfig,
|
SkipConfig: skipConfig,
|
||||||
PreferDelta: serverOptions.PreferDelta,
|
|
||||||
PreferSpanMetrics: serverOptions.PreferSpanMetrics,
|
PreferSpanMetrics: serverOptions.PreferSpanMetrics,
|
||||||
MaxIdleConns: serverOptions.MaxIdleConns,
|
MaxIdleConns: serverOptions.MaxIdleConns,
|
||||||
MaxOpenConns: serverOptions.MaxOpenConns,
|
MaxOpenConns: serverOptions.MaxOpenConns,
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ func main() {
|
|||||||
|
|
||||||
var cacheConfigPath, fluxInterval string
|
var cacheConfigPath, fluxInterval string
|
||||||
var enableQueryServiceLogOTLPExport bool
|
var enableQueryServiceLogOTLPExport bool
|
||||||
var preferDelta bool
|
|
||||||
var preferSpanMetrics bool
|
var preferSpanMetrics bool
|
||||||
|
|
||||||
var maxIdleConns int
|
var maxIdleConns int
|
||||||
@@ -100,14 +99,13 @@ func main() {
|
|||||||
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)")
|
||||||
flag.BoolVar(&preferDelta, "prefer-delta", false, "(prefer delta over cumulative metrics)")
|
|
||||||
flag.BoolVar(&preferSpanMetrics, "prefer-span-metrics", false, "(prefer span metrics for service level metrics)")
|
flag.BoolVar(&preferSpanMetrics, "prefer-span-metrics", false, "(prefer span metrics for service level metrics)")
|
||||||
flag.IntVar(&maxIdleConns, "max-idle-conns", 50, "(number of connections to maintain in the pool.)")
|
flag.IntVar(&maxIdleConns, "max-idle-conns", 50, "(number of connections to maintain in the pool.)")
|
||||||
flag.IntVar(&maxOpenConns, "max-open-conns", 100, "(max connections for use at any time.)")
|
flag.IntVar(&maxOpenConns, "max-open-conns", 100, "(max connections for use at any time.)")
|
||||||
flag.DurationVar(&dialTimeout, "dial-timeout", 5*time.Second, "(the maximum time to establish a connection.)")
|
flag.DurationVar(&dialTimeout, "dial-timeout", 5*time.Second, "(the maximum time to establish a connection.)")
|
||||||
flag.StringVar(&ruleRepoURL, "rules.repo-url", baseconst.AlertHelpPage, "(host address used to build rule link in alert messages)")
|
flag.StringVar(&ruleRepoURL, "rules.repo-url", baseconst.AlertHelpPage, "(host address used to build rule link in alert messages)")
|
||||||
flag.StringVar(&cacheConfigPath, "experimental.cache-config", "", "(cache config to use)")
|
flag.StringVar(&cacheConfigPath, "experimental.cache-config", "", "(cache config to use)")
|
||||||
flag.StringVar(&fluxInterval, "flux-interval", "5m", "(cache config to use)")
|
flag.StringVar(&fluxInterval, "flux-interval", "5m", "(the interval to exclude data from being cached to avoid incorrect cache for data in motion)")
|
||||||
flag.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)")
|
flag.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)")
|
||||||
flag.StringVar(&cluster, "cluster", "cluster", "(cluster name - defaults to 'cluster')")
|
flag.StringVar(&cluster, "cluster", "cluster", "(cluster name - defaults to 'cluster')")
|
||||||
flag.StringVar(&gatewayUrl, "gateway-url", "", "(url to the gateway)")
|
flag.StringVar(&gatewayUrl, "gateway-url", "", "(url to the gateway)")
|
||||||
@@ -125,7 +123,6 @@ func main() {
|
|||||||
HTTPHostPort: baseconst.HTTPHostPort,
|
HTTPHostPort: baseconst.HTTPHostPort,
|
||||||
PromConfigPath: promConfigPath,
|
PromConfigPath: promConfigPath,
|
||||||
SkipTopLvlOpsPath: skipTopLvlOpsPath,
|
SkipTopLvlOpsPath: skipTopLvlOpsPath,
|
||||||
PreferDelta: preferDelta,
|
|
||||||
PreferSpanMetrics: preferSpanMetrics,
|
PreferSpanMetrics: preferSpanMetrics,
|
||||||
PrivateHostPort: baseconst.PrivateHostPort,
|
PrivateHostPort: baseconst.PrivateHostPort,
|
||||||
DisableRules: disableRules,
|
DisableRules: disableRules,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const config: Config.InitialOptions = {
|
|||||||
modulePathIgnorePatterns: ['dist'],
|
modulePathIgnorePatterns: ['dist'],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'\\.(css|less|scss)$': '<rootDir>/__mocks__/cssMock.ts',
|
'\\.(css|less|scss)$': '<rootDir>/__mocks__/cssMock.ts',
|
||||||
|
'\\.md$': '<rootDir>/__mocks__/cssMock.ts',
|
||||||
},
|
},
|
||||||
globals: {
|
globals: {
|
||||||
extensionsToTreatAsEsm: ['.ts'],
|
extensionsToTreatAsEsm: ['.ts'],
|
||||||
|
|||||||
@@ -88,6 +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",
|
||||||
"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",
|
||||||
@@ -109,6 +110,8 @@
|
|||||||
"react-syntax-highlighter": "15.5.0",
|
"react-syntax-highlighter": "15.5.0",
|
||||||
"react-use": "^17.3.2",
|
"react-use": "^17.3.2",
|
||||||
"react-virtuoso": "4.0.3",
|
"react-virtuoso": "4.0.3",
|
||||||
|
"overlayscrollbars-react": "^0.5.6",
|
||||||
|
"overlayscrollbars": "^2.8.1",
|
||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"rehype-raw": "7.0.0",
|
"rehype-raw": "7.0.0",
|
||||||
|
|||||||
BIN
frontend/public/fonts/GeistMonoVF.woff2
Normal file
BIN
frontend/public/fonts/GeistMonoVF.woff2
Normal file
Binary file not shown.
8
frontend/public/locales/en-GB/onboarding.json
Normal file
8
frontend/public/locales/en-GB/onboarding.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"invite_user": "Invite your teammates",
|
||||||
|
"invite": "Invite",
|
||||||
|
"skip": "Skip",
|
||||||
|
"invite_user_helper_text": "Not the right person to get started? No worries! Invite someone who can.",
|
||||||
|
"select_use_case": "Select a use-case to get started",
|
||||||
|
"get_started": "Get Started"
|
||||||
|
}
|
||||||
@@ -6,5 +6,6 @@
|
|||||||
"share": "Share",
|
"share": "Share",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"logged_in": "Logged In"
|
"logged_in": "Logged In",
|
||||||
|
"pending_data_placeholder": "Just a bit of patience, just a little bit’s enough ⎯ we’re getting your {{dataSource}}!"
|
||||||
}
|
}
|
||||||
|
|||||||
8
frontend/public/locales/en/onboarding.json
Normal file
8
frontend/public/locales/en/onboarding.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"invite_user": "Invite your teammates",
|
||||||
|
"invite": "Invite",
|
||||||
|
"skip": "Skip",
|
||||||
|
"invite_user_helper_text": "Not the right person to get started? No worries! Invite someone who can.",
|
||||||
|
"select_use_case": "Select a use-case to get started",
|
||||||
|
"get_started": "Get Started"
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import { NotificationProvider } from 'hooks/useNotifications';
|
|||||||
import { ResourceProvider } from 'hooks/useResourceAttribute';
|
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 { 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';
|
||||||
@@ -38,7 +39,7 @@ import defaultRoutes, {
|
|||||||
|
|
||||||
function App(): JSX.Element {
|
function App(): JSX.Element {
|
||||||
const themeConfig = useThemeConfig();
|
const themeConfig = useThemeConfig();
|
||||||
const { data } = useLicense();
|
const { data: licenseData } = useLicense();
|
||||||
const [routes, setRoutes] = useState<AppRoutes[]>(defaultRoutes);
|
const [routes, setRoutes] = useState<AppRoutes[]>(defaultRoutes);
|
||||||
const { role, isLoggedIn: isLoggedInState, user, org } = useSelector<
|
const { role, isLoggedIn: isLoggedInState, user, org } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
@@ -92,10 +93,10 @@ function App(): JSX.Element {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isOnBasicPlan =
|
const isOnBasicPlan =
|
||||||
data?.payload?.licenses?.some(
|
licenseData?.payload?.licenses?.some(
|
||||||
(license) =>
|
(license) =>
|
||||||
license.isCurrent && license.planKey === LICENSE_PLAN_KEY.BASIC_PLAN,
|
license.isCurrent && license.planKey === LICENSE_PLAN_KEY.BASIC_PLAN,
|
||||||
) || data?.payload?.licenses === null;
|
) || licenseData?.payload?.licenses === null;
|
||||||
|
|
||||||
const enableAnalytics = (user: User): void => {
|
const enableAnalytics = (user: User): void => {
|
||||||
const orgName =
|
const orgName =
|
||||||
@@ -112,9 +113,7 @@ function App(): JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const sanitizedIdentifyPayload = pickBy(identifyPayload, identity);
|
const sanitizedIdentifyPayload = pickBy(identifyPayload, identity);
|
||||||
|
|
||||||
const domain = extractDomain(email);
|
const domain = extractDomain(email);
|
||||||
|
|
||||||
const hostNameParts = hostname.split('.');
|
const hostNameParts = hostname.split('.');
|
||||||
|
|
||||||
const groupTraits = {
|
const groupTraits = {
|
||||||
@@ -127,10 +126,30 @@ function App(): JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.analytics.identify(email, sanitizedIdentifyPayload);
|
window.analytics.identify(email, sanitizedIdentifyPayload);
|
||||||
|
|
||||||
window.analytics.group(domain, groupTraits);
|
window.analytics.group(domain, groupTraits);
|
||||||
|
|
||||||
window.clarity('identify', email, name);
|
window.clarity('identify', email, name);
|
||||||
|
|
||||||
|
posthog?.identify(email, {
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
orgName,
|
||||||
|
tenant_id: hostNameParts[0],
|
||||||
|
data_region: hostNameParts[1],
|
||||||
|
tenant_url: hostname,
|
||||||
|
company_domain: domain,
|
||||||
|
source: 'signoz-ui',
|
||||||
|
isPaidUser: !!licenseData?.payload?.trialConvertedToSubscription,
|
||||||
|
});
|
||||||
|
|
||||||
|
posthog?.group('company', domain, {
|
||||||
|
name: orgName,
|
||||||
|
tenant_id: hostNameParts[0],
|
||||||
|
data_region: hostNameParts[1],
|
||||||
|
tenant_url: hostname,
|
||||||
|
company_domain: domain,
|
||||||
|
source: 'signoz-ui',
|
||||||
|
isPaidUser: !!licenseData?.payload?.trialConvertedToSubscription,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -144,10 +163,6 @@ function App(): JSX.Element {
|
|||||||
!isIdentifiedUser
|
!isIdentifiedUser
|
||||||
) {
|
) {
|
||||||
setLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER, 'true');
|
setLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER, 'true');
|
||||||
|
|
||||||
if (isCloudUserVal) {
|
|
||||||
enableAnalytics(user);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -195,6 +210,11 @@ function App(): JSX.Element {
|
|||||||
console.error('Failed to parse local storage theme analytics event');
|
console.error('Failed to parse local storage theme analytics event');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isCloudUserVal && user && user.email) {
|
||||||
|
enableAnalytics(user);
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const apiV1 = '/api/v1/';
|
|||||||
export const apiV2 = '/api/v2/';
|
export const apiV2 = '/api/v2/';
|
||||||
export const apiV3 = '/api/v3/';
|
export const apiV3 = '/api/v3/';
|
||||||
export const apiV4 = '/api/v4/';
|
export const apiV4 = '/api/v4/';
|
||||||
export const gatewayApiV1 = '/api/gateway/v1';
|
export const gatewayApiV1 = '/api/gateway/v1/';
|
||||||
export const apiAlertManager = '/api/alertmanager';
|
export const apiAlertManager = '/api/alertmanager/';
|
||||||
|
|
||||||
export default apiV1;
|
export default apiV1;
|
||||||
|
|||||||
@@ -5,7 +5,13 @@ import { Button, Dropdown, MenuProps } from 'antd';
|
|||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
function DropDown({ element }: { element: JSX.Element[] }): JSX.Element {
|
function DropDown({
|
||||||
|
element,
|
||||||
|
onDropDownItemClick,
|
||||||
|
}: {
|
||||||
|
element: JSX.Element[];
|
||||||
|
onDropDownItemClick?: MenuProps['onClick'];
|
||||||
|
}): JSX.Element {
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
const items: MenuProps['items'] = element.map(
|
const items: MenuProps['items'] = element.map(
|
||||||
@@ -23,6 +29,7 @@ function DropDown({ element }: { element: JSX.Element[] }): JSX.Element {
|
|||||||
items,
|
items,
|
||||||
onMouseEnter: (): void => setDdOpen(true),
|
onMouseEnter: (): void => setDdOpen(true),
|
||||||
onMouseLeave: (): void => setDdOpen(false),
|
onMouseLeave: (): void => setDdOpen(false),
|
||||||
|
onClick: (item): void => onDropDownItemClick?.(item),
|
||||||
}}
|
}}
|
||||||
open={isDdOpen}
|
open={isDdOpen}
|
||||||
>
|
>
|
||||||
@@ -40,4 +47,8 @@ function DropDown({ element }: { element: JSX.Element[] }): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DropDown.defaultProps = {
|
||||||
|
onDropDownItemClick: (): void => {},
|
||||||
|
};
|
||||||
|
|
||||||
export default DropDown;
|
export default DropDown;
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
|
|
||||||
.log-body {
|
.log-body {
|
||||||
font-family: 'SF Mono';
|
font-family: 'SF Mono';
|
||||||
font-family: 'Space Mono', monospace;
|
font-family: 'Geist Mono';
|
||||||
|
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
font-weight: var(--font-weight-normal);
|
font-weight: var(--font-weight-normal);
|
||||||
|
|||||||
@@ -62,8 +62,6 @@ function RawLogView({
|
|||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
const isReadOnlyLog = !isLogsExplorerPage || isReadOnly;
|
const isReadOnlyLog = !isLogsExplorerPage || isReadOnly;
|
||||||
|
|
||||||
const severityText = data.severity_text ? `${data.severity_text} |` : '';
|
|
||||||
|
|
||||||
const logType = getLogIndicatorType(data);
|
const logType = getLogIndicatorType(data);
|
||||||
|
|
||||||
const updatedSelecedFields = useMemo(
|
const updatedSelecedFields = useMemo(
|
||||||
@@ -88,17 +86,16 @@ function RawLogView({
|
|||||||
attributesText += ' | ';
|
attributesText += ' | ';
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = useMemo(
|
const text = useMemo(() => {
|
||||||
() =>
|
const date =
|
||||||
typeof data.timestamp === 'string'
|
typeof data.timestamp === 'string'
|
||||||
? `${dayjs(data.timestamp).format(
|
? dayjs(data.timestamp)
|
||||||
'YYYY-MM-DD HH:mm:ss.SSS',
|
: dayjs(data.timestamp / 1e6);
|
||||||
)} | ${attributesText} ${severityText} ${data.body}`
|
|
||||||
: `${dayjs(data.timestamp / 1e6).format(
|
return `${date.format('YYYY-MM-DD HH:mm:ss.SSS')} | ${attributesText} ${
|
||||||
'YYYY-MM-DD HH:mm:ss.SSS',
|
data.body
|
||||||
)} | ${attributesText} ${severityText} ${data.body}`,
|
}`;
|
||||||
[data.timestamp, data.body, severityText, attributesText],
|
}, [data.timestamp, data.body, attributesText]);
|
||||||
);
|
|
||||||
|
|
||||||
const handleClickExpand = useCallback(() => {
|
const handleClickExpand = useCallback(() => {
|
||||||
if (activeContextLog || isReadOnly) return;
|
if (activeContextLog || isReadOnly) return;
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export const ExpandIconWrapper = styled(Col)`
|
|||||||
export const RawLogContent = styled.div<RawLogContentProps>`
|
export const RawLogContent = styled.div<RawLogContentProps>`
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
font-family: 'SF Mono', monospace;
|
font-family: 'SF Mono', monospace;
|
||||||
font-family: 'Space Mono', monospace;
|
font-family: 'Geist Mono';
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import TypicalOverlayScrollbar from 'components/TypicalOverlayScrollbar/TypicalOverlayScrollbar';
|
||||||
|
import VirtuosoOverlayScrollbar from 'components/VirtuosoOverlayScrollbar/VirtuosoOverlayScrollbar';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { PartialOptions } from 'overlayscrollbars';
|
||||||
|
import { CSSProperties, ReactElement, useMemo } from 'react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactElement;
|
||||||
|
isVirtuoso?: boolean;
|
||||||
|
style?: CSSProperties;
|
||||||
|
options?: PartialOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
function OverlayScrollbar({
|
||||||
|
children,
|
||||||
|
isVirtuoso,
|
||||||
|
style,
|
||||||
|
options: customOptions,
|
||||||
|
}: Props): any {
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
const options = useMemo(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
scrollbars: {
|
||||||
|
autoHide: 'scroll',
|
||||||
|
theme: isDarkMode ? 'os-theme-light' : 'os-theme-dark',
|
||||||
|
},
|
||||||
|
...(customOptions || {}),
|
||||||
|
} as PartialOptions),
|
||||||
|
[customOptions, isDarkMode],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isVirtuoso) {
|
||||||
|
return (
|
||||||
|
<VirtuosoOverlayScrollbar style={style} options={options}>
|
||||||
|
{children}
|
||||||
|
</VirtuosoOverlayScrollbar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TypicalOverlayScrollbar style={style} options={options}>
|
||||||
|
{children}
|
||||||
|
</TypicalOverlayScrollbar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
OverlayScrollbar.defaultProps = {
|
||||||
|
isVirtuoso: false,
|
||||||
|
style: {},
|
||||||
|
options: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OverlayScrollbar;
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
import './DynamicColumnTable.syles.scss';
|
import './DynamicColumnTable.syles.scss';
|
||||||
|
|
||||||
import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd';
|
import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd';
|
||||||
|
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 FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||||
import { SlidersHorizontal } from 'lucide-react';
|
import { SlidersHorizontal } from 'lucide-react';
|
||||||
import { memo, useEffect, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
@@ -22,6 +24,7 @@ function DynamicColumnTable({
|
|||||||
dynamicColumns,
|
dynamicColumns,
|
||||||
onDragColumn,
|
onDragColumn,
|
||||||
facingIssueBtn,
|
facingIssueBtn,
|
||||||
|
shouldSendAlertsLogEvent,
|
||||||
...restProps
|
...restProps
|
||||||
}: DynamicColumnTableProps): JSX.Element {
|
}: DynamicColumnTableProps): JSX.Element {
|
||||||
const [columnsData, setColumnsData] = useState<ColumnsType | undefined>(
|
const [columnsData, setColumnsData] = useState<ColumnsType | undefined>(
|
||||||
@@ -47,11 +50,18 @@ function DynamicColumnTable({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [columns, dynamicColumns]);
|
}, [columns, dynamicColumns]);
|
||||||
|
|
||||||
const onToggleHandler = (index: number) => (
|
const onToggleHandler = (
|
||||||
checked: boolean,
|
index: number,
|
||||||
event: React.MouseEvent<HTMLButtonElement>,
|
column: ColumnGroupType<any> | ColumnType<any>,
|
||||||
): void => {
|
) => (checked: boolean, event: React.MouseEvent<HTMLButtonElement>): void => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (shouldSendAlertsLogEvent) {
|
||||||
|
logEvent('Alert: Column toggled', {
|
||||||
|
column: column?.title,
|
||||||
|
action: checked ? 'Enable' : 'Disable',
|
||||||
|
});
|
||||||
|
}
|
||||||
setVisibleColumns({
|
setVisibleColumns({
|
||||||
tablesource,
|
tablesource,
|
||||||
dynamicColumns,
|
dynamicColumns,
|
||||||
@@ -75,7 +85,7 @@ function DynamicColumnTable({
|
|||||||
<div>{column.title?.toString()}</div>
|
<div>{column.title?.toString()}</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={columnsData?.findIndex((c) => c.key === column.key) !== -1}
|
checked={columnsData?.findIndex((c) => c.key === column.key) !== -1}
|
||||||
onChange={onToggleHandler(index)}
|
onChange={onToggleHandler(index, column)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { Table } from 'antd';
|
import { Table } from 'antd';
|
||||||
import { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
import { dragColumnParams } from 'hooks/useDragColumns/configs';
|
import { dragColumnParams } from 'hooks/useDragColumns/configs';
|
||||||
|
import { set } from 'lodash-es';
|
||||||
import {
|
import {
|
||||||
SyntheticEvent,
|
SyntheticEvent,
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -20,6 +21,7 @@ import { ResizeTableProps } from './types';
|
|||||||
function ResizeTable({
|
function ResizeTable({
|
||||||
columns,
|
columns,
|
||||||
onDragColumn,
|
onDragColumn,
|
||||||
|
pagination,
|
||||||
...restProps
|
...restProps
|
||||||
}: ResizeTableProps): JSX.Element {
|
}: ResizeTableProps): JSX.Element {
|
||||||
const [columnsData, setColumns] = useState<ColumnsType>([]);
|
const [columnsData, setColumns] = useState<ColumnsType>([]);
|
||||||
@@ -58,15 +60,22 @@ function ResizeTable({
|
|||||||
[columnsData, onDragColumn, handleResize],
|
[columnsData, onDragColumn, handleResize],
|
||||||
);
|
);
|
||||||
|
|
||||||
const tableParams = useMemo(
|
const tableParams = useMemo(() => {
|
||||||
() => ({
|
const props = {
|
||||||
...restProps,
|
...restProps,
|
||||||
components: { header: { cell: ResizableHeader } },
|
components: { header: { cell: ResizableHeader } },
|
||||||
columns: mergedColumns,
|
columns: mergedColumns,
|
||||||
}),
|
};
|
||||||
[mergedColumns, restProps],
|
|
||||||
|
set(
|
||||||
|
props,
|
||||||
|
'pagination',
|
||||||
|
pagination ? { ...pagination, hideOnSinglePage: true } : false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return props;
|
||||||
|
}, [mergedColumns, pagination, restProps]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (columns) {
|
if (columns) {
|
||||||
setColumns(columns);
|
setColumns(columns);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface DynamicColumnTableProps extends TableProps<any> {
|
|||||||
dynamicColumns: TableProps<any>['columns'];
|
dynamicColumns: TableProps<any>['columns'];
|
||||||
onDragColumn?: (fromIndex: number, toIndex: number) => void;
|
onDragColumn?: (fromIndex: number, toIndex: number) => void;
|
||||||
facingIssueBtn?: FacingIssueBtnProps;
|
facingIssueBtn?: FacingIssueBtnProps;
|
||||||
|
shouldSendAlertsLogEvent?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetVisibleColumnsFunction = (
|
export type GetVisibleColumnsFunction = (
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { Tooltip } from 'antd';
|
|||||||
import { themeColors } from 'constants/theme';
|
import { themeColors } from 'constants/theme';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
|
||||||
|
|
||||||
import { style } from './constant';
|
import { style } from './constant';
|
||||||
|
|
||||||
@@ -64,7 +63,7 @@ function TextToolTip({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip getTooltipContainer={popupContainer} overlay={overlay}>
|
<Tooltip overlay={overlay}>
|
||||||
{useFilledIcon ? (
|
{useFilledIcon ? (
|
||||||
<QuestionCircleFilled style={iconStyle} />
|
<QuestionCircleFilled style={iconStyle} />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import './typicalOverlayScrollbar.scss';
|
||||||
|
|
||||||
|
import { PartialOptions } from 'overlayscrollbars';
|
||||||
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||||
|
import { CSSProperties, ReactElement } from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactElement;
|
||||||
|
style?: CSSProperties;
|
||||||
|
options?: PartialOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TypicalOverlayScrollbar({
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
options,
|
||||||
|
}: Props): ReturnType<typeof OverlayScrollbarsComponent> {
|
||||||
|
return (
|
||||||
|
<OverlayScrollbarsComponent
|
||||||
|
defer
|
||||||
|
options={options}
|
||||||
|
style={style}
|
||||||
|
className="overlay-scrollbar"
|
||||||
|
data-overlayscrollbars-initialize
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TypicalOverlayScrollbar.defaultProps = { style: {}, options: {} };
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.overlay-scrollbar {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import './virtuosoOverlayScrollbar.scss';
|
||||||
|
|
||||||
|
import useInitializeOverlayScrollbar from 'hooks/useInitializeOverlayScrollbar/useInitializeOverlayScrollbar';
|
||||||
|
import { PartialOptions } from 'overlayscrollbars';
|
||||||
|
import React, { CSSProperties, ReactElement } from 'react';
|
||||||
|
|
||||||
|
interface VirtuosoOverlayScrollbarProps {
|
||||||
|
children: ReactElement;
|
||||||
|
style?: CSSProperties;
|
||||||
|
options: PartialOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function VirtuosoOverlayScrollbar({
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
options,
|
||||||
|
}: VirtuosoOverlayScrollbarProps): JSX.Element {
|
||||||
|
const { rootRef, setScroller } = useInitializeOverlayScrollbar(options);
|
||||||
|
|
||||||
|
const enhancedChild = React.cloneElement(children, {
|
||||||
|
scrollerRef: setScroller,
|
||||||
|
'data-overlayscrollbars-initialize': true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-overlayscrollbars-initialize
|
||||||
|
ref={rootRef}
|
||||||
|
className="overlay-scroll-wrapper"
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{enhancedChild}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtuosoOverlayScrollbar.defaultProps = { style: {} };
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.overlay-scroll-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
import { PlusOutlined } from '@ant-design/icons';
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
import { Tooltip, Typography } from 'antd';
|
import { Tooltip, Typography } from 'antd';
|
||||||
import getAll from 'api/channels/getAll';
|
import getAll from 'api/channels/getAll';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import TextToolTip from 'components/TextToolTip';
|
import TextToolTip from 'components/TextToolTip';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import useFetch from 'hooks/useFetch';
|
import useFetch from 'hooks/useFetch';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { useCallback } from 'react';
|
import { isUndefined } from 'lodash-es';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@@ -31,6 +33,14 @@ function AlertChannels(): JSX.Element {
|
|||||||
|
|
||||||
const { loading, payload, error, errorMessage } = useFetch(getAll);
|
const { loading, payload, error, errorMessage } = useFetch(getAll);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isUndefined(payload)) {
|
||||||
|
logEvent('Alert Channel: Channel list page visited', {
|
||||||
|
number: payload?.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [payload]);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <Typography>{errorMessage}</Typography>;
|
return <Typography>{errorMessage}</Typography>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { ColumnType, TablePaginationConfig } from 'antd/es/table';
|
|||||||
import { FilterValue, SorterResult } from 'antd/es/table/interface';
|
import { FilterValue, SorterResult } from 'antd/es/table/interface';
|
||||||
import { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
import { FilterConfirmProps } from 'antd/lib/table/interface';
|
import { FilterConfirmProps } from 'antd/lib/table/interface';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import getAll from 'api/errors/getAll';
|
import getAll from 'api/errors/getAll';
|
||||||
import getErrorCounts from 'api/errors/getErrorCounts';
|
import getErrorCounts from 'api/errors/getErrorCounts';
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
@@ -23,7 +24,8 @@ import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute
|
|||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import createQueryParams from 'lib/createQueryParams';
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { isUndefined } from 'lodash-es';
|
||||||
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQueries } from 'react-query';
|
import { useQueries } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
@@ -410,6 +412,26 @@ function AllErrors(): JSX.Element {
|
|||||||
[pathname],
|
[pathname],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const logEventCalledRef = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!logEventCalledRef.current &&
|
||||||
|
!isUndefined(errorCountResponse.data?.payload)
|
||||||
|
) {
|
||||||
|
const selectedEnvironments = queries.find(
|
||||||
|
(val) => val.tagKey === 'resource_deployment_environment',
|
||||||
|
)?.tagValue;
|
||||||
|
|
||||||
|
logEvent('Exception: List page visited', {
|
||||||
|
numberOfExceptions: errorCountResponse?.data?.payload,
|
||||||
|
selectedEnvironments,
|
||||||
|
resourceAttributeUsed: !!queries?.length,
|
||||||
|
});
|
||||||
|
logEventCalledRef.current = true;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [errorCountResponse.data?.payload]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResizeTable
|
<ResizeTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
.app-content {
|
.app-content {
|
||||||
width: calc(100% - 64px);
|
width: calc(100% - 64px);
|
||||||
overflow: auto;
|
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
|
|
||||||
.content-container {
|
.content-container {
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import './AppLayout.styles.scss';
|
|||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
import { Flex } from 'antd';
|
import { Flex } from 'antd';
|
||||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||||
import getDynamicConfigs from 'api/dynamicConfigs/getDynamicConfigs';
|
|
||||||
import getUserLatestVersion from 'api/user/getLatestVersion';
|
import getUserLatestVersion from 'api/user/getLatestVersion';
|
||||||
import getUserVersion from 'api/user/getVersion';
|
import getUserVersion from 'api/user/getVersion';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
|
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||||
import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
|
import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import SideNav from 'container/SideNav';
|
import SideNav from 'container/SideNav';
|
||||||
@@ -38,7 +38,6 @@ import { sideBarCollapse } from 'store/actions';
|
|||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import AppActions from 'types/actions';
|
import AppActions from 'types/actions';
|
||||||
import {
|
import {
|
||||||
UPDATE_CONFIGS,
|
|
||||||
UPDATE_CURRENT_ERROR,
|
UPDATE_CURRENT_ERROR,
|
||||||
UPDATE_CURRENT_VERSION,
|
UPDATE_CURRENT_VERSION,
|
||||||
UPDATE_LATEST_VERSION,
|
UPDATE_LATEST_VERSION,
|
||||||
@@ -66,11 +65,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const { t } = useTranslation(['titles']);
|
const { t } = useTranslation(['titles']);
|
||||||
|
|
||||||
const [
|
const [getUserVersionResponse, getUserLatestVersionResponse] = useQueries([
|
||||||
getUserVersionResponse,
|
|
||||||
getUserLatestVersionResponse,
|
|
||||||
getDynamicConfigsResponse,
|
|
||||||
] = useQueries([
|
|
||||||
{
|
{
|
||||||
queryFn: getUserVersion,
|
queryFn: getUserVersion,
|
||||||
queryKey: ['getUserVersion', user?.accessJwt],
|
queryKey: ['getUserVersion', user?.accessJwt],
|
||||||
@@ -81,10 +76,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
queryKey: ['getUserLatestVersion', user?.accessJwt],
|
queryKey: ['getUserLatestVersion', user?.accessJwt],
|
||||||
enabled: isLoggedIn,
|
enabled: isLoggedIn,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
queryFn: getDynamicConfigs,
|
|
||||||
queryKey: ['getDynamicConfigs', user?.accessJwt],
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -95,15 +86,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
if (getUserVersionResponse.status === 'idle' && isLoggedIn) {
|
if (getUserVersionResponse.status === 'idle' && isLoggedIn) {
|
||||||
getUserVersionResponse.refetch();
|
getUserVersionResponse.refetch();
|
||||||
}
|
}
|
||||||
if (getDynamicConfigsResponse.status === 'idle') {
|
}, [getUserLatestVersionResponse, getUserVersionResponse, isLoggedIn]);
|
||||||
getDynamicConfigsResponse.refetch();
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
getUserLatestVersionResponse,
|
|
||||||
getUserVersionResponse,
|
|
||||||
isLoggedIn,
|
|
||||||
getDynamicConfigsResponse,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
|
|
||||||
@@ -111,7 +94,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
|
|
||||||
const latestCurrentCounter = useRef(0);
|
const latestCurrentCounter = useRef(0);
|
||||||
const latestVersionCounter = useRef(0);
|
const latestVersionCounter = useRef(0);
|
||||||
const latestConfigCounter = useRef(0);
|
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
@@ -189,23 +171,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
getDynamicConfigsResponse.isFetched &&
|
|
||||||
getDynamicConfigsResponse.isSuccess &&
|
|
||||||
getDynamicConfigsResponse.data &&
|
|
||||||
getDynamicConfigsResponse.data.payload &&
|
|
||||||
latestConfigCounter.current === 0
|
|
||||||
) {
|
|
||||||
latestConfigCounter.current = 1;
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: UPDATE_CONFIGS,
|
|
||||||
payload: {
|
|
||||||
configs: getDynamicConfigsResponse.data.payload,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [
|
}, [
|
||||||
dispatch,
|
dispatch,
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
@@ -220,9 +185,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
getUserLatestVersionResponse.isFetched,
|
getUserLatestVersionResponse.isFetched,
|
||||||
getUserVersionResponse.isFetched,
|
getUserVersionResponse.isFetched,
|
||||||
getUserLatestVersionResponse.isSuccess,
|
getUserLatestVersionResponse.isSuccess,
|
||||||
getDynamicConfigsResponse.data,
|
|
||||||
getDynamicConfigsResponse.isFetched,
|
|
||||||
getDynamicConfigsResponse.isSuccess,
|
|
||||||
notifications,
|
notifications,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -342,9 +304,13 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className={cx('app-content', collapsed ? 'collapsed' : '')}>
|
<div
|
||||||
|
className={cx('app-content', collapsed ? 'collapsed' : '')}
|
||||||
|
data-overlayscrollbars-initialize
|
||||||
|
>
|
||||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||||
<LayoutContent>
|
<LayoutContent data-overlayscrollbars-initialize>
|
||||||
|
<OverlayScrollbar>
|
||||||
<ChildrenContainer
|
<ChildrenContainer
|
||||||
style={{
|
style={{
|
||||||
margin:
|
margin:
|
||||||
@@ -360,6 +326,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
{isToDisplayLayout && !renderFullScreen && <TopNav />}
|
{isToDisplayLayout && !renderFullScreen && <TopNav />}
|
||||||
{children}
|
{children}
|
||||||
</ChildrenContainer>
|
</ChildrenContainer>
|
||||||
|
</OverlayScrollbar>
|
||||||
</LayoutContent>
|
</LayoutContent>
|
||||||
</Sentry.ErrorBoundary>
|
</Sentry.ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export const Layout = styled(LayoutComponent)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const LayoutContent = styled(LayoutComponent.Content)`
|
export const LayoutContent = styled(LayoutComponent.Content)`
|
||||||
overflow-y: auto;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 0.1rem;
|
width: 0.1rem;
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ import testOpsGenie from 'api/channels/testOpsgenie';
|
|||||||
import testPagerApi from 'api/channels/testPager';
|
import testPagerApi from 'api/channels/testPager';
|
||||||
import testSlackApi from 'api/channels/testSlack';
|
import testSlackApi from 'api/channels/testSlack';
|
||||||
import testWebhookApi from 'api/channels/testWebhook';
|
import testWebhookApi from 'api/channels/testWebhook';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import FormAlertChannels from 'container/FormAlertChannels';
|
import FormAlertChannels from 'container/FormAlertChannels';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -43,6 +44,10 @@ function CreateAlertChannels({
|
|||||||
|
|
||||||
const [formInstance] = Form.useForm();
|
const [formInstance] = Form.useForm();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
logEvent('Alert Channel: Create channel page visited', {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [selectedConfig, setSelectedConfig] = useState<
|
const [selectedConfig, setSelectedConfig] = useState<
|
||||||
Partial<
|
Partial<
|
||||||
SlackChannel &
|
SlackChannel &
|
||||||
@@ -139,19 +144,25 @@ function CreateAlertChannels({
|
|||||||
description: t('channel_creation_done'),
|
description: t('channel_creation_done'),
|
||||||
});
|
});
|
||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
} else {
|
return { status: 'success', statusMessage: t('channel_creation_done') };
|
||||||
|
}
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: response.error || t('channel_creation_failed'),
|
description: response.error || t('channel_creation_failed'),
|
||||||
});
|
});
|
||||||
}
|
return {
|
||||||
|
status: 'failed',
|
||||||
|
statusMessage: response.error || t('channel_creation_failed'),
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: t('channel_creation_failed'),
|
description: t('channel_creation_failed'),
|
||||||
});
|
});
|
||||||
}
|
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
||||||
|
} finally {
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
|
}
|
||||||
}, [prepareSlackRequest, t, notifications]);
|
}, [prepareSlackRequest, t, notifications]);
|
||||||
|
|
||||||
const prepareWebhookRequest = useCallback(() => {
|
const prepareWebhookRequest = useCallback(() => {
|
||||||
@@ -200,19 +211,25 @@ function CreateAlertChannels({
|
|||||||
description: t('channel_creation_done'),
|
description: t('channel_creation_done'),
|
||||||
});
|
});
|
||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
} else {
|
return { status: 'success', statusMessage: t('channel_creation_done') };
|
||||||
|
}
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: response.error || t('channel_creation_failed'),
|
description: response.error || t('channel_creation_failed'),
|
||||||
});
|
});
|
||||||
}
|
return {
|
||||||
|
status: 'failed',
|
||||||
|
statusMessage: response.error || t('channel_creation_failed'),
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: t('channel_creation_failed'),
|
description: t('channel_creation_failed'),
|
||||||
});
|
});
|
||||||
}
|
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
||||||
|
} finally {
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
|
}
|
||||||
}, [prepareWebhookRequest, t, notifications]);
|
}, [prepareWebhookRequest, t, notifications]);
|
||||||
|
|
||||||
const preparePagerRequest = useCallback(() => {
|
const preparePagerRequest = useCallback(() => {
|
||||||
@@ -245,8 +262,8 @@ function CreateAlertChannels({
|
|||||||
setSavingState(true);
|
setSavingState(true);
|
||||||
const request = preparePagerRequest();
|
const request = preparePagerRequest();
|
||||||
|
|
||||||
if (request) {
|
|
||||||
try {
|
try {
|
||||||
|
if (request) {
|
||||||
const response = await createPagerApi(request);
|
const response = await createPagerApi(request);
|
||||||
|
|
||||||
if (response.statusCode === 200) {
|
if (response.statusCode === 200) {
|
||||||
@@ -255,20 +272,31 @@ function CreateAlertChannels({
|
|||||||
description: t('channel_creation_done'),
|
description: t('channel_creation_done'),
|
||||||
});
|
});
|
||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
} else {
|
return { status: 'success', statusMessage: t('channel_creation_done') };
|
||||||
|
}
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: response.error || t('channel_creation_failed'),
|
description: response.error || t('channel_creation_failed'),
|
||||||
});
|
});
|
||||||
|
return {
|
||||||
|
status: 'failed',
|
||||||
|
statusMessage: response.error || t('channel_creation_failed'),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: t('channel_creation_failed'),
|
description: t('channel_creation_failed'),
|
||||||
});
|
});
|
||||||
}
|
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
||||||
}
|
} catch (error) {
|
||||||
|
notifications.error({
|
||||||
|
message: 'Error',
|
||||||
|
description: t('channel_creation_failed'),
|
||||||
|
});
|
||||||
|
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
||||||
|
} finally {
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
|
}
|
||||||
}, [t, notifications, preparePagerRequest]);
|
}, [t, notifications, preparePagerRequest]);
|
||||||
|
|
||||||
const prepareOpsgenieRequest = useCallback(
|
const prepareOpsgenieRequest = useCallback(
|
||||||
@@ -295,19 +323,25 @@ function CreateAlertChannels({
|
|||||||
description: t('channel_creation_done'),
|
description: t('channel_creation_done'),
|
||||||
});
|
});
|
||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
} else {
|
return { status: 'success', statusMessage: t('channel_creation_done') };
|
||||||
|
}
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: response.error || t('channel_creation_failed'),
|
description: response.error || t('channel_creation_failed'),
|
||||||
});
|
});
|
||||||
}
|
return {
|
||||||
|
status: 'failed',
|
||||||
|
statusMessage: response.error || t('channel_creation_failed'),
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: t('channel_creation_failed'),
|
description: t('channel_creation_failed'),
|
||||||
});
|
});
|
||||||
}
|
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
||||||
|
} finally {
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
|
}
|
||||||
}, [prepareOpsgenieRequest, t, notifications]);
|
}, [prepareOpsgenieRequest, t, notifications]);
|
||||||
|
|
||||||
const prepareEmailRequest = useCallback(
|
const prepareEmailRequest = useCallback(
|
||||||
@@ -332,19 +366,25 @@ function CreateAlertChannels({
|
|||||||
description: t('channel_creation_done'),
|
description: t('channel_creation_done'),
|
||||||
});
|
});
|
||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
} else {
|
return { status: 'success', statusMessage: t('channel_creation_done') };
|
||||||
|
}
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: response.error || t('channel_creation_failed'),
|
description: response.error || t('channel_creation_failed'),
|
||||||
});
|
});
|
||||||
}
|
return {
|
||||||
|
status: 'failed',
|
||||||
|
statusMessage: response.error || t('channel_creation_failed'),
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: t('channel_creation_failed'),
|
description: t('channel_creation_failed'),
|
||||||
});
|
});
|
||||||
}
|
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
||||||
|
} finally {
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
|
}
|
||||||
}, [prepareEmailRequest, t, notifications]);
|
}, [prepareEmailRequest, t, notifications]);
|
||||||
|
|
||||||
const prepareMsTeamsRequest = useCallback(
|
const prepareMsTeamsRequest = useCallback(
|
||||||
@@ -370,19 +410,25 @@ function CreateAlertChannels({
|
|||||||
description: t('channel_creation_done'),
|
description: t('channel_creation_done'),
|
||||||
});
|
});
|
||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
} else {
|
return { status: 'success', statusMessage: t('channel_creation_done') };
|
||||||
|
}
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: response.error || t('channel_creation_failed'),
|
description: response.error || t('channel_creation_failed'),
|
||||||
});
|
});
|
||||||
}
|
return {
|
||||||
|
status: 'failed',
|
||||||
|
statusMessage: response.error || t('channel_creation_failed'),
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: t('channel_creation_failed'),
|
description: t('channel_creation_failed'),
|
||||||
});
|
});
|
||||||
}
|
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
||||||
|
} finally {
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
|
}
|
||||||
}, [prepareMsTeamsRequest, t, notifications]);
|
}, [prepareMsTeamsRequest, t, notifications]);
|
||||||
|
|
||||||
const onSaveHandler = useCallback(
|
const onSaveHandler = useCallback(
|
||||||
@@ -400,7 +446,15 @@ function CreateAlertChannels({
|
|||||||
const functionToCall = functionMapper[value as keyof typeof functionMapper];
|
const functionToCall = functionMapper[value as keyof typeof functionMapper];
|
||||||
|
|
||||||
if (functionToCall) {
|
if (functionToCall) {
|
||||||
functionToCall();
|
const result = await functionToCall();
|
||||||
|
logEvent('Alert Channel: Save channel', {
|
||||||
|
type: value,
|
||||||
|
sendResolvedAlert: selectedConfig?.send_resolved,
|
||||||
|
name: selectedConfig?.name,
|
||||||
|
new: 'true',
|
||||||
|
status: result?.status,
|
||||||
|
statusMessage: result?.statusMessage,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
@@ -409,6 +463,7 @@ function CreateAlertChannels({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[
|
[
|
||||||
onSlackHandler,
|
onSlackHandler,
|
||||||
onWebhookHandler,
|
onWebhookHandler,
|
||||||
@@ -472,14 +527,25 @@ function CreateAlertChannels({
|
|||||||
description: t('channel_test_failed'),
|
description: t('channel_test_failed'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logEvent('Alert Channel: Test notification', {
|
||||||
|
type: channelType,
|
||||||
|
sendResolvedAlert: selectedConfig?.send_resolved,
|
||||||
|
name: selectedConfig?.name,
|
||||||
|
new: 'true',
|
||||||
|
status:
|
||||||
|
response && response.statusCode === 200 ? 'Test success' : 'Test failed',
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: t('channel_test_unexpected'),
|
description: t('channel_test_unexpected'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setTestingState(false);
|
setTestingState(false);
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[
|
[
|
||||||
prepareWebhookRequest,
|
prepareWebhookRequest,
|
||||||
t,
|
t,
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { Row, Typography } from 'antd';
|
import { Row, Typography } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
@@ -34,6 +36,13 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logEvent('Alert: Sample alert link clicked', {
|
||||||
|
dataSource: ALERTS_DATA_SOURCE_MAP[option],
|
||||||
|
link: url,
|
||||||
|
page: 'New alert data source selection page',
|
||||||
|
});
|
||||||
|
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
}
|
}
|
||||||
const renderOptions = useMemo(
|
const renderOptions = useMemo(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Form, Row } from 'antd';
|
import { Form, Row } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import FormAlertRules from 'container/FormAlertRules';
|
import FormAlertRules from 'container/FormAlertRules';
|
||||||
@@ -68,6 +69,8 @@ function CreateRules(): JSX.Element {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (alertType) {
|
if (alertType) {
|
||||||
onSelectType(alertType);
|
onSelectType(alertType);
|
||||||
|
} else {
|
||||||
|
logEvent('Alert: New alert data source selection page visited', {});
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [alertType]);
|
}, [alertType]);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import testOpsgenie from 'api/channels/testOpsgenie';
|
|||||||
import testPagerApi from 'api/channels/testPager';
|
import testPagerApi from 'api/channels/testPager';
|
||||||
import testSlackApi from 'api/channels/testSlack';
|
import testSlackApi from 'api/channels/testSlack';
|
||||||
import testWebhookApi from 'api/channels/testWebhook';
|
import testWebhookApi from 'api/channels/testWebhook';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import {
|
import {
|
||||||
ChannelType,
|
ChannelType,
|
||||||
@@ -89,7 +90,7 @@ function EditAlertChannels({
|
|||||||
description: t('webhook_url_required'),
|
description: t('webhook_url_required'),
|
||||||
});
|
});
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
return;
|
return { status: 'failed', statusMessage: t('webhook_url_required') };
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await editSlackApi(prepareSlackRequest());
|
const response = await editSlackApi(prepareSlackRequest());
|
||||||
@@ -101,13 +102,17 @@ function EditAlertChannels({
|
|||||||
});
|
});
|
||||||
|
|
||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
} else {
|
return { status: 'success', statusMessage: t('channel_edit_done') };
|
||||||
|
}
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: response.error || t('channel_edit_failed'),
|
description: response.error || t('channel_edit_failed'),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
|
return {
|
||||||
|
status: 'failed',
|
||||||
|
statusMessage: response.error || t('channel_edit_failed'),
|
||||||
|
};
|
||||||
}, [prepareSlackRequest, t, notifications, selectedConfig]);
|
}, [prepareSlackRequest, t, notifications, selectedConfig]);
|
||||||
|
|
||||||
const prepareWebhookRequest = useCallback(() => {
|
const prepareWebhookRequest = useCallback(() => {
|
||||||
@@ -136,13 +141,13 @@ function EditAlertChannels({
|
|||||||
if (selectedConfig?.api_url === '') {
|
if (selectedConfig?.api_url === '') {
|
||||||
showError(t('webhook_url_required'));
|
showError(t('webhook_url_required'));
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
return;
|
return { status: 'failed', statusMessage: t('webhook_url_required') };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (username && (!password || password === '')) {
|
if (username && (!password || password === '')) {
|
||||||
showError(t('username_no_password'));
|
showError(t('username_no_password'));
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
return;
|
return { status: 'failed', statusMessage: t('username_no_password') };
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await editWebhookApi(prepareWebhookRequest());
|
const response = await editWebhookApi(prepareWebhookRequest());
|
||||||
@@ -154,10 +159,15 @@ function EditAlertChannels({
|
|||||||
});
|
});
|
||||||
|
|
||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
} else {
|
return { status: 'success', statusMessage: t('channel_edit_done') };
|
||||||
showError(response.error || t('channel_edit_failed'));
|
|
||||||
}
|
}
|
||||||
|
showError(response.error || t('channel_edit_failed'));
|
||||||
|
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
|
return {
|
||||||
|
status: 'failed',
|
||||||
|
statusMessage: response.error || t('channel_edit_failed'),
|
||||||
|
};
|
||||||
}, [prepareWebhookRequest, t, notifications, selectedConfig]);
|
}, [prepareWebhookRequest, t, notifications, selectedConfig]);
|
||||||
|
|
||||||
const prepareEmailRequest = useCallback(
|
const prepareEmailRequest = useCallback(
|
||||||
@@ -181,13 +191,18 @@ function EditAlertChannels({
|
|||||||
description: t('channel_edit_done'),
|
description: t('channel_edit_done'),
|
||||||
});
|
});
|
||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
} else {
|
return { status: 'success', statusMessage: t('channel_edit_done') };
|
||||||
|
}
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: response.error || t('channel_edit_failed'),
|
description: response.error || t('channel_edit_failed'),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
|
return {
|
||||||
|
status: 'failed',
|
||||||
|
statusMessage: response.error || t('channel_edit_failed'),
|
||||||
|
};
|
||||||
}, [prepareEmailRequest, t, notifications]);
|
}, [prepareEmailRequest, t, notifications]);
|
||||||
|
|
||||||
const preparePagerRequest = useCallback(
|
const preparePagerRequest = useCallback(
|
||||||
@@ -218,7 +233,7 @@ function EditAlertChannels({
|
|||||||
description: validationError,
|
description: validationError,
|
||||||
});
|
});
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
return;
|
return { status: 'failed', statusMessage: validationError };
|
||||||
}
|
}
|
||||||
const response = await editPagerApi(preparePagerRequest());
|
const response = await editPagerApi(preparePagerRequest());
|
||||||
|
|
||||||
@@ -229,13 +244,18 @@ function EditAlertChannels({
|
|||||||
});
|
});
|
||||||
|
|
||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
} else {
|
return { status: 'success', statusMessage: t('channel_edit_done') };
|
||||||
|
}
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: response.error || t('channel_edit_failed'),
|
description: response.error || t('channel_edit_failed'),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
|
return {
|
||||||
|
status: 'failed',
|
||||||
|
statusMessage: response.error || t('channel_edit_failed'),
|
||||||
|
};
|
||||||
}, [preparePagerRequest, notifications, selectedConfig, t]);
|
}, [preparePagerRequest, notifications, selectedConfig, t]);
|
||||||
|
|
||||||
const prepareOpsgenieRequest = useCallback(
|
const prepareOpsgenieRequest = useCallback(
|
||||||
@@ -259,7 +279,7 @@ function EditAlertChannels({
|
|||||||
description: t('api_key_required'),
|
description: t('api_key_required'),
|
||||||
});
|
});
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
return;
|
return { status: 'failed', statusMessage: t('api_key_required') };
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await editOpsgenie(prepareOpsgenieRequest());
|
const response = await editOpsgenie(prepareOpsgenieRequest());
|
||||||
@@ -271,13 +291,18 @@ function EditAlertChannels({
|
|||||||
});
|
});
|
||||||
|
|
||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
} else {
|
return { status: 'success', statusMessage: t('channel_edit_done') };
|
||||||
|
}
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: response.error || t('channel_edit_failed'),
|
description: response.error || t('channel_edit_failed'),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
|
return {
|
||||||
|
status: 'failed',
|
||||||
|
statusMessage: response.error || t('channel_edit_failed'),
|
||||||
|
};
|
||||||
}, [prepareOpsgenieRequest, t, notifications, selectedConfig]);
|
}, [prepareOpsgenieRequest, t, notifications, selectedConfig]);
|
||||||
|
|
||||||
const prepareMsTeamsRequest = useCallback(
|
const prepareMsTeamsRequest = useCallback(
|
||||||
@@ -301,7 +326,7 @@ function EditAlertChannels({
|
|||||||
description: t('webhook_url_required'),
|
description: t('webhook_url_required'),
|
||||||
});
|
});
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
return;
|
return { status: 'failed', statusMessage: t('webhook_url_required') };
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await editMsTeamsApi(prepareMsTeamsRequest());
|
const response = await editMsTeamsApi(prepareMsTeamsRequest());
|
||||||
@@ -313,31 +338,46 @@ function EditAlertChannels({
|
|||||||
});
|
});
|
||||||
|
|
||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
} else {
|
return { status: 'success', statusMessage: t('channel_edit_done') };
|
||||||
|
}
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: response.error || t('channel_edit_failed'),
|
description: response.error || t('channel_edit_failed'),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
|
return {
|
||||||
|
status: 'failed',
|
||||||
|
statusMessage: response.error || t('channel_edit_failed'),
|
||||||
|
};
|
||||||
}, [prepareMsTeamsRequest, t, notifications, selectedConfig]);
|
}, [prepareMsTeamsRequest, t, notifications, selectedConfig]);
|
||||||
|
|
||||||
const onSaveHandler = useCallback(
|
const onSaveHandler = useCallback(
|
||||||
(value: ChannelType) => {
|
async (value: ChannelType) => {
|
||||||
|
let result;
|
||||||
if (value === ChannelType.Slack) {
|
if (value === ChannelType.Slack) {
|
||||||
onSlackEditHandler();
|
result = await onSlackEditHandler();
|
||||||
} else if (value === ChannelType.Webhook) {
|
} else if (value === ChannelType.Webhook) {
|
||||||
onWebhookEditHandler();
|
result = await onWebhookEditHandler();
|
||||||
} else if (value === ChannelType.Pagerduty) {
|
} else if (value === ChannelType.Pagerduty) {
|
||||||
onPagerEditHandler();
|
result = await onPagerEditHandler();
|
||||||
} else if (value === ChannelType.MsTeams) {
|
} else if (value === ChannelType.MsTeams) {
|
||||||
onMsTeamsEditHandler();
|
result = await onMsTeamsEditHandler();
|
||||||
} else if (value === ChannelType.Opsgenie) {
|
} else if (value === ChannelType.Opsgenie) {
|
||||||
onOpsgenieEditHandler();
|
result = await onOpsgenieEditHandler();
|
||||||
} else if (value === ChannelType.Email) {
|
} else if (value === ChannelType.Email) {
|
||||||
onEmailEditHandler();
|
result = await onEmailEditHandler();
|
||||||
}
|
}
|
||||||
|
logEvent('Alert Channel: Save channel', {
|
||||||
|
type: value,
|
||||||
|
sendResolvedAlert: selectedConfig?.send_resolved,
|
||||||
|
name: selectedConfig?.name,
|
||||||
|
new: 'false',
|
||||||
|
status: result?.status,
|
||||||
|
statusMessage: result?.statusMessage,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[
|
[
|
||||||
onSlackEditHandler,
|
onSlackEditHandler,
|
||||||
onWebhookEditHandler,
|
onWebhookEditHandler,
|
||||||
@@ -399,6 +439,14 @@ function EditAlertChannels({
|
|||||||
description: t('channel_test_failed'),
|
description: t('channel_test_failed'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
logEvent('Alert Channel: Test notification', {
|
||||||
|
type: channelType,
|
||||||
|
sendResolvedAlert: selectedConfig?.send_resolved,
|
||||||
|
name: selectedConfig?.name,
|
||||||
|
new: 'false',
|
||||||
|
status:
|
||||||
|
response && response.statusCode === 200 ? 'Test success' : 'Test failed',
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
@@ -407,6 +455,7 @@ function EditAlertChannels({
|
|||||||
}
|
}
|
||||||
setTestingState(false);
|
setTestingState(false);
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[
|
[
|
||||||
t,
|
t,
|
||||||
prepareWebhookRequest,
|
prepareWebhookRequest,
|
||||||
|
|||||||
@@ -1,8 +1,34 @@
|
|||||||
import './EmptyLogsSearch.styles.scss';
|
import './EmptyLogsSearch.styles.scss';
|
||||||
|
|
||||||
import { Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { DataSource, PanelTypeKeys } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
export default function EmptyLogsSearch({
|
||||||
|
dataSource,
|
||||||
|
panelType,
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
panelType: PanelTypeKeys;
|
||||||
|
}): JSX.Element {
|
||||||
|
const logEventCalledRef = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!logEventCalledRef.current) {
|
||||||
|
if (dataSource === DataSource.TRACES) {
|
||||||
|
logEvent('Traces Explorer: No results', {
|
||||||
|
panelType,
|
||||||
|
});
|
||||||
|
} else if (dataSource === DataSource.LOGS) {
|
||||||
|
logEvent('Logs Explorer: No results', {
|
||||||
|
panelType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
logEventCalledRef.current = true;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
export default function EmptyLogsSearch(): JSX.Element {
|
|
||||||
return (
|
return (
|
||||||
<div className="empty-logs-search-container">
|
<div className="empty-logs-search-container">
|
||||||
<div className="empty-logs-search-container-content">
|
<div className="empty-logs-search-container-content">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import './styles.scss';
|
import './styles.scss';
|
||||||
|
|
||||||
import { Button, Divider, Space, Typography } from 'antd';
|
import { Button, Divider, Space, Typography } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import getNextPrevId from 'api/errors/getNextPrevId';
|
import getNextPrevId from 'api/errors/getNextPrevId';
|
||||||
import Editor from 'components/Editor';
|
import Editor from 'components/Editor';
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
@@ -9,8 +10,9 @@ import dayjs from 'dayjs';
|
|||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import createQueryParams from 'lib/createQueryParams';
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
|
import { isUndefined } from 'lodash-es';
|
||||||
import { urlKey } from 'pages/ErrorDetails/utils';
|
import { urlKey } from 'pages/ErrorDetails/utils';
|
||||||
import { useMemo, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
@@ -111,9 +113,29 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const onClickTraceHandler = (): void => {
|
const onClickTraceHandler = (): void => {
|
||||||
|
logEvent('Exception: Navigate to trace detail page', {
|
||||||
|
groupId: errorDetail?.groupID,
|
||||||
|
spanId: errorDetail.spanID,
|
||||||
|
traceId: errorDetail.traceID,
|
||||||
|
exceptionId: errorDetail?.errorId,
|
||||||
|
});
|
||||||
history.push(`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`);
|
history.push(`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const logEventCalledRef = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!logEventCalledRef.current && !isUndefined(data)) {
|
||||||
|
logEvent('Exception: Detail page visited', {
|
||||||
|
groupId: errorDetail?.groupID,
|
||||||
|
spanId: errorDetail.spanID,
|
||||||
|
traceId: errorDetail.traceID,
|
||||||
|
exceptionId: errorDetail?.errorId,
|
||||||
|
});
|
||||||
|
logEventCalledRef.current = true;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Typography>{errorDetail.exceptionType}</Typography>
|
<Typography>{errorDetail.exceptionType}</Typography>
|
||||||
|
|||||||
@@ -91,8 +91,7 @@
|
|||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
|
|
||||||
&.ant-btn-round {
|
&.ant-btn-round {
|
||||||
padding-inline-start: 10px;
|
padding: 8px 12px 8px 10px;
|
||||||
padding-inline-end: 8px;
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
|
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
|
||||||
@@ -93,7 +94,23 @@ function ExplorerOptions({
|
|||||||
setIsExport(value);
|
setIsExport(value);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const {
|
||||||
|
currentQuery,
|
||||||
|
panelType,
|
||||||
|
isStagedQueryUpdated,
|
||||||
|
redirectWithQueryBuilderData,
|
||||||
|
} = useQueryBuilder();
|
||||||
|
|
||||||
const handleSaveViewModalToggle = (): void => {
|
const handleSaveViewModalToggle = (): void => {
|
||||||
|
if (sourcepage === DataSource.TRACES) {
|
||||||
|
logEvent('Traces Explorer: Save view clicked', {
|
||||||
|
panelType,
|
||||||
|
});
|
||||||
|
} else if (sourcepage === DataSource.LOGS) {
|
||||||
|
logEvent('Logs Explorer: Save view clicked', {
|
||||||
|
panelType,
|
||||||
|
});
|
||||||
|
}
|
||||||
setIsSaveModalOpen(!isSaveModalOpen);
|
setIsSaveModalOpen(!isSaveModalOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,11 +121,21 @@ function ExplorerOptions({
|
|||||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
const onCreateAlertsHandler = useCallback(() => {
|
const onCreateAlertsHandler = useCallback(() => {
|
||||||
|
if (sourcepage === DataSource.TRACES) {
|
||||||
|
logEvent('Traces Explorer: Create alert', {
|
||||||
|
panelType,
|
||||||
|
});
|
||||||
|
} else if (sourcepage === DataSource.LOGS) {
|
||||||
|
logEvent('Logs Explorer: Create alert', {
|
||||||
|
panelType,
|
||||||
|
});
|
||||||
|
}
|
||||||
history.push(
|
history.push(
|
||||||
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
|
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
|
||||||
JSON.stringify(query),
|
JSON.stringify(query),
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [history, query]);
|
}, [history, query]);
|
||||||
|
|
||||||
const onCancel = (value: boolean) => (): void => {
|
const onCancel = (value: boolean) => (): void => {
|
||||||
@@ -116,6 +143,15 @@ function ExplorerOptions({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onAddToDashboard = (): void => {
|
const onAddToDashboard = (): void => {
|
||||||
|
if (sourcepage === DataSource.TRACES) {
|
||||||
|
logEvent('Traces Explorer: Add to dashboard clicked', {
|
||||||
|
panelType,
|
||||||
|
});
|
||||||
|
} else if (sourcepage === DataSource.LOGS) {
|
||||||
|
logEvent('Logs Explorer: Add to dashboard clicked', {
|
||||||
|
panelType,
|
||||||
|
});
|
||||||
|
}
|
||||||
setIsExport(true);
|
setIsExport(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -127,13 +163,6 @@ function ExplorerOptions({
|
|||||||
refetch: refetchAllView,
|
refetch: refetchAllView,
|
||||||
} = useGetAllViews(sourcepage);
|
} = useGetAllViews(sourcepage);
|
||||||
|
|
||||||
const {
|
|
||||||
currentQuery,
|
|
||||||
panelType,
|
|
||||||
isStagedQueryUpdated,
|
|
||||||
redirectWithQueryBuilderData,
|
|
||||||
} = useQueryBuilder();
|
|
||||||
|
|
||||||
const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType);
|
const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType);
|
||||||
|
|
||||||
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
|
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
|
||||||
@@ -224,6 +253,17 @@ function ExplorerOptions({
|
|||||||
onMenuItemSelectHandler({
|
onMenuItemSelectHandler({
|
||||||
key: option.key,
|
key: option.key,
|
||||||
});
|
});
|
||||||
|
if (sourcepage === DataSource.TRACES) {
|
||||||
|
logEvent('Traces Explorer: Select view', {
|
||||||
|
panelType,
|
||||||
|
viewName: option?.value,
|
||||||
|
});
|
||||||
|
} else if (sourcepage === DataSource.LOGS) {
|
||||||
|
logEvent('Logs Explorer: Select view', {
|
||||||
|
panelType,
|
||||||
|
viewName: option?.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
ref.current.blur();
|
ref.current.blur();
|
||||||
}
|
}
|
||||||
@@ -259,6 +299,17 @@ function ExplorerOptions({
|
|||||||
viewName: newViewName,
|
viewName: newViewName,
|
||||||
setNewViewName,
|
setNewViewName,
|
||||||
});
|
});
|
||||||
|
if (sourcepage === DataSource.TRACES) {
|
||||||
|
logEvent('Traces Explorer: Save view successful', {
|
||||||
|
panelType,
|
||||||
|
viewName: newViewName,
|
||||||
|
});
|
||||||
|
} else if (sourcepage === DataSource.LOGS) {
|
||||||
|
logEvent('Logs Explorer: Save view successful', {
|
||||||
|
panelType,
|
||||||
|
viewName: newViewName,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Remove this and move this to scss file
|
// TODO: Remove this and move this to scss file
|
||||||
@@ -499,7 +550,7 @@ function ExplorerOptions({
|
|||||||
|
|
||||||
export interface ExplorerOptionsProps {
|
export interface ExplorerOptionsProps {
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
onExport: (dashboard: Dashboard | null) => void;
|
onExport: (dashboard: Dashboard | null, isNewDashboard?: boolean) => void;
|
||||||
query: Query | null;
|
query: Query | null;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
sourcepage: DataSource;
|
sourcepage: DataSource;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Button, Typography } from 'antd';
|
import { Button, Typography } from 'antd';
|
||||||
import createDashboard from 'api/dashboard/create';
|
import createDashboard from 'api/dashboard/create';
|
||||||
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
||||||
import useAxiosError from 'hooks/useAxiosError';
|
import useAxiosError from 'hooks/useAxiosError';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
@@ -40,7 +41,7 @@ function ExportPanelContainer({
|
|||||||
} = useMutation(createDashboard, {
|
} = useMutation(createDashboard, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
if (data.payload) {
|
if (data.payload) {
|
||||||
onExport(data?.payload);
|
onExport(data?.payload, true);
|
||||||
}
|
}
|
||||||
refetch();
|
refetch();
|
||||||
},
|
},
|
||||||
@@ -54,7 +55,7 @@ function ExportPanelContainer({
|
|||||||
({ uuid }) => uuid === selectedDashboardId,
|
({ uuid }) => uuid === selectedDashboardId,
|
||||||
);
|
);
|
||||||
|
|
||||||
onExport(currentSelectedDashboard || null);
|
onExport(currentSelectedDashboard || null, false);
|
||||||
}, [data, selectedDashboardId, onExport]);
|
}, [data, selectedDashboardId, onExport]);
|
||||||
|
|
||||||
const handleSelect = useCallback(
|
const handleSelect = useCallback(
|
||||||
@@ -70,6 +71,7 @@ function ExportPanelContainer({
|
|||||||
ns: 'dashboard',
|
ns: 'dashboard',
|
||||||
}),
|
}),
|
||||||
uploadedGrafana: false,
|
uploadedGrafana: false,
|
||||||
|
version: ENTITY_VERSION_V4,
|
||||||
});
|
});
|
||||||
}, [t, createNewDashboard]);
|
}, [t, createNewDashboard]);
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ function ExportPanel({
|
|||||||
|
|
||||||
export interface ExportPanelProps {
|
export interface ExportPanelProps {
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
onExport: (dashboard: Dashboard | null) => void;
|
onExport: (dashboard: Dashboard | null, isNewDashboard?: boolean) => void;
|
||||||
query: Query | null;
|
query: Query | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import './FormAlertRules.styles.scss';
|
|||||||
import { PlusOutlined } from '@ant-design/icons';
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
import { Button, Form, Select, Switch, Tooltip } from 'antd';
|
import { Button, Form, Select, Switch, Tooltip } from 'antd';
|
||||||
import getChannels from 'api/channels/getAll';
|
import getChannels from 'api/channels/getAll';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import useFetch from 'hooks/useFetch';
|
import useFetch from 'hooks/useFetch';
|
||||||
@@ -10,6 +12,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
import { AlertDef, Labels } from 'types/api/alerts/def';
|
import { AlertDef, Labels } from 'types/api/alerts/def';
|
||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
|
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
|
||||||
@@ -73,9 +76,24 @@ function BasicInfo({
|
|||||||
|
|
||||||
const noChannels = channels.payload?.length === 0;
|
const noChannels = channels.payload?.length === 0;
|
||||||
const handleCreateNewChannels = useCallback(() => {
|
const handleCreateNewChannels = useCallback(() => {
|
||||||
|
logEvent('Alert: Create notification channel button clicked', {
|
||||||
|
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
|
||||||
|
ruleId: isNewRule ? 0 : alertDef?.id,
|
||||||
|
});
|
||||||
window.open(ROUTES.CHANNELS_NEW, '_blank');
|
window.open(ROUTES.CHANNELS_NEW, '_blank');
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!channels.loading && isNewRule) {
|
||||||
|
logEvent('Alert: New alert creation page visited', {
|
||||||
|
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
|
||||||
|
numberOfChannels: channels?.payload?.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [channels.payload, channels.loading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StepHeading> {t('alert_form_step3')} </StepHeading>
|
<StepHeading> {t('alert_form_step3')} </StepHeading>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export interface ChartPreviewProps {
|
|||||||
userQueryKey?: string;
|
userQueryKey?: string;
|
||||||
allowSelectedIntervalForStepGen?: boolean;
|
allowSelectedIntervalForStepGen?: boolean;
|
||||||
yAxisUnit: string;
|
yAxisUnit: string;
|
||||||
|
setQueryStatus?: (status: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
@@ -62,6 +63,7 @@ function ChartPreview({
|
|||||||
allowSelectedIntervalForStepGen = false,
|
allowSelectedIntervalForStepGen = false,
|
||||||
alertDef,
|
alertDef,
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
|
setQueryStatus,
|
||||||
}: ChartPreviewProps): JSX.Element | null {
|
}: ChartPreviewProps): JSX.Element | null {
|
||||||
const { t } = useTranslation('alerts');
|
const { t } = useTranslation('alerts');
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -149,10 +151,10 @@ function ChartPreview({
|
|||||||
|
|
||||||
useEffect((): void => {
|
useEffect((): void => {
|
||||||
const { startTime, endTime } = getTimeRange(queryResponse);
|
const { startTime, endTime } = getTimeRange(queryResponse);
|
||||||
|
if (setQueryStatus) setQueryStatus(queryResponse.status);
|
||||||
setMinTimeScale(startTime);
|
setMinTimeScale(startTime);
|
||||||
setMaxTimeScale(endTime);
|
setMaxTimeScale(endTime);
|
||||||
}, [maxTime, minTime, globalSelectedInterval, queryResponse]);
|
}, [maxTime, minTime, globalSelectedInterval, queryResponse, setQueryStatus]);
|
||||||
|
|
||||||
if (queryResponse.data && graphType === PANEL_TYPES.BAR) {
|
if (queryResponse.data && graphType === PANEL_TYPES.BAR) {
|
||||||
const sortedSeriesData = getSortedSeriesData(
|
const sortedSeriesData = getSortedSeriesData(
|
||||||
@@ -246,17 +248,19 @@ function ChartPreview({
|
|||||||
return (
|
return (
|
||||||
<ChartContainer>
|
<ChartContainer>
|
||||||
{headline}
|
{headline}
|
||||||
|
|
||||||
|
<div ref={graphRef} style={{ height: '100%' }}>
|
||||||
|
{queryResponse.isLoading && (
|
||||||
|
<Spinner size="large" tip="Loading..." height="100%" />
|
||||||
|
)}
|
||||||
{(queryResponse?.isError || queryResponse?.error) && (
|
{(queryResponse?.isError || queryResponse?.error) && (
|
||||||
<FailedMessageContainer color="red" title="Failed to refresh the chart">
|
<FailedMessageContainer color="red" title="Failed to refresh the chart">
|
||||||
<InfoCircleOutlined />{' '}
|
<InfoCircleOutlined />{' '}
|
||||||
{queryResponse.error.message || t('preview_chart_unexpected_error')}
|
{queryResponse.error.message || t('preview_chart_unexpected_error')}
|
||||||
</FailedMessageContainer>
|
</FailedMessageContainer>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{chartData && !queryResponse.isError && (
|
{chartData && !queryResponse.isError && (
|
||||||
<div ref={graphRef} style={{ height: '100%' }}>
|
|
||||||
{queryResponse.isLoading && (
|
|
||||||
<Spinner size="large" tip="Loading..." height="100%" />
|
|
||||||
)}
|
|
||||||
<GridPanelSwitch
|
<GridPanelSwitch
|
||||||
options={options}
|
options={options}
|
||||||
panelType={graphType}
|
panelType={graphType}
|
||||||
@@ -268,8 +272,8 @@ function ChartPreview({
|
|||||||
query={query || initialQueriesMap.metrics}
|
query={query || initialQueriesMap.metrics}
|
||||||
yAxisUnit={yAxisUnit}
|
yAxisUnit={yAxisUnit}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -282,6 +286,7 @@ ChartPreview.defaultProps = {
|
|||||||
userQueryKey: '',
|
userQueryKey: '',
|
||||||
allowSelectedIntervalForStepGen: false,
|
allowSelectedIntervalForStepGen: false,
|
||||||
alertDef: undefined,
|
alertDef: undefined,
|
||||||
|
setQueryStatus: (): void => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ChartPreview;
|
export default ChartPreview;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import './QuerySection.styles.scss';
|
|||||||
|
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Button, Tabs, Tooltip } from 'antd';
|
import { Button, Tabs, Tooltip } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import PromQLIcon from 'assets/Dashboard/PromQl';
|
import PromQLIcon from 'assets/Dashboard/PromQl';
|
||||||
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
@@ -31,6 +32,7 @@ function QuerySection({
|
|||||||
runQuery,
|
runQuery,
|
||||||
alertDef,
|
alertDef,
|
||||||
panelType,
|
panelType,
|
||||||
|
ruleId,
|
||||||
}: QuerySectionProps): JSX.Element {
|
}: QuerySectionProps): JSX.Element {
|
||||||
// init namespace for translations
|
// init namespace for translations
|
||||||
const { t } = useTranslation('alerts');
|
const { t } = useTranslation('alerts');
|
||||||
@@ -158,7 +160,15 @@ function QuerySection({
|
|||||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={runQuery}
|
onClick={(): void => {
|
||||||
|
runQuery();
|
||||||
|
logEvent('Alert: Stage and run query', {
|
||||||
|
dataSource: ALERTS_DATA_SOURCE_MAP[alertType],
|
||||||
|
isNewRule: !ruleId || ruleId === 0,
|
||||||
|
ruleId,
|
||||||
|
queryType: queryCategory,
|
||||||
|
});
|
||||||
|
}}
|
||||||
className="stage-run-query"
|
className="stage-run-query"
|
||||||
icon={<Play size={14} />}
|
icon={<Play size={14} />}
|
||||||
>
|
>
|
||||||
@@ -228,6 +238,7 @@ interface QuerySectionProps {
|
|||||||
runQuery: VoidFunction;
|
runQuery: VoidFunction;
|
||||||
alertDef: AlertDef;
|
alertDef: AlertDef;
|
||||||
panelType: PANEL_TYPES;
|
panelType: PANEL_TYPES;
|
||||||
|
ruleId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default QuerySection;
|
export default QuerySection;
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ import {
|
|||||||
} from 'antd';
|
} from 'antd';
|
||||||
import saveAlertApi from 'api/alerts/save';
|
import saveAlertApi from 'api/alerts/save';
|
||||||
import testAlertApi from 'api/alerts/testAlert';
|
import testAlertApi from 'api/alerts/testAlert';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||||
import { alertHelpMessage } from 'components/facingIssueBtn/util';
|
import { alertHelpMessage } from 'components/facingIssueBtn/util';
|
||||||
|
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
@@ -99,6 +101,7 @@ function FormAlertRules({
|
|||||||
const isNewRule = ruleId === 0;
|
const isNewRule = ruleId === 0;
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [queryStatus, setQueryStatus] = useState<string>('');
|
||||||
|
|
||||||
// alertDef holds the form values to be posted
|
// alertDef holds the form values to be posted
|
||||||
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
|
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
|
||||||
@@ -338,8 +341,13 @@ function FormAlertRules({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const postableAlert = memoizedPreparePostData();
|
const postableAlert = memoizedPreparePostData();
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
|
let logData = {
|
||||||
|
status: 'error',
|
||||||
|
statusMessage: t('unexpected_error'),
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const apiReq =
|
const apiReq =
|
||||||
ruleId && ruleId > 0
|
ruleId && ruleId > 0
|
||||||
@@ -349,10 +357,15 @@ function FormAlertRules({
|
|||||||
const response = await saveAlertApi(apiReq);
|
const response = await saveAlertApi(apiReq);
|
||||||
|
|
||||||
if (response.statusCode === 200) {
|
if (response.statusCode === 200) {
|
||||||
|
logData = {
|
||||||
|
status: 'success',
|
||||||
|
statusMessage:
|
||||||
|
!ruleId || ruleId === 0 ? t('rule_created') : t('rule_edited'),
|
||||||
|
};
|
||||||
|
|
||||||
notifications.success({
|
notifications.success({
|
||||||
message: 'Success',
|
message: 'Success',
|
||||||
description:
|
description: logData.statusMessage,
|
||||||
!ruleId || ruleId === 0 ? t('rule_created') : t('rule_edited'),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// invalidate rule in cache
|
// invalidate rule in cache
|
||||||
@@ -367,18 +380,42 @@ function FormAlertRules({
|
|||||||
history.replace(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
|
history.replace(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} else {
|
} else {
|
||||||
|
logData = {
|
||||||
|
status: 'error',
|
||||||
|
statusMessage: response.error || t('unexpected_error'),
|
||||||
|
};
|
||||||
|
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: response.error || t('unexpected_error'),
|
description: logData.statusMessage,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logData = {
|
||||||
|
status: 'error',
|
||||||
|
statusMessage: t('unexpected_error'),
|
||||||
|
};
|
||||||
|
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: t('unexpected_error'),
|
description: logData.statusMessage,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
|
logEvent('Alert: Save alert', {
|
||||||
|
...logData,
|
||||||
|
dataSource: ALERTS_DATA_SOURCE_MAP[postableAlert?.alertType as AlertTypes],
|
||||||
|
channelNames: postableAlert?.preferredChannels,
|
||||||
|
broadcastToAll: postableAlert?.broadcastToAll,
|
||||||
|
isNewRule: !ruleId || ruleId === 0,
|
||||||
|
ruleId,
|
||||||
|
queryType: currentQuery.queryType,
|
||||||
|
alertId: postableAlert?.id,
|
||||||
|
alertName: postableAlert?.alert,
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [
|
}, [
|
||||||
isFormValid,
|
isFormValid,
|
||||||
memoizedPreparePostData,
|
memoizedPreparePostData,
|
||||||
@@ -414,6 +451,7 @@ function FormAlertRules({
|
|||||||
}
|
}
|
||||||
const postableAlert = memoizedPreparePostData();
|
const postableAlert = memoizedPreparePostData();
|
||||||
|
|
||||||
|
let statusResponse = { status: 'failed', message: '' };
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await testAlertApi({ data: postableAlert });
|
const response = await testAlertApi({ data: postableAlert });
|
||||||
@@ -425,25 +463,43 @@ function FormAlertRules({
|
|||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: t('no_alerts_found'),
|
description: t('no_alerts_found'),
|
||||||
});
|
});
|
||||||
|
statusResponse = { status: 'failed', message: t('no_alerts_found') };
|
||||||
} else {
|
} else {
|
||||||
notifications.success({
|
notifications.success({
|
||||||
message: 'Success',
|
message: 'Success',
|
||||||
description: t('rule_test_fired'),
|
description: t('rule_test_fired'),
|
||||||
});
|
});
|
||||||
|
statusResponse = { status: 'success', message: t('rule_test_fired') };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: response.error || t('unexpected_error'),
|
description: response.error || t('unexpected_error'),
|
||||||
});
|
});
|
||||||
|
statusResponse = {
|
||||||
|
status: 'failed',
|
||||||
|
message: response.error || t('unexpected_error'),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: t('unexpected_error'),
|
description: t('unexpected_error'),
|
||||||
});
|
});
|
||||||
|
statusResponse = { status: 'failed', message: t('unexpected_error') };
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
logEvent('Alert: Test notification', {
|
||||||
|
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
|
||||||
|
channelNames: postableAlert?.preferredChannels,
|
||||||
|
broadcastToAll: postableAlert?.broadcastToAll,
|
||||||
|
isNewRule: !ruleId || ruleId === 0,
|
||||||
|
ruleId,
|
||||||
|
queryType: currentQuery.queryType,
|
||||||
|
status: statusResponse.status,
|
||||||
|
statusMessage: statusResponse.message,
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [t, isFormValid, memoizedPreparePostData, notifications]);
|
}, [t, isFormValid, memoizedPreparePostData, notifications]);
|
||||||
|
|
||||||
const renderBasicInfo = (): JSX.Element => (
|
const renderBasicInfo = (): JSX.Element => (
|
||||||
@@ -468,6 +524,7 @@ function FormAlertRules({
|
|||||||
alertDef={alertDef}
|
alertDef={alertDef}
|
||||||
yAxisUnit={yAxisUnit || ''}
|
yAxisUnit={yAxisUnit || ''}
|
||||||
graphType={panelType || PANEL_TYPES.TIME_SERIES}
|
graphType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||||
|
setQueryStatus={setQueryStatus}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -485,6 +542,7 @@ function FormAlertRules({
|
|||||||
selectedInterval={globalSelectedInterval}
|
selectedInterval={globalSelectedInterval}
|
||||||
yAxisUnit={yAxisUnit || ''}
|
yAxisUnit={yAxisUnit || ''}
|
||||||
graphType={panelType || PANEL_TYPES.TIME_SERIES}
|
graphType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||||
|
setQueryStatus={setQueryStatus}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -513,6 +571,16 @@ function FormAlertRules({
|
|||||||
|
|
||||||
const isRuleCreated = !ruleId || ruleId === 0;
|
const isRuleCreated = !ruleId || ruleId === 0;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isRuleCreated) {
|
||||||
|
logEvent('Alert: Edit page visited', {
|
||||||
|
ruleId,
|
||||||
|
dataSource: ALERTS_DATA_SOURCE_MAP[alertType as AlertTypes],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
function handleRedirection(option: AlertTypes): void {
|
function handleRedirection(option: AlertTypes): void {
|
||||||
let url = '';
|
let url = '';
|
||||||
switch (option) {
|
switch (option) {
|
||||||
@@ -535,6 +603,13 @@ function FormAlertRules({
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
logEvent('Alert: Check example alert clicked', {
|
||||||
|
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
|
||||||
|
isNewRule: !ruleId || ruleId === 0,
|
||||||
|
ruleId,
|
||||||
|
queryType: currentQuery.queryType,
|
||||||
|
link: url,
|
||||||
|
});
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,6 +647,7 @@ function FormAlertRules({
|
|||||||
alertDef={alertDef}
|
alertDef={alertDef}
|
||||||
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||||
key={currentQuery.queryType}
|
key={currentQuery.queryType}
|
||||||
|
ruleId={ruleId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RuleOptions
|
<RuleOptions
|
||||||
@@ -592,7 +668,8 @@ function FormAlertRules({
|
|||||||
disabled={
|
disabled={
|
||||||
isAlertNameMissing ||
|
isAlertNameMissing ||
|
||||||
isAlertAvailableToSave ||
|
isAlertAvailableToSave ||
|
||||||
!isChannelConfigurationValid
|
!isChannelConfigurationValid ||
|
||||||
|
queryStatus === 'error'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isNewRule ? t('button_createrule') : t('button_savechanges')}
|
{isNewRule ? t('button_createrule') : t('button_savechanges')}
|
||||||
@@ -601,7 +678,11 @@ function FormAlertRules({
|
|||||||
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
loading={loading || false}
|
loading={loading || false}
|
||||||
disabled={isAlertNameMissing || !isChannelConfigurationValid}
|
disabled={
|
||||||
|
isAlertNameMissing ||
|
||||||
|
!isChannelConfigurationValid ||
|
||||||
|
queryStatus === 'error'
|
||||||
|
}
|
||||||
type="default"
|
type="default"
|
||||||
onClick={onTestRuleHandler}
|
onClick={onTestRuleHandler}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -124,6 +124,9 @@ const getSpanWithoutChildren = (
|
|||||||
value: span.value,
|
value: span.value,
|
||||||
event: span.event,
|
event: span.event,
|
||||||
hasError: span.hasError,
|
hasError: span.hasError,
|
||||||
|
spanKind: span.spanKind,
|
||||||
|
statusCodeString: span.statusCodeString,
|
||||||
|
statusMessage: span.statusMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const isSpanPresentInSearchString = (
|
export const isSpanPresentInSearchString = (
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import './DashboardEmptyState.styles.scss';
|
|||||||
|
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
import { Button, Typography } from 'antd';
|
import { Button, Typography } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import SettingsDrawer from 'container/NewDashboard/DashboardDescription/SettingsDrawer';
|
import SettingsDrawer from 'container/NewDashboard/DashboardDescription/SettingsDrawer';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
@@ -36,6 +37,12 @@ export default function DashboardEmptyState(): JSX.Element {
|
|||||||
|
|
||||||
const onEmptyWidgetHandler = useCallback(() => {
|
const onEmptyWidgetHandler = useCallback(() => {
|
||||||
handleToggleDashboardSlider(true);
|
handleToggleDashboardSlider(true);
|
||||||
|
logEvent('Dashboard Detail: Add new panel clicked', {
|
||||||
|
dashboardId: selectedDashboard?.uuid,
|
||||||
|
dashboardName: selectedDashboard?.data.title,
|
||||||
|
numberOfPanels: selectedDashboard?.data.widgets?.length,
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [handleToggleDashboardSlider]);
|
}, [handleToggleDashboardSlider]);
|
||||||
return (
|
return (
|
||||||
<section className="dashboard-empty-state">
|
<section className="dashboard-empty-state">
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ function FullView({
|
|||||||
query: updatedQuery,
|
query: updatedQuery,
|
||||||
globalSelectedInterval: globalSelectedTime,
|
globalSelectedInterval: globalSelectedTime,
|
||||||
variables: getDashboardVariables(selectedDashboard?.data.variables),
|
variables: getDashboardVariables(selectedDashboard?.data.variables),
|
||||||
|
fillGaps: widget.fillSpans,
|
||||||
|
formatForWeb: widget.panelTypes === PANEL_TYPES.TABLE,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
updatedQuery.builder.queryData[0].pageSize = 10;
|
updatedQuery.builder.queryData[0].pageSize = 10;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { memo, useEffect, useRef, useState } from 'react';
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { UpdateTimeInterval } from 'store/actions';
|
import { UpdateTimeInterval } from 'store/actions';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
import { getGraphType } from 'utils/getGraphType';
|
import { getGraphType } from 'utils/getGraphType';
|
||||||
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
||||||
@@ -109,9 +110,11 @@ function GridCardGraph({
|
|||||||
globalSelectedInterval,
|
globalSelectedInterval,
|
||||||
variables: getDashboardVariables(variables),
|
variables: getDashboardVariables(variables),
|
||||||
fillGaps: widget.fillSpans,
|
fillGaps: widget.fillSpans,
|
||||||
|
formatForWeb: widget.panelTypes === PANEL_TYPES.TABLE,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
updatedQuery.builder.queryData[0].pageSize = 10;
|
updatedQuery.builder.queryData[0].pageSize = 10;
|
||||||
|
const initialDataSource = updatedQuery.builder.queryData[0].dataSource;
|
||||||
return {
|
return {
|
||||||
query: updatedQuery,
|
query: updatedQuery,
|
||||||
graphType: PANEL_TYPES.LIST,
|
graphType: PANEL_TYPES.LIST,
|
||||||
@@ -122,6 +125,10 @@ function GridCardGraph({
|
|||||||
offset: 0,
|
offset: 0,
|
||||||
limit: updatedQuery.builder.queryData[0].limit || 0,
|
limit: updatedQuery.builder.queryData[0].limit || 0,
|
||||||
},
|
},
|
||||||
|
selectColumns:
|
||||||
|
initialDataSource === DataSource.LOGS
|
||||||
|
? widget.selectedLogFields
|
||||||
|
: widget.selectedTracesFields,
|
||||||
},
|
},
|
||||||
fillGaps: widget.fillSpans,
|
fillGaps: widget.fillSpans,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
.fullscreen-grid-container {
|
.fullscreen-grid-container {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
margin: 8px -8px;
|
margin: 8px -8px;
|
||||||
|
margin-right: 0;
|
||||||
|
|
||||||
.react-grid-layout {
|
.react-grid-layout {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
@@ -49,7 +50,7 @@
|
|||||||
.footer {
|
.footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: absolute;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: -webkit-fill-available;
|
width: -webkit-fill-available;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import './GridCardLayout.styles.scss';
|
|||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Button, Form, Input, Modal, Typography } from 'antd';
|
import { Button, Form, Input, Modal, Typography } from 'antd';
|
||||||
import { useForm } from 'antd/es/form/Form';
|
import { useForm } from 'antd/es/form/Form';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
@@ -15,7 +16,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
|
|||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { defaultTo } from 'lodash-es';
|
import { defaultTo, isUndefined } from 'lodash-es';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
import {
|
import {
|
||||||
Check,
|
Check,
|
||||||
@@ -27,7 +28,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import { sortLayout } from 'providers/Dashboard/util';
|
import { sortLayout } from 'providers/Dashboard/util';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { FullScreen, FullScreenHandle } from 'react-full-screen';
|
import { FullScreen, FullScreenHandle } from 'react-full-screen';
|
||||||
import { ItemCallback, Layout } from 'react-grid-layout';
|
import { ItemCallback, Layout } from 'react-grid-layout';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
@@ -126,6 +127,18 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
|||||||
setDashboardLayout(sortLayout(layouts));
|
setDashboardLayout(sortLayout(layouts));
|
||||||
}, [layouts]);
|
}, [layouts]);
|
||||||
|
|
||||||
|
const logEventCalledRef = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!logEventCalledRef.current && !isUndefined(data)) {
|
||||||
|
logEvent('Dashboard Detail: Opened', {
|
||||||
|
dashboardId: data.uuid,
|
||||||
|
dashboardName: data.title,
|
||||||
|
numberOfPanels: data.widgets?.length,
|
||||||
|
numberOfVariables: Object.keys(data?.variables || {}).length || 0,
|
||||||
|
});
|
||||||
|
logEventCalledRef.current = true;
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
const onSaveHandler = (): void => {
|
const onSaveHandler = (): void => {
|
||||||
if (!selectedDashboard) return;
|
if (!selectedDashboard) return;
|
||||||
|
|
||||||
@@ -428,7 +441,11 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
|||||||
return isDashboardEmpty ? (
|
return isDashboardEmpty ? (
|
||||||
<DashboardEmptyState />
|
<DashboardEmptyState />
|
||||||
) : (
|
) : (
|
||||||
<FullScreen handle={handle} className="fullscreen-grid-container">
|
<FullScreen
|
||||||
|
handle={handle}
|
||||||
|
className="fullscreen-grid-container"
|
||||||
|
data-overlayscrollbars-initialize
|
||||||
|
>
|
||||||
<ReactGridLayout
|
<ReactGridLayout
|
||||||
cols={12}
|
cols={12}
|
||||||
rowHeight={45}
|
rowHeight={45}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ function WidgetHeader({
|
|||||||
);
|
);
|
||||||
}, [widget.id, widget.panelTypes, widget.query]);
|
}, [widget.id, widget.panelTypes, widget.query]);
|
||||||
|
|
||||||
const onCreateAlertsHandler = useCreateAlerts(widget);
|
const onCreateAlertsHandler = useCreateAlerts(widget, 'dashboardView');
|
||||||
|
|
||||||
const onDownloadHandler = useCallback((): void => {
|
const onDownloadHandler = useCallback((): void => {
|
||||||
const csv = unparse(tableProcessedDataRef.current);
|
const csv = unparse(tableProcessedDataRef.current);
|
||||||
@@ -234,6 +234,7 @@ function WidgetHeader({
|
|||||||
)}
|
)}
|
||||||
<Dropdown menu={menu} trigger={['hover']} placement="bottomRight">
|
<Dropdown menu={menu} trigger={['hover']} placement="bottomRight">
|
||||||
<MoreOutlined
|
<MoreOutlined
|
||||||
|
data-testid="widget-header-options"
|
||||||
className={`widget-header-more-options ${
|
className={`widget-header-more-options ${
|
||||||
parentHover ? 'widget-header-hover' : ''
|
parentHover ? 'widget-header-hover' : ''
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
215
frontend/src/container/GridTableComponent/__tests__/response.ts
Normal file
215
frontend/src/container/GridTableComponent/__tests__/response.ts
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
export const tableDataMultipleQueriesSuccessResponse = {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'service_name',
|
||||||
|
queryName: '',
|
||||||
|
isValueColumn: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'A',
|
||||||
|
queryName: 'A',
|
||||||
|
isValueColumn: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'B',
|
||||||
|
queryName: 'B',
|
||||||
|
isValueColumn: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
A: 4196.71,
|
||||||
|
B: 'n/a',
|
||||||
|
service_name: 'demo-app',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
A: 500.83,
|
||||||
|
B: 'n/a',
|
||||||
|
service_name: 'customer',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
A: 499.5,
|
||||||
|
B: 'n/a',
|
||||||
|
service_name: 'mysql',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
A: 293.22,
|
||||||
|
B: 'n/a',
|
||||||
|
service_name: 'frontend',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
A: 230.03,
|
||||||
|
B: 'n/a',
|
||||||
|
service_name: 'driver',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
A: 67.09,
|
||||||
|
B: 'n/a',
|
||||||
|
service_name: 'route',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
A: 30.96,
|
||||||
|
B: 'n/a',
|
||||||
|
service_name: 'redis',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
A: 'n/a',
|
||||||
|
B: 112.27,
|
||||||
|
service_name: 'n/a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const widgetQueryWithLegend = {
|
||||||
|
clickhouse_sql: [
|
||||||
|
{
|
||||||
|
name: 'A',
|
||||||
|
legend: '',
|
||||||
|
disabled: false,
|
||||||
|
query: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
promql: [
|
||||||
|
{
|
||||||
|
name: 'A',
|
||||||
|
query: '',
|
||||||
|
legend: '',
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
builder: {
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
dataSource: 'metrics',
|
||||||
|
queryName: 'A',
|
||||||
|
aggregateOperator: 'count',
|
||||||
|
aggregateAttribute: {
|
||||||
|
dataType: 'float64',
|
||||||
|
id: 'signoz_latency--float64--ExponentialHistogram--true',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
key: 'signoz_latency',
|
||||||
|
type: 'ExponentialHistogram',
|
||||||
|
},
|
||||||
|
timeAggregation: '',
|
||||||
|
spaceAggregation: 'p90',
|
||||||
|
functions: [],
|
||||||
|
filters: {
|
||||||
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
|
},
|
||||||
|
expression: 'A',
|
||||||
|
disabled: false,
|
||||||
|
stepInterval: 60,
|
||||||
|
having: [],
|
||||||
|
limit: null,
|
||||||
|
orderBy: [],
|
||||||
|
groupBy: [
|
||||||
|
{
|
||||||
|
dataType: 'string',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
key: 'service_name',
|
||||||
|
type: 'tag',
|
||||||
|
id: 'service_name--string--tag--false',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
legend: 'p99',
|
||||||
|
reduceTo: 'avg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataSource: 'metrics',
|
||||||
|
queryName: 'B',
|
||||||
|
aggregateOperator: 'rate',
|
||||||
|
aggregateAttribute: {
|
||||||
|
dataType: 'float64',
|
||||||
|
id: 'system_disk_operations--float64--Sum--true',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
key: 'system_disk_operations',
|
||||||
|
type: 'Sum',
|
||||||
|
},
|
||||||
|
timeAggregation: 'rate',
|
||||||
|
spaceAggregation: 'sum',
|
||||||
|
functions: [],
|
||||||
|
filters: {
|
||||||
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
|
},
|
||||||
|
expression: 'B',
|
||||||
|
disabled: false,
|
||||||
|
stepInterval: 60,
|
||||||
|
having: [],
|
||||||
|
limit: null,
|
||||||
|
orderBy: [],
|
||||||
|
groupBy: [],
|
||||||
|
legend: '',
|
||||||
|
reduceTo: 'avg',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryFormulas: [],
|
||||||
|
},
|
||||||
|
id: '48ad5a67-9a3c-49d4-a886-d7a34f8b875d',
|
||||||
|
queryType: 'builder',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const expectedOutputWithLegends = {
|
||||||
|
dataSource: [
|
||||||
|
{
|
||||||
|
A: 4196.71,
|
||||||
|
B: 'n/a',
|
||||||
|
service_name: 'demo-app',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
A: 500.83,
|
||||||
|
B: 'n/a',
|
||||||
|
service_name: 'customer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
A: 499.5,
|
||||||
|
B: 'n/a',
|
||||||
|
service_name: 'mysql',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
A: 293.22,
|
||||||
|
B: 'n/a',
|
||||||
|
service_name: 'frontend',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
A: 230.03,
|
||||||
|
B: 'n/a',
|
||||||
|
service_name: 'driver',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
A: 67.09,
|
||||||
|
B: 'n/a',
|
||||||
|
service_name: 'route',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
A: 30.96,
|
||||||
|
B: 'n/a',
|
||||||
|
service_name: 'redis',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
A: 'n/a',
|
||||||
|
B: 112.27,
|
||||||
|
service_name: 'n/a',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createColumnsAndDataSource,
|
||||||
|
getQueryLegend,
|
||||||
|
sortFunction,
|
||||||
|
} from '../utils';
|
||||||
|
import {
|
||||||
|
expectedOutputWithLegends,
|
||||||
|
tableDataMultipleQueriesSuccessResponse,
|
||||||
|
widgetQueryWithLegend,
|
||||||
|
} from './response';
|
||||||
|
|
||||||
|
describe('Table Panel utils', () => {
|
||||||
|
it('createColumnsAndDataSource function', () => {
|
||||||
|
const data = tableDataMultipleQueriesSuccessResponse;
|
||||||
|
const query = widgetQueryWithLegend as Query;
|
||||||
|
|
||||||
|
const { columns, dataSource } = createColumnsAndDataSource(data, query);
|
||||||
|
|
||||||
|
expect(dataSource).toStrictEqual(expectedOutputWithLegends.dataSource);
|
||||||
|
|
||||||
|
// this makes sure that the columns are rendered in the same order as response
|
||||||
|
expect(columns[0].title).toBe('service_name');
|
||||||
|
// the next specifically makes sure that the legends are properly applied in multiple queries
|
||||||
|
expect(columns[1].title).toBe('p99');
|
||||||
|
// this makes sure that the query without a legend takes the title from the query response
|
||||||
|
expect(columns[2].title).toBe('B');
|
||||||
|
|
||||||
|
// this is to ensure that the rows properly map to the column data indexes as the dataIndex should be equal to name of the columns
|
||||||
|
// returned in the response as the rows will be mapped with them
|
||||||
|
expect((columns[0] as any).dataIndex).toBe('service_name');
|
||||||
|
expect((columns[1] as any).dataIndex).toBe('A');
|
||||||
|
expect((columns[2] as any).dataIndex).toBe('B');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getQueryLegend function', () => {
|
||||||
|
const query = widgetQueryWithLegend as Query;
|
||||||
|
|
||||||
|
// query A has a legend of p99
|
||||||
|
expect(getQueryLegend(query, 'A')).toBe('p99');
|
||||||
|
|
||||||
|
// should return undefined when legend not present
|
||||||
|
expect(getQueryLegend(query, 'B')).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sorter function for table sorting', () => {
|
||||||
|
let rowA: {
|
||||||
|
A: string | number;
|
||||||
|
timestamp: number;
|
||||||
|
key: string;
|
||||||
|
} = {
|
||||||
|
A: 22.4,
|
||||||
|
timestamp: 111111,
|
||||||
|
key: '1111',
|
||||||
|
};
|
||||||
|
let rowB: {
|
||||||
|
A: string | number;
|
||||||
|
timestamp: number;
|
||||||
|
key: string;
|
||||||
|
} = {
|
||||||
|
A: 'n/a',
|
||||||
|
timestamp: 111112,
|
||||||
|
key: '1112',
|
||||||
|
};
|
||||||
|
const item = {
|
||||||
|
isValueColumn: true,
|
||||||
|
name: 'A',
|
||||||
|
queryName: 'A',
|
||||||
|
};
|
||||||
|
// A has value and value is considered bigger than n/a hence 1
|
||||||
|
expect(sortFunction(rowA, rowB, item)).toBe(1);
|
||||||
|
|
||||||
|
rowA = {
|
||||||
|
A: 'n/a',
|
||||||
|
timestamp: 111111,
|
||||||
|
key: '1111',
|
||||||
|
};
|
||||||
|
rowB = {
|
||||||
|
A: 22.4,
|
||||||
|
timestamp: 111112,
|
||||||
|
key: '1112',
|
||||||
|
};
|
||||||
|
|
||||||
|
// B has value and value is considered bigger than n/a hence -1
|
||||||
|
expect(sortFunction(rowA, rowB, item)).toBe(-1);
|
||||||
|
|
||||||
|
rowA = {
|
||||||
|
A: 11,
|
||||||
|
timestamp: 111111,
|
||||||
|
key: '1111',
|
||||||
|
};
|
||||||
|
rowB = {
|
||||||
|
A: 22,
|
||||||
|
timestamp: 111112,
|
||||||
|
key: '1112',
|
||||||
|
};
|
||||||
|
|
||||||
|
// A and B has value , since B > A hence A-B
|
||||||
|
expect(sortFunction(rowA, rowB, item)).toBe(-11);
|
||||||
|
|
||||||
|
rowA = {
|
||||||
|
A: 'read',
|
||||||
|
timestamp: 111111,
|
||||||
|
key: '1111',
|
||||||
|
};
|
||||||
|
rowB = {
|
||||||
|
A: 'write',
|
||||||
|
timestamp: 111112,
|
||||||
|
key: '1112',
|
||||||
|
};
|
||||||
|
|
||||||
|
// A and B are strings so A is smaller than B because r comes before w hence -1
|
||||||
|
expect(sortFunction(rowA, rowB, item)).toBe(-1);
|
||||||
|
|
||||||
|
rowA = {
|
||||||
|
A: 'n/a',
|
||||||
|
timestamp: 111111,
|
||||||
|
key: '1111',
|
||||||
|
};
|
||||||
|
rowB = {
|
||||||
|
A: 'n/a',
|
||||||
|
timestamp: 111112,
|
||||||
|
key: '1112',
|
||||||
|
};
|
||||||
|
|
||||||
|
// A and B are strings n/a , since both of them are same hence 0
|
||||||
|
expect(sortFunction(rowA, rowB, item)).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,10 +3,7 @@ import { Space, Tooltip } from 'antd';
|
|||||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||||
import { Events } from 'constants/events';
|
import { Events } from 'constants/events';
|
||||||
import { QueryTable } from 'container/QueryTable';
|
import { QueryTable } from 'container/QueryTable';
|
||||||
import {
|
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||||
createTableColumnsFromQuery,
|
|
||||||
RowData,
|
|
||||||
} from 'lib/query/createTableColumnsFromQuery';
|
|
||||||
import { cloneDeep, get, isEmpty, set } from 'lodash-es';
|
import { cloneDeep, get, isEmpty, set } from 'lodash-es';
|
||||||
import { memo, ReactNode, useCallback, useEffect, useMemo } from 'react';
|
import { memo, ReactNode, useCallback, useEffect, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -14,7 +11,11 @@ import { eventEmitter } from 'utils/getEventEmitter';
|
|||||||
|
|
||||||
import { WrapperStyled } from './styles';
|
import { WrapperStyled } from './styles';
|
||||||
import { GridTableComponentProps } from './types';
|
import { GridTableComponentProps } from './types';
|
||||||
import { findMatchingThreshold } from './utils';
|
import {
|
||||||
|
createColumnsAndDataSource,
|
||||||
|
findMatchingThreshold,
|
||||||
|
TableData,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
function GridTableComponent({
|
function GridTableComponent({
|
||||||
data,
|
data,
|
||||||
@@ -25,28 +26,26 @@ function GridTableComponent({
|
|||||||
...props
|
...props
|
||||||
}: GridTableComponentProps): JSX.Element {
|
}: GridTableComponentProps): JSX.Element {
|
||||||
const { t } = useTranslation(['valueGraph']);
|
const { t } = useTranslation(['valueGraph']);
|
||||||
|
|
||||||
|
// create columns and dataSource in the ui friendly structure
|
||||||
|
// use the query from the widget here to extract the legend information
|
||||||
const { columns, dataSource: originalDataSource } = useMemo(
|
const { columns, dataSource: originalDataSource } = useMemo(
|
||||||
() =>
|
() => createColumnsAndDataSource((data as unknown) as TableData, query),
|
||||||
createTableColumnsFromQuery({
|
[query, data],
|
||||||
query,
|
|
||||||
queryTableData: data,
|
|
||||||
}),
|
|
||||||
[data, query],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const createDataInCorrectFormat = useCallback(
|
const createDataInCorrectFormat = useCallback(
|
||||||
(dataSource: RowData[]): RowData[] =>
|
(dataSource: RowData[]): RowData[] =>
|
||||||
dataSource.map((d) => {
|
dataSource.map((d) => {
|
||||||
const finalObject = {};
|
const finalObject = {};
|
||||||
const keys = Object.keys(d);
|
|
||||||
keys.forEach((k) => {
|
// we use the order of the columns here to have similar download as the user view
|
||||||
const label = get(
|
columns.forEach((k) => {
|
||||||
columns.find((c) => get(c, 'dataIndex', '') === k) || {},
|
set(
|
||||||
'title',
|
finalObject,
|
||||||
'',
|
get(k, 'title', '') as string,
|
||||||
|
get(d, get(k, 'dataIndex', ''), 'n/a'),
|
||||||
);
|
);
|
||||||
if (label) {
|
|
||||||
set(finalObject, label as string, d[k]);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return finalObject as RowData;
|
return finalObject as RowData;
|
||||||
}),
|
}),
|
||||||
@@ -65,7 +64,11 @@ function GridTableComponent({
|
|||||||
const newValue = { ...val };
|
const newValue = { ...val };
|
||||||
Object.keys(val).forEach((k) => {
|
Object.keys(val).forEach((k) => {
|
||||||
if (columnUnits[k]) {
|
if (columnUnits[k]) {
|
||||||
newValue[k] = getYAxisFormattedValue(String(val[k]), columnUnits[k]);
|
// the check below takes care of not adding units for rows that have n/a values
|
||||||
|
newValue[k] =
|
||||||
|
val[k] !== 'n/a'
|
||||||
|
? getYAxisFormattedValue(String(val[k]), columnUnits[k])
|
||||||
|
: val[k];
|
||||||
newValue[`${k}_without_unit`] = val[k];
|
newValue[`${k}_without_unit`] = val[k];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
|
import { ColumnsType, ColumnType } from 'antd/es/table';
|
||||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||||
|
import { QUERY_TABLE_CONFIG } from 'container/QueryTable/config';
|
||||||
|
import { QueryTableProps } from 'container/QueryTable/QueryTable.intefaces';
|
||||||
|
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||||
|
import { isEmpty, isNaN } from 'lodash-es';
|
||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
|
|
||||||
// Helper function to evaluate the condition based on the operator
|
// Helper function to evaluate the condition based on the operator
|
||||||
function evaluateCondition(
|
function evaluateCondition(
|
||||||
@@ -56,3 +64,107 @@ export function findMatchingThreshold(
|
|||||||
hasMultipleMatches,
|
hasMultipleMatches,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TableData {
|
||||||
|
columns: { name: string; queryName: string; isValueColumn: boolean }[];
|
||||||
|
rows: { data: any }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getQueryLegend(
|
||||||
|
currentQuery: Query,
|
||||||
|
queryName: string,
|
||||||
|
): string | undefined {
|
||||||
|
let legend: string | undefined;
|
||||||
|
switch (currentQuery.queryType) {
|
||||||
|
case EQueryType.QUERY_BUILDER:
|
||||||
|
// check if the value is present in the queries
|
||||||
|
legend = currentQuery.builder.queryData.find(
|
||||||
|
(query) => query.queryName === queryName,
|
||||||
|
)?.legend;
|
||||||
|
|
||||||
|
if (!legend) {
|
||||||
|
// check if the value is present in the formula
|
||||||
|
legend = currentQuery.builder.queryFormulas.find(
|
||||||
|
(query) => query.queryName === queryName,
|
||||||
|
)?.legend;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case EQueryType.CLICKHOUSE:
|
||||||
|
legend = currentQuery.clickhouse_sql.find(
|
||||||
|
(query) => query.name === queryName,
|
||||||
|
)?.legend;
|
||||||
|
break;
|
||||||
|
case EQueryType.PROM:
|
||||||
|
legend = currentQuery.promql.find((query) => query.name === queryName)
|
||||||
|
?.legend;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
legend = undefined;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return legend;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortFunction(
|
||||||
|
a: RowData,
|
||||||
|
b: RowData,
|
||||||
|
item: {
|
||||||
|
name: string;
|
||||||
|
queryName: string;
|
||||||
|
isValueColumn: boolean;
|
||||||
|
},
|
||||||
|
): number {
|
||||||
|
// assumption :- number values is bigger than 'n/a'
|
||||||
|
const valueA = Number(a[`${item.name}_without_unit`] ?? a[item.name]);
|
||||||
|
const valueB = Number(b[`${item.name}_without_unit`] ?? b[item.name]);
|
||||||
|
|
||||||
|
// if both the values are numbers then return the difference here
|
||||||
|
if (!isNaN(valueA) && !isNaN(valueB)) {
|
||||||
|
return valueA - valueB;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if valueB is a number then make it bigger value
|
||||||
|
if (isNaN(valueA) && !isNaN(valueB)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if valueA is number make it the bigger value
|
||||||
|
if (!isNaN(valueA) && isNaN(valueB)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if both of them are strings do the localecompare
|
||||||
|
return ((a[item.name] as string) || '').localeCompare(
|
||||||
|
(b[item.name] as string) || '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function createColumnsAndDataSource(
|
||||||
|
data: TableData,
|
||||||
|
currentQuery: Query,
|
||||||
|
renderColumnCell?: QueryTableProps['renderColumnCell'],
|
||||||
|
): { columns: ColumnsType<RowData>; dataSource: RowData[] } {
|
||||||
|
const columns: ColumnsType<RowData> =
|
||||||
|
data.columns?.reduce<ColumnsType<RowData>>((acc, item) => {
|
||||||
|
// is the column is the value column then we need to check for the available legend
|
||||||
|
const legend = item.isValueColumn
|
||||||
|
? getQueryLegend(currentQuery, item.queryName)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const column: ColumnType<RowData> = {
|
||||||
|
dataIndex: item.name,
|
||||||
|
// if no legend present then rely on the column name value
|
||||||
|
title: !isEmpty(legend) ? legend : item.name,
|
||||||
|
width: QUERY_TABLE_CONFIG.width,
|
||||||
|
render: renderColumnCell && renderColumnCell[item.name],
|
||||||
|
sorter: (a: RowData, b: RowData): number => sortFunction(a, b, item),
|
||||||
|
};
|
||||||
|
|
||||||
|
return [...acc, column];
|
||||||
|
}, []) || [];
|
||||||
|
|
||||||
|
// the rows returned have data encapsulation hence removing the same here
|
||||||
|
const dataSource = data.rows?.map((d) => d.data) || [];
|
||||||
|
|
||||||
|
return { columns, dataSource };
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
|
import { License } from 'types/api/licenses/def';
|
||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
|
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
|
||||||
|
|
||||||
@@ -109,9 +110,13 @@ function HeaderContainer(): JSX.Element {
|
|||||||
|
|
||||||
const { data: licenseData, isFetching, status: licenseStatus } = useLicense();
|
const { data: licenseData, isFetching, status: licenseStatus } = useLicense();
|
||||||
|
|
||||||
|
const licensesStatus: string =
|
||||||
|
licenseData?.payload?.licenses?.find((e: License) => e.isCurrent)?.status ||
|
||||||
|
'';
|
||||||
|
|
||||||
const isLicenseActive =
|
const isLicenseActive =
|
||||||
licenseData?.payload?.licenses?.find((e) => e.isCurrent)?.status ===
|
licensesStatus?.toLocaleLowerCase() ===
|
||||||
LICENSE_PLAN_STATUS.VALID;
|
LICENSE_PLAN_STATUS.VALID.toLocaleLowerCase();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -7,17 +7,20 @@ interface AlertInfoCardProps {
|
|||||||
header: string;
|
header: string;
|
||||||
subheader: string;
|
subheader: string;
|
||||||
link: string;
|
link: string;
|
||||||
|
onClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertInfoCard({
|
function AlertInfoCard({
|
||||||
header,
|
header,
|
||||||
subheader,
|
subheader,
|
||||||
link,
|
link,
|
||||||
|
onClick,
|
||||||
}: AlertInfoCardProps): JSX.Element {
|
}: AlertInfoCardProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="alert-info-card"
|
className="alert-info-card"
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
|
onClick();
|
||||||
window.open(link, '_blank');
|
window.open(link, '_blank');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import './AlertsEmptyState.styles.scss';
|
|||||||
|
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
import { Button, Divider, Typography } from 'antd';
|
import { Button, Divider, Typography } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
@@ -10,12 +11,26 @@ import { useCallback, useState } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
import AlertInfoCard from './AlertInfoCard';
|
import AlertInfoCard from './AlertInfoCard';
|
||||||
import { ALERT_CARDS, ALERT_INFO_LINKS } from './alertLinks';
|
import { ALERT_CARDS, ALERT_INFO_LINKS } from './alertLinks';
|
||||||
import InfoLinkText from './InfoLinkText';
|
import InfoLinkText from './InfoLinkText';
|
||||||
|
|
||||||
|
const alertLogEvents = (
|
||||||
|
title: string,
|
||||||
|
link: string,
|
||||||
|
dataSource?: DataSource,
|
||||||
|
): void => {
|
||||||
|
const attributes = {
|
||||||
|
link,
|
||||||
|
page: 'Alert empty state page',
|
||||||
|
};
|
||||||
|
|
||||||
|
logEvent(title, dataSource ? { ...attributes, dataSource } : attributes);
|
||||||
|
};
|
||||||
|
|
||||||
export function AlertsEmptyState(): JSX.Element {
|
export function AlertsEmptyState(): JSX.Element {
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
const { role, featureResponse } = useSelector<AppState, AppReducer>(
|
const { role, featureResponse } = useSelector<AppState, AppReducer>(
|
||||||
@@ -91,18 +106,33 @@ export function AlertsEmptyState(): JSX.Element {
|
|||||||
link="https://youtu.be/xjxNIqiv4_M"
|
link="https://youtu.be/xjxNIqiv4_M"
|
||||||
leftIconVisible
|
leftIconVisible
|
||||||
rightIconVisible
|
rightIconVisible
|
||||||
|
onClick={(): void =>
|
||||||
|
alertLogEvents(
|
||||||
|
'Alert: Video tutorial link clicked',
|
||||||
|
'https://youtu.be/xjxNIqiv4_M',
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ALERT_INFO_LINKS.map((info) => (
|
{ALERT_INFO_LINKS.map((info) => {
|
||||||
|
const logEventTriggered = (): void =>
|
||||||
|
alertLogEvents(
|
||||||
|
'Alert: Tutorial doc link clicked',
|
||||||
|
info.link,
|
||||||
|
info.dataSource,
|
||||||
|
);
|
||||||
|
return (
|
||||||
<InfoLinkText
|
<InfoLinkText
|
||||||
key={info.link}
|
key={info.link}
|
||||||
infoText={info.infoText}
|
infoText={info.infoText}
|
||||||
link={info.link}
|
link={info.link}
|
||||||
leftIconVisible={info.leftIconVisible}
|
leftIconVisible={info.leftIconVisible}
|
||||||
rightIconVisible={info.rightIconVisible}
|
rightIconVisible={info.rightIconVisible}
|
||||||
|
onClick={logEventTriggered}
|
||||||
/>
|
/>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div className="get-started-text">
|
<div className="get-started-text">
|
||||||
@@ -113,14 +143,23 @@ export function AlertsEmptyState(): JSX.Element {
|
|||||||
</Divider>
|
</Divider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ALERT_CARDS.map((card) => (
|
{ALERT_CARDS.map((card) => {
|
||||||
|
const logEventTriggered = (): void =>
|
||||||
|
alertLogEvents(
|
||||||
|
'Alert: Sample alert link clicked',
|
||||||
|
card.link,
|
||||||
|
card.dataSource,
|
||||||
|
);
|
||||||
|
return (
|
||||||
<AlertInfoCard
|
<AlertInfoCard
|
||||||
key={card.link}
|
key={card.link}
|
||||||
header={card.header}
|
header={card.header}
|
||||||
subheader={card.subheader}
|
subheader={card.subheader}
|
||||||
link={card.link}
|
link={card.link}
|
||||||
|
onClick={logEventTriggered}
|
||||||
/>
|
/>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ interface InfoLinkTextProps {
|
|||||||
link: string;
|
link: string;
|
||||||
leftIconVisible: boolean;
|
leftIconVisible: boolean;
|
||||||
rightIconVisible: boolean;
|
rightIconVisible: boolean;
|
||||||
|
onClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function InfoLinkText({
|
function InfoLinkText({
|
||||||
@@ -13,10 +14,12 @@ function InfoLinkText({
|
|||||||
link,
|
link,
|
||||||
leftIconVisible,
|
leftIconVisible,
|
||||||
rightIconVisible,
|
rightIconVisible,
|
||||||
|
onClick,
|
||||||
}: InfoLinkTextProps): JSX.Element {
|
}: InfoLinkTextProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
|
onClick();
|
||||||
window.open(link, '_blank');
|
window.open(link, '_blank');
|
||||||
}}
|
}}
|
||||||
className="info-link-container"
|
className="info-link-container"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
export const ALERT_INFO_LINKS = [
|
export const ALERT_INFO_LINKS = [
|
||||||
{
|
{
|
||||||
infoText: 'How to create Metrics-based alerts',
|
infoText: 'How to create Metrics-based alerts',
|
||||||
@@ -5,6 +7,7 @@ export const ALERT_INFO_LINKS = [
|
|||||||
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-empty-page',
|
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-empty-page',
|
||||||
leftIconVisible: false,
|
leftIconVisible: false,
|
||||||
rightIconVisible: true,
|
rightIconVisible: true,
|
||||||
|
dataSource: DataSource.METRICS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
infoText: 'How to create Log-based alerts',
|
infoText: 'How to create Log-based alerts',
|
||||||
@@ -12,6 +15,7 @@ export const ALERT_INFO_LINKS = [
|
|||||||
'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-empty-page',
|
'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-empty-page',
|
||||||
leftIconVisible: false,
|
leftIconVisible: false,
|
||||||
rightIconVisible: true,
|
rightIconVisible: true,
|
||||||
|
dataSource: DataSource.LOGS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
infoText: 'How to create Trace-based alerts',
|
infoText: 'How to create Trace-based alerts',
|
||||||
@@ -19,6 +23,7 @@ export const ALERT_INFO_LINKS = [
|
|||||||
'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-empty-page',
|
'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-empty-page',
|
||||||
leftIconVisible: false,
|
leftIconVisible: false,
|
||||||
rightIconVisible: true,
|
rightIconVisible: true,
|
||||||
|
dataSource: DataSource.TRACES,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -26,24 +31,28 @@ export const ALERT_CARDS = [
|
|||||||
{
|
{
|
||||||
header: 'Alert on high memory usage',
|
header: 'Alert on high memory usage',
|
||||||
subheader: "Monitor your host's memory usage",
|
subheader: "Monitor your host's memory usage",
|
||||||
|
dataSource: DataSource.METRICS,
|
||||||
link:
|
link:
|
||||||
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-empty-page#1-alert-when-memory-usage-for-host-goes-above-400-mb-or-any-fixed-memory',
|
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-empty-page#1-alert-when-memory-usage-for-host-goes-above-400-mb-or-any-fixed-memory',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Alert on slow external API calls',
|
header: 'Alert on slow external API calls',
|
||||||
subheader: 'Monitor your external API calls',
|
subheader: 'Monitor your external API calls',
|
||||||
|
dataSource: DataSource.TRACES,
|
||||||
link:
|
link:
|
||||||
'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-empty-page#1-alert-when-external-api-latency-p90-is-over-1-second-for-last-5-mins',
|
'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-empty-page#1-alert-when-external-api-latency-p90-is-over-1-second-for-last-5-mins',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Alert on high percentage of timeout errors in logs',
|
header: 'Alert on high percentage of timeout errors in logs',
|
||||||
subheader: 'Monitor your logs for errors',
|
subheader: 'Monitor your logs for errors',
|
||||||
|
dataSource: DataSource.LOGS,
|
||||||
link:
|
link:
|
||||||
'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-empty-page#1-alert-when-percentage-of-redis-timeout-error-logs-greater-than-7-in-last-5-mins',
|
'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-empty-page#1-alert-when-percentage-of-redis-timeout-error-logs-greater-than-7-in-last-5-mins',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Alert on high error percentage of an endpoint',
|
header: 'Alert on high error percentage of an endpoint',
|
||||||
subheader: 'Monitor your API endpoint',
|
subheader: 'Monitor your API endpoint',
|
||||||
|
dataSource: DataSource.METRICS,
|
||||||
link:
|
link:
|
||||||
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-empty-page#3-alert-when-the-error-percentage-for-an-endpoint-exceeds-5',
|
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-empty-page#3-alert-when-the-error-percentage-for-an-endpoint-exceeds-5',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { PlusOutlined } from '@ant-design/icons';
|
|||||||
import { Input, Typography } from 'antd';
|
import { Input, Typography } from 'antd';
|
||||||
import type { ColumnsType } from 'antd/es/table/interface';
|
import type { ColumnsType } from 'antd/es/table/interface';
|
||||||
import saveAlertApi from 'api/alerts/save';
|
import saveAlertApi from 'api/alerts/save';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import DropDown from 'components/DropDown/DropDown';
|
import DropDown from 'components/DropDown/DropDown';
|
||||||
import { listAlertMessage } from 'components/facingIssueBtn/util';
|
import { listAlertMessage } from 'components/facingIssueBtn/util';
|
||||||
import {
|
import {
|
||||||
@@ -41,7 +42,7 @@ import {
|
|||||||
} from './styles';
|
} from './styles';
|
||||||
import Status from './TableComponents/Status';
|
import Status from './TableComponents/Status';
|
||||||
import ToggleAlertState from './ToggleAlertState';
|
import ToggleAlertState from './ToggleAlertState';
|
||||||
import { filterAlerts } from './utils';
|
import { alertActionLogEvent, filterAlerts } from './utils';
|
||||||
|
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
|
|
||||||
@@ -107,12 +108,16 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
|||||||
}, [notificationsApi, t]);
|
}, [notificationsApi, t]);
|
||||||
|
|
||||||
const onClickNewAlertHandler = useCallback(() => {
|
const onClickNewAlertHandler = useCallback(() => {
|
||||||
|
logEvent('Alert: New alert button clicked', {
|
||||||
|
number: allAlertRules?.length,
|
||||||
|
});
|
||||||
featureResponse
|
featureResponse
|
||||||
.refetch()
|
.refetch()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
history.push(ROUTES.ALERTS_NEW);
|
history.push(ROUTES.ALERTS_NEW);
|
||||||
})
|
})
|
||||||
.catch(handleError);
|
.catch(handleError);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [featureResponse, handleError]);
|
}, [featureResponse, handleError]);
|
||||||
|
|
||||||
const onEditHandler = (record: GettableAlert) => (): void => {
|
const onEditHandler = (record: GettableAlert) => (): void => {
|
||||||
@@ -321,6 +326,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
|||||||
width: 10,
|
width: 10,
|
||||||
render: (id: GettableAlert['id'], record): JSX.Element => (
|
render: (id: GettableAlert['id'], record): JSX.Element => (
|
||||||
<DropDown
|
<DropDown
|
||||||
|
onDropDownItemClick={(item): void => alertActionLogEvent(item.key, record)}
|
||||||
element={[
|
element={[
|
||||||
<ToggleAlertState
|
<ToggleAlertState
|
||||||
key="1"
|
key="1"
|
||||||
@@ -356,6 +362,9 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const paginationConfig = {
|
||||||
|
defaultCurrent: Number(paginationParam) || 1,
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SearchContainer>
|
<SearchContainer>
|
||||||
@@ -385,11 +394,10 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
|
shouldSendAlertsLogEvent
|
||||||
dynamicColumns={dynamicColumns}
|
dynamicColumns={dynamicColumns}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
pagination={{
|
pagination={paginationConfig}
|
||||||
defaultCurrent: Number(paginationParam) || 1,
|
|
||||||
}}
|
|
||||||
facingIssueBtn={{
|
facingIssueBtn={{
|
||||||
attributes: {
|
attributes: {
|
||||||
screen: 'Alert list page',
|
screen: 'Alert list page',
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { Space } from 'antd';
|
import { Space } from 'antd';
|
||||||
import getAll from 'api/alerts/getAll';
|
import getAll from 'api/alerts/getAll';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import ReleaseNote from 'components/ReleaseNote';
|
import ReleaseNote from 'components/ReleaseNote';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { useEffect } from 'react';
|
import { isUndefined } from 'lodash-es';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
@@ -19,8 +21,19 @@ function ListAlertRules(): JSX.Element {
|
|||||||
cacheTime: 0,
|
cacheTime: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const logEventCalledRef = useRef(false);
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!logEventCalledRef.current && !isUndefined(data?.payload)) {
|
||||||
|
logEvent('Alert: List page visited', {
|
||||||
|
number: data?.payload?.length,
|
||||||
|
});
|
||||||
|
logEventCalledRef.current = true;
|
||||||
|
}
|
||||||
|
}, [data?.payload]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === 'error' || (status === 'success' && data.statusCode >= 400)) {
|
if (status === 'error' || (status === 'success' && data.statusCode >= 400)) {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||||
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
import { GettableAlert } from 'types/api/alerts/get';
|
import { GettableAlert } from 'types/api/alerts/get';
|
||||||
|
|
||||||
export const filterAlerts = (
|
export const filterAlerts = (
|
||||||
@@ -23,3 +26,32 @@ export const filterAlerts = (
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const alertActionLogEvent = (
|
||||||
|
action: string,
|
||||||
|
record: GettableAlert,
|
||||||
|
): void => {
|
||||||
|
let actionValue = '';
|
||||||
|
switch (action) {
|
||||||
|
case '0':
|
||||||
|
actionValue = 'Enable/Disable';
|
||||||
|
break;
|
||||||
|
case '1':
|
||||||
|
actionValue = 'Edit';
|
||||||
|
break;
|
||||||
|
case '2':
|
||||||
|
actionValue = 'Clone';
|
||||||
|
break;
|
||||||
|
case '3':
|
||||||
|
actionValue = 'Delete';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
logEvent('Alert: Action', {
|
||||||
|
ruleId: record?.id,
|
||||||
|
dataSource: ALERTS_DATA_SOURCE_MAP[record.alertType as AlertTypes],
|
||||||
|
name: record?.alert,
|
||||||
|
action: actionValue,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { TableProps } from 'antd/lib';
|
import { TableProps } from 'antd/lib';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import createDashboard from 'api/dashboard/create';
|
import createDashboard from 'api/dashboard/create';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
@@ -34,7 +35,7 @@ import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
|||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { get, isEmpty } from 'lodash-es';
|
import { get, isEmpty, isUndefined } from 'lodash-es';
|
||||||
import {
|
import {
|
||||||
ArrowDownWideNarrow,
|
ArrowDownWideNarrow,
|
||||||
ArrowUpRight,
|
ArrowUpRight,
|
||||||
@@ -60,6 +61,7 @@ import {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -71,7 +73,6 @@ import { Dashboard } from 'types/api/dashboard/getAll';
|
|||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
import { isCloudUser } from 'utils/app';
|
import { isCloudUser } from 'utils/app';
|
||||||
|
|
||||||
import useUrlQuery from '../../hooks/useUrlQuery';
|
|
||||||
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
|
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
|
||||||
import ImportJSON from './ImportJSON';
|
import ImportJSON from './ImportJSON';
|
||||||
import { DeleteButton } from './TableComponents/DeleteButton';
|
import { DeleteButton } from './TableComponents/DeleteButton';
|
||||||
@@ -84,7 +85,7 @@ import {
|
|||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
function DashboardsList(): JSX.Element {
|
function DashboardsList(): JSX.Element {
|
||||||
const {
|
const {
|
||||||
data: dashboardListResponse = [],
|
data: dashboardListResponse,
|
||||||
isLoading: isDashboardListLoading,
|
isLoading: isDashboardListLoading,
|
||||||
error: dashboardFetchError,
|
error: dashboardFetchError,
|
||||||
refetch: refetchDashboardList,
|
refetch: refetchDashboardList,
|
||||||
@@ -97,12 +98,14 @@ function DashboardsList(): JSX.Element {
|
|||||||
setListSortOrder: setSortOrder,
|
setListSortOrder: setSortOrder,
|
||||||
} = useDashboard();
|
} = useDashboard();
|
||||||
|
|
||||||
|
const [searchString, setSearchString] = useState<string>(
|
||||||
|
sortOrder.search || '',
|
||||||
|
);
|
||||||
const [action, createNewDashboard] = useComponentPermission(
|
const [action, createNewDashboard] = useComponentPermission(
|
||||||
['action', 'create_new_dashboards'],
|
['action', 'create_new_dashboards'],
|
||||||
role,
|
role,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [searchValue, setSearchValue] = useState<string>('');
|
|
||||||
const [
|
const [
|
||||||
showNewDashboardTemplatesModal,
|
showNewDashboardTemplatesModal,
|
||||||
setShowNewDashboardTemplatesModal,
|
setShowNewDashboardTemplatesModal,
|
||||||
@@ -121,10 +124,6 @@ function DashboardsList(): JSX.Element {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
const params = useUrlQuery();
|
|
||||||
const searchParams = params.get('search');
|
|
||||||
const [searchString, setSearchString] = useState<string>(searchParams || '');
|
|
||||||
|
|
||||||
const getLocalStorageDynamicColumns = (): DashboardDynamicColumns => {
|
const getLocalStorageDynamicColumns = (): DashboardDynamicColumns => {
|
||||||
const dashboardDynamicColumnsString = localStorage.getItem('dashboard');
|
const dashboardDynamicColumnsString = localStorage.getItem('dashboard');
|
||||||
let dashboardDynamicColumns: DashboardDynamicColumns = {
|
let dashboardDynamicColumns: DashboardDynamicColumns = {
|
||||||
@@ -186,14 +185,6 @@ function DashboardsList(): JSX.Element {
|
|||||||
setDashboards(sortedDashboards);
|
setDashboards(sortedDashboards);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
params.set('columnKey', sortOrder.columnKey as string);
|
|
||||||
params.set('order', sortOrder.order as string);
|
|
||||||
params.set('page', sortOrder.pagination || '1');
|
|
||||||
history.replace({ search: params.toString() });
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [sortOrder]);
|
|
||||||
|
|
||||||
const sortHandle = (key: string): void => {
|
const sortHandle = (key: string): void => {
|
||||||
if (!dashboards) return;
|
if (!dashboards) return;
|
||||||
if (key === 'createdAt') {
|
if (key === 'createdAt') {
|
||||||
@@ -202,6 +193,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
columnKey: 'createdAt',
|
columnKey: 'createdAt',
|
||||||
order: 'descend',
|
order: 'descend',
|
||||||
pagination: sortOrder.pagination || '1',
|
pagination: sortOrder.pagination || '1',
|
||||||
|
search: sortOrder.search || '',
|
||||||
});
|
});
|
||||||
} else if (key === 'updatedAt') {
|
} else if (key === 'updatedAt') {
|
||||||
sortDashboardsByUpdatedAt(dashboards);
|
sortDashboardsByUpdatedAt(dashboards);
|
||||||
@@ -209,21 +201,19 @@ function DashboardsList(): JSX.Element {
|
|||||||
columnKey: 'updatedAt',
|
columnKey: 'updatedAt',
|
||||||
order: 'descend',
|
order: 'descend',
|
||||||
pagination: sortOrder.pagination || '1',
|
pagination: sortOrder.pagination || '1',
|
||||||
|
search: sortOrder.search || '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function handlePageSizeUpdate(page: number): void {
|
function handlePageSizeUpdate(page: number): void {
|
||||||
setSortOrder((order) => ({
|
setSortOrder({ ...sortOrder, pagination: String(page) });
|
||||||
...order,
|
|
||||||
pagination: String(page),
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const filteredDashboards = filterDashboard(
|
const filteredDashboards = filterDashboard(
|
||||||
searchString,
|
searchString,
|
||||||
dashboardListResponse,
|
dashboardListResponse || [],
|
||||||
);
|
);
|
||||||
if (sortOrder.columnKey === 'updatedAt') {
|
if (sortOrder.columnKey === 'updatedAt') {
|
||||||
sortDashboardsByUpdatedAt(filteredDashboards || []);
|
sortDashboardsByUpdatedAt(filteredDashboards || []);
|
||||||
@@ -234,6 +224,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
columnKey: 'updatedAt',
|
columnKey: 'updatedAt',
|
||||||
order: 'descend',
|
order: 'descend',
|
||||||
pagination: sortOrder.pagination || '1',
|
pagination: sortOrder.pagination || '1',
|
||||||
|
search: sortOrder.search || '',
|
||||||
});
|
});
|
||||||
sortDashboardsByUpdatedAt(filteredDashboards || []);
|
sortDashboardsByUpdatedAt(filteredDashboards || []);
|
||||||
}
|
}
|
||||||
@@ -243,6 +234,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
setSortOrder,
|
setSortOrder,
|
||||||
sortOrder.columnKey,
|
sortOrder.columnKey,
|
||||||
sortOrder.pagination,
|
sortOrder.pagination,
|
||||||
|
sortOrder.search,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [newDashboardState, setNewDashboardState] = useState({
|
const [newDashboardState, setNewDashboardState] = useState({
|
||||||
@@ -269,6 +261,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
|
|
||||||
const onNewDashboardHandler = useCallback(async () => {
|
const onNewDashboardHandler = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
logEvent('Dashboard List: Create dashboard clicked', {});
|
||||||
setNewDashboardState({
|
setNewDashboardState({
|
||||||
...newDashboardState,
|
...newDashboardState,
|
||||||
loading: true,
|
loading: true,
|
||||||
@@ -305,18 +298,23 @@ function DashboardsList(): JSX.Element {
|
|||||||
}, [newDashboardState, t]);
|
}, [newDashboardState, t]);
|
||||||
|
|
||||||
const onModalHandler = (uploadedGrafana: boolean): void => {
|
const onModalHandler = (uploadedGrafana: boolean): void => {
|
||||||
|
logEvent('Dashboard List: Import JSON clicked', {});
|
||||||
|
|
||||||
setIsImportJSONModalVisible((state) => !state);
|
setIsImportJSONModalVisible((state) => !state);
|
||||||
setUploadedGrafana(uploadedGrafana);
|
setUploadedGrafana(uploadedGrafana);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearch = (event: ChangeEvent<HTMLInputElement>): void => {
|
const handleSearch = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||||
setIsFilteringDashboards(true);
|
setIsFilteringDashboards(true);
|
||||||
setSearchValue(event.target.value);
|
|
||||||
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
|
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
|
||||||
const filteredDashboards = filterDashboard(searchText, dashboardListResponse);
|
const filteredDashboards = filterDashboard(
|
||||||
|
searchText,
|
||||||
|
dashboardListResponse || [],
|
||||||
|
);
|
||||||
setDashboards(filteredDashboards);
|
setDashboards(filteredDashboards);
|
||||||
setIsFilteringDashboards(false);
|
setIsFilteringDashboards(false);
|
||||||
setSearchString(searchText);
|
setSearchString(searchText);
|
||||||
|
setSortOrder({ ...sortOrder, search: searchText });
|
||||||
};
|
};
|
||||||
|
|
||||||
const [state, setCopy] = useCopyToClipboard();
|
const [state, setCopy] = useCopyToClipboard();
|
||||||
@@ -407,7 +405,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
{
|
{
|
||||||
title: 'Dashboards',
|
title: 'Dashboards',
|
||||||
key: 'dashboard',
|
key: 'dashboard',
|
||||||
render: (dashboard: Data): JSX.Element => {
|
render: (dashboard: Data, _, index): JSX.Element => {
|
||||||
const timeOptions: Intl.DateTimeFormatOptions = {
|
const timeOptions: Intl.DateTimeFormatOptions = {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
@@ -441,6 +439,10 @@ function DashboardsList(): JSX.Element {
|
|||||||
} else {
|
} else {
|
||||||
history.push(getLink());
|
history.push(getLink());
|
||||||
}
|
}
|
||||||
|
logEvent('Dashboard List: Clicked on dashboard', {
|
||||||
|
dashboardId: dashboard.id,
|
||||||
|
dashboardName: dashboard.name,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -452,7 +454,9 @@ function DashboardsList(): JSX.Element {
|
|||||||
style={{ height: '14px', width: '14px' }}
|
style={{ height: '14px', width: '14px' }}
|
||||||
alt="dashboard-image"
|
alt="dashboard-image"
|
||||||
/>
|
/>
|
||||||
<Typography.Text>{dashboard.name}</Typography.Text>
|
<Typography.Text data-testid={`dashboard-title-${index}`}>
|
||||||
|
{dashboard.name}
|
||||||
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="tags-with-actions">
|
<div className="tags-with-actions">
|
||||||
@@ -609,6 +613,31 @@ function DashboardsList(): JSX.Element {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const paginationConfig = data.length > 20 && {
|
||||||
|
pageSize: 20,
|
||||||
|
showTotal: showPaginationItem,
|
||||||
|
showSizeChanger: false,
|
||||||
|
onChange: (page: any): void => handlePageSizeUpdate(page),
|
||||||
|
current: Number(sortOrder.pagination),
|
||||||
|
defaultCurrent: Number(sortOrder.pagination) || 1,
|
||||||
|
hideOnSinglePage: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const logEventCalledRef = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!logEventCalledRef.current &&
|
||||||
|
!isDashboardListLoading &&
|
||||||
|
!isUndefined(dashboardListResponse)
|
||||||
|
) {
|
||||||
|
logEvent('Dashboard List: Page visited', {
|
||||||
|
number: dashboardListResponse?.length,
|
||||||
|
});
|
||||||
|
logEventCalledRef.current = true;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [isDashboardListLoading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dashboards-list-container">
|
<div className="dashboards-list-container">
|
||||||
<div className="dashboards-list-view-content">
|
<div className="dashboards-list-view-content">
|
||||||
@@ -667,7 +696,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
<ArrowUpRight size={16} className="learn-more-arrow" />
|
<ArrowUpRight size={16} className="learn-more-arrow" />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
) : dashboards?.length === 0 && !searchValue ? (
|
) : dashboards?.length === 0 && !searchString ? (
|
||||||
<div className="dashboard-empty-state">
|
<div className="dashboard-empty-state">
|
||||||
<img
|
<img
|
||||||
src="/Icons/dashboards.svg"
|
src="/Icons/dashboards.svg"
|
||||||
@@ -695,6 +724,9 @@ function DashboardsList(): JSX.Element {
|
|||||||
type="text"
|
type="text"
|
||||||
className="new-dashboard"
|
className="new-dashboard"
|
||||||
icon={<Plus size={14} />}
|
icon={<Plus size={14} />}
|
||||||
|
onClick={(): void => {
|
||||||
|
logEvent('Dashboard List: New dashboard clicked', {});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
New Dashboard
|
New Dashboard
|
||||||
</Button>
|
</Button>
|
||||||
@@ -702,6 +734,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
className="learn-more"
|
className="learn-more"
|
||||||
|
data-testid="learn-more"
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
window.open(
|
window.open(
|
||||||
'https://signoz.io/docs/userguide/manage-dashboards?utm_source=product&utm_medium=dashboard-list-empty-state',
|
'https://signoz.io/docs/userguide/manage-dashboards?utm_source=product&utm_medium=dashboard-list-empty-state',
|
||||||
@@ -721,7 +754,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Search by name, description, or tags..."
|
placeholder="Search by name, description, or tags..."
|
||||||
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
|
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
|
||||||
value={searchValue}
|
value={searchString}
|
||||||
onChange={handleSearch}
|
onChange={handleSearch}
|
||||||
/>
|
/>
|
||||||
{createNewDashboard && (
|
{createNewDashboard && (
|
||||||
@@ -735,6 +768,9 @@ function DashboardsList(): JSX.Element {
|
|||||||
type="primary"
|
type="primary"
|
||||||
className="periscope-btn primary btn"
|
className="periscope-btn primary btn"
|
||||||
icon={<Plus size={14} />}
|
icon={<Plus size={14} />}
|
||||||
|
onClick={(): void => {
|
||||||
|
logEvent('Dashboard List: New dashboard clicked', {});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
New dashboard
|
New dashboard
|
||||||
</Button>
|
</Button>
|
||||||
@@ -746,7 +782,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
<div className="no-search">
|
<div className="no-search">
|
||||||
<img src="/Icons/emptyState.svg" alt="img" className="img" />
|
<img src="/Icons/emptyState.svg" alt="img" className="img" />
|
||||||
<Typography.Text className="text">
|
<Typography.Text className="text">
|
||||||
No dashboards found for {searchValue}. Create a new dashboard?
|
No dashboards found for {searchString}. Create a new dashboard?
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -768,6 +804,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
type="text"
|
type="text"
|
||||||
className={cx('sort-btns')}
|
className={cx('sort-btns')}
|
||||||
onClick={(): void => sortHandle('createdAt')}
|
onClick={(): void => sortHandle('createdAt')}
|
||||||
|
data-testid="sort-by-last-created"
|
||||||
>
|
>
|
||||||
Last created
|
Last created
|
||||||
{sortOrder.columnKey === 'createdAt' && <Check size={14} />}
|
{sortOrder.columnKey === 'createdAt' && <Check size={14} />}
|
||||||
@@ -776,6 +813,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
type="text"
|
type="text"
|
||||||
className={cx('sort-btns')}
|
className={cx('sort-btns')}
|
||||||
onClick={(): void => sortHandle('updatedAt')}
|
onClick={(): void => sortHandle('updatedAt')}
|
||||||
|
data-testid="sort-by-last-updated"
|
||||||
>
|
>
|
||||||
Last updated
|
Last updated
|
||||||
{sortOrder.columnKey === 'updatedAt' && <Check size={14} />}
|
{sortOrder.columnKey === 'updatedAt' && <Check size={14} />}
|
||||||
@@ -786,7 +824,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
placement="bottomRight"
|
placement="bottomRight"
|
||||||
arrow={false}
|
arrow={false}
|
||||||
>
|
>
|
||||||
<ArrowDownWideNarrow size={14} />
|
<ArrowDownWideNarrow size={14} data-testid="sort-by" />
|
||||||
</Popover>
|
</Popover>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Popover
|
<Popover
|
||||||
@@ -822,16 +860,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
showSorterTooltip
|
showSorterTooltip
|
||||||
loading={isDashboardListLoading || isFilteringDashboards}
|
loading={isDashboardListLoading || isFilteringDashboards}
|
||||||
showHeader={false}
|
showHeader={false}
|
||||||
pagination={
|
pagination={paginationConfig}
|
||||||
data.length > 20 && {
|
|
||||||
pageSize: 20,
|
|
||||||
showTotal: showPaginationItem,
|
|
||||||
showSizeChanger: false,
|
|
||||||
onChange: (page): void => handlePageSizeUpdate(page),
|
|
||||||
current: Number(sortOrder.pagination),
|
|
||||||
defaultCurrent: Number(sortOrder.pagination) || 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ExclamationCircleTwoTone } from '@ant-design/icons';
|
|||||||
import MEditor, { Monaco } from '@monaco-editor/react';
|
import MEditor, { Monaco } from '@monaco-editor/react';
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Button, Modal, Space, Typography, Upload, UploadProps } from 'antd';
|
import { Button, Modal, Space, Typography, Upload, UploadProps } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import createDashboard from 'api/dashboard/create';
|
import createDashboard from 'api/dashboard/create';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
@@ -67,6 +68,8 @@ function ImportJSON({
|
|||||||
const onClickLoadJsonHandler = async (): Promise<void> => {
|
const onClickLoadJsonHandler = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
setDashboardCreating(true);
|
setDashboardCreating(true);
|
||||||
|
logEvent('Dashboard List: Import and next clicked', {});
|
||||||
|
|
||||||
const dashboardData = JSON.parse(editorValue) as DashboardData;
|
const dashboardData = JSON.parse(editorValue) as DashboardData;
|
||||||
|
|
||||||
if (dashboardData?.layout) {
|
if (dashboardData?.layout) {
|
||||||
@@ -86,6 +89,10 @@ function ImportJSON({
|
|||||||
dashboardId: response.payload.uuid,
|
dashboardId: response.payload.uuid,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
logEvent('Dashboard List: New dashboard imported successfully', {
|
||||||
|
dashboardId: response.payload?.uuid,
|
||||||
|
dashboardName: response.payload?.data?.title,
|
||||||
|
});
|
||||||
} else if (response.error === 'feature usage exceeded') {
|
} else if (response.error === 'feature usage exceeded') {
|
||||||
setIsFeatureAlert(true);
|
setIsFeatureAlert(true);
|
||||||
notifications.error({
|
notifications.error({
|
||||||
@@ -141,11 +148,6 @@ function ImportJSON({
|
|||||||
colors: {
|
colors: {
|
||||||
'editor.background': Color.BG_INK_300,
|
'editor.background': Color.BG_INK_300,
|
||||||
},
|
},
|
||||||
fontFamily: 'Space Mono',
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: 'normal',
|
|
||||||
lineHeight: 18,
|
|
||||||
letterSpacing: -0.06,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,6 +187,9 @@ function ImportJSON({
|
|||||||
type="default"
|
type="default"
|
||||||
className="periscope-btn"
|
className="periscope-btn"
|
||||||
icon={<MonitorDot size={14} />}
|
icon={<MonitorDot size={14} />}
|
||||||
|
onClick={(): void => {
|
||||||
|
logEvent('Dashboard List: Upload JSON file clicked', {});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{' '}
|
{' '}
|
||||||
{t('upload_json_file')}
|
{t('upload_json_file')}
|
||||||
@@ -233,6 +238,11 @@ function ImportJSON({
|
|||||||
fontFamily: 'Space Mono',
|
fontFamily: 'Space Mono',
|
||||||
}}
|
}}
|
||||||
theme={isDarkMode ? 'my-theme' : 'light'}
|
theme={isDarkMode ? 'my-theme' : 'light'}
|
||||||
|
onMount={(_, monaco): void => {
|
||||||
|
document.fonts.ready.then(() => {
|
||||||
|
monaco.editor.remeasureFonts();
|
||||||
|
});
|
||||||
|
}}
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
beforeMount={setEditorTheme}
|
beforeMount={setEditorTheme}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import LogDetail from 'components/LogDetail';
|
|||||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||||
import ListLogView from 'components/Logs/ListLogView';
|
import ListLogView from 'components/Logs/ListLogView';
|
||||||
import RawLogView from 'components/Logs/RawLogView';
|
import RawLogView from 'components/Logs/RawLogView';
|
||||||
|
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { CARD_BODY_STYLE } from 'constants/card';
|
import { CARD_BODY_STYLE } from 'constants/card';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
@@ -128,6 +129,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Card style={{ width: '100%' }} bodyStyle={CARD_BODY_STYLE}>
|
<Card style={{ width: '100%' }} bodyStyle={CARD_BODY_STYLE}>
|
||||||
|
<OverlayScrollbar isVirtuoso>
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
ref={ref}
|
ref={ref}
|
||||||
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
|
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
|
||||||
@@ -135,6 +137,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
|||||||
totalCount={logs.length}
|
totalCount={logs.length}
|
||||||
itemContent={getItemContent}
|
itemContent={getItemContent}
|
||||||
/>
|
/>
|
||||||
|
</OverlayScrollbar>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</InfinityWrapperStyled>
|
</InfinityWrapperStyled>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import './ContextLogRenderer.styles.scss';
|
|||||||
|
|
||||||
import { Skeleton } from 'antd';
|
import { Skeleton } from 'antd';
|
||||||
import RawLogView from 'components/Logs/RawLogView';
|
import RawLogView from 'components/Logs/RawLogView';
|
||||||
|
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||||
import ShowButton from 'container/LogsContextList/ShowButton';
|
import ShowButton from 'container/LogsContextList/ShowButton';
|
||||||
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
|
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
@@ -94,6 +95,7 @@ function ContextLogRenderer({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<OverlayScrollbar isVirtuoso>
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
className="virtuoso-list"
|
className="virtuoso-list"
|
||||||
initialTopMostItemIndex={0}
|
initialTopMostItemIndex={0}
|
||||||
@@ -101,6 +103,7 @@ function ContextLogRenderer({
|
|||||||
itemContent={getItemContent}
|
itemContent={getItemContent}
|
||||||
style={{ height: `calc(${logs.length} * 32px)` }}
|
style={{ height: `calc(${logs.length} * 32px)` }}
|
||||||
/>
|
/>
|
||||||
|
</OverlayScrollbar>
|
||||||
{isAfterLogsFetching && (
|
{isAfterLogsFetching && (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
.label {
|
.label {
|
||||||
color: var(--text-robin-400);
|
color: var(--text-robin-400);
|
||||||
font-family: SF Mono;
|
font-family: SF Mono;
|
||||||
font-family: 'Space Mono', monospace;
|
font-family: 'Geist Mono';
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: var(--font-weight-normal);
|
font-weight: var(--font-weight-normal);
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function JSONView({ logData }: JSONViewProps): JSX.Element {
|
|||||||
},
|
},
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
// fontFamily: 'SF Mono',
|
// fontFamily: 'SF Mono',
|
||||||
fontFamily: 'Space Mono',
|
fontFamily: 'Geist Mono',
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
lineHeight: '18px',
|
lineHeight: '18px',
|
||||||
colorDecorators: true,
|
colorDecorators: true,
|
||||||
|
|||||||
@@ -53,8 +53,7 @@ function Overview({
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
// fontFamily: 'SF Mono',
|
fontFamily: 'Geist Mono',
|
||||||
fontFamily: 'Space Mono',
|
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
lineHeight: '18px',
|
lineHeight: '18px',
|
||||||
colorDecorators: true,
|
colorDecorators: true,
|
||||||
@@ -80,12 +79,6 @@ function Overview({
|
|||||||
colors: {
|
colors: {
|
||||||
'editor.background': Color.BG_INK_400,
|
'editor.background': Color.BG_INK_400,
|
||||||
},
|
},
|
||||||
// fontFamily: 'SF Mono',
|
|
||||||
fontFamily: 'Space Mono',
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 'normal',
|
|
||||||
lineHeight: 18,
|
|
||||||
letterSpacing: -0.06,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +117,11 @@ function Overview({
|
|||||||
onChange={(): void => {}}
|
onChange={(): void => {}}
|
||||||
height="20vh"
|
height="20vh"
|
||||||
theme={isDarkMode ? 'my-theme' : 'light'}
|
theme={isDarkMode ? 'my-theme' : 'light'}
|
||||||
|
onMount={(_, monaco): void => {
|
||||||
|
document.fonts.ready.then(() => {
|
||||||
|
monaco.editor.remeasureFonts();
|
||||||
|
});
|
||||||
|
}}
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
beforeMount={setEditorTheme}
|
beforeMount={setEditorTheme}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -57,6 +57,8 @@
|
|||||||
background: rgba(22, 25, 34, 0.4);
|
background: rgba(22, 25, 34, 0.4);
|
||||||
|
|
||||||
.value-field {
|
.value-field {
|
||||||
|
font-family: 'Geist Mono';
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -289,7 +289,13 @@ function TableView({
|
|||||||
return (
|
return (
|
||||||
<div className="value-field">
|
<div className="value-field">
|
||||||
<CopyClipboardHOC textToCopy={textToCopy}>
|
<CopyClipboardHOC textToCopy={textToCopy}>
|
||||||
<span style={{ color: Color.BG_SIENNA_400 }}>
|
<span
|
||||||
|
style={{
|
||||||
|
color: Color.BG_SIENNA_400,
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
tabSize: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{removeEscapeCharacters(fieldData.value)}
|
{removeEscapeCharacters(fieldData.value)}
|
||||||
</span>
|
</span>
|
||||||
</CopyClipboardHOC>
|
</CopyClipboardHOC>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import './LogsContextList.styles.scss';
|
import './LogsContextList.styles.scss';
|
||||||
|
|
||||||
import RawLogView from 'components/Logs/RawLogView';
|
import RawLogView from 'components/Logs/RawLogView';
|
||||||
|
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
@@ -187,7 +188,7 @@ function LogsContextList({
|
|||||||
<EmptyText>No Data</EmptyText>
|
<EmptyText>No Data</EmptyText>
|
||||||
)}
|
)}
|
||||||
{isFetching && <Spinner size="large" height="10rem" />}
|
{isFetching && <Spinner size="large" height="10rem" />}
|
||||||
|
<OverlayScrollbar isVirtuoso>
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
className="virtuoso-list"
|
className="virtuoso-list"
|
||||||
initialTopMostItemIndex={0}
|
initialTopMostItemIndex={0}
|
||||||
@@ -195,6 +196,7 @@ function LogsContextList({
|
|||||||
itemContent={getItemContent}
|
itemContent={getItemContent}
|
||||||
followOutput={order === ORDERBY_FILTERS.DESC}
|
followOutput={order === ORDERBY_FILTERS.DESC}
|
||||||
/>
|
/>
|
||||||
|
</OverlayScrollbar>
|
||||||
</ListContainer>
|
</ListContainer>
|
||||||
|
|
||||||
{order === ORDERBY_FILTERS.DESC && (
|
{order === ORDERBY_FILTERS.DESC && (
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ export const CardStyled = styled(Card)`
|
|||||||
height: 200px;
|
height: 200px;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
padding: 0 16px 16px 16px;
|
padding: 0 16px 16px 16px;
|
||||||
font-family: 'Space Mono', monospace;
|
font-family: 'Geist Mono';
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { VIEW_TYPES } from 'components/LogDetail/constants';
|
|||||||
// components
|
// components
|
||||||
import ListLogView from 'components/Logs/ListLogView';
|
import ListLogView from 'components/Logs/ListLogView';
|
||||||
import RawLogView from 'components/Logs/RawLogView';
|
import RawLogView from 'components/Logs/RawLogView';
|
||||||
|
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { CARD_BODY_STYLE } from 'constants/card';
|
import { CARD_BODY_STYLE } from 'constants/card';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
@@ -133,6 +134,7 @@ function LogsExplorerList({
|
|||||||
style={{ width: '100%', marginTop: '20px' }}
|
style={{ width: '100%', marginTop: '20px' }}
|
||||||
bodyStyle={CARD_BODY_STYLE}
|
bodyStyle={CARD_BODY_STYLE}
|
||||||
>
|
>
|
||||||
|
<OverlayScrollbar isVirtuoso>
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
ref={ref}
|
ref={ref}
|
||||||
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
|
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
|
||||||
@@ -142,6 +144,7 @@ function LogsExplorerList({
|
|||||||
itemContent={getItemContent}
|
itemContent={getItemContent}
|
||||||
components={components}
|
components={components}
|
||||||
/>
|
/>
|
||||||
|
</OverlayScrollbar>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
@@ -169,7 +172,9 @@ function LogsExplorerList({
|
|||||||
!isFetching &&
|
!isFetching &&
|
||||||
logs.length === 0 &&
|
logs.length === 0 &&
|
||||||
!isError &&
|
!isError &&
|
||||||
isFilterApplied && <EmptyLogsSearch />}
|
isFilterApplied && (
|
||||||
|
<EmptyLogsSearch dataSource={DataSource.LOGS} panelType="LIST" />
|
||||||
|
)}
|
||||||
|
|
||||||
{isError && !isLoading && !isFetching && <LogsError />}
|
{isError && !isLoading && !isFetching && <LogsError />}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import './LogsExplorerViews.styles.scss';
|
import './LogsExplorerViews.styles.scss';
|
||||||
|
|
||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
|
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
|
||||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
@@ -37,7 +38,14 @@ import { useNotifications } from 'hooks/useNotifications';
|
|||||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||||
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
|
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
|
||||||
import { cloneDeep, defaultTo, isEmpty, omit, set } from 'lodash-es';
|
import {
|
||||||
|
cloneDeep,
|
||||||
|
defaultTo,
|
||||||
|
isEmpty,
|
||||||
|
isUndefined,
|
||||||
|
omit,
|
||||||
|
set,
|
||||||
|
} from 'lodash-es';
|
||||||
import { Sliders } from 'lucide-react';
|
import { Sliders } from 'lucide-react';
|
||||||
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
||||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
@@ -63,10 +71,10 @@ import { v4 } from 'uuid';
|
|||||||
|
|
||||||
function LogsExplorerViews({
|
function LogsExplorerViews({
|
||||||
selectedView,
|
selectedView,
|
||||||
showHistogram,
|
showFrequencyChart,
|
||||||
}: {
|
}: {
|
||||||
selectedView: SELECTED_VIEWS;
|
selectedView: SELECTED_VIEWS;
|
||||||
showHistogram: boolean;
|
showFrequencyChart: boolean;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@@ -310,6 +318,19 @@ function LogsExplorerViews({
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const logEventCalledRef = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!logEventCalledRef.current && !isUndefined(data?.payload)) {
|
||||||
|
const currentData = data?.payload?.data?.newResult?.data?.result || [];
|
||||||
|
logEvent('Logs Explorer: Page visited', {
|
||||||
|
panelType,
|
||||||
|
isEmpty: !currentData?.[0]?.list,
|
||||||
|
});
|
||||||
|
logEventCalledRef.current = true;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [data?.payload]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mutate: updateDashboard,
|
mutate: updateDashboard,
|
||||||
isLoading: isUpdateDashboardLoading,
|
isLoading: isUpdateDashboardLoading,
|
||||||
@@ -324,7 +345,7 @@ function LogsExplorerViews({
|
|||||||
}, [currentQuery]);
|
}, [currentQuery]);
|
||||||
|
|
||||||
const handleExport = useCallback(
|
const handleExport = useCallback(
|
||||||
(dashboard: Dashboard | null): void => {
|
(dashboard: Dashboard | null, isNewDashboard?: boolean): void => {
|
||||||
if (!dashboard || !panelType) return;
|
if (!dashboard || !panelType) return;
|
||||||
|
|
||||||
const panelTypeParam = AVAILABLE_EXPORT_PANEL_TYPES.includes(panelType)
|
const panelTypeParam = AVAILABLE_EXPORT_PANEL_TYPES.includes(panelType)
|
||||||
@@ -346,6 +367,12 @@ function LogsExplorerViews({
|
|||||||
options.selectColumns,
|
options.selectColumns,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
logEvent('Logs Explorer: Add to dashboard successful', {
|
||||||
|
panelType,
|
||||||
|
isNewDashboard,
|
||||||
|
dashboardName: dashboard?.data?.title,
|
||||||
|
});
|
||||||
|
|
||||||
updateDashboard(updatedDashboard, {
|
updateDashboard(updatedDashboard, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
@@ -561,7 +588,7 @@ function LogsExplorerViews({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="logs-explorer-views-container">
|
<div className="logs-explorer-views-container">
|
||||||
{showHistogram && (
|
{showFrequencyChart && (
|
||||||
<LogsExplorerChart
|
<LogsExplorerChart
|
||||||
className="logs-histogram"
|
className="logs-histogram"
|
||||||
isLoading={isFetchingListChartData || isLoadingListChartData}
|
isLoading={isFetchingListChartData || isLoadingListChartData}
|
||||||
|
|||||||
@@ -76,7 +76,10 @@ const renderer = (): RenderResult =>
|
|||||||
<VirtuosoMockContext.Provider
|
<VirtuosoMockContext.Provider
|
||||||
value={{ viewportHeight: 300, itemHeight: 100 }}
|
value={{ viewportHeight: 300, itemHeight: 100 }}
|
||||||
>
|
>
|
||||||
<LogsExplorerViews selectedView={SELECTED_VIEWS.SEARCH} showHistogram />
|
<LogsExplorerViews
|
||||||
|
selectedView={SELECTED_VIEWS.SEARCH}
|
||||||
|
showFrequencyChart
|
||||||
|
/>
|
||||||
</VirtuosoMockContext.Provider>
|
</VirtuosoMockContext.Provider>
|
||||||
</QueryBuilderProvider>
|
</QueryBuilderProvider>
|
||||||
</MockQueryClientProvider>
|
</MockQueryClientProvider>
|
||||||
@@ -120,11 +123,7 @@ describe('LogsExplorerViews -', () => {
|
|||||||
|
|
||||||
// switch to table view
|
// switch to table view
|
||||||
await userEvent.click(queryByTestId('table-view') as HTMLElement);
|
await userEvent.click(queryByTestId('table-view') as HTMLElement);
|
||||||
expect(
|
expect(queryByText('pending_data_placeholder')).toBeInTheDocument();
|
||||||
queryByText(
|
|
||||||
'Just a bit of patience, just a little bit’s enough ⎯ we’re getting your logs!',
|
|
||||||
),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('check error state', async () => {
|
it('check error state', async () => {
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import './LogsLoading.styles.scss';
|
import './LogsLoading.styles.scss';
|
||||||
|
|
||||||
import { Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
export function LogsLoading(): JSX.Element {
|
export function LogsLoading(): JSX.Element {
|
||||||
|
const { t } = useTranslation('common');
|
||||||
return (
|
return (
|
||||||
<div className="loading-logs">
|
<div className="loading-logs">
|
||||||
<div className="loading-logs-content">
|
<div className="loading-logs-content">
|
||||||
@@ -13,8 +16,7 @@ export function LogsLoading(): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Typography>
|
<Typography>
|
||||||
Just a bit of patience, just a little bit’s enough ⎯ we’re getting your
|
{t('pending_data_placeholder', { dataSource: DataSource.LOGS })}
|
||||||
logs!
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import './LogsPanelComponent.styles.scss';
|
|||||||
import { Table } from 'antd';
|
import { Table } from 'antd';
|
||||||
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 OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import Controls from 'container/Controls';
|
import Controls from 'container/Controls';
|
||||||
@@ -207,6 +208,7 @@ function LogsPanelComponent({
|
|||||||
<>
|
<>
|
||||||
<div className="logs-table">
|
<div className="logs-table">
|
||||||
<div className="resize-table">
|
<div className="resize-table">
|
||||||
|
<OverlayScrollbar>
|
||||||
<Table
|
<Table
|
||||||
pagination={false}
|
pagination={false}
|
||||||
tableLayout="fixed"
|
tableLayout="fixed"
|
||||||
@@ -218,6 +220,7 @@ function LogsPanelComponent({
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
onRow={handleRow}
|
onRow={handleRow}
|
||||||
/>
|
/>
|
||||||
|
</OverlayScrollbar>
|
||||||
</div>
|
</div>
|
||||||
{!widget.query.builder.queryData[0].limit && (
|
{!widget.query.builder.queryData[0].limit && (
|
||||||
<div className="controller">
|
<div className="controller">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { VIEW_TYPES } from 'components/LogDetail/constants';
|
|||||||
import ListLogView from 'components/Logs/ListLogView';
|
import ListLogView from 'components/Logs/ListLogView';
|
||||||
import RawLogView from 'components/Logs/RawLogView';
|
import RawLogView from 'components/Logs/RawLogView';
|
||||||
import LogsTableView from 'components/Logs/TableView';
|
import LogsTableView from 'components/Logs/TableView';
|
||||||
|
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { CARD_BODY_STYLE } from 'constants/card';
|
import { CARD_BODY_STYLE } from 'constants/card';
|
||||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||||
@@ -97,7 +98,9 @@ function LogsTable(props: LogsTableProps): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="logs-card" bodyStyle={CARD_BODY_STYLE}>
|
<Card className="logs-card" bodyStyle={CARD_BODY_STYLE}>
|
||||||
|
<OverlayScrollbar isVirtuoso>
|
||||||
<Virtuoso totalCount={logs.length} itemContent={getItemContent} />
|
<Virtuoso totalCount={logs.length} itemContent={getItemContent} />
|
||||||
|
</OverlayScrollbar>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}, [getItemContent, linesPerRow, logs, onSetActiveLog, selected, viewMode]);
|
}, [getItemContent, linesPerRow, logs, onSetActiveLog, selected, viewMode]);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Col } from 'antd';
|
import { Col } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import Graph from 'container/GridCardLayout/GridCard';
|
import Graph from 'container/GridCardLayout/GridCard';
|
||||||
@@ -11,7 +12,7 @@ import {
|
|||||||
convertRawQueriesToTraceSelectedTags,
|
convertRawQueriesToTraceSelectedTags,
|
||||||
resourceAttributesToTagFilterItems,
|
resourceAttributesToTagFilterItems,
|
||||||
} from 'hooks/useResourceAttribute/utils';
|
} from 'hooks/useResourceAttribute/utils';
|
||||||
import { useMemo, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
@@ -97,6 +98,24 @@ function DBCall(): JSX.Element {
|
|||||||
[servicename, tagFilterItems],
|
[servicename, tagFilterItems],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const logEventCalledRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!logEventCalledRef.current) {
|
||||||
|
const selectedEnvironments = queries.find(
|
||||||
|
(val) => val.tagKey === 'resource_deployment_environment',
|
||||||
|
)?.tagValue;
|
||||||
|
|
||||||
|
logEvent('APM: Service detail page visited', {
|
||||||
|
selectedEnvironments,
|
||||||
|
resourceAttributeUsed: !!queries?.length,
|
||||||
|
section: 'dbMetrics',
|
||||||
|
});
|
||||||
|
logEventCalledRef.current = true;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const apmToTraceQuery = useGetAPMToTracesQueries({
|
const apmToTraceQuery = useGetAPMToTracesQueries({
|
||||||
servicename,
|
servicename,
|
||||||
isDBCall: true,
|
isDBCall: true,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Col } from 'antd';
|
import { Col } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import Graph from 'container/GridCardLayout/GridCard';
|
import Graph from 'container/GridCardLayout/GridCard';
|
||||||
@@ -13,8 +14,9 @@ import {
|
|||||||
convertRawQueriesToTraceSelectedTags,
|
convertRawQueriesToTraceSelectedTags,
|
||||||
resourceAttributesToTagFilterItems,
|
resourceAttributesToTagFilterItems,
|
||||||
} from 'hooks/useResourceAttribute/utils';
|
} from 'hooks/useResourceAttribute/utils';
|
||||||
import { useMemo, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
@@ -93,6 +95,43 @@ function External(): JSX.Element {
|
|||||||
[servicename, tagFilterItems],
|
[servicename, tagFilterItems],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const errorApmToTraceQuery = useGetAPMToTracesQueries({
|
||||||
|
servicename,
|
||||||
|
isExternalCall: true,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
id: uuid().slice(0, 8),
|
||||||
|
key: {
|
||||||
|
key: 'hasError',
|
||||||
|
dataType: DataTypes.bool,
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'hasError--bool--tag--true',
|
||||||
|
},
|
||||||
|
op: 'in',
|
||||||
|
value: ['true'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const logEventCalledRef = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!logEventCalledRef.current) {
|
||||||
|
const selectedEnvironments = queries.find(
|
||||||
|
(val) => val.tagKey === 'resource_deployment_environment',
|
||||||
|
)?.tagValue;
|
||||||
|
|
||||||
|
logEvent('APM: Service detail page visited', {
|
||||||
|
selectedEnvironments,
|
||||||
|
resourceAttributeUsed: !!queries?.length,
|
||||||
|
section: 'externalMetrics',
|
||||||
|
});
|
||||||
|
logEventCalledRef.current = true;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const externalCallRPSWidget = useMemo(
|
const externalCallRPSWidget = useMemo(
|
||||||
() =>
|
() =>
|
||||||
getWidgetQueryBuilder({
|
getWidgetQueryBuilder({
|
||||||
@@ -156,7 +195,7 @@ function External(): JSX.Element {
|
|||||||
servicename,
|
servicename,
|
||||||
selectedTraceTags,
|
selectedTraceTags,
|
||||||
timestamp: selectedTimeStamp,
|
timestamp: selectedTimeStamp,
|
||||||
apmToTraceQuery,
|
apmToTraceQuery: errorApmToTraceQuery,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
View Traces
|
View Traces
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import getTopLevelOperations, {
|
import getTopLevelOperations, {
|
||||||
ServiceDataProps,
|
ServiceDataProps,
|
||||||
} from 'api/metrics/getTopLevelOperations';
|
} from 'api/metrics/getTopLevelOperations';
|
||||||
@@ -17,7 +18,7 @@ import useUrlQuery from 'hooks/useUrlQuery';
|
|||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||||
import { defaultTo } from 'lodash-es';
|
import { defaultTo } from 'lodash-es';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useLocation, useParams } from 'react-router-dom';
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
@@ -81,6 +82,23 @@ function Application(): JSX.Element {
|
|||||||
[handleSetTimeStamp],
|
[handleSetTimeStamp],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const logEventCalledRef = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!logEventCalledRef.current) {
|
||||||
|
const selectedEnvironments = queries.find(
|
||||||
|
(val) => val.tagKey === 'resource_deployment_environment',
|
||||||
|
)?.tagValue;
|
||||||
|
|
||||||
|
logEvent('APM: Service detail page visited', {
|
||||||
|
selectedEnvironments,
|
||||||
|
resourceAttributeUsed: !!queries?.length,
|
||||||
|
section: 'overview',
|
||||||
|
});
|
||||||
|
logEventCalledRef.current = true;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: topLevelOperations,
|
data: topLevelOperations,
|
||||||
error: topLevelOperationsError,
|
error: topLevelOperationsError,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import './ComponentSlider.styles.scss';
|
import './ComponentSlider.styles.scss';
|
||||||
|
|
||||||
import { Card, Modal } from 'antd';
|
import { Card, Modal } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import createQueryParams from 'lib/createQueryParams';
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
@@ -20,6 +21,13 @@ function DashboardGraphSlider(): JSX.Element {
|
|||||||
const onClickHandler = (name: PANEL_TYPES) => (): void => {
|
const onClickHandler = (name: PANEL_TYPES) => (): void => {
|
||||||
const id = uuid();
|
const id = uuid();
|
||||||
handleToggleDashboardSlider(false);
|
handleToggleDashboardSlider(false);
|
||||||
|
logEvent('Dashboard Detail: New panel type selected', {
|
||||||
|
// dashboardId: '',
|
||||||
|
// dashboardName: '',
|
||||||
|
// numberOfPanels: 0, // todo - at this point we don't know these attributes
|
||||||
|
panelType: name,
|
||||||
|
widgetId: id,
|
||||||
|
});
|
||||||
const queryParamsLog = {
|
const queryParamsLog = {
|
||||||
graphType: name,
|
graphType: name,
|
||||||
widgetId: id,
|
widgetId: id,
|
||||||
@@ -47,7 +55,6 @@ function DashboardGraphSlider(): JSX.Element {
|
|||||||
PANEL_TYPES_INITIAL_QUERY[name],
|
PANEL_TYPES_INITIAL_QUERY[name],
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (name === PANEL_TYPES.LIST) {
|
if (name === PANEL_TYPES.LIST) {
|
||||||
history.push(
|
history.push(
|
||||||
`${history.location.pathname}/new?${createQueryParams(queryParamsLog)}`,
|
`${history.location.pathname}/new?${createQueryParams(queryParamsLog)}`,
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
margin-right: 16px;
|
||||||
|
|
||||||
.dashboard-breadcrumbs {
|
.dashboard-breadcrumbs {
|
||||||
height: 48px;
|
height: 48px;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import './Description.styles.scss';
|
|||||||
|
|
||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
|
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
|
||||||
|
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
|
|
||||||
import DashboardSettingsContent from '../DashboardSettings';
|
import DashboardSettingsContent from '../DashboardSettings';
|
||||||
@@ -41,7 +42,9 @@ function SettingsDrawer({ drawerTitle }: { drawerTitle: string }): JSX.Element {
|
|||||||
open={visible}
|
open={visible}
|
||||||
rootClassName="settings-container-root"
|
rootClassName="settings-container-root"
|
||||||
>
|
>
|
||||||
|
<OverlayScrollbar>
|
||||||
<DashboardSettingsContent variableViewModeRef={variableViewModeRef} />
|
<DashboardSettingsContent variableViewModeRef={variableViewModeRef} />
|
||||||
|
</OverlayScrollbar>
|
||||||
</DrawerContainer>
|
</DrawerContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import './Description.styles.scss';
|
|||||||
|
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
import { Button, Card, Input, Modal, Popover, Tag, Typography } from 'antd';
|
import { Button, Card, Input, Modal, Popover, Tag, Typography } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||||
import { dashboardHelpMessage } from 'components/facingIssueBtn/util';
|
import { dashboardHelpMessage } from 'components/facingIssueBtn/util';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
@@ -126,6 +127,12 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
|
|
||||||
const onEmptyWidgetHandler = useCallback(() => {
|
const onEmptyWidgetHandler = useCallback(() => {
|
||||||
handleToggleDashboardSlider(true);
|
handleToggleDashboardSlider(true);
|
||||||
|
logEvent('Dashboard Detail: Add new panel clicked', {
|
||||||
|
dashboardId: selectedDashboard?.uuid,
|
||||||
|
dashboardName: selectedDashboard?.data.title,
|
||||||
|
numberOfPanels: selectedDashboard?.data.widgets?.length,
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [handleToggleDashboardSlider]);
|
}, [handleToggleDashboardSlider]);
|
||||||
|
|
||||||
const handleLockDashboardToggle = (): void => {
|
const handleLockDashboardToggle = (): void => {
|
||||||
@@ -259,6 +266,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
urlQuery.set('columnKey', listSortOrder.columnKey as string);
|
urlQuery.set('columnKey', listSortOrder.columnKey as string);
|
||||||
urlQuery.set('order', listSortOrder.order as string);
|
urlQuery.set('order', listSortOrder.order as string);
|
||||||
urlQuery.set('page', listSortOrder.pagination as string);
|
urlQuery.set('page', listSortOrder.pagination as string);
|
||||||
|
urlQuery.set('search', listSortOrder.search as string);
|
||||||
urlQuery.delete(QueryParams.relativeTime);
|
urlQuery.delete(QueryParams.relativeTime);
|
||||||
|
|
||||||
const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlQuery.toString()}`;
|
const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlQuery.toString()}`;
|
||||||
@@ -404,7 +412,12 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
trigger="click"
|
trigger="click"
|
||||||
placement="bottomRight"
|
placement="bottomRight"
|
||||||
>
|
>
|
||||||
<Button icon={<Ellipsis size={14} />} type="text" className="icons" />
|
<Button
|
||||||
|
icon={<Ellipsis size={14} />}
|
||||||
|
type="text"
|
||||||
|
className="icons"
|
||||||
|
data-testid="options"
|
||||||
|
/>
|
||||||
</Popover>
|
</Popover>
|
||||||
{!isDashboardLocked && editDashboard && (
|
{!isDashboardLocked && editDashboard && (
|
||||||
<SettingsDrawer drawerTitle="Dashboard Configuration" />
|
<SettingsDrawer drawerTitle="Dashboard Configuration" />
|
||||||
@@ -415,7 +428,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
onClick={onEmptyWidgetHandler}
|
onClick={onEmptyWidgetHandler}
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
type="primary"
|
type="primary"
|
||||||
data-testid="add-panel"
|
data-testid="add-panel-header"
|
||||||
>
|
>
|
||||||
New Panel
|
New Panel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import GridGraphs from './GridGraphs';
|
|||||||
function NewDashboard(): JSX.Element {
|
function NewDashboard(): JSX.Element {
|
||||||
const handle = useFullScreenHandle();
|
const handle = useFullScreenHandle();
|
||||||
return (
|
return (
|
||||||
<div style={{ overflowX: 'hidden' }}>
|
<div>
|
||||||
<Description handle={handle} />
|
<Description handle={handle} />
|
||||||
<GridGraphs handle={handle} />
|
<GridGraphs handle={handle} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -87,9 +87,6 @@ function ClickHouseQueryBuilder({
|
|||||||
'editor.background': Color.BG_INK_300,
|
'editor.background': Color.BG_INK_300,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
document.fonts.ready.then(() => {
|
|
||||||
monaco.editor.remeasureFonts();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -105,6 +102,11 @@ function ClickHouseQueryBuilder({
|
|||||||
height="200px"
|
height="200px"
|
||||||
onChange={handleUpdateEditor}
|
onChange={handleUpdateEditor}
|
||||||
value={queryData.query}
|
value={queryData.query}
|
||||||
|
onMount={(_, monaco): void => {
|
||||||
|
document.fonts.ready.then(() => {
|
||||||
|
monaco.editor.remeasureFonts();
|
||||||
|
});
|
||||||
|
}}
|
||||||
options={{
|
options={{
|
||||||
scrollbar: {
|
scrollbar: {
|
||||||
alwaysConsumeMouseWheel: false,
|
alwaysConsumeMouseWheel: false,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import './QuerySection.styles.scss';
|
|||||||
|
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Button, Tabs, Tooltip, Typography } from 'antd';
|
import { Button, Tabs, Tooltip, Typography } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import PromQLIcon from 'assets/Dashboard/PromQl';
|
import PromQLIcon from 'assets/Dashboard/PromQl';
|
||||||
import TextToolTip from 'components/TextToolTip';
|
import TextToolTip from 'components/TextToolTip';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
@@ -14,7 +15,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
|||||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import { defaultTo } from 'lodash-es';
|
import { defaultTo, isUndefined } from 'lodash-es';
|
||||||
import { Atom, Play, Terminal } from 'lucide-react';
|
import { Atom, Play, Terminal } from 'lucide-react';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import {
|
import {
|
||||||
@@ -122,6 +123,18 @@ function QuerySection({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRunQuery = (): void => {
|
const handleRunQuery = (): void => {
|
||||||
|
const widgetId = urlQuery.get('widgetId');
|
||||||
|
const isNewPanel = isUndefined(widgets?.find((e) => e.id === widgetId));
|
||||||
|
|
||||||
|
logEvent('Panel Edit: Stage and run query', {
|
||||||
|
dataSource: currentQuery.builder?.queryData?.[0]?.dataSource,
|
||||||
|
panelType: selectedWidget.panelTypes,
|
||||||
|
queryType: currentQuery.queryType,
|
||||||
|
widgetId: selectedWidget.id,
|
||||||
|
dashboardId: selectedDashboard?.uuid,
|
||||||
|
dashboardName: selectedDashboard?.data.title,
|
||||||
|
isNewPanel,
|
||||||
|
});
|
||||||
handleStageQuery(currentQuery);
|
handleStageQuery(currentQuery);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { Card, Typography } from 'antd';
|
|||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { WidgetGraphContainerProps } from 'container/NewWidget/types';
|
import { WidgetGraphContainerProps } from 'container/NewWidget/types';
|
||||||
// import useUrlQuery from 'hooks/useUrlQuery';
|
|
||||||
// import { useDashboard } from 'providers/Dashboard/Dashboard';
|
|
||||||
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
||||||
|
|
||||||
import { NotFoundContainer } from './styles';
|
import { NotFoundContainer } from './styles';
|
||||||
@@ -14,6 +12,7 @@ function WidgetGraphContainer({
|
|||||||
queryResponse,
|
queryResponse,
|
||||||
setRequestData,
|
setRequestData,
|
||||||
selectedWidget,
|
selectedWidget,
|
||||||
|
isLoadingPanelData,
|
||||||
}: WidgetGraphContainerProps): JSX.Element {
|
}: WidgetGraphContainerProps): JSX.Element {
|
||||||
if (queryResponse.data && selectedGraph === PANEL_TYPES.BAR) {
|
if (queryResponse.data && selectedGraph === PANEL_TYPES.BAR) {
|
||||||
const sortedSeriesData = getSortedSeriesData(
|
const sortedSeriesData = getSortedSeriesData(
|
||||||
@@ -38,6 +37,10 @@ function WidgetGraphContainer({
|
|||||||
return <Spinner size="large" tip="Loading..." />;
|
return <Spinner size="large" tip="Loading..." />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isLoadingPanelData) {
|
||||||
|
return <Spinner size="large" tip="Loading..." />;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
selectedGraph !== PANEL_TYPES.LIST &&
|
selectedGraph !== PANEL_TYPES.LIST &&
|
||||||
queryResponse.data?.payload.data?.result?.length === 0
|
queryResponse.data?.payload.data?.result?.length === 0
|
||||||
@@ -59,6 +62,14 @@ function WidgetGraphContainer({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (queryResponse.isIdle) {
|
||||||
|
return (
|
||||||
|
<NotFoundContainer>
|
||||||
|
<Typography>No Data</Typography>
|
||||||
|
</NotFoundContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WidgetGraph
|
<WidgetGraph
|
||||||
selectedWidget={selectedWidget}
|
selectedWidget={selectedWidget}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ function WidgetGraph({
|
|||||||
queryResponse,
|
queryResponse,
|
||||||
setRequestData,
|
setRequestData,
|
||||||
selectedWidget,
|
selectedWidget,
|
||||||
|
isLoadingPanelData,
|
||||||
}: WidgetGraphContainerProps): JSX.Element {
|
}: WidgetGraphContainerProps): JSX.Element {
|
||||||
const { currentQuery } = useQueryBuilder();
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ function WidgetGraph({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<WidgetGraphComponent
|
<WidgetGraphComponent
|
||||||
|
isLoadingPanelData={isLoadingPanelData}
|
||||||
selectedGraph={selectedGraph}
|
selectedGraph={selectedGraph}
|
||||||
queryResponse={queryResponse}
|
queryResponse={queryResponse}
|
||||||
setRequestData={setRequestData}
|
setRequestData={setRequestData}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user