Compare commits

...

43 Commits

Author SHA1 Message Date
primus-bot[bot]
3b4a8e5e0f chore(release): bump to v0.77.0 (#7502)
#### Summary
 - Release SigNoz v0.77.0
 - Bump SigNoz OTel Collector to v0.111.37

 Created by [Primus-Bot](https://github.com/apps/primus-bot)
2025-04-02 12:19:03 +05:30
Yunus M
5ef3b8ee3f feat: support request data source and improve layout (#7485)
* feat: support request data source and improve layout

* feat: update config

* feat: update config with related keywords

* update config

---------

Co-authored-by: makeavish <makeavish786@gmail.com>
2025-04-02 11:59:53 +05:30
Yunus M
597752a4bc fix: licenses in community edition & improve messaging (#7456)
Enhance platform to handle cloud, self-hosted, community, and enterprise user types with tailored routing, error handling, and feature access.
2025-04-02 01:12:42 +05:30
Nityananda Gohain
07a244f569 chore: add migration for pat to add default values (#7492)
* chore: add migration for pat to add default values

* fix: minor changes

* fix: don't panic in GetClickhouseColumnName

* fix: use new function for pat

* fix: address minor comments

* fix: address comments

* fix: remove generatepat

* fix: minor changes

* fix: remove extra check

---------

Co-authored-by: Vibhu Pandey <vibhupandey28@gmail.com>
2025-04-01 23:49:37 +05:30
sawhil
eb9385840f fix: minor error handler message 2025-04-01 22:47:48 +05:30
sawhil
30b689037a fix: minor fix 2025-04-01 22:47:48 +05:30
sawhil
ba33c885d5 fix: added auth token refresh logic in event source provider error handler 2025-04-01 22:47:48 +05:30
Amlan Kumar Nandy
a4ed9e4d47 feat: base setup for inspect metrics feature (#7490) 2025-04-01 11:47:38 +00:00
Nityananda Gohain
df5767198c fix: don't panic in GetClickhouseColumnName (#7493) 2025-03-31 22:26:37 +05:30
Vibhu Pandey
81c7f3221a feat(prometheus): create a dedicated prometheus package (#7397) 2025-03-31 14:11:11 +00:00
Amlan Kumar Nandy
2cbd8733a1 chore: remove new banner from infra monitoring tab in side nav (#7483) 2025-03-31 13:22:23 +00:00
Nityananda Gohain
71d1dfe9bd chore: use new uuid in pipelines (#7487) 2025-03-31 16:45:00 +05:30
aniketio-ctrl
459712d25c fix(nil-pointer): wrong error passed | 2262 (#7463)
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-03-31 09:47:37 +00:00
Aditya Singh
61de2d414d fix: handle 404 redirection on root route (#7454)
* fix: handle 404 redirection on root route

* fix: add home component for root route

---------

Co-authored-by: Aditya Singh <adityasingh@Adityas-MacBook-Pro.local>
2025-03-31 14:35:58 +05:30
Sahil Khan
0b7cd4c1a7 feat: api monitoring feedback - 2 (#7432)
* feat: new dropdown styles

* fix: added new tag

* feat: added endpoint name and port in endpoint details

* feat: endpoint details feedback

* feat: analytics added

* fix: title fixed

* fix: domain list breaking for non available data

* feat: added third party api feature flag

* fix: console removed

* feat: added traces corelation in api monitoring charts

* feat: added customondragselect in grid card full view to handle breaking flow

* fix: minor failsafes added:

* fix: minor ux fix

* feat: incorporated pr comments - 0
2025-03-30 03:10:43 +05:30
Shaheer Kochai
62c033ccf8 chore: trace funnels feature flag changes (#7478)
* chore: trace funnels feature flag
2025-03-29 19:33:15 +05:30
Nageshbansal
e637487984 Fix the hyperlink for otel-demo-docs in contributing guide (#7462)
Co-authored-by: CheetoDa <31571545+Calm-Rock@users.noreply.github.com>
Co-authored-by: Vibhu Pandey <vibhupandey28@gmail.com>
2025-03-28 14:58:11 +00:00
Nityananda Gohain
8fc43a00f8 fix: collector connection to opamp without orgID (#7474) 2025-03-28 20:19:37 +05:30
Srikanth Chekuri
031d62ca44 Revert "fix: added default value to time,space aggregation to fix query_range…" (#7464)
This reverts commit 8c4c357351.
2025-03-28 12:43:31 +05:30
SagarRajput-7
8c4c357351 fix: added default value to time,space aggregation to fix query_range getting 500 for metric (#7414)
* fix: added default value to time,space aggregation to fix query_range getting 500 for metric

* fix: added all available operators as default when no attribute type is present

* fix: changed operator, time and space values to avg when empty attribute type
2025-03-28 06:36:09 +00:00
SagarRajput-7
d8d8191a32 feat: allow width customisation and persist it across users and view (#7273)
* feat: removed ellipsis prop

* feat: prevent unnecessary save calls

* feat: fix dashboard detail resize icon

* feat: adjusted resizable header - set minConstraint

* feat: fixed dashboard vanishing issue

* feat: removed dependency causing maximum callstack warning

* feat: corrected the list edit view render issue and resize handler fix

* feat: style fix

* feat: removed comments

* fix: updated test cases

* feat: updated the test cases

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-03-28 06:06:40 +00:00
SagarRajput-7
a876c0a744 chore: added doc title to messaging queues (#7460) 2025-03-28 11:25:41 +05:30
Vishal Sharma
c36f913a90 fix: telemetry version function call (#7453) 2025-03-27 20:07:09 +05:30
SagarRajput-7
ed597f00c0 fix: fixed copy to clipboard popup getting flooded on every click (#7448) 2025-03-27 05:54:34 +00:00
Vibhu Pandey
4957d3ae93 feat(sqlstore): move postgres to enterprise codebase (#7445) 2025-03-27 11:16:43 +05:30
Srikanth Chekuri
8835e3493d chore: skip logfield/spanfield type in the suggestions (#7433) 2025-03-27 10:36:27 +05:30
Vibhu Pandey
027a1631ef feat(httpclient): add an extensible http client (#7446) 2025-03-26 19:33:52 +00:00
Shaheer Kochai
d7a6607a25 fix: use search v2 component for traces data source & minor improvements to search v2 component (#7404) 2025-03-26 18:00:54 +00:00
Sahil Khan
7a58bc58c9 fix: stage and run query button same url navigation enabled (#7415) 2025-03-26 23:25:01 +05:30
Srikanth Chekuri
88be23c3e3 chore: pass through substitutions for CH query (#7389) 2025-03-26 12:58:55 +00:00
Srikanth Chekuri
8f095dfbc9 fix: handle expected value less than zero (#7410) 2025-03-26 12:50:46 +00:00
aniketio-ctrl
72207691a3 fix(metrics-explorer): added time filter in inner sub queries of list and samples (#7436) 2025-03-26 09:57:21 +00:00
Raj Kamal Singh
8998ca652e chore: aws integration: bump recommended agent version (#7434) 2025-03-26 09:14:05 +00:00
Piyush Singariya
f4ae5f19ff feat: AWS Managed Streaming Kafka service integration (#7350)
* feat: msk integration

* feat: logs not available in msk

* fix: minor suggestions made by ellipsis

* fix: changes based on review, added Variables, Units, Legends, SVG

* fix: update in global variables, and query operators

* fix: update in rx tx panel, region variable query update

---------

Co-authored-by: Raj Kamal Singh <1133322+raj-k-singh@users.noreply.github.com>
2025-03-26 12:57:39 +05:30
Vikrant Gupta
d1ea608671 feat(sqlmigration): migrate invites table from bigint to uuid (#7428)
* feat(sqlmigration): added migration for schema cleanup

* feat(sqlmigration): drop sites,licenses table and added uuid v7 for saved views

* feat(sqlmigration): commit the transaction

* feat(sqlmigration): address review comments

* feat(sqlmigration): address review comments

* feat(sqlmigration): frontend changes for saved views

* feat(sqlmigration): frontend changes for saved views

* feat(sqlmigration): frontend changes for saved views

* feat(sqlmigration): frontend changes for saved views

* feat(sqlmigration): frontend changes for saved views

* feat(sqlmigration): migrate invites table from bigint to uuid

* feat(sqlmigration): add support for idempotant dialect based migration

* feat(sqlmigration): add support for idempotant dialect based migration

* feat(sqlmigration): add foreign key constraints for all new tables
2025-03-25 22:02:34 +05:30
Shivanshu Raj Shrivastava
ac7ecac2c1 chore: check http.url exists (#7429)
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-03-25 18:25:12 +05:30
Vikrant Gupta
64071165c4 feat(sqlmigration): cleanup the licenses and sites table (#7422)
* feat(sqlmigration): added migration for schema cleanup

* feat(sqlmigration): drop sites,licenses table and added uuid v7 for saved views

* feat(sqlmigration): commit the transaction

* feat(sqlmigration): address review comments

* feat(sqlmigration): address review comments

* feat(sqlmigration): frontend changes for saved views

* feat(sqlmigration): frontend changes for saved views

* feat(sqlmigration): frontend changes for saved views

* feat(sqlmigration): frontend changes for saved views

* feat(sqlmigration): frontend changes for saved views
2025-03-25 04:05:40 +05:30
SagarRajput-7
9c25a33cd9 fix: prevented infraMonitoring styles from overriding at other places (#7425) 2025-03-24 21:13:49 +05:30
Nityananda Gohain
3100d602c4 Revert "feat: adds trace funnels (#7315)" (#7423)
This reverts commit b36d2ec4c6.
2025-03-24 14:00:14 +00:00
Vibhu Pandey
d80908a1fc fix(logger): remove color encoding with json logs (#7421)
remove color encoding with json logs. Fixes the irritating: 
```
{"level":"\u001b[34mINFO\u001b[0m","timestamp":"2025-03-24T13:03:58.889Z",
```

in both enterprise and community
2025-03-24 13:22:44 +00:00
SagarRajput-7
bc17a10550 feat: added logarithmic scale option for panels (#7413) 2025-03-24 13:12:04 +00:00
SagarRajput-7
694c185373 feat: added logic to fill invalid value with null, so the chart dont have broken lines (#7412) 2025-03-24 18:28:41 +05:30
Sahil Khan
02f3dfefb9 feat: api monitoring domain details (#7308)
* feat: basic scaffolding for api monitoring & page level components added

* feat: added hardcoded attribute keys in querybuildersearcv2

* feat: added utils for formatting

* feat: api monitoring dashboard - 0

* feat: refactored the domain list

* feat: domain details drawer with all functionality added

* fix: minor eslint revert

* feat: adding ui styling to domain list table

* feat: adding pagination and minor styling to domain list table

* feat: added ui for domain details drawer all endpoints tab

* feat: added ui for domain details drawer all endpoints table with groupby

* feat: endpoint details tab ui revamped

* feat: endpoint details tab zero state styling

* fix: syntax error fixed

* feat: added conditional rendering of dep service and fixed graphs

* feat: added status code charts

* feat: added error states and loading states

* feat: added groupby persistence for endpoints

* feat: added domain navigation in the domain details drawer

* feat: added domain navigation in the domain details drawer - fix

* feat: isolated endpoint details zerostate

* feat: Implemented series aggregation with charts

* feat: ui for domain list table

* feat: react query keys added and basic pr comments resolved

* feat: fixed types

* feat: light mode fixed

* feat: empty states and light mode styling

* fix: bug with the endpoint filters

* feat: added port column and isolated endpoint in domain details

* feat: added port column and isolated endpoint in domain details - minor cleanup

* fix: minor type fix

* fix: pr comments incorporated - 0

* fix: pr comments incorporated - 1

---------

Co-authored-by: Sahil <sahil@Sahils-MacBook-Pro.local>
2025-03-24 18:01:39 +05:30
244 changed files with 13820 additions and 3078 deletions

1
.gitignore vendored
View File

@@ -54,6 +54,7 @@ ee/query-service/tests/test-deploy/data/
bin/
.local/
*/query-service/queries.active
ee/query-service/db
# e2e

View File

@@ -77,4 +77,4 @@ Need assistance? Join our Slack community:
## Where do I go from here?
- Set up your [development environment](docs/contributing/development.md)
- Deploy and observe [SigNoz in action with OpenTelemetry Demo Application](docs/otel-demo/otel-demo-docs.md)
- Deploy and observe [SigNoz in action with OpenTelemetry Demo Application](docs/otel-demo-docs.md)

View File

@@ -74,6 +74,10 @@ go-run-enterprise: ## Runs the enterprise go backend server
--use-logs-new-schema true \
--use-trace-new-schema true
.PHONY: go-test
go-test: ## Runs go unit tests
@go test -race ./...
.PHONY: go-run-community
go-run-community: ## Runs the community go backend server
@SIGNOZ_INSTRUMENTATION_LOGS_LEVEL=debug \

View File

@@ -72,7 +72,6 @@ sqlstore:
# The path to the SQLite database file.
path: /var/lib/signoz/signoz.db
##################### APIServer #####################
apiserver:
timeout:
@@ -91,20 +90,29 @@ apiserver:
- /api/v1/version
- /
##################### TelemetryStore #####################
telemetrystore:
# Specifies the telemetrystore provider to use.
provider: clickhouse
# Maximum number of idle connections in the connection pool.
max_idle_conns: 50
# Maximum number of open connections to the database.
max_open_conns: 100
# Maximum time to wait for a connection to be established.
dial_timeout: 5s
# Specifies the telemetrystore provider to use.
provider: clickhouse
clickhouse:
# The DSN to use for ClickHouse.
dsn: http://localhost:9000
# The DSN to use for clickhouse.
dsn: tcp://localhost:9000
##################### Prometheus #####################
prometheus:
active_query_tracker:
# Whether to enable the active query tracker.
enabled: true
# The path to use for the active query tracker.
path: ""
# The maximum number of concurrent queries.
max_concurrent: 20
##################### Alertmanager #####################
alertmanager:
@@ -117,7 +125,7 @@ alertmanager:
# The poll interval for periodically syncing the alertmanager with the config in the store.
poll_interval: 1m
# The URL under which Alertmanager is externally reachable (for example, if Alertmanager is served via a reverse proxy). Used for generating relative and absolute links back to Alertmanager itself.
external_url: http://localhost:9093
external_url: http://localhost:8080
# The global configuration for the alertmanager. All the exahustive fields can be found in the upstream: https://github.com/prometheus/alertmanager/blob/efa05feffd644ba4accb526e98a8c6545d26a783/config/config.go#L833
global:
# ResolveTimeout is the time after which an alert is declared resolved if it has not been updated.

View File

@@ -174,7 +174,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.76.2
image: signoz/signoz:v0.77.0
command:
- --config=/root/config/prometheus.yml
- --use-logs-new-schema=true
@@ -208,7 +208,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.111.34
image: signoz/signoz-otel-collector:v0.111.37
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -232,7 +232,7 @@ services:
- signoz
schema-migrator:
!!merge <<: *common
image: signoz/signoz-schema-migrator:v0.111.34
image: signoz/signoz-schema-migrator:v0.111.37
deploy:
restart_policy:
condition: on-failure

View File

@@ -110,7 +110,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.76.2
image: signoz/signoz:v0.77.0
command:
- --config=/root/config/prometheus.yml
- --use-logs-new-schema=true
@@ -143,7 +143,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.111.34
image: signoz/signoz-otel-collector:v0.111.37
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -167,7 +167,7 @@ services:
- signoz
schema-migrator:
!!merge <<: *common
image: signoz/signoz-schema-migrator:v0.111.34
image: signoz/signoz-schema-migrator:v0.111.37
deploy:
restart_policy:
condition: on-failure

View File

@@ -177,7 +177,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.76.2}
image: signoz/signoz:${VERSION:-v0.77.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml
@@ -212,7 +212,7 @@ services:
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.34}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.37}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -238,7 +238,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.34}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.37}
container_name: schema-migrator-sync
command:
- sync
@@ -249,7 +249,7 @@ services:
condition: service_healthy
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.34}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.37}
container_name: schema-migrator-async
command:
- async

View File

@@ -110,7 +110,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.76.2}
image: signoz/signoz:${VERSION:-v0.77.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml
@@ -146,7 +146,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.34}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.37}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -168,7 +168,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.34}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.37}
container_name: schema-migrator-sync
command:
- sync
@@ -180,7 +180,7 @@ services:
restart: on-failure
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.34}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.37}
container_name: schema-migrator-async
command:
- async

View File

@@ -110,7 +110,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.76.2}
image: signoz/signoz:${VERSION:-v0.77.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml
@@ -144,7 +144,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.34}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.37}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -166,7 +166,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.34}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.37}
container_name: schema-migrator-sync
command:
- sync
@@ -178,7 +178,7 @@ services:
restart: on-failure
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.34}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.37}
container_name: schema-migrator-async
command:
- async

View File

@@ -4,6 +4,7 @@ import (
"net/http"
"time"
eeTypes "github.com/SigNoz/signoz/ee/types"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
@@ -24,7 +25,7 @@ func (p *Pat) Wrap(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var values []string
var patToken string
var pat types.StorablePersonalAccessToken
var pat eeTypes.StorablePersonalAccessToken
for _, header := range p.headers {
values = append(values, r.Header.Get(header))

View File

@@ -313,6 +313,9 @@ func (p *BaseSeasonalProvider) getScore(
series, prevSeries, weekSeries, weekPrevSeries, past2SeasonSeries, past3SeasonSeries *v3.Series, value float64, idx int,
) float64 {
expectedValue := p.getExpectedValue(series, prevSeries, weekSeries, weekPrevSeries, past2SeasonSeries, past3SeasonSeries, idx)
if expectedValue < 0 {
expectedValue = p.getMovingAvg(prevSeries, movingAvgWindowSize, idx)
}
return (value - expectedValue) / p.getStdDev(weekSeries)
}

View File

@@ -11,7 +11,7 @@ import (
"time"
"github.com/SigNoz/signoz/ee/query-service/constants"
"github.com/SigNoz/signoz/ee/query-service/model"
eeTypes "github.com/SigNoz/signoz/ee/types"
"github.com/SigNoz/signoz/pkg/query-service/auth"
baseconstants "github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/query-service/dao"
@@ -135,19 +135,12 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
zap.String("cloudProvider", cloudProvider),
)
newPAT := model.PAT{
StorablePersonalAccessToken: types.StorablePersonalAccessToken{
Token: generatePATToken(),
UserID: integrationUser.ID,
Name: integrationPATName,
Role: baseconstants.ViewerGroup,
ExpiresAt: 0,
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
},
}
newPAT := eeTypes.NewGettablePAT(
integrationPATName,
baseconstants.ViewerGroup,
integrationUser.ID,
0,
)
integrationPAT, err := ah.AppDao().CreatePAT(ctx, orgId, newPAT)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(

View File

@@ -2,31 +2,21 @@ package api
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/SigNoz/signoz/ee/query-service/model"
"github.com/SigNoz/signoz/ee/types"
eeTypes "github.com/SigNoz/signoz/ee/types"
"github.com/SigNoz/signoz/pkg/query-service/auth"
baseconstants "github.com/SigNoz/signoz/pkg/query-service/constants"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/types"
"github.com/gorilla/mux"
"go.uber.org/zap"
)
func generatePATToken() string {
// Generate a 32-byte random token.
token := make([]byte, 32)
rand.Read(token)
// Encode the token in base64.
encodedToken := base64.StdEncoding.EncodeToString(token)
return encodedToken
}
func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
@@ -43,31 +33,18 @@ func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) {
}, nil)
return
}
pat := model.PAT{
StorablePersonalAccessToken: types.StorablePersonalAccessToken{
Name: req.Name,
Role: req.Role,
ExpiresAt: req.ExpiresInDays,
},
}
pat := eeTypes.NewGettablePAT(
req.Name,
req.Role,
user.ID,
req.ExpiresInDays,
)
err = validatePATRequest(pat)
if err != nil {
RespondError(w, model.BadRequest(err), nil)
return
}
// All the PATs are associated with the user creating the PAT.
pat.UserID = user.ID
pat.CreatedAt = time.Now()
pat.UpdatedAt = time.Now()
pat.LastUsed = 0
pat.Token = generatePATToken()
if pat.ExpiresAt != 0 {
// convert expiresAt to unix timestamp from days
pat.ExpiresAt = time.Now().Unix() + (pat.ExpiresAt * 24 * 60 * 60)
}
zap.L().Info("Got Create PAT request", zap.Any("pat", pat))
var apierr basemodel.BaseApiError
if pat, apierr = ah.AppDao().CreatePAT(ctx, user.OrgID, pat); apierr != nil {
@@ -78,7 +55,7 @@ func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) {
ah.Respond(w, &pat)
}
func validatePATRequest(req model.PAT) error {
func validatePATRequest(req types.GettablePAT) error {
if req.Role == "" || (req.Role != baseconstants.ViewerGroup && req.Role != baseconstants.EditorGroup && req.Role != baseconstants.AdminGroup) {
return fmt.Errorf("valid role is required")
}
@@ -94,7 +71,7 @@ func validatePATRequest(req model.PAT) error {
func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
req := model.PAT{}
req := types.GettablePAT{}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
RespondError(w, model.BadRequest(err), nil)
return

View File

@@ -8,8 +8,10 @@ import (
"github.com/jmoiron/sqlx"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/prometheus"
basechr "github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
"github.com/SigNoz/signoz/pkg/telemetrystore"
)
type ClickhouseReader struct {
@@ -20,8 +22,8 @@ type ClickhouseReader struct {
func NewDataConnector(
localDB *sqlx.DB,
ch clickhouse.Conn,
promConfigPath string,
telemetryStore telemetrystore.TelemetryStore,
prometheus prometheus.Prometheus,
lm interfaces.FeatureLookup,
cluster string,
useLogsNewSchema bool,
@@ -29,14 +31,10 @@ func NewDataConnector(
fluxIntervalForTraceDetail time.Duration,
cache cache.Cache,
) *ClickhouseReader {
chReader := basechr.NewReader(localDB, ch, promConfigPath, lm, cluster, useLogsNewSchema, useTraceNewSchema, fluxIntervalForTraceDetail, cache)
chReader := basechr.NewReader(localDB, telemetryStore, prometheus, lm, cluster, useLogsNewSchema, useTraceNewSchema, fluxIntervalForTraceDetail, cache)
return &ClickhouseReader{
conn: ch,
conn: telemetryStore.ClickhouseDB(),
appdb: localDB,
ClickHouseReader: chReader,
}
}
func (r *ClickhouseReader) Start(readerReady chan bool) {
r.ClickHouseReader.Start(readerReady)
}

View File

@@ -18,13 +18,14 @@ import (
"github.com/SigNoz/signoz/ee/query-service/constants"
"github.com/SigNoz/signoz/ee/query-service/dao"
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
"github.com/SigNoz/signoz/ee/query-service/interfaces"
"github.com/SigNoz/signoz/ee/query-service/rules"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/query-service/auth"
"github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/web"
@@ -49,7 +50,6 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/healthcheck"
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
pqle "github.com/SigNoz/signoz/pkg/query-service/pqlEngine"
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
"github.com/SigNoz/signoz/pkg/query-service/telemetry"
"github.com/SigNoz/signoz/pkg/query-service/utils"
@@ -137,18 +137,16 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
// set license manager as feature flag provider in dao
modelDao.SetFlagProvider(lm)
readerReady := make(chan bool)
fluxIntervalForTraceDetail, err := time.ParseDuration(serverOptions.FluxIntervalForTraceDetail)
if err != nil {
return nil, err
}
var reader interfaces.DataConnector
qb := db.NewDataConnector(
reader := db.NewDataConnector(
serverOptions.SigNoz.SQLStore.SQLxDB(),
serverOptions.SigNoz.TelemetryStore.ClickHouseDB(),
serverOptions.PromConfigPath,
serverOptions.SigNoz.TelemetryStore,
serverOptions.SigNoz.Prometheus,
lm,
serverOptions.Cluster,
serverOptions.UseLogsNewSchema,
@@ -156,8 +154,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
fluxIntervalForTraceDetail,
serverOptions.SigNoz.Cache,
)
go qb.Start(readerReady)
reader = qb
skipConfig := &basemodel.SkipConfig{}
if serverOptions.SkipTopLvlOpsPath != "" {
@@ -176,9 +172,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
c = cache.NewCache(cacheOpts)
}
<-readerReady
rm, err := makeRulesManager(
serverOptions.PromConfigPath,
serverOptions.RuleRepoURL,
serverOptions.SigNoz.SQLStore.SQLxDB(),
reader,
@@ -189,6 +183,8 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
serverOptions.UseTraceNewSchema,
serverOptions.SigNoz.Alertmanager,
serverOptions.SigNoz.SQLStore,
serverOptions.SigNoz.TelemetryStore,
serverOptions.SigNoz.Prometheus,
)
if err != nil {
@@ -233,7 +229,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
// start the usagemanager
usageManager, err := usage.New(modelDao, lm.GetRepo(), serverOptions.SigNoz.TelemetryStore.ClickHouseDB(), serverOptions.Config.TelemetryStore.ClickHouse.DSN)
usageManager, err := usage.New(modelDao, lm.GetRepo(), serverOptions.SigNoz.TelemetryStore.ClickhouseDB(), serverOptions.Config.TelemetryStore.Clickhouse.DSN)
if err != nil {
return nil, err
}
@@ -304,7 +300,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
&opAmpModel.AllAgents, agentConfMgr,
)
errorList := qb.PreloadMetricsMetadata(context.Background())
errorList := reader.PreloadMetricsMetadata(context.Background())
for _, er := range errorList {
zap.L().Error("failed to preload metrics metadata", zap.Error(er))
}
@@ -385,7 +381,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
apiHandler.RegisterMessagingQueuesRoutes(r, am)
apiHandler.RegisterThirdPartyApiRoutes(r, am)
apiHandler.MetricExplorerRoutes(r, am)
apiHandler.RegisterTraceFunnelsRoutes(r, am)
c := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
@@ -538,7 +533,6 @@ func (s *Server) Stop() error {
}
func makeRulesManager(
promConfigPath,
ruleRepoURL string,
db *sqlx.DB,
ch baseint.Reader,
@@ -549,16 +543,13 @@ func makeRulesManager(
useTraceNewSchema bool,
alertmanager alertmanager.Alertmanager,
sqlstore sqlstore.SQLStore,
telemetryStore telemetrystore.TelemetryStore,
prometheus prometheus.Prometheus,
) (*baserules.Manager, error) {
// create engine
pqle, err := pqle.FromConfigPath(promConfigPath)
if err != nil {
return nil, fmt.Errorf("failed to create pql engine : %v", err)
}
// create manager opts
managerOpts := &baserules.ManagerOptions{
PqlEngine: pqle,
TelemetryStore: telemetryStore,
Prometheus: prometheus,
RepoURL: ruleRepoURL,
DBConn: db,
Context: context.Background(),

View File

@@ -4,7 +4,6 @@ import (
"context"
"net/url"
"github.com/SigNoz/signoz/ee/query-service/model"
"github.com/SigNoz/signoz/ee/types"
basedao "github.com/SigNoz/signoz/pkg/query-service/dao"
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
@@ -36,11 +35,11 @@ type ModelDao interface {
DeleteDomain(ctx context.Context, id uuid.UUID) basemodel.BaseApiError
GetDomainByEmail(ctx context.Context, email string) (*types.GettableOrgDomain, basemodel.BaseApiError)
CreatePAT(ctx context.Context, orgID string, p model.PAT) (model.PAT, basemodel.BaseApiError)
UpdatePAT(ctx context.Context, orgID string, p model.PAT, id string) basemodel.BaseApiError
GetPAT(ctx context.Context, pat string) (*model.PAT, basemodel.BaseApiError)
GetPATByID(ctx context.Context, orgID string, id string) (*model.PAT, basemodel.BaseApiError)
CreatePAT(ctx context.Context, orgID string, p types.GettablePAT) (types.GettablePAT, basemodel.BaseApiError)
UpdatePAT(ctx context.Context, orgID string, p types.GettablePAT, id string) basemodel.BaseApiError
GetPAT(ctx context.Context, pat string) (*types.GettablePAT, basemodel.BaseApiError)
GetPATByID(ctx context.Context, orgID string, id string) (*types.GettablePAT, basemodel.BaseApiError)
GetUserByPAT(ctx context.Context, orgID string, token string) (*ossTypes.GettableUser, basemodel.BaseApiError)
ListPATs(ctx context.Context, orgID string) ([]model.PAT, basemodel.BaseApiError)
ListPATs(ctx context.Context, orgID string) ([]types.GettablePAT, basemodel.BaseApiError)
RevokePAT(ctx context.Context, orgID string, id string, userID string) basemodel.BaseApiError
}

View File

@@ -6,41 +6,47 @@ import (
"time"
"github.com/SigNoz/signoz/ee/query-service/model"
"github.com/SigNoz/signoz/ee/types"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/types"
ossTypes "github.com/SigNoz/signoz/pkg/types"
"go.uber.org/zap"
)
func (m *modelDao) CreatePAT(ctx context.Context, orgID string, p model.PAT) (model.PAT, basemodel.BaseApiError) {
func (m *modelDao) CreatePAT(ctx context.Context, orgID string, p types.GettablePAT) (types.GettablePAT, basemodel.BaseApiError) {
p.StorablePersonalAccessToken.OrgID = orgID
_, err := m.DB().NewInsert().
Model(&p.StorablePersonalAccessToken).
Returning("id").
Exec(ctx)
if err != nil {
zap.L().Error("Failed to insert PAT in db, err: %v", zap.Error(err))
return model.PAT{}, model.InternalError(fmt.Errorf("PAT insertion failed"))
return types.GettablePAT{}, model.InternalError(fmt.Errorf("PAT insertion failed"))
}
createdByUser, _ := m.GetUser(ctx, p.UserID)
if createdByUser == nil {
p.CreatedByUser = model.User{
p.CreatedByUser = types.PatUser{
NotFound: true,
}
} else {
p.CreatedByUser = model.User{
Id: createdByUser.ID,
Name: createdByUser.Name,
Email: createdByUser.Email,
CreatedAt: createdByUser.CreatedAt.Unix(),
ProfilePictureURL: createdByUser.ProfilePictureURL,
NotFound: false,
p.CreatedByUser = types.PatUser{
User: ossTypes.User{
ID: createdByUser.ID,
Name: createdByUser.Name,
Email: createdByUser.Email,
TimeAuditable: ossTypes.TimeAuditable{
CreatedAt: createdByUser.CreatedAt,
UpdatedAt: createdByUser.UpdatedAt,
},
ProfilePictureURL: createdByUser.ProfilePictureURL,
},
NotFound: false,
}
}
return p, nil
}
func (m *modelDao) UpdatePAT(ctx context.Context, orgID string, p model.PAT, id string) basemodel.BaseApiError {
func (m *modelDao) UpdatePAT(ctx context.Context, orgID string, p types.GettablePAT, id string) basemodel.BaseApiError {
_, err := m.DB().NewUpdate().
Model(&p.StorablePersonalAccessToken).
Column("role", "name", "updated_at", "updated_by_user_id").
@@ -55,7 +61,7 @@ func (m *modelDao) UpdatePAT(ctx context.Context, orgID string, p model.PAT, id
return nil
}
func (m *modelDao) ListPATs(ctx context.Context, orgID string) ([]model.PAT, basemodel.BaseApiError) {
func (m *modelDao) ListPATs(ctx context.Context, orgID string) ([]types.GettablePAT, basemodel.BaseApiError) {
pats := []types.StorablePersonalAccessToken{}
if err := m.DB().NewSelect().
@@ -68,41 +74,51 @@ func (m *modelDao) ListPATs(ctx context.Context, orgID string) ([]model.PAT, bas
return nil, model.InternalError(fmt.Errorf("failed to fetch PATs"))
}
patsWithUsers := []model.PAT{}
patsWithUsers := []types.GettablePAT{}
for i := range pats {
patWithUser := model.PAT{
patWithUser := types.GettablePAT{
StorablePersonalAccessToken: pats[i],
}
createdByUser, _ := m.GetUser(ctx, pats[i].UserID)
if createdByUser == nil {
patWithUser.CreatedByUser = model.User{
patWithUser.CreatedByUser = types.PatUser{
NotFound: true,
}
} else {
patWithUser.CreatedByUser = model.User{
Id: createdByUser.ID,
Name: createdByUser.Name,
Email: createdByUser.Email,
CreatedAt: createdByUser.CreatedAt.Unix(),
ProfilePictureURL: createdByUser.ProfilePictureURL,
NotFound: false,
patWithUser.CreatedByUser = types.PatUser{
User: ossTypes.User{
ID: createdByUser.ID,
Name: createdByUser.Name,
Email: createdByUser.Email,
TimeAuditable: ossTypes.TimeAuditable{
CreatedAt: createdByUser.CreatedAt,
UpdatedAt: createdByUser.UpdatedAt,
},
ProfilePictureURL: createdByUser.ProfilePictureURL,
},
NotFound: false,
}
}
updatedByUser, _ := m.GetUser(ctx, pats[i].UpdatedByUserID)
if updatedByUser == nil {
patWithUser.UpdatedByUser = model.User{
patWithUser.UpdatedByUser = types.PatUser{
NotFound: true,
}
} else {
patWithUser.UpdatedByUser = model.User{
Id: updatedByUser.ID,
Name: updatedByUser.Name,
Email: updatedByUser.Email,
CreatedAt: updatedByUser.CreatedAt.Unix(),
ProfilePictureURL: updatedByUser.ProfilePictureURL,
NotFound: false,
patWithUser.UpdatedByUser = types.PatUser{
User: ossTypes.User{
ID: updatedByUser.ID,
Name: updatedByUser.Name,
Email: updatedByUser.Email,
TimeAuditable: ossTypes.TimeAuditable{
CreatedAt: updatedByUser.CreatedAt,
UpdatedAt: updatedByUser.UpdatedAt,
},
ProfilePictureURL: updatedByUser.ProfilePictureURL,
},
NotFound: false,
}
}
@@ -128,7 +144,7 @@ func (m *modelDao) RevokePAT(ctx context.Context, orgID string, id string, userI
return nil
}
func (m *modelDao) GetPAT(ctx context.Context, token string) (*model.PAT, basemodel.BaseApiError) {
func (m *modelDao) GetPAT(ctx context.Context, token string) (*types.GettablePAT, basemodel.BaseApiError) {
pats := []types.StorablePersonalAccessToken{}
if err := m.DB().NewSelect().
@@ -146,14 +162,14 @@ func (m *modelDao) GetPAT(ctx context.Context, token string) (*model.PAT, basemo
}
}
patWithUser := model.PAT{
patWithUser := types.GettablePAT{
StorablePersonalAccessToken: pats[0],
}
return &patWithUser, nil
}
func (m *modelDao) GetPATByID(ctx context.Context, orgID string, id string) (*model.PAT, basemodel.BaseApiError) {
func (m *modelDao) GetPATByID(ctx context.Context, orgID string, id string) (*types.GettablePAT, basemodel.BaseApiError) {
pats := []types.StorablePersonalAccessToken{}
if err := m.DB().NewSelect().
@@ -172,7 +188,7 @@ func (m *modelDao) GetPATByID(ctx context.Context, orgID string, id string) (*mo
}
}
patWithUser := model.PAT{
patWithUser := types.GettablePAT{
StorablePersonalAccessToken: pats[0],
}
@@ -180,8 +196,8 @@ func (m *modelDao) GetPATByID(ctx context.Context, orgID string, id string) (*mo
}
// deprecated
func (m *modelDao) GetUserByPAT(ctx context.Context, orgID string, token string) (*types.GettableUser, basemodel.BaseApiError) {
users := []types.GettableUser{}
func (m *modelDao) GetUserByPAT(ctx context.Context, orgID string, token string) (*ossTypes.GettableUser, basemodel.BaseApiError) {
users := []ossTypes.GettableUser{}
if err := m.DB().NewSelect().
Model(&users).

View File

@@ -7,6 +7,5 @@ import (
// Connector defines methods for interaction
// with o11y data. for example - clickhouse
type DataConnector interface {
Start(readerReady chan bool)
baseint.Reader
}

View File

@@ -7,34 +7,29 @@ import (
"time"
"github.com/SigNoz/signoz/ee/query-service/app"
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
"github.com/SigNoz/signoz/pkg/config"
"github.com/SigNoz/signoz/pkg/config/envprovider"
"github.com/SigNoz/signoz/pkg/config/fileprovider"
"github.com/SigNoz/signoz/pkg/query-service/auth"
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/version"
prommodel "github.com/prometheus/common/model"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func initZapLog() *zap.Logger {
config := zap.NewProductionConfig()
config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
config.EncoderConfig.TimeKey = "timestamp"
config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
logger, _ := config.Build()
return logger
}
func init() {
prommodel.NameValidationScheme = prommodel.UTF8Validation
}
func main() {
var promConfigPath, skipTopLvlOpsPath string
@@ -88,6 +83,7 @@ func main() {
MaxIdleConns: maxIdleConns,
MaxOpenConns: maxOpenConns,
DialTimeout: dialTimeout,
Config: promConfigPath,
})
if err != nil {
zap.L().Fatal("Failed to create config", zap.Error(err))
@@ -95,16 +91,21 @@ func main() {
version.Info.PrettyPrint(config.Version)
sqlStoreFactories := signoz.NewSQLStoreProviderFactories()
if err := sqlStoreFactories.Add(postgressqlstore.NewFactory(sqlstorehook.NewLoggingFactory())); err != nil {
zap.L().Fatal("Failed to add postgressqlstore factory", zap.Error(err))
}
signoz, err := signoz.New(
context.Background(),
config,
signoz.NewCacheProviderFactories(),
signoz.NewWebProviderFactories(),
signoz.NewSQLStoreProviderFactories(),
sqlStoreFactories,
signoz.NewTelemetryStoreProviderFactories(),
)
if err != nil {
zap.L().Fatal("Failed to create signoz struct", zap.Error(err))
zap.L().Fatal("Failed to create signoz", zap.Error(err))
}
jwtSecret := os.Getenv("SIGNOZ_JWT_SECRET")

View File

@@ -1,25 +1,7 @@
package model
import "github.com/SigNoz/signoz/pkg/types"
type User struct {
Id string `json:"id" db:"id"`
Name string `json:"name" db:"name"`
Email string `json:"email" db:"email"`
CreatedAt int64 `json:"createdAt" db:"created_at"`
ProfilePictureURL string `json:"profilePictureURL" db:"profile_picture_url"`
NotFound bool `json:"notFound"`
}
type CreatePATRequestBody struct {
Name string `json:"name"`
Role string `json:"role"`
ExpiresInDays int64 `json:"expiresInDays"`
}
type PAT struct {
CreatedByUser User `json:"createdByUser"`
UpdatedByUser User `json:"updatedByUser"`
types.StorablePersonalAccessToken
}

View File

@@ -157,6 +157,13 @@ var BasicPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.TraceFunnels,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}
var ProPlan = basemodel.FeatureSet{
@@ -279,6 +286,13 @@ var ProPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.TraceFunnels,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}
var EnterprisePlan = basemodel.FeatureSet{
@@ -415,4 +429,11 @@ var EnterprisePlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.TraceFunnels,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}

View File

@@ -48,7 +48,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
opts.Rule,
opts.Logger,
opts.Reader,
opts.ManagerOpts.PqlEngine,
opts.ManagerOpts.Prometheus,
baserules.WithSQLStore(opts.SQLStore),
)
@@ -145,7 +145,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
parsedRule,
opts.Logger,
opts.Reader,
opts.ManagerOpts.PqlEngine,
opts.ManagerOpts.Prometheus,
baserules.WithSendAlways(),
baserules.WithSendUnmatched(),
baserules.WithSQLStore(opts.SQLStore),

View File

@@ -0,0 +1,220 @@
package postgressqlstore
import (
"context"
"fmt"
"reflect"
"github.com/uptrace/bun"
)
type dialect struct {
}
func (dialect *dialect) MigrateIntToTimestamp(ctx context.Context, bun bun.IDB, table string, column string) error {
columnType, err := dialect.GetColumnType(ctx, bun, table, column)
if err != nil {
return err
}
// bigint for postgres and INTEGER for sqlite
if columnType != "bigint" {
return nil
}
// if the columns is integer then do this
if _, err := bun.
ExecContext(ctx, `ALTER TABLE `+table+` RENAME COLUMN `+column+` TO `+column+`_old`); err != nil {
return err
}
// add new timestamp column
if _, err := bun.
NewAddColumn().
Table(table).
ColumnExpr(column + " TIMESTAMP").
Exec(ctx); err != nil {
return err
}
if _, err := bun.
NewUpdate().
Table(table).
Set(column + " = to_timestamp(cast(" + column + "_old as INTEGER))").
Where("1=1").
Exec(ctx); err != nil {
return err
}
// drop old column
if _, err := bun.
NewDropColumn().
Table(table).
Column(column + "_old").
Exec(ctx); err != nil {
return err
}
return nil
}
func (dialect *dialect) MigrateIntToBoolean(ctx context.Context, bun bun.IDB, table string, column string) error {
columnType, err := dialect.GetColumnType(ctx, bun, table, column)
if err != nil {
return err
}
if columnType != "bigint" {
return nil
}
if _, err := bun.
ExecContext(ctx, `ALTER TABLE `+table+` RENAME COLUMN `+column+` TO `+column+`_old`); err != nil {
return err
}
// add new boolean column
if _, err := bun.
NewAddColumn().
Table(table).
ColumnExpr(column + " BOOLEAN").
Exec(ctx); err != nil {
return err
}
// copy data from old column to new column, converting from int to boolean
if _, err := bun.NewUpdate().
Table(table).
Set(column + " = CASE WHEN " + column + "_old = 1 THEN true ELSE false END").
Where("1=1").
Exec(ctx); err != nil {
return err
}
// drop old column
if _, err := bun.NewDropColumn().Table(table).Column(column + "_old").Exec(ctx); err != nil {
return err
}
return nil
}
func (dialect *dialect) GetColumnType(ctx context.Context, bun bun.IDB, table string, column string) (string, error) {
var columnType string
err := bun.NewSelect().
ColumnExpr("data_type").
TableExpr("information_schema.columns").
Where("table_name = ?", table).
Where("column_name = ?", column).
Scan(ctx, &columnType)
if err != nil {
return "", err
}
return columnType, nil
}
func (dialect *dialect) ColumnExists(ctx context.Context, bun bun.IDB, table string, column string) (bool, error) {
var count int
err := bun.NewSelect().
ColumnExpr("COUNT(*)").
TableExpr("information_schema.columns").
Where("table_name = ?", table).
Where("column_name = ?", column).
Scan(ctx, &count)
if err != nil {
return false, err
}
return count > 0, nil
}
func (dialect *dialect) RenameColumn(ctx context.Context, bun bun.IDB, table string, oldColumnName string, newColumnName string) (bool, error) {
oldColumnExists, err := dialect.ColumnExists(ctx, bun, table, oldColumnName)
if err != nil {
return false, err
}
newColumnExists, err := dialect.ColumnExists(ctx, bun, table, newColumnName)
if err != nil {
return false, err
}
if !oldColumnExists && newColumnExists {
return true, nil
}
_, err = bun.
ExecContext(ctx, "ALTER TABLE "+table+" RENAME COLUMN "+oldColumnName+" TO "+newColumnName)
if err != nil {
return false, err
}
return true, nil
}
func (dialect *dialect) TableExists(ctx context.Context, bun bun.IDB, table interface{}) (bool, error) {
count := 0
err := bun.
NewSelect().
ColumnExpr("count(*)").
Table("pg_catalog.pg_tables").
Where("tablename = ?", bun.Dialect().Tables().Get(reflect.TypeOf(table)).Name).
Scan(ctx, &count)
if err != nil {
return false, err
}
if count == 0 {
return false, nil
}
return true, nil
}
func (dialect *dialect) RenameTableAndModifyModel(ctx context.Context, bun bun.IDB, oldModel interface{}, newModel interface{}, cb func(context.Context) error) error {
exists, err := dialect.TableExists(ctx, bun, newModel)
if err != nil {
return err
}
if exists {
return nil
}
_, err = bun.
NewCreateTable().
IfNotExists().
Model(newModel).
Exec(ctx)
if err != nil {
return err
}
err = cb(ctx)
if err != nil {
return err
}
_, err = bun.
NewDropTable().
IfExists().
Model(oldModel).
Exec(ctx)
if err != nil {
return err
}
return nil
}
func (dialect *dialect) AddNotNullDefaultToColumn(ctx context.Context, bun bun.IDB, table string, column, columnType, defaultValue string) error {
query := fmt.Sprintf("ALTER TABLE %s ALTER COLUMN %s SET DEFAULT %s, ALTER COLUMN %s SET NOT NULL", table, column, defaultValue, column)
if _, err := bun.ExecContext(ctx, query); err != nil {
return err
}
return nil
}

View File

@@ -18,7 +18,7 @@ type provider struct {
sqldb *sql.DB
bundb *sqlstore.BunDB
sqlxdb *sqlx.DB
dialect *PGDialect
dialect *dialect
}
func NewFactory(hookFactories ...factory.ProviderFactory[sqlstore.SQLStoreHook, sqlstore.Config]) factory.ProviderFactory[sqlstore.SQLStore, sqlstore.Config] {
@@ -60,7 +60,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
sqldb: sqldb,
bundb: sqlstore.NewBunDB(settings, sqldb, pgdialect.New(), hooks),
sqlxdb: sqlx.NewDb(sqldb, "postgres"),
dialect: &PGDialect{},
dialect: new(dialect),
}, nil
}

View File

@@ -0,0 +1,73 @@
package types
import (
"crypto/rand"
"encoding/base64"
"time"
"github.com/SigNoz/signoz/pkg/types"
"github.com/uptrace/bun"
)
type GettablePAT struct {
CreatedByUser PatUser `json:"createdByUser"`
UpdatedByUser PatUser `json:"updatedByUser"`
StorablePersonalAccessToken
}
type PatUser struct {
types.User
NotFound bool `json:"notFound"`
}
func NewGettablePAT(name, role, userID string, expiresAt int64) GettablePAT {
return GettablePAT{
StorablePersonalAccessToken: NewStorablePersonalAccessToken(name, role, userID, expiresAt),
}
}
type StorablePersonalAccessToken struct {
bun.BaseModel `bun:"table:personal_access_tokens"`
types.TimeAuditable
OrgID string `json:"orgId" bun:"org_id,type:text,notnull"`
ID int `json:"id" bun:"id,pk,autoincrement"`
Role string `json:"role" bun:"role,type:text,notnull,default:'ADMIN'"`
UserID string `json:"userId" bun:"user_id,type:text,notnull"`
Token string `json:"token" bun:"token,type:text,notnull,unique"`
Name string `json:"name" bun:"name,type:text,notnull"`
ExpiresAt int64 `json:"expiresAt" bun:"expires_at,notnull,default:0"`
LastUsed int64 `json:"lastUsed" bun:"last_used,notnull,default:0"`
Revoked bool `json:"revoked" bun:"revoked,notnull,default:false"`
UpdatedByUserID string `json:"updatedByUserId" bun:"updated_by_user_id,type:text,notnull,default:''"`
}
func NewStorablePersonalAccessToken(name, role, userID string, expiresAt int64) StorablePersonalAccessToken {
now := time.Now()
if expiresAt != 0 {
// convert expiresAt to unix timestamp from days
expiresAt = now.Unix() + (expiresAt * 24 * 60 * 60)
}
// Generate a 32-byte random token.
token := make([]byte, 32)
rand.Read(token)
// Encode the token in base64.
encodedToken := base64.StdEncoding.EncodeToString(token)
return StorablePersonalAccessToken{
Token: encodedToken,
Name: name,
Role: role,
UserID: userID,
ExpiresAt: expiresAt,
LastUsed: 0,
Revoked: false,
UpdatedByUserID: "",
TimeAuditable: types.TimeAuditable{
CreatedAt: now,
UpdatedAt: now,
},
}
}

View File

@@ -90,7 +90,7 @@
"less": "^4.1.2",
"less-loader": "^10.2.0",
"lodash-es": "^4.17.21",
"lucide-react": "0.379.0",
"lucide-react": "0.427.0",
"mini-css-extract-plugin": "2.4.5",
"motion": "12.4.13",
"overlayscrollbars": "^2.8.1",

View File

@@ -60,10 +60,14 @@
"INTEGRATIONS": "SigNoz | Integrations",
"ALERT_HISTORY": "SigNoz | Alert Rule History",
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
"MESSAGING_QUEUES": "SigNoz | Messaging Queues",
"MESSAGING_QUEUES_OVERVIEW": "SigNoz | Messaging Queues",
"MESSAGING_QUEUES_KAFKA": "SigNoz | Messaging Queues | Kafka",
"MESSAGING_QUEUES_KAFKA_DETAIL": "SigNoz | Messaging Queues | Kafka",
"MESSAGING_QUEUES_CELERY_TASK": "SigNoz | Messaging Queues | Celery",
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
"METRICS_EXPLORER": "SigNoz | Metrics Explorer",
"METRICS_EXPLORER_EXPLORER": "SigNoz | Metrics Explorer",
"METRICS_EXPLORER_VIEWS": "SigNoz | Metrics Explorer"
"METRICS_EXPLORER_VIEWS": "SigNoz | Metrics Explorer",
"API_MONITORING": "SigNoz | API Monitoring"
}

View File

@@ -1,3 +1,4 @@
import * as Sentry from '@sentry/react';
import { ConfigProvider } from 'antd';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
@@ -15,6 +16,7 @@ import { LICENSE_PLAN_KEY } from 'hooks/useLicense';
import { NotificationProvider } from 'hooks/useNotifications';
import { ResourceProvider } from 'hooks/useResourceAttribute';
import history from 'lib/history';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import posthog from 'posthog-js';
import AlertRuleProvider from 'providers/Alert';
import { useAppContext } from 'providers/App/App';
@@ -26,6 +28,7 @@ import { Route, Router, Switch } from 'react-router-dom';
import { CompatRouter } from 'react-router-dom-v5-compat';
import { extractDomain } from 'utils/app';
import { Home } from './pageComponents';
import PrivateRoute from './Private';
import defaultRoutes, {
AppRoutes,
@@ -45,7 +48,6 @@ function App(): JSX.Element {
activeLicenseV3,
isFetchingActiveLicenseV3,
userFetchError,
licensesFetchError,
featureFlagsFetchError,
isLoggedIn: isLoggedInState,
featureFlags,
@@ -55,10 +57,7 @@ function App(): JSX.Element {
const { hostname, pathname } = window.location;
const {
isCloudUser: isCloudUserVal,
isEECloudUser: isEECloudUserVal,
} = useGetTenantLicense();
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
const enableAnalytics = useCallback(
(user: IUser): void => {
@@ -168,7 +167,7 @@ function App(): JSX.Element {
let updatedRoutes = defaultRoutes;
// if the user is a cloud user
if (isCloudUserVal || isEECloudUserVal) {
if (isCloudUser || isEnterpriseSelfHostedUser) {
// if the user is on basic plan then remove billing
if (isOnBasicPlan) {
updatedRoutes = updatedRoutes.filter(
@@ -190,10 +189,10 @@ function App(): JSX.Element {
isLoggedInState,
user,
licenses,
isCloudUserVal,
isCloudUser,
isEnterpriseSelfHostedUser,
isFetchingLicenses,
isFetchingUser,
isEECloudUserVal,
]);
useEffect(() => {
@@ -208,6 +207,7 @@ function App(): JSX.Element {
}
}, [pathname]);
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => {
// feature flag shouldn't be loading and featureFlags or fetchError any one of this should be true indicating that req is complete
// licenses should also be present. there is no check for licenses for loading and error as that is mandatory if not present then routing
@@ -233,7 +233,12 @@ function App(): JSX.Element {
const showAddCreditCardModal =
!isPremiumSupportEnabled && !trialInfo?.trialConvertedToSubscription;
if (isLoggedInState && isChatSupportEnabled && !showAddCreditCardModal) {
if (
isLoggedInState &&
isChatSupportEnabled &&
!showAddCreditCardModal &&
(isCloudUser || isEnterpriseSelfHostedUser)
) {
window.Intercom('boot', {
app_id: process.env.INTERCOM_APP_ID,
email: user?.email || '',
@@ -252,13 +257,53 @@ function App(): JSX.Element {
licenses,
activeLicenseV3,
trialInfo,
isCloudUser,
isEnterpriseSelfHostedUser,
]);
useEffect(() => {
if (!isFetchingUser && isCloudUserVal && user && user.email) {
if (!isFetchingUser && isCloudUser && user && user.email) {
enableAnalytics(user);
}
}, [user, isFetchingUser, isCloudUserVal, enableAnalytics]);
}, [user, isFetchingUser, isCloudUser, enableAnalytics]);
useEffect(() => {
if (isCloudUser || isEnterpriseSelfHostedUser) {
if (process.env.POSTHOG_KEY) {
posthog.init(process.env.POSTHOG_KEY, {
api_host: 'https://us.i.posthog.com',
person_profiles: 'identified_only', // or 'always' to create profiles for anonymous users as well
});
}
Sentry.init({
dsn: process.env.SENTRY_DSN,
tunnel: process.env.TUNNEL_URL,
environment: 'production',
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration({
maskAllText: false,
blockAllMedia: false,
}),
],
// Performance Monitoring
tracesSampleRate: 1.0, // Capture 100% of the transactions
// Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled
tracePropagationTargets: [],
// Session Replay
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
});
} else {
posthog.reset();
Sentry.close();
if (window.cioanalytics && typeof window.cioanalytics.reset === 'function') {
window.cioanalytics.reset();
}
}
}, [isCloudUser, isEnterpriseSelfHostedUser]);
// if the user is in logged in state
if (isLoggedInState) {
@@ -270,60 +315,55 @@ function App(): JSX.Element {
// if the required calls fails then return a something went wrong error
// this needs to be on top of data missing error because if there is an error, data will never be loaded and it will
// move to indefinitive loading
if (
(userFetchError || licensesFetchError) &&
pathname !== ROUTES.SOMETHING_WENT_WRONG
) {
if (userFetchError && pathname !== ROUTES.SOMETHING_WENT_WRONG) {
history.replace(ROUTES.SOMETHING_WENT_WRONG);
}
// if all of the data is not set then return a spinner, this is required because there is some gap between loading states and data setting
if (
(!licenses || !user.email || !featureFlags) &&
!userFetchError &&
!licensesFetchError
) {
if ((!licenses || !user.email || !featureFlags) && !userFetchError) {
return <Spinner tip="Loading..." />;
}
}
return (
<ConfigProvider theme={themeConfig}>
<Router history={history}>
<CompatRouter>
<NotificationProvider>
<PrivateRoute>
<ResourceProvider>
<QueryBuilderProvider>
<DashboardProvider>
<KeyboardHotkeysProvider>
<AlertRuleProvider>
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</AlertRuleProvider>
</KeyboardHotkeysProvider>
</DashboardProvider>
</QueryBuilderProvider>
</ResourceProvider>
</PrivateRoute>
</NotificationProvider>
</CompatRouter>
</Router>
</ConfigProvider>
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<ConfigProvider theme={themeConfig}>
<Router history={history}>
<CompatRouter>
<NotificationProvider>
<PrivateRoute>
<ResourceProvider>
<QueryBuilderProvider>
<DashboardProvider>
<KeyboardHotkeysProvider>
<AlertRuleProvider>
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route exact path="/" component={Home} />
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</AlertRuleProvider>
</KeyboardHotkeysProvider>
</DashboardProvider>
</QueryBuilderProvider>
</ResourceProvider>
</PrivateRoute>
</NotificationProvider>
</CompatRouter>
</Router>
</ConfigProvider>
</Sentry.ErrorBoundary>
);
}

View File

@@ -295,3 +295,7 @@ export const MetricsExplorer = Loadable(
() =>
import(/* webpackChunkName: "MetricsExplorer" */ 'pages/MetricsExplorer'),
);
export const ApiMonitoring = Loadable(
() => import(/* webpackChunkName: "ApiMonitoring" */ 'pages/ApiMonitoring'),
);

View File

@@ -8,6 +8,7 @@ import {
AllAlertChannels,
AllErrors,
APIKeys,
ApiMonitoring,
BillingPage,
CreateAlertChannelAlerts,
CreateNewAlerts,
@@ -497,6 +498,13 @@ const routes: AppRoutes[] = [
key: 'METRICS_EXPLORER_VIEWS',
isPrivate: true,
},
{
path: ROUTES.API_MONITORING,
exact: true,
component: ApiMonitoring,
key: 'API_MONITORING',
isPrivate: true,
},
];
export const SUPPORT_ROUTE: AppRoutes = {

View File

@@ -521,7 +521,7 @@ export default function CeleryOverviewTable({
locale={{
emptyText: isLoading ? null : <Typography.Text>No data</Typography.Text>,
}}
scroll={{ x: true }}
scroll={{ x: 'max-content' }}
showSorterTooltip
onDragColumn={handleDragColumn}
onRow={(record): { onClick: () => void; className: string } => ({

View File

@@ -199,12 +199,12 @@ function ExplorerCard({
value={viewName || undefined}
>
{viewsData?.data.data.map((view) => (
<Select.Option key={view.uuid} value={view.name}>
<Select.Option key={view.id} value={view.name}>
<MenuItemGenerator
viewName={view.name}
viewKey={viewKey}
createdBy={view.createdBy}
uuid={view.uuid}
uuid={view.id}
refetchAllView={refetchAllView}
viewData={viewsData.data.data}
sourcePage={sourcepage}

View File

@@ -53,17 +53,12 @@ function MenuItemGenerator({
({ key }: { key: string }): void => {
const currentViewDetails = getViewDetailsUsingViewKey(key, viewData);
if (!currentViewDetails) return;
const {
query,
name,
uuid,
panelType: currentPanelType,
} = currentViewDetails;
const { query, name, id, panelType: currentPanelType } = currentViewDetails;
handleExplorerTabChange(currentPanelType, {
query,
name,
uuid,
id,
});
},
[viewData, handleExplorerTabChange],

View File

@@ -4,7 +4,7 @@ import { DataSource } from 'types/common/queryBuilder';
export const viewMockData: ViewProps[] = [
{
uuid: 'view1',
id: 'view1',
name: 'View 1',
createdBy: 'User 1',
category: 'category 1',
@@ -17,7 +17,7 @@ export const viewMockData: ViewProps[] = [
updatedBy: 'User 1',
},
{
uuid: 'view2',
id: 'view2',
name: 'View 2',
createdBy: 'User 2',
category: 'category 2',

View File

@@ -25,9 +25,9 @@ describe('MenuItemGenerator', () => {
<MockQueryClientProvider>
<MenuItemGenerator
viewName={viewMockData[0].name}
viewKey={viewMockData[0].uuid}
viewKey={viewMockData[0].id}
createdBy={viewMockData[0].createdBy}
uuid={viewMockData[0].uuid}
uuid={viewMockData[0].id}
refetchAllView={jest.fn()}
viewData={viewMockData}
sourcePage={DataSource.TRACES}
@@ -43,9 +43,9 @@ describe('MenuItemGenerator', () => {
<MockQueryClientProvider>
<MenuItemGenerator
viewName={viewMockData[0].name}
viewKey={viewMockData[0].uuid}
viewKey={viewMockData[0].id}
createdBy={viewMockData[0].createdBy}
uuid={viewMockData[0].uuid}
uuid={viewMockData[0].id}
refetchAllView={jest.fn()}
viewData={viewMockData}
sourcePage={DataSource.TRACES}

View File

@@ -26,7 +26,7 @@ export type GetViewDetailsUsingViewKey = (
| {
query: Query;
name: string;
uuid: string;
id: string;
panelType: PANEL_TYPES;
extraData?: string;
}

View File

@@ -27,11 +27,11 @@ export const getViewDetailsUsingViewKey: GetViewDetailsUsingViewKey = (
viewKey,
data,
) => {
const selectedView = data?.find((view) => view.uuid === viewKey);
const selectedView = data?.find((view) => view.id === viewKey);
if (selectedView) {
const { compositeQuery, name, uuid, extraData } = selectedView;
const { compositeQuery, name, id, extraData } = selectedView;
const query = mapQueryDataFromApi(compositeQuery);
return { query, name, uuid, panelType: compositeQuery.panelType, extraData };
return { query, name, id, panelType: compositeQuery.panelType, extraData };
}
return undefined;
};

View File

@@ -18,6 +18,7 @@ function CopyClipboardHOC({
notifications.success({
message: notificationMessage,
key: notificationMessage,
});
}
}, [value, notifications, entityKey]);

View File

@@ -63,30 +63,31 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
return (
<div className="quick-filters">
{source !== QuickFiltersSource.INFRA_MONITORING && (
<section className="header">
<section className="left-actions">
<FilterOutlined />
<Typography.Text className="text">Filters for</Typography.Text>
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
</Tooltip>
</section>
{source !== QuickFiltersSource.INFRA_MONITORING &&
source !== QuickFiltersSource.API_MONITORING && (
<section className="header">
<section className="left-actions">
<FilterOutlined />
<Typography.Text className="text">Filters for</Typography.Text>
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
</Tooltip>
</section>
<section className="right-actions">
<Tooltip title="Reset All">
<SyncOutlined className="sync-icon" onClick={handleReset} />
</Tooltip>
<div className="divider-filter" />
<Tooltip title="Collapse Filters">
<VerticalAlignTopOutlined
rotate={270}
onClick={handleFilterVisibilityChange}
/>
</Tooltip>
<section className="right-actions">
<Tooltip title="Reset All">
<SyncOutlined className="sync-icon" onClick={handleReset} />
</Tooltip>
<div className="divider-filter" />
<Tooltip title="Collapse Filters">
<VerticalAlignTopOutlined
rotate={270}
onClick={handleFilterVisibilityChange}
/>
</Tooltip>
</section>
</section>
</section>
)}
)}
<section className="filters">
{config.map((filter) => {

View File

@@ -39,4 +39,5 @@ export enum QuickFiltersSource {
LOGS_EXPLORER = 'logs-explorer',
INFRA_MONITORING = 'infra-monitoring',
TRACES_EXPLORER = 'traces-explorer',
API_MONITORING = 'api-monitoring',
}

View File

@@ -1,3 +1,5 @@
import './ResizeTable.styles.scss';
import { SyntheticEvent, useMemo } from 'react';
import { Resizable, ResizeCallbackData } from 'react-resizable';
@@ -10,8 +12,8 @@ function ResizableHeader(props: ResizableHeaderProps): JSX.Element {
const handle = useMemo(
() => (
<SpanStyle
className="react-resizable-handle"
onClick={(e): void => e.stopPropagation()}
className="resize-handle"
/>
),
[],
@@ -19,7 +21,7 @@ function ResizableHeader(props: ResizableHeaderProps): JSX.Element {
if (!width) {
// eslint-disable-next-line react/jsx-props-no-spreading
return <th {...restProps} />;
return <th {...restProps} className="resizable-header" />;
}
return (
@@ -29,9 +31,10 @@ function ResizableHeader(props: ResizableHeaderProps): JSX.Element {
handle={handle}
onResize={onResize}
draggableOpts={enableUserSelectHack}
minConstraints={[150, 0]}
>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<th {...restProps} />
<th {...restProps} className="resizable-header" />
</Resizable>
);
}

View File

@@ -0,0 +1,53 @@
.resizable-header {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
position: relative;
.ant-table-column-title {
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
}
}
.resize-main-table {
.ant-table-body {
.ant-table-tbody {
.ant-table-row {
.ant-table-cell {
.ant-typography {
white-space: unset;
}
}
}
}
}
}
.logs-table,
.traces-table {
.resize-table {
.resize-handle {
position: absolute;
top: 0;
bottom: 0;
inset-inline-end: -5px;
width: 10px;
cursor: col-resize;
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 1px;
height: 1.6em;
background-color: var(--bg-slate-200);
transition: background-color 0.2s;
}
}
}
}

View File

@@ -2,35 +2,63 @@
import { Table } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import cx from 'classnames';
import { dragColumnParams } from 'hooks/useDragColumns/configs';
import { set } from 'lodash-es';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { debounce, set } from 'lodash-es';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
SyntheticEvent,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import ReactDragListView from 'react-drag-listview';
import { ResizeCallbackData } from 'react-resizable';
import { Widgets } from 'types/api/dashboard/getAll';
import ResizableHeader from './ResizableHeader';
import { DragSpanStyle } from './styles';
import { ResizeTableProps } from './types';
// eslint-disable-next-line sonarjs/cognitive-complexity
function ResizeTable({
columns,
onDragColumn,
pagination,
widgetId,
shouldPersistColumnWidths = false,
...restProps
}: ResizeTableProps): JSX.Element {
const [columnsData, setColumns] = useState<ColumnsType>([]);
const { setColumnWidths, selectedDashboard } = useDashboard();
const columnWidths = shouldPersistColumnWidths
? (selectedDashboard?.data?.widgets?.find(
(widget) => widget.id === widgetId,
) as Widgets)?.columnWidths
: undefined;
const updateAllColumnWidths = useRef(
debounce((widthsConfig: Record<string, number>) => {
if (!widgetId || !shouldPersistColumnWidths) return;
setColumnWidths?.((prev) => ({
...prev,
[widgetId]: widthsConfig,
}));
}, 1000),
).current;
const handleResize = useCallback(
(index: number) => (
_e: SyntheticEvent<Element>,
e: SyntheticEvent<Element>,
{ size }: ResizeCallbackData,
): void => {
e.preventDefault();
e.stopPropagation();
const newColumns = [...columnsData];
newColumns[index] = {
...newColumns[index],
@@ -65,6 +93,7 @@ function ResizeTable({
...restProps,
components: { header: { cell: ResizableHeader } },
columns: mergedColumns,
className: cx('resize-main-table', restProps.className),
};
set(
@@ -78,9 +107,39 @@ function ResizeTable({
useEffect(() => {
if (columns) {
setColumns(columns);
// Apply stored column widths from widget configuration
const columnsWithStoredWidths = columns.map((col) => {
const dataIndex = (col as RowData).dataIndex as string;
if (dataIndex && columnWidths && columnWidths[dataIndex]) {
return {
...col,
width: columnWidths[dataIndex], // Apply stored width
};
}
return col;
});
setColumns(columnsWithStoredWidths);
}
}, [columns]);
}, [columns, columnWidths]);
useEffect(() => {
if (!shouldPersistColumnWidths) return;
// Collect all column widths in a single object
const newColumnWidths: Record<string, number> = {};
mergedColumns.forEach((col) => {
if (col.width && (col as RowData).dataIndex) {
const dataIndex = (col as RowData).dataIndex as string;
newColumnWidths[dataIndex] = col.width as number;
}
});
// Only update if there are actual widths to set
if (Object.keys(newColumnWidths).length > 0) {
updateAllColumnWidths(newColumnWidths);
}
}, [mergedColumns, updateAllColumnWidths, shouldPersistColumnWidths]);
return onDragColumn ? (
<ReactDragListView.DragColumn {...dragColumnParams} onDragEnd={onDragColumn}>

View File

@@ -8,6 +8,8 @@ export const SpanStyle = styled.span`
width: 0.625rem;
height: 100%;
cursor: col-resize;
margin-left: 4px;
margin-right: 4px;
`;
export const DragSpanStyle = styled.span`

View File

@@ -9,6 +9,8 @@ import { TableDataSource } from './contants';
export interface ResizeTableProps extends TableProps<any> {
onDragColumn?: (fromIndex: number, toIndex: number) => void;
widgetId?: string;
shouldPersistColumnWidths?: boolean;
}
export interface DynamicColumnTableProps extends TableProps<any> {
tablesource: typeof TableDataSource[keyof typeof TableDataSource];

View File

@@ -25,4 +25,6 @@ export enum FeatureKeys {
ANOMALY_DETECTION = 'ANOMALY_DETECTION',
AWS_INTEGRATION = 'AWS_INTEGRATION',
ONBOARDING_V3 = 'ONBOARDING_V3',
THIRD_PARTY_API = 'THIRD_PARTY_API',
TRACE_FUNNELS = 'TRACE_FUNNELS',
}

View File

@@ -51,6 +51,21 @@ export const REACT_QUERY_KEY = {
GET_METRICS_LIST_FILTER_VALUES: 'GET_METRICS_LIST_FILTER_VALUES',
GET_METRIC_DETAILS: 'GET_METRIC_DETAILS',
GET_RELATED_METRICS: 'GET_RELATED_METRICS',
// API Monitoring Query Keys
GET_DOMAINS_LIST: 'GET_DOMAINS_LIST',
GET_ENDPOINTS_LIST_BY_DOMAIN: 'GET_ENDPOINTS_LIST_BY_DOMAIN',
GET_NESTED_ENDPOINTS_LIST: 'GET_NESTED_ENDPOINTS_LIST',
GET_ENDPOINT_METRICS_DATA: 'GET_ENDPOINT_METRICS_DATA',
GET_ENDPOINT_STATUS_CODE_DATA: 'GET_ENDPOINT_STATUS_CODE_DATA',
GET_ENDPOINT_RATE_OVER_TIME_DATA: 'GET_ENDPOINT_RATE_OVER_TIME_DATA',
GET_ENDPOINT_LATENCY_OVER_TIME_DATA: 'GET_ENDPOINT_LATENCY_OVER_TIME_DATA',
GET_ENDPOINT_DROPDOWN_DATA: 'GET_ENDPOINT_DROPDOWN_DATA',
GET_ENDPOINT_DEPENDENT_SERVICES_DATA: 'GET_ENDPOINT_DEPENDENT_SERVICES_DATA',
GET_ENDPOINT_STATUS_CODE_BAR_CHARTS_DATA:
'GET_ENDPOINT_STATUS_CODE_BAR_CHARTS_DATA',
GET_ENDPOINT_STATUS_CODE_LATENCY_BAR_CHARTS_DATA:
'GET_ENDPOINT_STATUS_CODE_LATENCY_BAR_CHARTS_DATA',
GET_FUNNELS_LIST: 'GET_FUNNELS_LIST',
GET_FUNNEL_DETAILS: 'GET_FUNNEL_DETAILS',
} as const;

View File

@@ -71,6 +71,7 @@ const ROUTES = {
METRICS_EXPLORER: '/metrics-explorer/summary',
METRICS_EXPLORER_EXPLORER: '/metrics-explorer/explorer',
METRICS_EXPLORER_VIEWS: '/metrics-explorer/views',
API_MONITORING: '/api-monitoring/explorer',
METRICS_EXPLORER_BASE: '/metrics-explorer',
WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted',
HOME_PAGE: '/',

View File

@@ -1,3 +1,4 @@
import ROUTES from 'constants/routes';
import AlertChannels from 'container/AllAlertChannels';
import { allAlertChannels } from 'mocks-server/__mockdata__/alerts';
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
@@ -20,6 +21,13 @@ jest.mock('hooks/useNotifications', () => ({
})),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALL_CHANNELS}`,
}),
}));
describe('Alert Channels Settings List page', () => {
beforeEach(() => {
render(<AlertChannels />);

View File

@@ -1,3 +1,4 @@
import ROUTES from 'constants/routes';
import AlertChannels from 'container/AllAlertChannels';
import { allAlertChannels } from 'mocks-server/__mockdata__/alerts';
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
@@ -25,6 +26,13 @@ jest.mock('hooks/useComponentPermission', () => ({
default: jest.fn().mockImplementation(() => [false]),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALL_CHANNELS}`,
}),
}));
describe('Alert Channels Settings List page (Normal User)', () => {
beforeEach(() => {
render(<AlertChannels />);

View File

@@ -0,0 +1,241 @@
import { LoadingOutlined } from '@ant-design/icons';
import { Select, Spin, Table, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import {
EndPointsTableRowData,
formatEndPointsDataForTable,
getEndPointsColumnsConfig,
getEndPointsQueryPayload,
} from 'container/ApiMonitoring/utils';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueries } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import ErrorState from './components/ErrorState';
import ExpandedRow from './components/ExpandedRow';
import { VIEW_TYPES, VIEWS } from './constants';
function AllEndPoints({
domainName,
setSelectedEndPointName,
setSelectedView,
groupBy,
setGroupBy,
}: {
domainName: string;
setSelectedEndPointName: (name: string) => void;
setSelectedView: (tab: VIEWS) => void;
groupBy: IBuilderQuery['groupBy'];
setGroupBy: (groupBy: IBuilderQuery['groupBy']) => void;
}): JSX.Element {
const {
data: groupByFiltersData,
isLoading: isLoadingGroupByFilters,
} = useGetAggregateKeys({
dataSource: DataSource.TRACES,
aggregateAttribute: '',
aggregateOperator: 'noop',
searchText: '',
tagType: '',
});
const [groupByOptions, setGroupByOptions] = useState<
{ value: string; label: string }[]
>([]);
const [expandedRowKeys, setExpandedRowKeys] = useState<React.Key[]>([]);
const handleGroupByChange = useCallback(
(value: IBuilderQuery['groupBy']) => {
const groupBy = [];
for (let index = 0; index < value.length; index++) {
const element = (value[index] as unknown) as string;
const key = groupByFiltersData?.payload?.attributeKeys?.find(
(key) => key.key === element,
);
if (key) {
groupBy.push(key);
}
}
setGroupBy(groupBy);
},
[groupByFiltersData, setGroupBy],
);
useEffect(() => {
if (groupByFiltersData?.payload) {
setGroupByOptions(
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
value: filter.key,
label: filter.key,
})) || [],
);
}
}, [groupByFiltersData]);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const queryPayloads = useMemo(
() =>
getEndPointsQueryPayload(
groupBy,
domainName,
Math.floor(minTime / 1e9),
Math.floor(maxTime / 1e9),
),
[groupBy, domainName, minTime, maxTime],
);
// Since only one query here
const endPointsDataQueries = useQueries(
queryPayloads.map((payload) => ({
queryKey: [
REACT_QUERY_KEY.GET_ENDPOINTS_LIST_BY_DOMAIN,
payload,
ENTITY_VERSION_V4,
groupBy,
],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
enabled: !!payload,
staleTime: 60 * 1000, // 1 minute stale time : optimize this part
})),
);
const endPointsDataQuery = endPointsDataQueries[0];
const {
data: allEndPointsData,
isLoading,
isRefetching,
isError,
refetch,
} = endPointsDataQuery;
const endPointsColumnsConfig = useMemo(
() => getEndPointsColumnsConfig(groupBy.length > 0, expandedRowKeys),
[groupBy.length, expandedRowKeys],
);
const expandedRowRender = (record: EndPointsTableRowData): JSX.Element => (
<ExpandedRow
domainName={domainName}
selectedRowData={record}
setSelectedEndPointName={setSelectedEndPointName}
setSelectedView={setSelectedView}
/>
);
const handleGroupByRowClick = (record: EndPointsTableRowData): void => {
if (expandedRowKeys.includes(record.key)) {
setExpandedRowKeys(expandedRowKeys.filter((key) => key !== record.key));
} else {
setExpandedRowKeys((expandedRowKeys) => [...expandedRowKeys, record.key]);
}
};
const handleRowClick = (record: EndPointsTableRowData): void => {
if (groupBy.length === 0) {
setSelectedEndPointName(record.endpointName); // this will open up the endpoint details tab
setSelectedView(VIEW_TYPES.ENDPOINT_DETAILS);
logEvent('API Monitoring: Endpoint name row clicked', {});
} else {
handleGroupByRowClick(record); // this will prepare the nested query payload
}
};
const formattedEndPointsData = useMemo(
() =>
formatEndPointsDataForTable(
allEndPointsData?.payload?.data?.result[0]?.table?.rows,
groupBy,
),
[groupBy, allEndPointsData],
);
if (isError) {
return (
<div className="all-endpoints-error-state-wrapper">
<ErrorState refetch={refetch} />
</div>
);
}
return (
<div className="all-endpoints-container">
<div className="group-by-container">
<div className="group-by-label"> Group by </div>
<Select
className="group-by-select"
loading={isLoadingGroupByFilters}
mode="multiple"
value={groupBy}
allowClear
maxTagCount="responsive"
placeholder="Search for attribute"
options={groupByOptions}
onChange={handleGroupByChange}
/>{' '}
</div>
<div className="endpoints-table-container">
<div className="endpoints-table-header">Endpoint overview</div>
<Table
columns={endPointsColumnsConfig}
loading={{
spinning: isLoading || isRefetching,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
dataSource={isLoading || isRefetching ? [] : formattedEndPointsData}
locale={{
emptyText:
isLoading || isRefetching ? null : (
<div className="no-filtered-endpoints-message-container">
<div className="no-filtered-endpoints-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text className="no-filtered-endpoints-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
</div>
),
}}
scroll={{ x: true }}
tableLayout="fixed"
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{
expandedRowRender: groupBy.length > 0 ? expandedRowRender : undefined,
expandedRowKeys,
expandIconColumnIndex: -1,
}}
rowClassName={(_, index): string =>
index % 2 === 0 ? 'table-row-dark' : 'table-row-light'
}
/>
</div>
</div>
);
}
export default AllEndPoints;

View File

@@ -0,0 +1,146 @@
import './DomainDetails.styles.scss';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Typography } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { ArrowDown, ArrowUp, X } from 'lucide-react';
import { useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import AllEndPoints from './AllEndPoints';
import DomainMetrics from './components/DomainMetrics';
import { VIEW_TYPES, VIEWS } from './constants';
import EndPointDetailsWrapper from './EndPointDetailsWrapper';
function DomainDetails({
domainData,
handleClose,
selectedDomainIndex,
setSelectedDomainIndex,
domainListLength,
domainListFilters,
}: {
domainData: any;
handleClose: () => void;
selectedDomainIndex: number;
setSelectedDomainIndex: (index: number) => void;
domainListLength: number;
domainListFilters: IBuilderQuery['filters'];
}): JSX.Element {
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.ALL_ENDPOINTS);
const [selectedEndPointName, setSelectedEndPointName] = useState<string>('');
const [endPointsGroupBy, setEndPointsGroupBy] = useState<
IBuilderQuery['groupBy']
>([]);
const isDarkMode = useIsDarkMode();
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
};
return (
<Drawer
width="60%"
title={
<div className="domain-details-drawer-header">
<div className="domain-details-drawer-header-title">
<Divider type="vertical" />
<Typography.Text className="title">
{domainData.domainName}
</Typography.Text>
</div>
<Button.Group className="domain-details-drawer-header-ctas">
<Button
className="domain-navigate-cta"
onClick={(): void => {
setSelectedDomainIndex(selectedDomainIndex - 1);
setSelectedEndPointName('');
setEndPointsGroupBy([]);
setSelectedView(VIEW_TYPES.ALL_ENDPOINTS);
}}
icon={<ArrowUp size={16} />}
disabled={selectedDomainIndex === 0}
title="Previous domain"
/>
<Button
className="domain-navigate-cta"
onClick={(): void => {
setSelectedDomainIndex(selectedDomainIndex + 1);
setSelectedEndPointName('');
setEndPointsGroupBy([]);
setSelectedView(VIEW_TYPES.ALL_ENDPOINTS);
}}
icon={<ArrowDown size={16} />}
disabled={selectedDomainIndex === domainListLength - 1}
title="Next domain"
/>
</Button.Group>
</div>
}
placement="right"
onClose={handleClose}
open={!!domainData}
style={{
overscrollBehavior: 'contain',
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
}}
className="domain-detail-drawer"
destroyOnClose
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
>
{domainData && (
<>
<DomainMetrics domainData={domainData} />
<div className="views-tabs-container">
<Radio.Group
className="views-tabs"
onChange={handleTabChange}
value={selectedView}
>
<Radio.Button
className={
// eslint-disable-next-line sonarjs/no-duplicate-string
selectedView === VIEW_TYPES.ALL_ENDPOINTS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.ALL_ENDPOINTS}
>
<div className="view-title">All Endpoints</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.ENDPOINT_DETAILS
? 'tab selected_view'
: 'tab'
}
value={VIEW_TYPES.ENDPOINT_DETAILS}
>
<div className="view-title">Endpoint Details</div>
</Radio.Button>
</Radio.Group>
</div>
{selectedView === VIEW_TYPES.ALL_ENDPOINTS && (
<AllEndPoints
domainName={domainData.domainName}
setSelectedEndPointName={setSelectedEndPointName}
setSelectedView={setSelectedView}
groupBy={endPointsGroupBy}
setGroupBy={setEndPointsGroupBy}
/>
)}
{selectedView === VIEW_TYPES.ENDPOINT_DETAILS && (
<EndPointDetailsWrapper
domainName={domainData.domainName}
endPointName={selectedEndPointName}
setSelectedEndPointName={setSelectedEndPointName}
domainListFilters={domainListFilters}
/>
)}
</>
)}
</Drawer>
);
}
export default DomainDetails;

View File

@@ -0,0 +1,199 @@
import { ENTITY_VERSION_V4 } from 'constants/app';
import { initialQueriesMap } from 'constants/queryBuilder';
import {
END_POINT_DETAILS_QUERY_KEYS_ARRAY,
extractPortAndEndpoint,
getEndPointDetailsQueryPayload,
getLatencyOverTimeWidgetData,
getRateOverTimeWidgetData,
} from 'container/ApiMonitoring/utils';
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { useMemo, useState } from 'react';
import { useQueries } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import DependentServices from './components/DependentServices';
import EndPointMetrics from './components/EndPointMetrics';
import EndPointsDropDown from './components/EndPointsDropDown';
import MetricOverTimeGraph from './components/MetricOverTimeGraph';
import StatusCodeBarCharts from './components/StatusCodeBarCharts';
import StatusCodeTable from './components/StatusCodeTable';
function EndPointDetails({
domainName,
endPointName,
setSelectedEndPointName,
domainListFilters,
}: {
domainName: string;
endPointName: string;
setSelectedEndPointName: (value: string) => void;
domainListFilters: IBuilderQuery['filters'];
}): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const currentQuery = initialQueriesMap[DataSource.TRACES];
const [filters, setFilters] = useState<IBuilderQuery['filters']>({
op: 'AND',
items: [],
});
// Manually update the query to include the filters
// Because using the hook is causing the global domain
// query to be updated and causing main domain list to
// refetch with the filters of endpoints
const updatedCurrentQuery = useMemo(
() => ({
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
dataSource: DataSource.TRACES,
filters,
},
],
},
}),
[filters, currentQuery],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
const isServicesFilterApplied = useMemo(
() => filters.items.some((item) => item.key?.key === 'service.name'),
[filters],
);
const endPointDetailsQueryPayload = useMemo(
() =>
getEndPointDetailsQueryPayload(
domainName,
endPointName,
Math.floor(minTime / 1e9),
Math.floor(maxTime / 1e9),
filters,
),
[domainName, endPointName, filters, minTime, maxTime],
);
const endPointDetailsDataQueries = useQueries(
endPointDetailsQueryPayload.map((payload, index) => ({
queryKey: [
END_POINT_DETAILS_QUERY_KEYS_ARRAY[index],
payload,
filters.items,
ENTITY_VERSION_V4,
],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
enabled: !!payload,
})),
);
const [
endPointMetricsDataQuery,
endPointStatusCodeDataQuery,
endPointDropDownDataQuery,
endPointDependentServicesDataQuery,
endPointStatusCodeBarChartsDataQuery,
endPointStatusCodeLatencyBarChartsDataQuery,
] = useMemo(
() => [
endPointDetailsDataQueries[0],
endPointDetailsDataQueries[1],
endPointDetailsDataQueries[2],
endPointDetailsDataQueries[3],
endPointDetailsDataQueries[4],
endPointDetailsDataQueries[5],
],
[endPointDetailsDataQueries],
);
const { endpoint, port } = useMemo(
() => extractPortAndEndpoint(endPointName),
[endPointName],
);
const [rateOverTimeWidget, latencyOverTimeWidget] = useMemo(
() => [
getRateOverTimeWidgetData(domainName, endPointName, {
items: [...domainListFilters.items, ...filters.items],
op: filters.op,
}),
getLatencyOverTimeWidgetData(domainName, endPointName, {
items: [...domainListFilters.items, ...filters.items],
op: filters.op,
}),
],
[domainName, endPointName, filters, domainListFilters],
);
return (
<div className="endpoint-details-container">
<div className="endpoint-details-filters-container">
<div className="endpoint-details-filters-container-dropdown">
<EndPointsDropDown
selectedEndPointName={endPointName}
setSelectedEndPointName={setSelectedEndPointName}
endPointDropDownDataQuery={endPointDropDownDataQuery}
parentContainerDiv=".endpoint-details-filters-container"
dropdownStyle={{ width: 'calc(100% - 36px)' }}
/>
</div>
<div className="endpoint-details-filters-container-search">
<QueryBuilderSearchV2
query={query}
onChange={(searchFilters): void => {
setFilters(searchFilters);
}}
placeholder="Search for filters..."
/>
</div>
</div>
<div className="endpoint-meta-data">
<div className="endpoint-meta-data-pill">
<div className="endpoint-meta-data-label">Endpoint</div>
<div className="endpoint-meta-data-value">{endpoint || '-'}</div>
</div>
<div className="endpoint-meta-data-pill">
<div className="endpoint-meta-data-label">Port</div>
<div className="endpoint-meta-data-value">{port || '-'}</div>
</div>
</div>
<EndPointMetrics endPointMetricsDataQuery={endPointMetricsDataQuery} />
{!isServicesFilterApplied && (
<DependentServices
dependentServicesQuery={endPointDependentServicesDataQuery}
/>
)}
<StatusCodeBarCharts
endPointStatusCodeBarChartsDataQuery={endPointStatusCodeBarChartsDataQuery}
endPointStatusCodeLatencyBarChartsDataQuery={
endPointStatusCodeLatencyBarChartsDataQuery
}
domainName={domainName}
endPointName={endPointName}
domainListFilters={domainListFilters}
filters={filters}
/>
<StatusCodeTable endPointStatusCodeDataQuery={endPointStatusCodeDataQuery} />
<MetricOverTimeGraph widget={rateOverTimeWidget} />
<MetricOverTimeGraph widget={latencyOverTimeWidget} />
</div>
);
}
export default EndPointDetails;

View File

@@ -0,0 +1,80 @@
import { ENTITY_VERSION_V4 } from 'constants/app';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { getEndPointZeroStateQueryPayload } from 'container/ApiMonitoring/utils';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { useMemo } from 'react';
import { useQueries } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import EndPointDetailsZeroState from './components/EndPointDetailsZeroState';
import EndPointDetails from './EndPointDetails';
function EndPointDetailsWrapper({
domainName,
endPointName,
setSelectedEndPointName,
domainListFilters,
}: {
domainName: string;
endPointName: string;
setSelectedEndPointName: (value: string) => void;
domainListFilters: IBuilderQuery['filters'];
}): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const endPointZeroStateQueryPayload = useMemo(
() =>
getEndPointZeroStateQueryPayload(
domainName,
Math.floor(minTime / 1e9),
Math.floor(maxTime / 1e9),
),
[domainName, minTime, maxTime],
);
const endPointZeroStateDataQueries = useQueries(
endPointZeroStateQueryPayload.map((payload) => ({
queryKey: [
// Since only one query here
REACT_QUERY_KEY.GET_ENDPOINT_DROPDOWN_DATA,
payload,
ENTITY_VERSION_V4,
],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
enabled: !!payload,
})),
);
const [endPointZeroStateDataQuery] = useMemo(
() => [endPointZeroStateDataQueries[0]],
[endPointZeroStateDataQueries],
);
if (endPointName === '') {
return (
<EndPointDetailsZeroState
setSelectedEndPointName={setSelectedEndPointName}
endPointDropDownDataQuery={endPointZeroStateDataQuery}
/>
);
}
return (
<EndPointDetails
domainName={domainName}
endPointName={endPointName}
setSelectedEndPointName={setSelectedEndPointName}
domainListFilters={domainListFilters}
/>
);
}
export default EndPointDetailsWrapper;

View File

@@ -0,0 +1,108 @@
import { Typography } from 'antd';
import Skeleton from 'antd/lib/skeleton';
import { getFormattedDependentServicesData } from 'container/ApiMonitoring/utils';
import { UnfoldVertical } from 'lucide-react';
import { useMemo, useState } from 'react';
import { UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import ErrorState from './ErrorState';
interface DependentServicesProps {
dependentServicesQuery: UseQueryResult<SuccessResponse<any>, unknown>;
}
function DependentServices({
dependentServicesQuery,
}: DependentServicesProps): JSX.Element {
const {
data,
refetch,
isError,
isLoading,
isRefetching,
} = dependentServicesQuery;
const [currentRenderCount, setCurrentRenderCount] = useState(0);
const dependentServicesData = useMemo(() => {
const formattedDependentServicesData = getFormattedDependentServicesData(
data?.payload?.data?.result[0].table.rows,
);
setCurrentRenderCount(Math.min(formattedDependentServicesData.length, 5));
return formattedDependentServicesData;
}, [data]);
const renderItems = useMemo(
() => dependentServicesData.slice(0, currentRenderCount),
[currentRenderCount, dependentServicesData],
);
if (isLoading || isRefetching) {
return <Skeleton />;
}
if (isError) {
return <ErrorState refetch={refetch} />;
}
return (
<div className="top-services-content">
<div className="top-services-title">
<span className="title-wrapper">Dependent Services</span>
</div>
<div className="dependent-services-container">
{renderItems.length === 0 ? (
<div className="no-dependent-services-message-container">
<div className="no-dependent-services-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text className="no-dependent-services-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
</div>
) : (
renderItems.map((item) => (
<div className="top-services-item" key={item.key}>
<div className="top-services-item-progress">
<div className="top-services-item-key">{item.serviceName}</div>
<div className="top-services-item-count">{item.count}</div>
<div
className="top-services-item-progress-bar"
style={{ width: `${item.percentage}%` }}
/>
</div>
<div className="top-services-item-percentage">
{item.percentage.toFixed(2)}%
</div>
</div>
))
)}
{currentRenderCount < dependentServicesData.length && (
<div
className="top-services-load-more"
onClick={(): void => setCurrentRenderCount(dependentServicesData.length)}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
setCurrentRenderCount(dependentServicesData.length);
}
}}
role="button"
tabIndex={0}
>
<UnfoldVertical size={14} />
Show more...
</div>
)}
</div>
</div>
);
}
export default DependentServices;

View File

@@ -0,0 +1,82 @@
import { Color } from '@signozhq/design-tokens';
import { Progress, Tooltip, Typography } from 'antd';
import { getLastUsedRelativeTime } from 'container/ApiMonitoring/utils';
function DomainMetrics({ domainData }: { domainData: any }): JSX.Element {
return (
<div className="domain-detail-drawer__endpoint">
<div className="domain-details-grid">
<div className="labels-row">
<Typography.Text
type="secondary"
className="domain-details-metadata-label"
>
EXTERNAL API
</Typography.Text>
<Typography.Text
type="secondary"
className="domain-details-metadata-label"
>
AVERAGE LATENCY
</Typography.Text>
<Typography.Text
type="secondary"
className="domain-details-metadata-label"
>
ERROR RATE
</Typography.Text>
<Typography.Text
type="secondary"
className="domain-details-metadata-label"
>
LAST USED
</Typography.Text>
</div>
<div className="values-row">
<Typography.Text className="domain-details-metadata-value">
<Tooltip title={domainData.endpointCount}>
<span className="round-metric-tag">{domainData.endpointCount}</span>
</Tooltip>
</Typography.Text>
{/* // update the tooltip as well */}
<Typography.Text className="domain-details-metadata-value">
<Tooltip title={domainData.latency}>
<span className="round-metric-tag">
{(domainData.latency / 1000).toFixed(3)}s
</span>
</Tooltip>
</Typography.Text>
{/* // update the tooltip as well */}
<Typography.Text className="domain-details-metadata-value error-rate">
<Tooltip title={domainData.errorRate}>
<Progress
status="active"
percent={Number((domainData.errorRate * 100).toFixed(1))}
strokeLinecap="butt"
size="small"
strokeColor={((): string => {
const errorRatePercent = Number(
(domainData.errorRate * 100).toFixed(1),
);
if (errorRatePercent >= 90) return Color.BG_SAKURA_500;
if (errorRatePercent >= 60) return Color.BG_AMBER_500;
return Color.BG_FOREST_500;
})()}
className="progress-bar"
/>
</Tooltip>
</Typography.Text>
{/* // update the tooltip as well */}
<Typography.Text className="domain-details-metadata-value">
<Tooltip title={domainData.lastUsed}>
{getLastUsedRelativeTime(domainData.lastUsed)}
</Tooltip>
</Typography.Text>
</div>
</div>
</div>
);
}
export default DomainMetrics;

View File

@@ -0,0 +1,40 @@
import { UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import EndPointsDropDown from './EndPointsDropDown';
function EndPointDetailsZeroState({
setSelectedEndPointName,
endPointDropDownDataQuery,
}: {
setSelectedEndPointName: (endPointName: string) => void;
endPointDropDownDataQuery: UseQueryResult<SuccessResponse<any>>;
}): JSX.Element {
return (
<div className="end-point-details-zero-state-wrapper">
<div className="end-point-details-zero-state-content">
<img
src="/Icons/no-data.svg"
alt="no-data"
width={32}
height={32}
className="end-point-details-zero-state-icon"
/>
<div className="end-point-details-zero-state-content-wrapper">
<div className="end-point-details-zero-state-text-content">
<div className="title">No endpoint selected yet</div>
<div className="description">Select an endpoint to see the details</div>
</div>
<EndPointsDropDown
setSelectedEndPointName={setSelectedEndPointName}
endPointDropDownDataQuery={endPointDropDownDataQuery}
parentContainerDiv=".end-point-details-zero-state-wrapper"
dropdownStyle={{ width: '60%' }}
/>
</div>
</div>
</div>
);
}
export default EndPointDetailsZeroState;

View File

@@ -0,0 +1,121 @@
import { Color } from '@signozhq/design-tokens';
import { Progress, Skeleton, Tooltip, Typography } from 'antd';
import { getFormattedEndPointMetricsData } from 'container/ApiMonitoring/utils';
import { useMemo } from 'react';
import { UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import ErrorState from './ErrorState';
function EndPointMetrics({
endPointMetricsDataQuery,
}: {
endPointMetricsDataQuery: UseQueryResult<SuccessResponse<any>, unknown>;
}): JSX.Element {
const {
isLoading,
isRefetching,
isError,
data,
refetch,
} = endPointMetricsDataQuery;
const metricsData = useMemo(() => {
if (isLoading || isRefetching || isError) {
return null;
}
return getFormattedEndPointMetricsData(
data?.payload?.data?.result[0].table.rows,
);
}, [data?.payload?.data?.result, isLoading, isRefetching, isError]);
if (isError) {
return <ErrorState refetch={refetch} />;
}
return (
<div className="domain-detail-drawer__endpoint">
<div className="domain-details-grid">
<div className="labels-row">
<Typography.Text
type="secondary"
className="domain-details-metadata-label"
>
Rate
</Typography.Text>
<Typography.Text
type="secondary"
className="domain-details-metadata-label"
>
AVERAGE LATENCY
</Typography.Text>
<Typography.Text
type="secondary"
className="domain-details-metadata-label"
>
ERROR RATE
</Typography.Text>
<Typography.Text
type="secondary"
className="domain-details-metadata-label"
>
LAST USED
</Typography.Text>
</div>
<div className="values-row">
<Typography.Text className="domain-details-metadata-value">
{isLoading || isRefetching ? (
<Skeleton.Button active size="small" />
) : (
<Tooltip title={metricsData?.rate}>
<span className="round-metric-tag">{metricsData?.rate} ops/sec</span>
</Tooltip>
)}
</Typography.Text>
<Typography.Text className="domain-details-metadata-value">
{isLoading || isRefetching ? (
<Skeleton.Button active size="small" />
) : (
<Tooltip title={metricsData?.latency}>
<span className="round-metric-tag">{metricsData?.latency}ms</span>
</Tooltip>
)}
</Typography.Text>
<Typography.Text className="domain-details-metadata-value error-rate">
{isLoading || isRefetching ? (
<Skeleton.Button active size="small" />
) : (
<Tooltip title={metricsData?.errorRate}>
<Progress
percent={Number((metricsData?.errorRate ?? 0 * 100).toFixed(1))}
strokeLinecap="butt"
size="small"
strokeColor={((): string => {
const errorRatePercent = Number(
(metricsData?.errorRate ?? 0 * 100).toFixed(1),
);
if (errorRatePercent >= 90) return Color.BG_SAKURA_500;
if (errorRatePercent >= 60) return Color.BG_AMBER_500;
return Color.BG_FOREST_500;
})()}
className="progress-bar"
/>
</Tooltip>
)}
</Typography.Text>
<Typography.Text className="domain-details-metadata-value">
{isLoading || isRefetching ? (
<Skeleton.Button active size="small" />
) : (
<Tooltip title={metricsData?.lastUsed}>{metricsData?.lastUsed}</Tooltip>
)}
</Typography.Text>
</div>
</div>
</div>
);
}
export default EndPointMetrics;

View File

@@ -0,0 +1,61 @@
import { Select } from 'antd';
import { getFormattedEndPointDropDownData } from 'container/ApiMonitoring/utils';
import { useMemo } from 'react';
import { UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
interface EndPointsDropDownProps {
selectedEndPointName?: string;
setSelectedEndPointName: (value: string) => void;
endPointDropDownDataQuery: UseQueryResult<SuccessResponse<any>, unknown>;
parentContainerDiv?: string;
dropdownStyle?: React.CSSProperties;
}
const defaultProps = {
selectedEndPointName: '',
parentContainerDiv: '',
dropdownStyle: {},
};
function EndPointsDropDown({
selectedEndPointName,
setSelectedEndPointName,
endPointDropDownDataQuery,
parentContainerDiv,
dropdownStyle,
}: EndPointsDropDownProps): JSX.Element {
const { data, isLoading, isFetching } = endPointDropDownDataQuery;
const handleChange = (value: string): void => {
setSelectedEndPointName(value);
};
const formattedData = useMemo(
() =>
getFormattedEndPointDropDownData(data?.payload.data.result[0].table.rows),
[data?.payload.data.result],
);
return (
<Select
value={selectedEndPointName || undefined}
placeholder="Select endpoint"
loading={isLoading || isFetching}
style={{ width: '100%' }}
onChange={handleChange}
options={formattedData}
getPopupContainer={
parentContainerDiv
? (): HTMLElement =>
document.querySelector(parentContainerDiv) as HTMLElement
: (triggerNode): HTMLElement => triggerNode.parentNode as HTMLElement
}
dropdownStyle={dropdownStyle}
/>
);
}
EndPointsDropDown.defaultProps = defaultProps;
export default EndPointsDropDown;

View File

@@ -0,0 +1,31 @@
import { Button, Typography } from 'antd';
import { RotateCw } from 'lucide-react';
function ErrorState({ refetch }: { refetch: () => void }): JSX.Element {
return (
<div className="error-state-container">
<div className="error-state-content-wrapper">
<div className="error-state-content">
<div className="icon">
<img src="/Icons/awwSnap.svg" alt="awwSnap" width={32} height={32} />
</div>
<div className="error-state-text">
<Typography.Text>Uh-oh :/ We ran into an error.</Typography.Text>
<Typography.Text type="secondary">
Please refresh this panel.
</Typography.Text>
</div>
</div>
<Button
className="refresh-cta"
onClick={(): void => refetch()}
icon={<RotateCw size={16} />}
>
Refresh this panel
</Button>
</div>
</div>
);
}
export default ErrorState;

View File

@@ -0,0 +1,129 @@
import { LoadingOutlined } from '@ant-design/icons';
import { Spin, Table } from 'antd';
import { ColumnType } from 'antd/lib/table';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import {
createFiltersForSelectedRowData,
EndPointsTableRowData,
formatEndPointsDataForTable,
getEndPointsColumnsConfig,
getEndPointsQueryPayload,
} from 'container/ApiMonitoring/utils';
import LoadingContainer from 'container/InfraMonitoringK8s/LoadingContainer';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { useMemo } from 'react';
import { useQueries } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { GlobalReducer } from 'types/reducer/globalTime';
import { VIEW_TYPES, VIEWS } from '../constants';
function ExpandedRow({
domainName,
selectedRowData,
setSelectedEndPointName,
setSelectedView,
}: {
domainName: string;
selectedRowData: EndPointsTableRowData;
setSelectedEndPointName: (name: string) => void;
setSelectedView: (view: VIEWS) => void;
}): JSX.Element {
const nestedColumns = useMemo(() => getEndPointsColumnsConfig(false, []), []);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const groupedByRowDataQueryPayload = useMemo(() => {
if (!selectedRowData) return null;
const filters = createFiltersForSelectedRowData(selectedRowData);
const baseQueryPayload = getEndPointsQueryPayload(
[],
domainName,
Math.floor(minTime / 1e9),
Math.floor(maxTime / 1e9),
);
return baseQueryPayload.map((currentQueryPayload) => ({
...currentQueryPayload,
query: {
...currentQueryPayload.query,
builder: {
...currentQueryPayload.query.builder,
queryData: currentQueryPayload.query.builder.queryData.map(
(queryData) => ({
...queryData,
filters: {
items: [...(queryData.filters?.items || []), ...filters.items],
op: 'AND',
},
}),
),
},
},
}));
}, [domainName, minTime, maxTime, selectedRowData]);
const groupedByRowQueries = useQueries(
groupedByRowDataQueryPayload
? groupedByRowDataQueryPayload.map((payload) => ({
queryKey: [
`${REACT_QUERY_KEY.GET_NESTED_ENDPOINTS_LIST}-${domainName}-${selectedRowData?.key}`,
payload,
ENTITY_VERSION_V4,
selectedRowData?.key,
],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
enabled: !!payload && !!selectedRowData,
}))
: [],
);
const groupedByRowQuery = groupedByRowQueries[0];
return (
<div className="expanded-table-container">
{groupedByRowQuery?.isFetching || groupedByRowQuery?.isLoading ? (
<LoadingContainer />
) : (
<div className="expanded-table">
<Table
columns={nestedColumns as ColumnType<EndPointsTableRowData>[]}
dataSource={
groupedByRowQuery?.data
? formatEndPointsDataForTable(
groupedByRowQuery.data?.payload.data.result[0].table?.rows,
[],
)
: []
}
pagination={false}
scroll={{ x: true }}
tableLayout="fixed"
showHeader={false}
loading={{
spinning: groupedByRowQuery?.isFetching || groupedByRowQuery?.isLoading,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
setSelectedEndPointName(record.endpointName);
setSelectedView(VIEW_TYPES.ENDPOINT_DETAILS);
logEvent('API Monitoring: Endpoint name row clicked', {});
},
className: 'expanded-clickable-row',
})}
/>
</div>
)}
</div>
);
}
export default ExpandedRow;

View File

@@ -0,0 +1,22 @@
import { Card } from 'antd';
import GridCard from 'container/GridCardLayout/GridCard';
import { Widgets } from 'types/api/dashboard/getAll';
function MetricOverTimeGraph({ widget }: { widget: Widgets }): JSX.Element {
return (
<div>
<Card bordered className="endpoint-details-card">
<div className="graph-container">
<GridCard
widget={widget}
isQueryEnabled
onDragSelect={(): void => {}}
customOnDragSelect={(): void => {}}
/>
</div>
</Card>
</div>
);
}
export default MetricOverTimeGraph;

View File

@@ -0,0 +1,257 @@
import { Color } from '@signozhq/design-tokens';
import { Button, Card, Skeleton, Typography } from 'antd';
import cx from 'classnames';
import { useGetGraphCustomSeries } from 'components/CeleryTask/useGetGraphCustomSeries';
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
import Uplot from 'components/Uplot';
import { PANEL_TYPES } from 'constants/queryBuilder';
import {
getCustomFiltersForBarChart,
getFormattedEndPointStatusCodeChartData,
getStatusCodeBarChartWidgetData,
statusCodeWidgetInfo,
} from 'container/ApiMonitoring/utils';
import { handleGraphClick } from 'container/GridCardLayout/GridCard/utils';
import { useGraphClickToShowButton } from 'container/GridCardLayout/useGraphClickToShowButton';
import useNavigateToExplorerPages from 'container/GridCardLayout/useNavigateToExplorerPages';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { useNotifications } from 'hooks/useNotifications';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useCallback, useMemo, useRef, useState } from 'react';
import { UseQueryResult } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { Options } from 'uplot';
import ErrorState from './ErrorState';
function StatusCodeBarCharts({
endPointStatusCodeBarChartsDataQuery,
endPointStatusCodeLatencyBarChartsDataQuery,
domainName,
endPointName,
domainListFilters,
filters,
}: {
endPointStatusCodeBarChartsDataQuery: UseQueryResult<
SuccessResponse<any>,
unknown
>;
endPointStatusCodeLatencyBarChartsDataQuery: UseQueryResult<
SuccessResponse<any>,
unknown
>;
domainName: string;
endPointName: string;
domainListFilters: IBuilderQuery['filters'];
filters: IBuilderQuery['filters'];
}): JSX.Element {
// 0 : Status Code Count
// 1 : Status Code Latency
const [currentWidgetInfoIndex, setCurrentWidgetInfoIndex] = useState(0);
const {
data: endPointStatusCodeBarChartsData,
} = endPointStatusCodeBarChartsDataQuery;
const {
data: endPointStatusCodeLatencyBarChartsData,
} = endPointStatusCodeLatencyBarChartsDataQuery;
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const graphRef = useRef<HTMLDivElement>(null);
const dimensions = useResizeObserver(graphRef);
const formattedEndPointStatusCodeBarChartsDataPayload = useMemo(
() =>
getFormattedEndPointStatusCodeChartData(
endPointStatusCodeBarChartsData?.payload,
'sum',
),
[endPointStatusCodeBarChartsData?.payload],
);
const formattedEndPointStatusCodeLatencyBarChartsDataPayload = useMemo(
() =>
getFormattedEndPointStatusCodeChartData(
endPointStatusCodeLatencyBarChartsData?.payload,
'average',
),
[endPointStatusCodeLatencyBarChartsData?.payload],
);
const chartData = useMemo(
() =>
getUPlotChartData(
currentWidgetInfoIndex === 0
? formattedEndPointStatusCodeBarChartsDataPayload
: formattedEndPointStatusCodeLatencyBarChartsDataPayload,
),
[
currentWidgetInfoIndex,
formattedEndPointStatusCodeBarChartsDataPayload,
formattedEndPointStatusCodeLatencyBarChartsDataPayload,
],
);
const isDarkMode = useIsDarkMode();
const graphClick = useGraphClickToShowButton({
graphRef,
isButtonEnabled: true,
buttonClassName: 'view-onclick-show-button',
});
const navigateToExplorer = useNavigateToExplorer();
const navigateToExplorerPages = useNavigateToExplorerPages();
const { notifications } = useNotifications();
const { getCustomSeries } = useGetGraphCustomSeries({
isDarkMode,
drawStyle: 'bars',
colorMapping: {
'200-299': Color.BG_FOREST_500,
'300-399': Color.BG_AMBER_400,
'400-499': Color.BG_CHERRY_500,
'500-599': Color.BG_ROBIN_500,
Other: Color.BG_SIENNA_500,
},
});
const widget = useMemo<Widgets>(
() =>
getStatusCodeBarChartWidgetData(domainName, endPointName, {
items: [...domainListFilters.items, ...filters.items],
op: filters.op,
}),
[domainName, endPointName, domainListFilters, filters],
);
const graphClickHandler = useCallback(
(
xValue: number,
yValue: number,
mouseX: number,
mouseY: number,
metric?: { [key: string]: string },
queryData?: { queryName: string; inFocusOrNot: boolean },
): void => {
const customFilters = getCustomFiltersForBarChart(metric);
handleGraphClick({
xValue,
yValue,
mouseX,
mouseY,
metric,
queryData,
widget,
navigateToExplorerPages,
navigateToExplorer,
notifications,
graphClick,
customFilters,
});
},
[
widget,
navigateToExplorerPages,
navigateToExplorer,
notifications,
graphClick,
],
);
const options = useMemo(
() =>
getUPlotChartOptions({
apiResponse:
currentWidgetInfoIndex === 0
? formattedEndPointStatusCodeBarChartsDataPayload
: formattedEndPointStatusCodeLatencyBarChartsDataPayload,
isDarkMode,
dimensions,
yAxisUnit: statusCodeWidgetInfo[currentWidgetInfoIndex].yAxisUnit,
softMax: null,
softMin: null,
minTimeScale: Math.floor(minTime / 1e9),
maxTimeScale: Math.floor(maxTime / 1e9),
panelType: PANEL_TYPES.BAR,
onClickHandler: graphClickHandler,
customSeries: getCustomSeries,
}),
[
minTime,
maxTime,
currentWidgetInfoIndex,
dimensions,
formattedEndPointStatusCodeBarChartsDataPayload,
formattedEndPointStatusCodeLatencyBarChartsDataPayload,
isDarkMode,
graphClickHandler,
getCustomSeries,
],
);
const renderCardContent = useCallback(
(query: UseQueryResult<SuccessResponse<any>, unknown>): JSX.Element => {
if (query.isLoading) {
return <Skeleton />;
}
if (query.error) {
return <ErrorState refetch={query.refetch} />;
}
return (
<div
className={cx('chart-container', {
'no-data-container':
!query.isLoading && !query?.data?.payload?.data?.result?.length,
})}
>
<Uplot options={options as Options} data={chartData} />
</div>
);
},
[options, chartData],
);
return (
<div>
<Card bordered className="endpoint-details-card">
<div className="header">
<Typography.Text>Call response status</Typography.Text>
<Button.Group className="views-tabs">
<Button
value={0}
className={currentWidgetInfoIndex === 0 ? 'selected_view tab' : 'tab'}
disabled={false}
onClick={(): void => setCurrentWidgetInfoIndex(0)}
>
Number of calls
</Button>
<Button
value={1}
className={currentWidgetInfoIndex === 1 ? 'selected_view tab' : 'tab'}
onClick={(): void => setCurrentWidgetInfoIndex(1)}
>
Latency
</Button>
</Button.Group>
</div>
<div className="graph-container" ref={graphRef}>
{renderCardContent(endPointStatusCodeBarChartsDataQuery)}
</div>
</Card>
</div>
);
}
export default StatusCodeBarCharts;

View File

@@ -0,0 +1,72 @@
import { Table, Typography } from 'antd';
import {
endPointStatusCodeColumns,
getFormattedEndPointStatusCodeData,
} from 'container/ApiMonitoring/utils';
import { useMemo } from 'react';
import { UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import ErrorState from './ErrorState';
function StatusCodeTable({
endPointStatusCodeDataQuery,
}: {
endPointStatusCodeDataQuery: UseQueryResult<SuccessResponse<any>, unknown>;
}): JSX.Element {
const {
isLoading,
isRefetching,
isError,
data,
refetch,
} = endPointStatusCodeDataQuery;
const statusCodeData = useMemo(() => {
if (isLoading || isRefetching || isError) {
return [];
}
return getFormattedEndPointStatusCodeData(
data?.payload?.data?.result[0].table.rows,
);
}, [data?.payload?.data?.result, isLoading, isRefetching, isError]);
if (isError) {
return <ErrorState refetch={refetch} />;
}
return (
<div className="status-code-table-container">
<Table
loading={isLoading || isRefetching}
dataSource={statusCodeData || []}
columns={endPointStatusCodeColumns}
pagination={false}
rowClassName={(_, index): string =>
index % 2 === 0 ? 'table-row-dark' : 'table-row-light'
}
locale={{
emptyText:
isLoading || isRefetching ? null : (
<div className="no-status-code-data-message-container">
<div className="no-status-code-data-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text className="no-status-code-data-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
</div>
),
}}
/>
</div>
);
}
export default StatusCodeTable;

View File

@@ -0,0 +1,9 @@
export enum VIEWS {
ALL_ENDPOINTS = 'all_endpoints',
ENDPOINT_DETAILS = 'endpoint_details',
}
export const VIEW_TYPES = {
ALL_ENDPOINTS: VIEWS.ALL_ENDPOINTS,
ENDPOINT_DETAILS: VIEWS.ENDPOINT_DETAILS,
};

View File

@@ -0,0 +1,159 @@
import '../Explorer.styles.scss';
import { LoadingOutlined } from '@ant-design/icons';
import { Spin, Table, Typography } from 'antd';
import axios from 'api';
import logEvent from 'api/common/logEvent';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import cx from 'classnames';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { HandleChangeQueryData } from 'types/common/operations.types';
import { GlobalReducer } from 'types/reducer/globalTime';
import {
columnsConfig,
formatDataForTable,
hardcodedAttributeKeys,
} from '../../utils';
import DomainDetails from './DomainDetails/DomainDetails';
function DomainList({
query,
showIP,
handleChangeQueryData,
}: {
query: IBuilderQuery;
showIP: boolean;
handleChangeQueryData: HandleChangeQueryData;
}): JSX.Element {
const [selectedDomainIndex, setSelectedDomainIndex] = useState<number>(-1);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const fetchApiOverview = async (): Promise<
SuccessResponse<any> | ErrorResponse
> => {
const requestBody = {
start: minTime,
end: maxTime,
show_ip: showIP,
filters: {
op: 'AND',
items: query?.filters.items,
},
};
try {
const response = await axios.post(
'/third-party-apis/overview/list',
requestBody,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
const { data, isLoading, isFetching } = useQuery(
[REACT_QUERY_KEY.GET_DOMAINS_LIST, minTime, maxTime, query, showIP],
fetchApiOverview,
);
const formattedDataForTable = useMemo(
() => formatDataForTable(data?.payload?.data?.result[0]?.table?.rows),
[data],
);
return (
<section className={cx('api-module-right-section')}>
<div className={cx('api-monitoring-list-header')}>
<QueryBuilderSearchV2
query={query}
onChange={(searchFilters): void =>
handleChangeQueryData('filters', searchFilters)
}
placeholder="Search filters..."
hardcodedAttributeKeys={hardcodedAttributeKeys}
/>
<DateTimeSelectionV2
showAutoRefresh={false}
showRefreshText={false}
hideShareModal
/>
</div>
<Table
className={cx('api-monitoring-domain-list-table')}
dataSource={isFetching || isLoading ? [] : formattedDataForTable}
columns={columnsConfig}
loading={{
spinning: isFetching || isLoading,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText:
isFetching || isLoading ? null : (
<div className="no-filtered-domains-message-container">
<div className="no-filtered-domains-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text className="no-filtered-domains-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
</div>
),
}}
scroll={{ x: true }}
tableLayout="fixed"
onRow={(record, index): { onClick: () => void; className: string } => ({
onClick: (): void => {
if (index !== undefined) {
const dataIndex = formattedDataForTable.findIndex(
(item) => item.key === record.key,
);
setSelectedDomainIndex(dataIndex);
logEvent('API Monitoring: Domain name row clicked', {});
}
},
className: 'expanded-clickable-row',
})}
rowClassName={(_, index): string =>
index % 2 === 0 ? 'table-row-dark' : 'table-row-light'
}
/>
{selectedDomainIndex !== -1 && (
<DomainDetails
domainData={formattedDataForTable[selectedDomainIndex]}
selectedDomainIndex={selectedDomainIndex}
setSelectedDomainIndex={setSelectedDomainIndex}
domainListLength={formattedDataForTable.length}
handleClose={(): void => {
setSelectedDomainIndex(-1);
}}
domainListFilters={query?.filters}
/>
)}
</section>
);
}
export default DomainList;

View File

@@ -0,0 +1,219 @@
.api-monitoring-page {
display: flex;
height: 100%;
.api-quick-filter-left-section {
width: 0%;
flex-shrink: 0;
.api-quick-filters-header {
padding: 12px;
border-bottom: 1px solid var(--bg-slate-400);
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
line-height: 18px;
}
}
.api-module-right-section {
display: flex;
flex-direction: column;
width: 100%;
.api-monitoring-list-header {
width: 100%;
padding: 8px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.query-builder-search-v2 {
min-width: 80%;
flex: 1;
}
}
.api-monitoring-domain-list-table {
.ant-table {
.ant-table-thead > tr > th {
padding: 12px;
font-weight: 500;
font-size: 12px;
line-height: 18px;
border-bottom: none;
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 18px;
/* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
background: none;
&::before {
background-color: transparent;
}
}
.ant-table-thead > tr > th:has(.domain-list-name-col-header) {
background: var(--bg-ink-300);
opacity: 0.6;
}
.ant-table-cell {
padding: 12px;
font-size: 13px;
line-height: 20px;
color: var(--bg-vanilla-100);
border-bottom: none;
}
.ant-table-cell:has(.domain-list-name-col-value) {
background: var(--bg-ink-300);
opacity: 0.6;
}
.round-metric-tag {
display: inline-flex;
padding: 2px 8px;
align-items: center;
gap: 6px;
width: fit-content;
border-radius: 50px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-slate-500);
text-transform: lowercase;
}
.ant-table-tbody > tr:hover > td {
background: rgba(255, 255, 255, 0.04);
}
.ant-table-cell:first-child {
text-align: justify;
}
.ant-table-cell:nth-child(2) {
padding-left: 16px;
padding-right: 16px;
}
.ant-table-cell:nth-child(n + 3) {
padding-right: 24px;
}
.column-header-right {
text-align: right;
}
.ant-table-tbody > tr > td {
border-bottom: none;
}
.ant-table-thead
> tr
> th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before {
background-color: transparent;
}
.ant-empty-normal {
visibility: hidden;
}
.table-row-light {
background: none;
}
.table-row-dark {
background: var(--bg-ink-300);
}
.error-rate {
width: 120px;
}
}
}
&.filter-visible {
.api-quick-filter-left-section {
width: 260px;
}
.api-module-right-section {
width: calc(100% - 260px);
}
}
}
.no-filtered-domains-message-container {
height: 30vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.no-filtered-domains-message-content {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
width: fit-content;
padding: 24px;
}
.no-filtered-domains-message {
margin-top: 8px;
}
}
.lightMode {
.api-monitoring-domain-list-table {
.ant-table {
.ant-table-thead > tr > th {
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
.ant-table-thead > tr > th:has(.domain-list-name-col-header) {
background: var(--bg-vanilla-100);
}
.ant-table-cell {
background: var(--bg-vanilla-100);
color: var(--bg-ink-500);
}
.ant-table-cell:has(.domain-list-name-col-value) {
background: var(--bg-vanilla-100);
}
.ant-table-tbody > tr:hover > td {
background: rgba(0, 0, 0, 0.04);
}
.table-row-light {
background: none;
}
.table-row-dark {
background: none;
}
.round-metric-tag {
color: var(--bg-vanilla-100);
}
}
}
}

View File

@@ -0,0 +1,101 @@
import './Explorer.styles.scss';
import { FilterOutlined } from '@ant-design/icons';
import * as Sentry from '@sentry/react';
import { Switch, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useEffect, useMemo, useState } from 'react';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { ApiMonitoringQuickFiltersConfig } from '../utils';
import DomainList from './Domains/DomainList';
function Explorer(): JSX.Element {
const [showIP, setShowIP] = useState<boolean>(true);
const { currentQuery } = useQueryBuilder();
useEffect(() => {
logEvent('API Monitoring: Landing page visited', {});
}, []);
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: currentQuery.builder.queryData[0],
entityVersion: '',
});
const updatedCurrentQuery = useMemo(
() => ({
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
dataSource: DataSource.TRACES,
aggregateOperator: 'noop',
aggregateAttribute: {
...currentQuery.builder.queryData[0].aggregateAttribute,
},
},
],
},
}),
[currentQuery],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className={cx('api-monitoring-page', 'filter-visible')}>
<section className="api-quick-filter-left-section">
<div className="api-quick-filters-header">
<FilterOutlined />
<Typography.Text>Filters</Typography.Text>
</div>
<div className="api-quick-filters-header">
<Typography.Text>Show IP addresses</Typography.Text>
<Switch
size="small"
style={{ marginLeft: 'auto' }}
checked={showIP}
onClick={(): void => {
setShowIP((showIP): boolean => {
logEvent('API Monitoring: Show IP addresses clicked', {
showIP: !showIP,
});
return !showIP;
});
}}
/>
</div>
<QuickFilters
source={QuickFiltersSource.API_MONITORING}
config={ApiMonitoringQuickFiltersConfig}
handleFilterVisibilityChange={(): void => {}}
onFilterChange={(query: Query): void =>
handleChangeQueryData('filters', query.builder.queryData[0].filters)
}
/>
</section>
<DomainList
query={query}
showIP={showIP}
handleChangeQueryData={handleChangeQueryData}
/>
</div>
</Sentry.ErrorBoundary>
);
}
export default Explorer;

File diff suppressed because it is too large Load Diff

View File

@@ -337,6 +337,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
routeKey === 'LOGS_PIPELINES' ||
routeKey === 'LOGS_SAVE_VIEWS';
const isApiMonitoringView = (): boolean => routeKey === 'API_MONITORING';
const isTracesView = (): boolean =>
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
@@ -658,7 +660,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
isAlertOverview() ||
isMessagingQueues() ||
isCloudIntegrationPage() ||
isInfraMonitoring()
isInfraMonitoring() ||
isApiMonitoringView()
? 0
: '0 1rem',

View File

@@ -61,7 +61,7 @@ export default function CustomDomainSettings(): JSX.Element {
isLoading: isLoadingDeploymentsData,
isFetching: isFetchingDeploymentsData,
refetch: refetchDeploymentsData,
} = useGetDeploymentsData();
} = useGetDeploymentsData(true);
const {
mutate: updateSubDomain,

View File

@@ -223,7 +223,7 @@ function ExplorerOptions({
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
const viewKey = useGetSearchQueryParam(QueryParams.viewKey) || '';
const extraData = viewsData?.data?.data?.find((view) => view.uuid === viewKey)
const extraData = viewsData?.data?.data?.find((view) => view.id === viewKey)
?.extraData;
const extraDataColor = extraData ? JSON.parse(extraData).color : '';
@@ -357,17 +357,12 @@ function ExplorerOptions({
viewsData?.data?.data,
);
if (!currentViewDetails) return;
const {
query,
name,
uuid,
panelType: currentPanelType,
} = currentViewDetails;
const { query, name, id, panelType: currentPanelType } = currentViewDetails;
handleExplorerTabChange(currentPanelType, {
query,
name,
uuid,
id,
});
},
[viewsData, handleExplorerTabChange],
@@ -694,7 +689,7 @@ function ExplorerOptions({
bgColor = extraData.color;
}
return (
<Select.Option key={view.uuid} value={view.name}>
<Select.Option key={view.id} value={view.name}>
<div className="render-options">
<span
className="dot"

View File

@@ -49,6 +49,7 @@ function FullView({
isDependedDataLoaded = false,
onToggleModelHandler,
onClickHandler,
customOnDragSelect,
setCurrentGraphRef,
}: FullViewProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
@@ -252,7 +253,7 @@ function FullView({
onToggleModelHandler={onToggleModelHandler}
setGraphVisibility={setGraphsVisibilityStates}
graphVisibility={graphsVisibilityStates}
onDragSelect={onDragSelect}
onDragSelect={customOnDragSelect ?? onDragSelect}
tableProcessedDataRef={tableProcessedDataRef}
searchTerm={searchTerm}
onClickHandler={onClickHandler}

View File

@@ -50,6 +50,7 @@ export interface FullViewProps {
widget: Widgets;
fullViewOptions?: boolean;
onClickHandler?: OnClickPluginOpts['onClick'];
customOnDragSelect?: (start: number, end: number) => void;
name: string;
tableProcessedDataRef: MutableRefObject<RowData[]>;
version?: string;

View File

@@ -50,6 +50,7 @@ function WidgetGraphComponent({
setRequestData,
onClickHandler,
onDragSelect,
customOnDragSelect,
customTooltipElement,
openTracesButton,
onOpenTraceBtnClick,
@@ -327,6 +328,7 @@ function WidgetGraphComponent({
onToggleModelHandler={onToggleModelHandler}
tableProcessedDataRef={tableProcessedDataRef}
onClickHandler={onClickHandler ?? graphClickHandler}
customOnDragSelect={customOnDragSelect}
setCurrentGraphRef={setCurrentGraphRef}
/>
</Modal>

View File

@@ -36,6 +36,7 @@ function GridCardGraph({
version,
onClickHandler,
onDragSelect,
customOnDragSelect,
customTooltipElement,
dataAvailable,
getGraphData,
@@ -272,6 +273,7 @@ function GridCardGraph({
setRequestData={setRequestData}
onClickHandler={onClickHandler}
onDragSelect={onDragSelect}
customOnDragSelect={customOnDragSelect}
customTooltipElement={customTooltipElement}
openTracesButton={openTracesButton}
onOpenTraceBtnClick={onOpenTraceBtnClick}

View File

@@ -33,6 +33,7 @@ export interface WidgetGraphComponentProps {
setRequestData?: Dispatch<SetStateAction<GetQueryResultsProps>>;
onClickHandler?: OnClickPluginOpts['onClick'];
onDragSelect: (start: number, end: number) => void;
customOnDragSelect?: (start: number, end: number) => void;
customTooltipElement?: HTMLDivElement;
openTracesButton?: boolean;
onOpenTraceBtnClick?: (record: RowData) => void;
@@ -49,6 +50,7 @@ export interface GridCardGraphProps {
variables?: Dashboard['data']['variables'];
version?: string;
onDragSelect: (start: number, end: number) => void;
customOnDragSelect?: (start: number, end: number) => void;
customTooltipElement?: HTMLDivElement;
dataAvailable?: (isDataAvailable: boolean) => void;
getGraphData?: (graphData?: MetricRangePayloadProps['data']) => void;

View File

@@ -178,6 +178,7 @@ interface HandleGraphClickParams {
navigateToExplorer: (props: NavigateToExplorerProps) => void;
notifications: NotificationInstance;
graphClick: (props: GraphClickProps) => void;
customFilters?: TagFilterItem[];
}
export const handleGraphClick = async ({
@@ -192,6 +193,7 @@ export const handleGraphClick = async ({
navigateToExplorer,
notifications,
graphClick,
customFilters,
}: HandleGraphClickParams): Promise<void> => {
const { stepInterval } = widget?.query?.builder?.queryData?.[0] ?? {};
@@ -221,7 +223,7 @@ export const handleGraphClick = async ({
}: ${key}`,
onClick: (): void =>
navigateToExplorer({
filters: result[key].filters,
filters: [...result[key].filters, ...(customFilters || [])],
dataSource: result[key].dataSource as DataSource,
startTime: xValue,
endTime: xValue + (stepInterval ?? 60),

View File

@@ -44,7 +44,10 @@ import { EditMenuAction, ViewMenuAction } from './config';
import DashboardEmptyState from './DashboardEmptyState/DashboardEmptyState';
import GridCard from './GridCard';
import { Card, CardContainer, ReactGridLayout } from './styles';
import { removeUndefinedValuesFromLayout } from './utils';
import {
hasColumnWidthsChanged,
removeUndefinedValuesFromLayout,
} from './utils';
import { MenuItemKeys } from './WidgetHeader/contants';
import { WidgetRowHeader } from './WidgetRow';
@@ -68,6 +71,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
setDashboardQueryRangeCalled,
setSelectedRowWidgetId,
isDashboardFetching,
columnWidths,
} = useDashboard();
const { data } = selectedDashboard || {};
const { pathname } = useLocation();
@@ -162,6 +166,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
logEventCalledRef.current = true;
}
}, [data]);
const onSaveHandler = (): void => {
if (!selectedDashboard) return;
@@ -171,6 +176,15 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
...selectedDashboard.data,
panelMap: { ...currentPanelMap },
layout: dashboardLayout.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET),
widgets: selectedDashboard?.data?.widgets?.map((widget) => {
if (columnWidths?.[widget.id]) {
return {
...widget,
columnWidths: columnWidths[widget.id],
};
}
return widget;
}),
},
uuid: selectedDashboard.uuid,
};
@@ -227,20 +241,31 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
useEffect(() => {
if (
isDashboardLocked ||
!saveLayoutPermission ||
updateDashboardMutation.isLoading ||
isDashboardFetching
) {
return;
}
const shouldSaveLayout =
dashboardLayout &&
Array.isArray(dashboardLayout) &&
dashboardLayout.length > 0 &&
!isEqual(layouts, dashboardLayout) &&
!isDashboardLocked &&
saveLayoutPermission &&
!updateDashboardMutation.isLoading &&
!isDashboardFetching
) {
!isEqual(layouts, dashboardLayout);
const shouldSaveColumnWidths =
dashboardLayout &&
Array.isArray(dashboardLayout) &&
dashboardLayout.length > 0 &&
hasColumnWidthsChanged(columnWidths, selectedDashboard);
if (shouldSaveLayout || shouldSaveColumnWidths) {
onSaveHandler();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dashboardLayout]);
}, [dashboardLayout, columnWidths]);
const onSettingsModalSubmit = (): void => {
const newTitle = form.getFieldValue('title');

View File

@@ -12,7 +12,7 @@ import { v4 } from 'uuid';
import { extractQueryNamesFromExpression } from './utils';
type GraphClickMetaData = {
export type GraphClickMetaData = {
[key: string]: string | boolean;
queryName: string;
inFocusOrNot: boolean;

View File

@@ -1,5 +1,7 @@
import { FORMULA_REGEXP } from 'constants/regExp';
import { isEmpty, isEqual } from 'lodash-es';
import { Layout } from 'react-grid-layout';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
export const removeUndefinedValuesFromLayout = (layout: Layout[]): Layout[] =>
layout.map((obj) =>
@@ -25,3 +27,27 @@ export function extractQueryNamesFromExpression(expression: string): string[] {
// Extract matches and deduplicate
return [...new Set(expression.match(queryNameRegex) || [])];
}
export const hasColumnWidthsChanged = (
columnWidths: Record<string, Record<string, number>>,
selectedDashboard?: Dashboard,
): boolean => {
// If no column widths stored, no changes
if (isEmpty(columnWidths) || !selectedDashboard) return false;
// Check each widget's column widths
return Object.keys(columnWidths).some((widgetId) => {
const dashboardWidget = selectedDashboard?.data?.widgets?.find(
(widget) => widget.id === widgetId,
) as Widgets;
const newWidths = columnWidths[widgetId];
const existingWidths = dashboardWidget?.columnWidths;
// If both are empty/undefined, no change
if (isEmpty(newWidths) || isEmpty(existingWidths)) return false;
// Compare stored column widths with dashboard widget's column widths
return !isEqual(newWidths, existingWidths);
});
};

View File

@@ -43,6 +43,7 @@ function GridTableComponent({
sticky,
openTracesButton,
onOpenTraceBtnClick,
widgetId,
...props
}: GridTableComponentProps): JSX.Element {
const { t } = useTranslation(['valueGraph']);
@@ -229,6 +230,7 @@ function GridTableComponent({
columns={openTracesButton ? columnDataWithOpenTracesButton : newColumnData}
dataSource={dataSource}
sticky={sticky}
widgetId={widgetId}
onRow={
openTracesButton
? (record): React.HTMLAttributes<HTMLElement> => ({

View File

@@ -17,6 +17,7 @@ export type GridTableComponentProps = {
searchTerm?: string;
openTracesButton?: boolean;
onOpenTraceBtnClick?: (record: RowData) => void;
widgetId?: string;
} & Pick<LogsExplorerTableProps, 'data'> &
Omit<TableProps<RowData>, 'columns' | 'dataSource'>;

View File

@@ -23,10 +23,13 @@ function DataSourceInfo({
const notSendingData = !dataSentToSigNoz;
const isEnabled =
activeLicenseV3 && activeLicenseV3.platform === LicensePlatform.CLOUD;
const {
data: deploymentsData,
isError: isErrorDeploymentsData,
} = useGetDeploymentsData();
} = useGetDeploymentsData(isEnabled || false);
const [region, setRegion] = useState<string>('');
const [url, setUrl] = useState<string>('');

View File

@@ -63,17 +63,17 @@ export default function SavedViews({
const handleRedirectQuery = (view: ViewProps): void => {
logEvent('Homepage: Saved view clicked', {
viewId: view.uuid,
viewId: view.id,
viewName: view.name,
entity: selectedEntity,
});
const currentViewDetails = getViewDetailsUsingViewKey(
view.uuid,
view.id,
selectedEntity === 'logs' ? logsViews : tracesViews,
);
if (!currentViewDetails) return;
const { query, name, uuid, panelType: currentPanelType } = currentViewDetails;
const { query, name, id, panelType: currentPanelType } = currentViewDetails;
if (selectedEntity) {
handleExplorerTabChange(
@@ -81,7 +81,7 @@ export default function SavedViews({
{
query,
name,
uuid,
id,
},
SOURCEPAGE_VS_ROUTES[selectedEntity],
);

View File

@@ -702,29 +702,36 @@
}
}
.ant-table-cell {
min-width: 140px !important;
max-width: 140px !important;
}
.ant-table-cell {
&:has(.pod-name-header) {
min-width: 250px !important;
max-width: 250px !important;
}
}
.ant-table-cell {
&:has(.med-col) {
min-width: 180px !important;
max-width: 180px !important;
}
}
.expanded-k8s-list-table {
.infra-monitoring-container {
.ant-table-cell {
min-width: 180px !important;
max-width: 180px !important;
min-width: 140px !important;
max-width: 140px !important;
}
.ant-table-cell {
&:has(.pod-name-header) {
min-width: 250px !important;
max-width: 250px !important;
}
}
.ant-table-cell {
&:has(.med-col) {
min-width: 180px !important;
max-width: 180px !important;
}
}
.expanded-k8s-list-table {
.ant-table-cell {
min-width: 180px !important;
max-width: 180px !important;
}
.ant-table-row-expand-icon-cell {
min-width: 30px !important;
max-width: 30px !important;
}
}
.ant-table-row-expand-icon-cell {
@@ -733,11 +740,6 @@
}
}
.ant-table-row-expand-icon-cell {
min-width: 30px !important;
max-width: 30px !important;
}
.event-content-container {
.ant-table {
background: var(--bg-ink-400);

View File

@@ -293,7 +293,7 @@ function MultiIngestionSettings(): JSX.Element {
isLoading: isLoadingDeploymentsData,
isFetching: isFetchingDeploymentsData,
isError: isErrorDeploymentsData,
} = useGetDeploymentsData();
} = useGetDeploymentsData(true);
const {
mutate: createIngestionKey,

View File

@@ -10,7 +10,6 @@ import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQue
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useEventSourceEvent } from 'hooks/useEventSourceEvent';
import { useNotifications } from 'hooks/useNotifications';
import { prepareQueryRangePayload } from 'lib/dashboard/prepareQueryRangePayload';
import { useEventSource } from 'providers/EventSource';
import { useCallback, useEffect, useRef, useState } from 'react';
@@ -38,8 +37,6 @@ function LiveLogsContainer(): JSX.Element {
const batchedEventsRef = useRef<ILog[]>([]);
const { notifications } = useNotifications();
const { selectedTime: globalSelectedTime } = useSelector<
AppState,
GlobalReducer
@@ -50,6 +47,8 @@ function LiveLogsContainer(): JSX.Element {
handleCloseConnection,
initialLoading,
isConnectionLoading,
isConnectionError,
reconnectDueToError,
} = useEventSource();
const compositeQuery = useGetCompositeQueryParam();
@@ -86,8 +85,8 @@ function LiveLogsContainer(): JSX.Element {
);
const handleError = useCallback(() => {
notifications.error({ message: 'Sorry, something went wrong' });
}, [notifications]);
console.error('Sorry, something went wrong');
}, []);
useEventSourceEvent('message', handleGetLiveLogs);
useEventSourceEvent('error', handleError);
@@ -153,6 +152,23 @@ function LiveLogsContainer(): JSX.Element {
handleStartNewConnection,
]);
useEffect((): (() => void) | undefined => {
if (isConnectionError && reconnectDueToError && compositeQuery) {
// Small delay to prevent immediate reconnection attempts
const reconnectTimer = setTimeout(() => {
handleStartNewConnection(compositeQuery);
}, 1000);
return (): void => clearTimeout(reconnectTimer);
}
return undefined;
}, [
isConnectionError,
reconnectDueToError,
compositeQuery,
handleStartNewConnection,
]);
useEffect(() => {
const prefetchedList = queryLocationState?.listQueryPayload[0]?.list;

View File

@@ -1,9 +1,9 @@
import './LogsPanelComponent.styles.scss';
import { Table } from 'antd';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { ResizeTable } from 'components/ResizeTable';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { PANEL_TYPES } from 'constants/queryBuilder';
import Controls from 'container/Controls';
@@ -79,9 +79,14 @@ function LogsPanelComponent({
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const columns = getLogPanelColumnsList(
widget.selectedLogFields,
formatTimezoneAdjustedTimestamp,
const columns = useMemo(
() =>
getLogPanelColumnsList(
widget.selectedLogFields,
formatTimezoneAdjustedTimestamp,
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[widget.selectedLogFields],
);
const dataLength =
@@ -216,16 +221,18 @@ function LogsPanelComponent({
<div className="logs-table">
<div className="resize-table">
<OverlayScrollbar>
<Table
<ResizeTable
pagination={false}
tableLayout="fixed"
scroll={{ x: `calc(50vw - 10px)` }}
scroll={{ x: `max-content` }}
sticky
loading={queryResponse.isFetching}
style={tableStyles}
dataSource={flattenLogData}
columns={columns}
onRow={handleRow}
widgetId={widget.id}
shouldPersistColumnWidths
/>
</OverlayScrollbar>
</div>

View File

@@ -0,0 +1,16 @@
.inspect-metrics-modal {
.inspect-metrics-title {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 6px;
.inspect-metrics-button {
display: flex;
align-items: center;
justify-content: center;
cursor: default;
color: var(--text-vanilla-500);
}
}
}

View File

@@ -0,0 +1,48 @@
import './Inspect.styles.scss';
import * as Sentry from '@sentry/react';
import { Color } from '@signozhq/design-tokens';
import { Button, Drawer, Typography } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { Compass } from 'lucide-react';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { InspectProps } from './types';
function Inspect({ metricName, isOpen, onClose }: InspectProps): JSX.Element {
const isDarkMode = useIsDarkMode();
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<Drawer
width="100%"
title={
<div className="inspect-metrics-title">
<Typography.Text>Metrics Explorer </Typography.Text>
<Button
className="inspect-metrics-button"
size="small"
icon={<Compass size={14} />}
disabled
>
Inspect Metric
</Button>
</div>
}
open={isOpen}
onClose={onClose}
style={{
overscrollBehavior: 'contain',
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
}}
className="inspect-metrics-modal"
destroyOnClose
>
<div>Inspect</div>
<div>{metricName}</div>
</Drawer>
</Sentry.ErrorBoundary>
);
}
export default Inspect;

View File

@@ -0,0 +1 @@
export const INSPECT_FEATURE_FLAG_KEY = 'metrics-explorer-inspect-feature-flag';

View File

@@ -0,0 +1,3 @@
import Inspect from './Inspect';
export default Inspect;

View File

@@ -0,0 +1,5 @@
export type InspectProps = {
metricName: string | null;
isOpen: boolean;
onClose: () => void;
};

View File

@@ -0,0 +1,11 @@
import { INSPECT_FEATURE_FLAG_KEY } from './constants';
/**
* Check if the inspect feature flag is enabled
* returns true if the feature flag is enabled, false otherwise
* Show the inspect button in metrics explorer if the feature flag is enabled
*/
export function isInspectEnabled(): boolean {
const featureFlag = localStorage.getItem(INSPECT_FEATURE_FLAG_KEY);
return featureFlag === 'true';
}

View File

@@ -20,6 +20,12 @@
gap: 4px;
background-color: var(--bg-slate-300);
}
.inspect-metrics-button {
display: flex;
align-items: center;
justify-content: center;
}
}
.metric-details-content {

View File

@@ -2,12 +2,21 @@ import './MetricDetails.styles.scss';
import '../Summary/Summary.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Divider, Drawer, Empty, Skeleton, Tooltip, Typography } from 'antd';
import {
Button,
Divider,
Drawer,
Empty,
Skeleton,
Tooltip,
Typography,
} from 'antd';
import { useGetMetricDetails } from 'hooks/metricsExplorer/useGetMetricDetails';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { X } from 'lucide-react';
import { Compass, X } from 'lucide-react';
import { useMemo } from 'react';
import { isInspectEnabled } from '../Inspect/utils';
import { formatNumberIntoHumanReadableFormat } from '../Summary/utils';
import AllAttributes from './AllAttributes';
import DashboardsAndAlertsPopover from './DashboardsAndAlertsPopover';
@@ -22,6 +31,7 @@ function MetricDetails({
onClose,
isOpen,
metricName,
openInspectModal,
}: MetricDetailsProps): JSX.Element {
const isDarkMode = useIsDarkMode();
// const { safeNavigate } = useSafeNavigate();
@@ -43,6 +53,8 @@ function MetricDetails({
return formatTimestampToReadableDate(metric.lastReceived);
}, [metric]);
const showInspectFeature = useMemo(() => isInspectEnabled(), []);
const isMetricDetailsLoading = isLoading || isFetching;
const timeSeries = useMemo(() => {
@@ -92,6 +104,19 @@ function MetricDetails({
>
Open in Explorer
</Button> */}
{/* Show the based on the feature flag. Will remove before releasing the feature */}
{showInspectFeature && (
<Button
className="inspect-metrics-button"
aria-label="Inspect Metric"
icon={<Compass size={18} />}
onClick={(): void => {
if (metric?.name) {
openInspectModal(metric.name);
}
}}
/>
)}
</div>
}
placement="right"

View File

@@ -10,6 +10,7 @@ export interface MetricDetailsProps {
isOpen: boolean;
metricName: string | null;
isModalTimeSelection: boolean;
openInspectModal: (metricName: string) => void;
}
export interface DashboardsAndAlertsPopoverProps {

View File

@@ -13,6 +13,7 @@ import { AppState } from 'store/reducers';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import InspectModal from '../Inspect';
import MetricDetails from '../MetricDetails';
import MetricsSearch from './MetricsSearch';
import MetricsTable from './MetricsTable';
@@ -35,6 +36,7 @@ function Summary(): JSX.Element {
TreemapViewType.TIMESERIES,
);
const [isMetricDetailsOpen, setIsMetricDetailsOpen] = useState(false);
const [isInspectModalOpen, setIsInspectModalOpen] = useState(false);
const [selectedMetricName, setSelectedMetricName] = useState<string | null>(
null,
);
@@ -150,6 +152,17 @@ function Summary(): JSX.Element {
setIsMetricDetailsOpen(false);
};
const openInspectModal = (metricName: string): void => {
setSelectedMetricName(metricName);
setIsInspectModalOpen(true);
setIsMetricDetailsOpen(false);
};
const closeInspectModal = (): void => {
setIsInspectModalOpen(false);
setSelectedMetricName(null);
};
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="metrics-explorer-summary-tab">
@@ -184,6 +197,14 @@ function Summary(): JSX.Element {
onClose={closeMetricDetails}
metricName={selectedMetricName}
isModalTimeSelection={false}
openInspectModal={openInspectModal}
/>
)}
{isInspectModalOpen && (
<InspectModal
isOpen={isInspectModalOpen}
onClose={closeInspectModal}
metricName={selectedMetricName}
/>
)}
</Sentry.ErrorBoundary>

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