Compare commits

...

32 Commits

Author SHA1 Message Date
SagarRajput-7
d22ecb9f7c feat: allowed user to edit panel while the API is loading (#7007)
* feat: allowed user to edit panel while the API is loading

* feat: added test case
2025-02-11 18:32:13 +05:30
SagarRajput-7
02c2b55d5e feat: added new and cloned panel at the bottom of the page (#6993)
* feat: added new and cloned panel at the bottom of the page

* feat: added common util and took possible space available in last row in account

* feat: added changes for empty layout

* feat: added different test cases

* feat: remove console.log

* feat: added default value to widgetWidth
2025-02-11 17:01:17 +05:30
Vishal Sharma
9a75e27ec3 chore: add customerio identity calls (#7079) 2025-02-11 11:20:15 +00:00
SagarRajput-7
1fb3953614 feat: added create alerts option for panel in locked dashboard (#7071) 2025-02-11 16:32:25 +05:30
Vishal Sharma
37558facbe chore: add customerio to frontend (#7062) 2025-02-07 20:13:35 +05:30
Vikrant Gupta
12f65f4a72 Revert "chore: add related values (#6619)" (#7067)
This reverts commit c5219ac157.
2025-02-07 11:51:11 +00:00
Srikanth Chekuri
398760006b fix: address the first point increase without previous measurement value (#7054) 2025-02-07 07:44:18 +00:00
Raj Kamal Singh
37323a64cf chore: also use signoz_api_key from connection params (#7061) 2025-02-07 11:26:42 +05:30
Srikanth Chekuri
6d8d2e6b11 chore: use any of [cpu.usage, cpu.utilization] for cpu (#7017) 2025-02-06 11:56:58 +00:00
Srikanth Chekuri
a8e8f31b00 chore: parse string values for __value filter (#7035) 2025-02-06 11:47:29 +00:00
Shaheer Kochai
c3164912e6 AWS Integration changes (#7025)
* fix: update AWS accounts API response to return accounts list

* feat: display skeleton UI for account actions and refactored rendering logic

* chore: update AWS service naming from "AWS Web Services" to "Amazon Web Services"

* feat: aws integration success modal changes

* feat: auto-select first service when no service is active

* feat: display 'enable service' if service hasn't been configured and 'Configure (x/2)' if configured

* fix: display no data yet if status is not available

* feat: properly handle remove integration account flow

* fix: rename accountId param to cloudAccountId

* fix: update the aws service list and details api parameter from account_id to cloud_account_id

* fix: fix the issue of stale service config modal enabled/disabled state

* chore: improve the UI of configure button

* feat: add connection parameters support for AWS cloud integration

* feat: add optional link support for cloud service dashboards

* fix: get the correct supported signals count + a minor refactoring

* fix: remove cloudAccountId on success of account remove

* chore: update the remove integration copy

* refactor: add react query key for AWS connection parameters

* fix: correct typo in integration loading state variable name

* refactor: move skeleton inline styles to style file and do overall refactoring

* chore: address the requested changes
2025-02-06 15:13:19 +04:30
Shaheer Kochai
b215c6a0ce feat: aws integration feature flag changes (#7033)
* feat: aws integration feature flag changes

* fix: fix the failing build
2025-02-06 09:26:10 +00:00
Ekansh Gupta
94c2398a08 feat: bug fix for segregating entrypoint spans and root spans in span filtering (#7046) 2025-02-06 13:26:18 +05:30
Raj Kamal Singh
acd9b97ee3 Feat: aws integrations: service connection status (#7032)
* feat: add ability to get latest metric received ts by labelValues filter

* feat: svc metrics connection status check

* feat: aws integration svc logs connection check

* chore: fix broken test

* chore: address PR review comments

* chore: address PR feedback

* chore: fix broken test expectation

* fix: use resource filter for logs connection status
2025-02-06 13:08:47 +05:30
Srikanth Chekuri
c5219ac157 chore: add related values (#6619) 2025-02-06 05:09:01 +00:00
Vikrant Gupta
2b32ce190f task(licenses): update the license events and the state names (#7034)
* Revert "Revert "chore(licenses): update the license events and the state name…"

This reverts commit 66adc7fbf9.

* chore(license): fix comment
2025-02-05 18:25:36 +00:00
Amlan Kumar Nandy
c7c7b25651 feat: metrics explorer base setup (#7024) 2025-02-05 12:57:12 +00:00
Amlan Kumar Nandy
f548afe284 chore: share filters across logs and traces views in entity details (#7014) 2025-02-05 18:18:35 +05:30
primus-bot[bot]
586f5255f0 chore(release): bump to v0.71.0 (#7031)
#### Summary
 - Release SigNoz v0.71.0
 - Bump SigNoz OTel Collector to v0.111.26

 Created by [Primus-Bot](https://github.com/apps/primus-bot)
2025-02-05 15:13:58 +05:30
SagarRajput-7
62064f136d feat: corrected the color map for error % and added throughput unit (#7030) 2025-02-05 14:56:31 +05:30
Vikrant Gupta
66adc7fbf9 Revert "chore(licenses): update the license events and the state names (#7021)" (#7029)
This reverts commit e414215786.
2025-02-05 12:38:56 +05:30
Yunus M
f6b2d5a519 feat: change grid to flex - flower metrics section (#7027) 2025-02-05 11:48:44 +05:30
Vibhu Pandey
035999250e fix(add_pats): fix name of column (#7026) 2025-02-05 11:02:17 +05:30
Shaheer Kochai
aecbb71ce4 feat: truncate very long lines in traces explorer list view (#6987)
* feat: truncate very long lines in traces explorer list view

* fix: update snapshot tests with new line-clamped text class to fix the failing test
2025-02-04 13:29:23 +00:00
Ekansh Gupta
01b6e22bbd feat: bug fix for the issue in span filtering feature. No spans returned in traces tab (#7023)
* feat: bug fix for the issue in span filtering feature. No spans returned in traces tab

* feat: bug fix for the issue in span filtering feature. No spans returned in traces tab
2025-02-04 18:36:07 +05:30
Vibhu Pandey
dc15ee8176 feat(sqlmigration): consolidate all sqlmigrations into one package (#7018)
* feat(sqlmigration): add sqlmigrations

* feat(sqlmigration): test sqlmigrations

* feat(sqlmigration): add remaining factories

* feat(sqlmigration): consolidate into single package

* fix(telemetrystore): remove existing env variables

* fix(telemetrystore): fix DSN
2025-02-04 09:23:36 +00:00
Vikrant Gupta
e414215786 chore(licenses): update the license events and the state names (#7021) 2025-02-04 14:11:13 +05:30
Amlan Kumar Nandy
5fe04078e5 chore: update logs in infra monitoring for analytics (#6994) 2025-02-04 06:01:37 +00:00
SagarRajput-7
cf95b15ba1 feat: celery - misc feedback fixes (#7019)
* feat: celery - misc feedback fixes

* feat: enabled better sharing and auto-refresh

* feat: made task name filter more visible
2025-02-04 09:41:27 +05:30
Raj Kamal Singh
3b550c485d Feat: cloud integrations: agent check-in api (#7004)
* chore: cloudintegrations: rename rds def

* feat: cloudintegrations: shape of agent check in response for v0 release

* chore: cloudintegrations: add validation for response expected after agent check in

* chore: cloudintegrations: accumulate teletry collection strategies for enabled services

* chore: cloudintegrations: use map struct to parse from map to struct with json tags

* chore: cloudintegrations: telemetry collection strategy for services

* chore: cloudintegrations: wrap up test for agent check in resp

* chore: some cleanup

* chore: some cleanup

* chore: some minor renaming

* chore: address review comment

* chore: some cleanup

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-02-03 20:52:15 +05:30
Amlan Kumar Nandy
784dccf298 feat: fix incorrect suggestions while moving between explores (#7008) 2025-02-03 14:31:01 +05:30
Shaheer Kochai
aa26dc77af fix(styles): fix the gap between text and button in logs quick filters (#7006) 2025-02-03 04:54:21 +00:00
198 changed files with 3391 additions and 1883 deletions

View File

@@ -68,6 +68,8 @@ jobs:
echo 'TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> frontend/.env
echo 'TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> frontend/.env
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> frontend/.env
echo 'CUSTOMERIO_ID="${{ secrets.CUSTOMERIO_ID }}"' >> frontend/.env
echo 'CUSTOMERIO_SITE_ID="${{ secrets.CUSTOMERIO_SITE_ID }}"' >> frontend/.env
- name: Setup golang
uses: actions/setup-go@v4
with:

View File

@@ -181,7 +181,7 @@ services:
- query-service
query-service:
!!merge <<: *db-depend
image: signoz/query-service:0.70.1
image: signoz/query-service:0.71.0
command:
- --config=/root/config/prometheus.yml
- --use-logs-new-schema=true
@@ -214,7 +214,7 @@ services:
retries: 3
frontend:
!!merge <<: *common
image: signoz/frontend:0.70.1
image: signoz/frontend:0.71.0
depends_on:
- alertmanager
- query-service
@@ -224,7 +224,7 @@ services:
- ../common/signoz/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:0.111.25
image: signoz/signoz-otel-collector:0.111.26
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml

View File

@@ -117,7 +117,7 @@ services:
- query-service
query-service:
!!merge <<: *db-depend
image: signoz/query-service:0.70.1
image: signoz/query-service:0.71.0
command:
- --config=/root/config/prometheus.yml
- --use-logs-new-schema=true
@@ -150,7 +150,7 @@ services:
retries: 3
frontend:
!!merge <<: *common
image: signoz/frontend:0.70.1
image: signoz/frontend:0.71.0
depends_on:
- alertmanager
- query-service
@@ -160,7 +160,7 @@ services:
- ../common/signoz/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:0.111.25
image: signoz/signoz-otel-collector:0.111.26
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml

View File

@@ -188,7 +188,7 @@ services:
condition: service_healthy
query-service:
!!merge <<: *db-depend
image: signoz/query-service:${DOCKER_TAG:-0.70.1}
image: signoz/query-service:${DOCKER_TAG:-0.71.0}
container_name: signoz-query-service
command:
- --config=/root/config/prometheus.yml
@@ -222,7 +222,7 @@ services:
retries: 3
frontend:
!!merge <<: *common
image: signoz/frontend:${DOCKER_TAG:-0.70.1}
image: signoz/frontend:${DOCKER_TAG:-0.71.0}
container_name: signoz-frontend
depends_on:
- alertmanager
@@ -234,7 +234,7 @@ services:
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.25}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.26}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml

View File

@@ -121,7 +121,7 @@ services:
condition: service_healthy
query-service:
!!merge <<: *db-depend
image: signoz/query-service:${DOCKER_TAG:-0.70.1}
image: signoz/query-service:${DOCKER_TAG:-0.71.0}
container_name: signoz-query-service
command:
- --config=/root/config/prometheus.yml
@@ -157,7 +157,7 @@ services:
retries: 3
frontend:
!!merge <<: *common
image: signoz/frontend:${DOCKER_TAG:-0.70.1}
image: signoz/frontend:${DOCKER_TAG:-0.71.0}
container_name: signoz-frontend
depends_on:
- alertmanager
@@ -168,7 +168,7 @@ services:
- ../common/signoz/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.25}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.26}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml

View File

@@ -121,7 +121,7 @@ services:
condition: service_healthy
query-service:
!!merge <<: *db-depend
image: signoz/query-service:${DOCKER_TAG:-0.70.1}
image: signoz/query-service:${DOCKER_TAG:-0.71.0}
container_name: signoz-query-service
command:
- --config=/root/config/prometheus.yml
@@ -155,7 +155,7 @@ services:
retries: 3
frontend:
!!merge <<: *common
image: signoz/frontend:${DOCKER_TAG:-0.70.1}
image: signoz/frontend:${DOCKER_TAG:-0.71.0}
container_name: signoz-frontend
depends_on:
- alertmanager
@@ -166,7 +166,7 @@ services:
- ../common/signoz/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.25}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.26}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml

View File

@@ -11,7 +11,6 @@ import (
"net"
"net/http"
_ "net/http/pprof" // http profiler
"os"
"regexp"
"time"
@@ -149,25 +148,20 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
var reader interfaces.DataConnector
storage := os.Getenv("STORAGE")
if storage == "clickhouse" {
zap.L().Info("Using ClickHouse as datastore ...")
qb := db.NewDataConnector(
serverOptions.SigNoz.SQLStore.SQLxDB(),
serverOptions.SigNoz.TelemetryStore.ClickHouseDB(),
serverOptions.PromConfigPath,
lm,
serverOptions.Cluster,
serverOptions.UseLogsNewSchema,
serverOptions.UseTraceNewSchema,
fluxIntervalForTraceDetail,
serverOptions.SigNoz.Cache,
)
go qb.Start(readerReady)
reader = qb
} else {
return nil, fmt.Errorf("storage type: %s is not supported in query service", storage)
}
qb := db.NewDataConnector(
serverOptions.SigNoz.SQLStore.SQLxDB(),
serverOptions.SigNoz.TelemetryStore.ClickHouseDB(),
serverOptions.PromConfigPath,
lm,
serverOptions.Cluster,
serverOptions.UseLogsNewSchema,
serverOptions.UseTraceNewSchema,
fluxIntervalForTraceDetail,
serverOptions.SigNoz.Cache,
)
go qb.Start(readerReady)
reader = qb
skipConfig := &basemodel.SkipConfig{}
if serverOptions.SkipTopLvlOpsPath != "" {
// read skip config

View File

@@ -7,7 +7,6 @@ import (
basedao "go.signoz.io/signoz/pkg/query-service/dao"
basedsql "go.signoz.io/signoz/pkg/query-service/dao/sqlite"
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
"go.uber.org/zap"
)
type modelDao struct {
@@ -29,41 +28,6 @@ func (m *modelDao) checkFeature(key string) error {
return m.flags.CheckFeature(key)
}
func columnExists(db *sqlx.DB, tableName, columnName string) bool {
query := fmt.Sprintf("PRAGMA table_info(%s);", tableName)
rows, err := db.Query(query)
if err != nil {
zap.L().Error("Failed to query table info", zap.Error(err))
return false
}
defer rows.Close()
var (
cid int
name string
ctype string
notnull int
dflt_value *string
pk int
)
for rows.Next() {
err := rows.Scan(&cid, &name, &ctype, &notnull, &dflt_value, &pk)
if err != nil {
zap.L().Error("Failed to scan table info", zap.Error(err))
return false
}
if name == columnName {
return true
}
}
err = rows.Err()
if err != nil {
zap.L().Error("Failed to scan table info", zap.Error(err))
return false
}
return false
}
// InitDB creates and extends base model DB repository
func InitDB(inputDB *sqlx.DB) (*modelDao, error) {
dao, err := basedsql.InitDB(inputDB)
@@ -73,69 +37,6 @@ func InitDB(inputDB *sqlx.DB) (*modelDao, error) {
// set package variable so dependent base methods (e.g. AuthCache) will work
basedao.SetDB(dao)
m := &modelDao{ModelDaoSqlite: dao}
table_schema := `
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS org_domains(
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
name VARCHAR(50) NOT NULL UNIQUE,
created_at INTEGER NOT NULL,
updated_at INTEGER,
data TEXT NOT NULL,
FOREIGN KEY(org_id) REFERENCES organizations(id)
);
CREATE TABLE IF NOT EXISTS personal_access_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
role TEXT NOT NULL,
user_id TEXT NOT NULL,
token TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
last_used INTEGER NOT NULL,
revoked BOOLEAN NOT NULL,
updated_by_user_id TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id)
);
`
_, err = m.DB().Exec(table_schema)
if err != nil {
return nil, fmt.Errorf("error in creating tables: %v", err.Error())
}
if !columnExists(m.DB(), "personal_access_tokens", "role") {
_, err = m.DB().Exec("ALTER TABLE personal_access_tokens ADD COLUMN role TEXT NOT NULL DEFAULT 'ADMIN';")
if err != nil {
return nil, fmt.Errorf("error in adding column: %v", err.Error())
}
}
if !columnExists(m.DB(), "personal_access_tokens", "updated_at") {
_, err = m.DB().Exec("ALTER TABLE personal_access_tokens ADD COLUMN updated_at INTEGER NOT NULL DEFAULT 0;")
if err != nil {
return nil, fmt.Errorf("error in adding column: %v", err.Error())
}
}
if !columnExists(m.DB(), "personal_access_tokens", "last_used") {
_, err = m.DB().Exec("ALTER TABLE personal_access_tokens ADD COLUMN last_used INTEGER NOT NULL DEFAULT 0;")
if err != nil {
return nil, fmt.Errorf("error in adding column: %v", err.Error())
}
}
if !columnExists(m.DB(), "personal_access_tokens", "revoked") {
_, err = m.DB().Exec("ALTER TABLE personal_access_tokens ADD COLUMN revoked BOOLEAN NOT NULL DEFAULT FALSE;")
if err != nil {
return nil, fmt.Errorf("error in adding column: %v", err.Error())
}
}
if !columnExists(m.DB(), "personal_access_tokens", "updated_by_user_id") {
_, err = m.DB().Exec("ALTER TABLE personal_access_tokens ADD COLUMN updated_by_user_id TEXT NOT NULL DEFAULT '';")
if err != nil {
return nil, fmt.Errorf("error in adding column: %v", err.Error())
}
}
return m, nil
}

View File

@@ -10,7 +10,6 @@ import (
"github.com/jmoiron/sqlx"
"github.com/mattn/go-sqlite3"
"go.signoz.io/signoz/ee/query-service/license/sqlite"
"go.signoz.io/signoz/ee/query-service/model"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.uber.org/zap"
@@ -28,10 +27,6 @@ func NewLicenseRepo(db *sqlx.DB) Repo {
}
}
func (r *Repo) InitDB(inputDB *sqlx.DB) error {
return sqlite.InitDB(inputDB)
}
func (r *Repo) GetLicensesV3(ctx context.Context) ([]*model.LicenseV3, error) {
licensesData := []model.LicenseDB{}
licenseV3Data := []*model.LicenseV3{}

View File

@@ -2,7 +2,6 @@ package license
import (
"context"
"fmt"
"sync/atomic"
"time"
@@ -50,11 +49,6 @@ func StartManager(db *sqlx.DB, features ...basemodel.Feature) (*Manager, error)
}
repo := NewLicenseRepo(db)
err := repo.InitDB(db)
if err != nil {
return nil, fmt.Errorf("failed to initiate license repo: %v", err)
}
m := &Manager{
repo: &repo,
}

View File

@@ -1,63 +0,0 @@
package sqlite
import (
"fmt"
"github.com/jmoiron/sqlx"
)
func InitDB(db *sqlx.DB) error {
var err error
if db == nil {
return fmt.Errorf("invalid db connection")
}
table_schema := `CREATE TABLE IF NOT EXISTS licenses(
key TEXT PRIMARY KEY,
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
planDetails TEXT,
activationId TEXT,
validationMessage TEXT,
lastValidated TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS sites(
uuid TEXT PRIMARY KEY,
alias VARCHAR(180) DEFAULT 'PROD',
url VARCHAR(300),
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`
_, err = db.Exec(table_schema)
if err != nil {
return fmt.Errorf("error in creating licenses table: %s", err.Error())
}
table_schema = `CREATE TABLE IF NOT EXISTS feature_status (
name TEXT PRIMARY KEY,
active bool,
usage INTEGER DEFAULT 0,
usage_limit INTEGER DEFAULT 0,
route TEXT
);`
_, err = db.Exec(table_schema)
if err != nil {
return fmt.Errorf("error in creating feature_status table: %s", err.Error())
}
table_schema = `CREATE TABLE IF NOT EXISTS licenses_v3 (
id TEXT PRIMARY KEY,
key TEXT NOT NULL UNIQUE,
data TEXT
);`
_, err = db.Exec(table_schema)
if err != nil {
return fmt.Errorf("error in creating licenses_v3 table: %s", err.Error())
}
return nil
}

View File

@@ -18,7 +18,6 @@ import (
"go.signoz.io/signoz/pkg/config/fileprovider"
"go.signoz.io/signoz/pkg/query-service/auth"
baseconst "go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/migrate"
"go.signoz.io/signoz/pkg/query-service/version"
"go.signoz.io/signoz/pkg/signoz"
"google.golang.org/grpc"
@@ -183,12 +182,6 @@ func main() {
zap.L().Info("JWT secret key set successfully.")
}
if err := migrate.Migrate(signoz.SQLStore.SQLxDB()); err != nil {
zap.L().Error("Failed to migrate", zap.Error(err))
} else {
zap.L().Info("Migration successful")
}
server, err := app.NewServer(serverOptions)
if err != nil {
zap.L().Fatal("Failed to create server", zap.Error(err))

View File

@@ -139,8 +139,8 @@ func NewLicenseV3(data map[string]interface{}) (*LicenseV3, error) {
if err != nil {
return nil, err
}
// if license status is inactive then default it to basic
if status == LicenseStatusInactive {
// if license status is invalid then default it to basic
if status == LicenseStatusInvalid {
planName = PlanNameBasic
}

View File

@@ -21,7 +21,7 @@ var (
)
var (
LicenseStatusInactive = "INACTIVE"
LicenseStatusInvalid = "INVALID"
)
const DisableUpsell = "DISABLE_UPSELL"
@@ -157,6 +157,13 @@ var BasicPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AWSIntegration,
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.AWSIntegration,
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.AWSIntegration,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}

View File

@@ -92,7 +92,7 @@
"overlayscrollbars": "^2.8.1",
"overlayscrollbars-react": "^0.5.6",
"papaparse": "5.4.1",
"posthog-js": "1.160.3",
"posthog-js": "1.215.5",
"rc-tween-one": "3.0.6",
"react": "18.2.0",
"react-addons-update": "15.6.3",

View File

@@ -57,5 +57,8 @@
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
"MESSAGING_QUEUES": "SigNoz | Messaging Queues",
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
"INFRASTRUCTURE_MONITORING_KUBERNETES": "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"
}

View File

@@ -156,7 +156,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const currentRoute = mapRoutes.get('current');
const shouldSuspendWorkspace =
activeLicenseV3.status === LicenseStatus.SUSPENDED &&
activeLicenseV3.state === LicenseState.PAYMENT_FAILED;
activeLicenseV3.state === LicenseState.DEFAULTED;
if (shouldSuspendWorkspace && currentRoute) {
navigateToWorkSpaceSuspended(currentRoute);

View File

@@ -110,6 +110,18 @@ function App(): JSX.Element {
source: 'signoz-ui',
isPaidUser: !!licenses?.trialConvertedToSubscription,
});
if (
window.cioanalytics &&
typeof window.cioanalytics.identify === 'function'
) {
window.cioanalytics.reset();
window.cioanalytics.identify(email, {
name: user.name,
email,
role: user.role,
});
}
}
},
[hostname, isFetchingLicenses, licenses, org],

View File

@@ -264,3 +264,8 @@ export const CeleryOverview = Loadable(
/* webpackChunkName: "CeleryOverview" */ 'pages/Celery/CeleryOverview/CeleryOverview'
),
);
export const MetricsExplorer = Loadable(
() =>
import(/* webpackChunkName: "MetricsExplorer" */ 'pages/MetricsExplorer'),
);

View File

@@ -28,6 +28,7 @@ import {
LogsExplorer,
LogsIndexToFields,
LogsSaveViews,
MetricsExplorer,
MySettings,
NewDashboardPage,
OldLogsExplorer,
@@ -435,6 +436,27 @@ const routes: AppRoutes[] = [
key: 'INFRASTRUCTURE_MONITORING_KUBERNETES',
isPrivate: true,
},
{
path: ROUTES.METRICS_EXPLORER,
exact: true,
component: MetricsExplorer,
key: 'METRICS_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.METRICS_EXPLORER_EXPLORER,
exact: true,
component: MetricsExplorer,
key: 'METRICS_EXPLORER_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.METRICS_EXPLORER_VIEWS,
exact: true,
component: MetricsExplorer,
key: 'METRICS_EXPLORER_VIEWS',
isPrivate: true,
},
];
export const SUPPORT_ROUTE: AppRoutes = {

View File

@@ -0,0 +1,19 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
const removeAwsIntegrationAccount = async (
accountId: string,
): Promise<SuccessResponse<Record<string, never>> | ErrorResponse> => {
const response = await axios.post(
`/cloud-integrations/aws/accounts/${accountId}/disconnect`,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default removeAwsIntegrationAccount;

View File

@@ -9,19 +9,22 @@ import {
import {
AccountConfigPayload,
AccountConfigResponse,
ConnectionParams,
ConnectionUrlResponse,
} from 'types/api/integrations/aws';
export const getAwsAccounts = async (): Promise<CloudAccount[]> => {
const response = await axios.get('/cloud-integrations/aws/accounts');
return response.data.data;
return response.data.data.accounts;
};
export const getAwsServices = async (
accountId?: string,
cloudAccountId?: string,
): Promise<Service[]> => {
const params = accountId ? { account_id: accountId } : undefined;
const params = cloudAccountId
? { cloud_account_id: cloudAccountId }
: undefined;
const response = await axios.get('/cloud-integrations/aws/services', {
params,
});
@@ -31,9 +34,11 @@ export const getAwsServices = async (
export const getServiceDetails = async (
serviceId: string,
accountId?: string,
cloudAccountId?: string,
): Promise<ServiceData> => {
const params = accountId ? { account_id: accountId } : undefined;
const params = cloudAccountId
? { cloud_account_id: cloudAccountId }
: undefined;
const response = await axios.get(
`/cloud-integrations/aws/services/${serviceId}`,
{ params },
@@ -74,3 +79,10 @@ export const updateServiceConfig = async (
);
return response.data;
};
export const getConnectionParams = async (): Promise<ConnectionParams> => {
const response = await axios.get(
'/cloud-integrations/aws/accounts/generate-connection-params',
);
return response.data.data;
};

View File

@@ -1,7 +1,6 @@
import './CeleryOverviewConfigOptions.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Row, Select, Spin, Tooltip } from 'antd';
import { Row, Select, Spin } from 'antd';
import {
getValuesFromQueryParams,
setQueryParamsFromOptions,
@@ -10,10 +9,7 @@ import { useCeleryFilterOptions } from 'components/CeleryTask/useCeleryFilterOpt
import { SelectMaxTagPlaceholder } from 'components/MessagingQueues/MQCommon/MQCommon';
import { QueryParams } from 'constants/query';
import useUrlQuery from 'hooks/useUrlQuery';
import { Check, Share2 } from 'lucide-react';
import { useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
interface SelectOptionConfig {
placeholder: string;
@@ -66,10 +62,6 @@ function FilterSelect({
}
function CeleryOverviewConfigOptions(): JSX.Element {
const [isURLCopied, setIsURLCopied] = useState(false);
const [, handleCopyToClipboard] = useCopyToClipboard();
const selectConfigs: SelectOptionConfig[] = [
{
placeholder: 'Service Name',
@@ -98,14 +90,6 @@ function CeleryOverviewConfigOptions(): JSX.Element {
},
];
const handleShareURL = (): void => {
handleCopyToClipboard(window.location.href);
setIsURLCopied(true);
setTimeout(() => {
setIsURLCopied(false);
}, 1000);
};
return (
<div className="celery-overview-filters">
<Row className="celery-filters">
@@ -118,19 +102,6 @@ function CeleryOverviewConfigOptions(): JSX.Element {
/>
))}
</Row>
<Tooltip title="Share this" arrow={false}>
<Button
className="periscope-btn copy-url-btn"
onClick={handleShareURL}
icon={
isURLCopied ? (
<Check size={14} color={Color.BG_FOREST_500} />
) : (
<Share2 size={14} />
)
}
/>
</Tooltip>
</div>
);
}

View File

@@ -37,7 +37,6 @@
font-weight: 600;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
&::before {
background-color: transparent;

View File

@@ -218,35 +218,44 @@ function getColumns(data: RowData[]): TableColumnsType<RowData> {
showTitle: false,
},
width: 200,
sorter: (a: RowData, b: RowData): number =>
String(a.error_percentage).localeCompare(String(b.error_percentage)),
sorter: (a: RowData, b: RowData): number => {
const aValue = Number(a.error_percentage);
const bValue = Number(b.error_percentage);
return aValue - bValue;
},
render: ProgressRender,
},
{
title: 'LATENCY (P95)',
title: 'LATENCY (P95) in ms',
dataIndex: 'p95_latency',
key: 'p95_latency',
ellipsis: {
showTitle: false,
},
width: 100,
sorter: (a: RowData, b: RowData): number =>
String(a.p95_latency).localeCompare(String(b.p95_latency)),
sorter: (a: RowData, b: RowData): number => {
const aValue = Number(a.p95_latency);
const bValue = Number(b.p95_latency);
return aValue - bValue;
},
render: (value: number | string): string => {
if (!isNumber(value)) return value.toString();
return (typeof value === 'string' ? parseFloat(value) : value).toFixed(3);
},
},
{
title: 'THROUGHPUT',
title: 'THROUGHPUT (ops/s)',
dataIndex: 'throughput',
key: 'throughput',
ellipsis: {
showTitle: false,
},
width: 100,
sorter: (a: RowData, b: RowData): number =>
String(a.throughput).localeCompare(String(b.throughput)),
sorter: (a: RowData, b: RowData): number => {
const aValue = Number(a.throughput);
const bValue = Number(b.throughput);
return aValue - bValue;
},
render: (value: number | string): string => {
if (!isNumber(value)) return value.toString();
return (typeof value === 'string' ? parseFloat(value) : value).toFixed(3);

View File

@@ -2,24 +2,16 @@ import './CeleryTaskDetail.style.scss';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Divider, Drawer, Typography } from 'antd';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { X } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { useState } from 'react';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuidv4 } from 'uuid';
import CeleryTaskGraph from '../CeleryTaskGraph/CeleryTaskGraph';
import { createFiltersFromData } from '../CeleryUtils';
import { useNavigateToTraces } from '../useNavigateToTraces';
export type CeleryTaskData = {
@@ -39,40 +31,6 @@ export type CeleryTaskDetailProps = {
drawerOpen: boolean;
};
const createFiltersFromData = (
data: Record<string, any>,
): Array<{
id: string;
key: {
key: string;
dataType: DataTypes;
type: string;
isColumn: boolean;
isJSON: boolean;
id: string;
};
op: string;
value: string;
}> => {
const excludeKeys = ['A', 'A_without_unit'];
return Object.entries(data)
.filter(([key]) => !excludeKeys.includes(key))
.map(([key, value]) => ({
id: uuidv4(),
key: {
key,
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
id: `${key}--string--tag--false`,
},
op: '=',
value: value.toString(),
}));
};
export default function CeleryTaskDetail({
widgetData,
taskData,
@@ -85,7 +43,7 @@ export default function CeleryTaskDetail({
!!taskData.entity && !!taskData.timeRange[0] && drawerOpen;
const formatTimestamp = (timestamp: number): string =>
dayjs(timestamp * 1000).format('MM-DD-YYYY hh:mm A');
dayjs(timestamp).format('DD-MM-YYYY hh:mm A');
const [totalTask, setTotalTask] = useState(0);
@@ -93,52 +51,9 @@ export default function CeleryTaskDetail({
setTotalTask((graphData?.result?.[0] as any)?.table?.rows.length);
};
// set time range
const { minTime, maxTime, selectedTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const startTime = taskData.timeRange[0];
const endTime = taskData.timeRange[1];
const urlQuery = useUrlQuery();
const location = useLocation();
const history = useHistory();
const dispatch = useDispatch();
useEffect(() => {
urlQuery.delete(QueryParams.relativeTime);
urlQuery.set(QueryParams.startTime, startTime.toString());
urlQuery.set(QueryParams.endTime, endTime.toString());
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
if (startTime !== endTime) {
dispatch(UpdateTimeInterval('custom', [startTime, endTime]));
}
return (): void => {
urlQuery.delete(QueryParams.relativeTime);
urlQuery.delete(QueryParams.startTime);
urlQuery.delete(QueryParams.endTime);
if (selectedTime !== 'custom') {
dispatch(UpdateTimeInterval(selectedTime));
urlQuery.set(QueryParams.relativeTime, selectedTime);
} else {
dispatch(UpdateTimeInterval('custom', [minTime / 1e6, maxTime / 1e6]));
urlQuery.set(QueryParams.startTime, Math.floor(minTime / 1e6).toString());
urlQuery.set(QueryParams.endTime, Math.floor(maxTime / 1e6).toString());
}
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const navigateToTrace = useNavigateToTraces();
return (
@@ -149,10 +64,8 @@ export default function CeleryTaskDetail({
<Typography.Text className="title">{`Details - ${taskData.entity}`}</Typography.Text>
<div>
<Typography.Text className="subtitle">
{`${formatTimestamp(taskData.timeRange[0])} ${
taskData.timeRange[1]
? `- ${formatTimestamp(taskData.timeRange[1])}`
: ''
{`${formatTimestamp(startTime)} ${
endTime ? `- ${formatTimestamp(endTime)}` : ''
}`}
</Typography.Text>
<Divider type="vertical" />
@@ -185,8 +98,10 @@ export default function CeleryTaskDetail({
...rowData,
[taskData.entity]: taskData.value,
});
navigateToTrace(filters);
navigateToTrace(filters, startTime, endTime);
}}
start={startTime}
end={endTime}
/>
</Drawer>
);

View File

@@ -19,6 +19,10 @@ import { Widgets } from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail';
import {
applyCeleryFilterOnWidgetData,
getFiltersFromQueryParams,
} from '../CeleryUtils';
import { useGetGraphCustomSeries } from '../useGetGraphCustomSeries';
import {
celeryAllStateWidgetData,
@@ -72,26 +76,60 @@ function CeleryTaskBar({
const [barState, setBarState] = useState<CeleryTaskState>(CeleryTaskState.All);
const selectedFilters = useMemo(
() =>
getFiltersFromQueryParams(
QueryParams.taskName,
urlQuery,
'celery.task_name',
),
[urlQuery],
);
const celeryAllStateData = useMemo(
() => celeryAllStateWidgetData(minTime, maxTime),
[minTime, maxTime],
);
const celeryAllStateFilteredData = useMemo(
() =>
applyCeleryFilterOnWidgetData(selectedFilters || [], celeryAllStateData),
[selectedFilters, celeryAllStateData],
);
const celeryFailedStateData = useMemo(
() => celeryFailedStateWidgetData(minTime, maxTime),
[minTime, maxTime],
);
const celeryFailedStateFilteredData = useMemo(
() =>
applyCeleryFilterOnWidgetData(selectedFilters || [], celeryFailedStateData),
[selectedFilters, celeryFailedStateData],
);
const celeryRetryStateData = useMemo(
() => celeryRetryStateWidgetData(minTime, maxTime),
[minTime, maxTime],
);
const celeryRetryStateFilteredData = useMemo(
() =>
applyCeleryFilterOnWidgetData(selectedFilters || [], celeryRetryStateData),
[selectedFilters, celeryRetryStateData],
);
const celerySuccessStateData = useMemo(
() => celerySuccessStateWidgetData(minTime, maxTime),
[minTime, maxTime],
);
const celerySuccessStateFilteredData = useMemo(
() =>
applyCeleryFilterOnWidgetData(selectedFilters || [], celerySuccessStateData),
[selectedFilters, celerySuccessStateData],
);
const onGraphClick = (
widgetData: Widgets,
xValue: number,
@@ -141,7 +179,7 @@ function CeleryTaskBar({
<div className="celery-task-graph-grid-content">
{barState === CeleryTaskState.All && (
<GridCard
widget={celeryAllStateData}
widget={celeryAllStateFilteredData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
isQueryEnabled={queryEnabled}
@@ -153,7 +191,7 @@ function CeleryTaskBar({
)}
{barState === CeleryTaskState.Failed && (
<GridCard
widget={celeryFailedStateData}
widget={celeryFailedStateFilteredData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
isQueryEnabled={queryEnabled}
@@ -165,7 +203,7 @@ function CeleryTaskBar({
)}
{barState === CeleryTaskState.Retry && (
<GridCard
widget={celeryRetryStateData}
widget={celeryRetryStateFilteredData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
isQueryEnabled={queryEnabled}
@@ -177,7 +215,7 @@ function CeleryTaskBar({
)}
{barState === CeleryTaskState.Successful && (
<GridCard
widget={celerySuccessStateData}
widget={celerySuccessStateFilteredData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
isQueryEnabled={queryEnabled}

View File

@@ -116,9 +116,8 @@
gap: 10px;
.metric-page-grid {
display: grid;
grid-template-columns: 50% 50%;
align-items: flex-start;
display: flex;
flex-direction: row;
gap: 10px;
width: 100%;
@@ -144,6 +143,11 @@
gap: 16px;
align-items: center;
border: 1px dashed var(--bg-slate-50);
border-radius: 4px;
padding: 6px 24px 6px 12px;
width: max-content;
.configure-option-Info-text {
color: var(--bg-vanilla-400);
font-size: 12px;
@@ -241,11 +245,13 @@
.lightMode {
.celery-task-graph-grid-container {
.celery-task-graph-grid {
.celery-task-graph-worker-count {
border: 1px solid var(--bg-vanilla-300);
background: unset;
}
.celery-task-graph-worker-count {
border: 1px solid var(--bg-vanilla-300);
background: unset;
}
.row-panel .row-panel-section .section-title {
color: var(--bg-ink-400);
}
}
@@ -274,4 +280,8 @@
background-color: var(--bg-ink-400);
}
}
.configure-option-Info {
border: 1px dashed var(--bg-robin-400);
}
}

View File

@@ -32,6 +32,9 @@ function CeleryTaskGraph({
openTracesButton,
onOpenTraceBtnClick,
applyCeleryTaskFilter,
customErrorMessage,
start,
end,
}: {
widgetData: Widgets;
onClick?: (task: CaptureDataProps) => void;
@@ -42,6 +45,9 @@ function CeleryTaskGraph({
openTracesButton?: boolean;
onOpenTraceBtnClick?: (record: RowData) => void;
applyCeleryTaskFilter?: boolean;
customErrorMessage?: string;
start?: number;
end?: number;
}): JSX.Element {
const history = useHistory();
const { pathname } = useLocation();
@@ -116,6 +122,9 @@ function CeleryTaskGraph({
openTracesButton={openTracesButton}
onOpenTraceBtnClick={onOpenTraceBtnClick}
version={ENTITY_VERSION_V4}
customErrorMessage={customErrorMessage}
start={start}
end={end}
/>
</Card>
);
@@ -129,6 +138,9 @@ CeleryTaskGraph.defaultProps = {
openTracesButton: false,
onOpenTraceBtnClick: undefined,
applyCeleryTaskFilter: false,
customErrorMessage: undefined,
start: undefined,
end: undefined,
};
export default CeleryTaskGraph;

View File

@@ -123,11 +123,12 @@ export default function CeleryTaskGraphGrid({
key={celeryActiveTasksData.id}
widgetData={celeryActiveTasksData}
queryEnabled={queryEnabled}
customErrorMessage="Enable Flower metrics to view this graph"
/>
<Card className="celery-task-graph-worker-count">
<div className="worker-count-header">
<Typography.Text className="worker-count-header-text">
Worker Count
Worker Online
</Typography.Text>
</div>
<div className="worker-count-text-container">
@@ -173,7 +174,7 @@ export default function CeleryTaskGraphGrid({
{!collapsedSections.traceBasedGraphs && (
<>
<CeleryTaskBar queryEnabled={queryEnabled} onClick={onClick} />
<CeleryTaskLatencyGraph onClick={onClick} queryEnabled={queryEnabled} />
<CeleryTaskLatencyGraph queryEnabled={queryEnabled} />
<div className="celery-task-graph-grid-bottom">
{bottomWidgetData.map((widgetData, index) => (
<CeleryTaskGraph

View File

@@ -42,21 +42,7 @@ export const celeryAllStateWidgetData = (
disabled: false,
expression: 'A',
filters: {
items: [
{
id: uuidv4(),
key: {
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
op: '=',
value: 'tasks.tasks.divide',
},
],
items: [],
op: 'AND',
},
functions: [],
@@ -113,7 +99,7 @@ export const celeryRetryStateWidgetData = (
filters: {
items: [
{
id: '6d97eed3',
id: uuidv4(),
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
@@ -179,7 +165,7 @@ export const celeryFailedStateWidgetData = (
filters: {
items: [
{
id: '5983eae2',
id: uuidv4(),
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
@@ -245,7 +231,7 @@ export const celerySuccessStateWidgetData = (
filters: {
items: [
{
id: '000c5a93',
id: uuidv4(),
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
@@ -602,7 +588,7 @@ export const celeryTaskLatencyWidgetData = (
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: getStepInterval(startTime, endTime),
timeAggregation: 'p99',
timeAggregation: type || 'p99',
},
],
yAxisUnit: 'ns',
@@ -686,7 +672,7 @@ export const celeryRetryTasksTableWidgetData = getWidgetQueryBuilder(
filters: {
items: [
{
id: '9e09c9ed',
id: uuidv4(),
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
@@ -755,7 +741,7 @@ export const celeryFailedTasksTableWidgetData = getWidgetQueryBuilder(
filters: {
items: [
{
id: '2330f906',
id: uuidv4(),
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
@@ -822,7 +808,7 @@ export const celerySuccessTasksTableWidgetData = getWidgetQueryBuilder(
filters: {
items: [
{
id: 'ec3df7b7',
id: uuidv4(),
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
@@ -945,33 +931,19 @@ export const celeryAllStateCountWidgetData = getWidgetQueryBuilder(
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.EMPTY,
id: '------false',
isColumn: false,
dataType: DataTypes.String,
id: 'span_id--string----true',
isColumn: true,
isJSON: false,
key: '',
key: 'span_id',
type: '',
},
aggregateOperator: 'count',
aggregateOperator: 'count_distinct',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [
{
id: uuidv4(),
key: {
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
op: '=',
value: 'tasks.tasks.divide',
},
],
items: [],
op: 'AND',
},
functions: [],
@@ -981,10 +953,10 @@ export const celeryAllStateCountWidgetData = getWidgetQueryBuilder(
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
reduceTo: 'last',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
timeAggregation: 'count_distinct',
},
],
}),
@@ -998,14 +970,14 @@ export const celerySuccessStateCountWidgetData = getWidgetQueryBuilder(
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.EMPTY,
id: '------false',
isColumn: false,
dataType: DataTypes.String,
id: 'span_id--string----true',
isColumn: true,
isJSON: false,
key: '',
key: 'span_id',
type: '',
},
aggregateOperator: 'count',
aggregateOperator: 'count_distinct',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
@@ -1034,10 +1006,10 @@ export const celerySuccessStateCountWidgetData = getWidgetQueryBuilder(
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
reduceTo: 'last',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
timeAggregation: 'count_distinct',
},
],
}),
@@ -1051,14 +1023,14 @@ export const celeryFailedStateCountWidgetData = getWidgetQueryBuilder(
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.EMPTY,
id: '------false',
isColumn: false,
dataType: DataTypes.String,
id: 'span_id--string----true',
isColumn: true,
isJSON: false,
key: '',
key: 'span_id',
type: '',
},
aggregateOperator: 'count',
aggregateOperator: 'count_distinct',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
@@ -1087,10 +1059,10 @@ export const celeryFailedStateCountWidgetData = getWidgetQueryBuilder(
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
reduceTo: 'last',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
timeAggregation: 'count_distinct',
},
],
}),
@@ -1104,13 +1076,13 @@ export const celeryRetryStateCountWidgetData = getWidgetQueryBuilder(
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.EMPTY,
id: '------false',
isColumn: false,
key: '',
dataType: DataTypes.String,
id: 'span_id--string----true',
isColumn: true,
key: 'span_id',
type: '',
},
aggregateOperator: 'count',
aggregateOperator: 'count_distinct',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
@@ -1139,10 +1111,10 @@ export const celeryRetryStateCountWidgetData = getWidgetQueryBuilder(
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
reduceTo: 'last',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
timeAggregation: 'count_distinct',
},
],
}),

View File

@@ -6,8 +6,11 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import { ViewMenuAction } from 'container/GridCardLayout/config';
import GridCard from 'container/GridCardLayout/GridCard';
import { Card } from 'container/GridCardLayout/styles';
import { Button } from 'container/MetricsApplication/Tabs/styles';
import { onGraphClickHandler } from 'container/MetricsApplication/Tabs/util';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
@@ -16,15 +19,13 @@ import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail';
import {
applyCeleryFilterOnWidgetData,
createFiltersFromData,
getFiltersFromQueryParams,
} from '../CeleryUtils';
import {
celeryTaskLatencyWidgetData,
celeryTimeSeriesTablesWidgetData,
} from './CeleryTaskGraphUtils';
import { useNavigateToTraces } from '../useNavigateToTraces';
import { celeryTaskLatencyWidgetData } from './CeleryTaskGraphUtils';
interface TabData {
label: string;
@@ -38,10 +39,8 @@ export enum CeleryTaskGraphState {
}
function CeleryTaskLatencyGraph({
onClick,
queryEnabled,
}: {
onClick: (task: CaptureDataProps) => void;
queryEnabled: boolean;
}): JSX.Element {
const history = useHistory();
@@ -106,31 +105,51 @@ function CeleryTaskLatencyGraph({
[celeryTaskLatencyData, selectedFilters],
);
const onGraphClick = (
xValue: number,
_yValue: number,
_mouseX: number,
_mouseY: number,
data?: {
[key: string]: string;
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
const [entityData, setEntityData] = useState<{
entity: string;
value: string;
}>();
const handleSetTimeStamp = useCallback((selectTime: number) => {
setSelectedTimeStamp(selectTime);
}, []);
const onGraphClick = useCallback(
(type: string): OnClickPluginOpts['onClick'] => (
xValue,
yValue,
mouseX,
mouseY,
data,
): Promise<void> => {
const [firstDataPoint] = Object.entries(data || {});
const [entity, value] = firstDataPoint;
setEntityData({
entity,
value,
});
return onGraphClickHandler(handleSetTimeStamp)(
xValue,
yValue,
mouseX,
mouseY,
type,
);
},
): void => {
const { start, end } = getStartAndEndTimesInMilliseconds(xValue);
[handleSetTimeStamp],
);
// Extract entity and value from data
const [firstDataPoint] = Object.entries(data || {});
const [entity, value] = (firstDataPoint || ([] as unknown)) as [
string,
string,
];
const navigateToTraces = useNavigateToTraces();
onClick?.({
entity,
value,
timeRange: [start, end],
widgetData: celeryTimeSeriesTablesWidgetData(entity, value, 'Task Latency'),
const goToTraces = useCallback(() => {
const { start, end } = getStartAndEndTimesInMilliseconds(selectedTimeStamp);
const filters = createFiltersFromData({
[entityData?.entity as string]: entityData?.value,
});
};
navigateToTraces(filters, start, end, true);
}, [entityData, navigateToTraces, selectedTimeStamp]);
return (
<Card
@@ -161,32 +180,62 @@ function CeleryTaskLatencyGraph({
</Row>
<div className="celery-task-graph-grid-content">
{graphState === CeleryTaskGraphState.P99 && (
<GridCard
widget={updatedWidgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={onGraphClick}
isQueryEnabled={queryEnabled}
/>
<>
<Button
type="default"
size="small"
id="Celery_p99_latency_button"
onClick={goToTraces}
>
View Traces
</Button>
<GridCard
widget={updatedWidgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={onGraphClick('Celery_p99_latency')}
isQueryEnabled={queryEnabled}
/>
</>
)}
{graphState === CeleryTaskGraphState.P95 && (
<GridCard
widget={updatedWidgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={onGraphClick}
isQueryEnabled={queryEnabled}
/>
<>
<Button
type="default"
size="small"
id="Celery_p95_latency_button"
onClick={goToTraces}
>
View Traces
</Button>
<GridCard
widget={updatedWidgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={onGraphClick('Celery_p95_latency')}
isQueryEnabled={queryEnabled}
/>
</>
)}
{graphState === CeleryTaskGraphState.P90 && (
<GridCard
widget={updatedWidgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={onGraphClick}
isQueryEnabled={queryEnabled}
/>
<>
<Button
type="default"
size="small"
id="Celery_p90_latency_button"
onClick={goToTraces}
>
View Traces
</Button>
<GridCard
widget={updatedWidgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={onGraphClick('Celery_p90_latency')}
isQueryEnabled={queryEnabled}
/>
</>
)}
</div>
</Card>

View File

@@ -2,8 +2,14 @@
import './CeleryTaskGraph.style.scss';
import { Col, Row } from 'antd';
import { Dispatch, SetStateAction } from 'react';
import { QueryParams } from 'constants/query';
import useUrlQuery from 'hooks/useUrlQuery';
import { Dispatch, SetStateAction, useMemo } from 'react';
import {
applyCeleryFilterOnWidgetData,
getFiltersFromQueryParams,
} from '../CeleryUtils';
import {
celeryAllStateCountWidgetData,
celeryFailedStateCountWidgetData,
@@ -42,16 +48,29 @@ function CeleryTaskStateGraphConfig({
setBarState(key as CeleryTaskState);
};
const { values, isLoading, isError } = useGetValueFromWidget(
[
celeryAllStateCountWidgetData,
celeryFailedStateCountWidgetData,
celeryRetryStateCountWidgetData,
celerySuccessStateCountWidgetData,
],
['celery-task-states'],
const urlQuery = useUrlQuery();
const selectedFilters = useMemo(
() =>
getFiltersFromQueryParams(
QueryParams.taskName,
urlQuery,
'celery.task_name',
),
[urlQuery],
);
const widgetData = [
celeryAllStateCountWidgetData,
celeryFailedStateCountWidgetData,
celeryRetryStateCountWidgetData,
celerySuccessStateCountWidgetData,
].map((data) => applyCeleryFilterOnWidgetData(selectedFilters || [], data));
const { values, isLoading, isError } = useGetValueFromWidget(widgetData, [
'celery-task-states',
]);
return (
<Row className="celery-task-states">
{tabs.map((tab, index) => (
@@ -66,7 +85,13 @@ function CeleryTaskStateGraphConfig({
<div className="celery-task-states__label-wrapper">
<div className="celery-task-states__label">{tab.label}</div>
<div className="celery-task-states__value">
{isLoading ? '-' : isError ? '-' : values[index]}
{isLoading
? '-'
: isError
? '-'
: Number.isNaN(values[index])
? '-'
: Math.round(Number(values[index]))}
</div>
</div>
{tab.key === barState && <div className="celery-task-states__indicator" />}

View File

@@ -90,3 +90,40 @@ export const paths = (
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
};
export const createFiltersFromData = (
data: Record<string, any>,
): Array<{
id: string;
key: {
key: string;
dataType: DataTypes;
type: string;
isColumn: boolean;
isJSON: boolean;
id: string;
};
op: string;
value: string;
}> => {
const excludeKeys = ['A', 'A_without_unit'];
return (
Object.entries(data)
.filter(([key]) => !excludeKeys.includes(key))
// eslint-disable-next-line sonarjs/no-identical-functions
.map(([key, value]) => ({
id: uuidv4(),
key: {
key,
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
id: `${key}--string--tag--false`,
},
op: '=',
value: value.toString(),
}))
);
};

View File

@@ -12,6 +12,7 @@ export function useNavigateToTraces(): (
filters: TagFilterItem[],
startTime?: number,
endTime?: number,
sameTab?: boolean,
) => void {
const { currentQuery } = useQueryBuilder();
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
@@ -38,7 +39,12 @@ export function useNavigateToTraces(): (
);
return useCallback(
(filters: TagFilterItem[], startTime?: number, endTime?: number): void => {
(
filters: TagFilterItem[],
startTime?: number,
endTime?: number,
sameTab?: boolean,
): void => {
const urlParams = new URLSearchParams();
if (startTime && endTime) {
urlParams.set(QueryParams.startTime, startTime.toString());
@@ -58,7 +64,7 @@ export function useNavigateToTraces(): (
QueryParams.compositeQuery
}=${JSONCompositeQuery}`;
window.open(newTraceExplorerPath, '_blank');
window.open(newTraceExplorerPath, sameTab ? '_self' : '_blank');
},
[minTime, maxTime, prepareQuery],
);

View File

@@ -133,7 +133,7 @@
.go-to-docs {
display: flex;
flex-direction: column;
gap: 64px;
gap: 44px;
&__container {
display: flex;
flex-direction: column;

View File

@@ -23,4 +23,5 @@ export enum FeatureKeys {
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
QUERY_BUILDER_SEARCH_V2 = 'QUERY_BUILDER_SEARCH_V2',
ANOMALY_DETECTION = 'ANOMALY_DETECTION',
AWS_INTEGRATION = 'AWS_INTEGRATION',
}

View File

@@ -41,5 +41,6 @@ export const REACT_QUERY_KEY = {
AWS_UPDATE_ACCOUNT_CONFIG: 'AWS_UPDATE_ACCOUNT_CONFIG',
AWS_UPDATE_SERVICE_CONFIG: 'AWS_UPDATE_SERVICE_CONFIG',
AWS_GENERATE_CONNECTION_URL: 'AWS_GENERATE_CONNECTION_URL',
AWS_GET_CONNECTION_PARAMS: 'AWS_GET_CONNECTION_PARAMS',
GET_ATTRIBUTE_VALUES: 'GET_ATTRIBUTE_VALUES',
};

View File

@@ -65,6 +65,9 @@ const ROUTES = {
INFRASTRUCTURE_MONITORING_KUBERNETES: '/infrastructure-monitoring/kubernetes',
MESSAGING_QUEUES_CELERY_TASK: '/messaging-queues/celery-task',
MESSAGING_QUEUES_OVERVIEW: '/messaging-queues/overview',
METRICS_EXPLORER: '/metrics-explorer/summary',
METRICS_EXPLORER_EXPLORER: '/metrics-explorer/explorer',
METRICS_EXPLORER_VIEWS: '/metrics-explorer/views',
} as const;
export default ROUTES;

View File

@@ -250,7 +250,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
if (
!isFetchingActiveLicenseV3 &&
!isNull(activeLicenseV3) &&
activeLicenseV3?.event_queue?.event === LicenseEvent.FAILED_PAYMENT
activeLicenseV3?.event_queue?.event === LicenseEvent.DEFAULT
) {
setShowPaymentFailedWarning(true);
}

View File

@@ -24,7 +24,9 @@ function Header(): JSX.Element {
},
{
title: (
<div className="cloud-header__breadcrumb-title">AWS web services</div>
<div className="cloud-header__breadcrumb-title">
Amazon Web Services
</div>
),
},
]}

View File

@@ -21,7 +21,7 @@ function HeroSection(): JSX.Element {
<img src="/Logos/aws-dark.svg" alt="aws-logo" />
</div>
<div className="hero-section__details">
<div className="title">AWS Web Services</div>
<div className="title">Amazon Web Services</div>
<div className="description">
One-click setup for AWS monitoring with SigNoz
</div>

View File

@@ -1,41 +1,56 @@
.hero-section__actions {
margin-top: 12px;
.hero-section {
&__actions {
margin-top: 12px;
&-with-account {
display: flex;
flex-direction: column;
gap: 10px;
}
}
.hero-section__action-buttons {
display: flex;
align-items: center;
gap: 8px;
}
.hero-section__action-button {
font-family: 'Inter';
border-radius: 2px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
line-height: 16px;
padding: 8px 17px;
&.primary {
background: var(--bg-robin-500);
border: none;
color: var(--bg-vanilla-100);
&-with-account {
display: flex;
flex-direction: column;
gap: 10px;
}
}
&.secondary {
&__input-skeleton {
width: 300px;
margin-bottom: 16px;
}
&__new-account-button-skeleton {
width: 180px;
margin-right: 8px;
}
&__account-settings-button-skeleton {
width: 140px;
}
&__action-buttons {
display: flex;
align-items: center;
border: 1px solid var(--bg-ink-300);
color: var(--bg-vanilla-100);
gap: 8px;
}
&__action-button {
font-family: 'Inter';
border-radius: 2px;
background: var(--bg-slate-400);
box-shadow: none;
cursor: pointer;
font-size: 12px;
font-weight: 500;
line-height: 16px;
padding: 8px 17px;
&.primary {
background: var(--bg-robin-500);
border: none;
color: var(--bg-vanilla-100);
}
&.secondary {
display: flex;
align-items: center;
border: 1px solid var(--bg-ink-300);
color: var(--bg-vanilla-100);
border-radius: 2px;
background: var(--bg-slate-400);
box-shadow: none;
}
}
}

View File

@@ -1,7 +1,7 @@
import './AccountActions.style.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Select } from 'antd';
import { Button, Select, Skeleton } from 'antd';
import { SelectProps } from 'antd/lib';
import { useAwsAccounts } from 'hooks/integrations/aws/useAwsAccounts';
import useUrlQuery from 'hooks/useUrlQuery';
@@ -53,15 +53,100 @@ const getAccountById = (
): CloudAccount | null =>
accounts.find((account) => account.cloud_account_id === accountId) || null;
function AccountActionsRenderer({
accounts,
isLoading,
activeAccount,
selectOptions,
onAccountChange,
onIntegrationModalOpen,
onAccountSettingsModalOpen,
}: {
accounts: CloudAccount[] | undefined;
isLoading: boolean;
activeAccount: CloudAccount | null;
selectOptions: SelectProps['options'];
onAccountChange: (value: string) => void;
onIntegrationModalOpen: () => void;
onAccountSettingsModalOpen: () => void;
}): JSX.Element {
if (isLoading) {
return (
<div className="hero-section__actions-with-account">
<Skeleton.Input
active
size="large"
block
className="hero-section__input-skeleton"
/>
<div className="hero-section__action-buttons">
<Skeleton.Button
active
size="large"
className="hero-section__new-account-button-skeleton"
/>
<Skeleton.Button
active
size="large"
className="hero-section__account-settings-button-skeleton"
/>
</div>
</div>
);
}
if (accounts?.length) {
return (
<div className="hero-section__actions-with-account">
<Select
value={`Account: ${activeAccount?.cloud_account_id}`}
options={selectOptions}
rootClassName="cloud-account-selector"
placeholder="Select AWS Account"
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
optionRender={(option): JSX.Element =>
renderOption(option, activeAccount?.cloud_account_id)
}
onChange={onAccountChange}
/>
<div className="hero-section__action-buttons">
<Button
type="primary"
className="hero-section__action-button primary"
onClick={onIntegrationModalOpen}
>
Add New AWS Account
</Button>
<Button
type="default"
className="hero-section__action-button secondary"
onClick={onAccountSettingsModalOpen}
>
Account Settings
</Button>
</div>
</div>
);
}
return (
<Button
className="hero-section__action-button primary"
onClick={onIntegrationModalOpen}
>
Integrate Now
</Button>
);
}
function AccountActions(): JSX.Element {
const urlQuery = useUrlQuery();
const navigate = useNavigate();
const { data: accounts } = useAwsAccounts();
const { data: accounts, isLoading } = useAwsAccounts();
const initialAccount = useMemo(
() =>
accounts?.length
? getAccountById(accounts, urlQuery.get('accountId') || '') || accounts[0]
? getAccountById(accounts, urlQuery.get('cloudAccountId') || '') ||
accounts[0]
: null,
[accounts, urlQuery],
);
@@ -74,7 +159,7 @@ function AccountActions(): JSX.Element {
useEffect(() => {
if (initialAccount !== null) {
setActiveAccount(initialAccount);
urlQuery.set('accountId', initialAccount.cloud_account_id);
urlQuery.set('cloudAccountId', initialAccount.cloud_account_id);
navigate({ search: urlQuery.toString() });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -98,60 +183,35 @@ function AccountActions(): JSX.Element {
return (
<div className="hero-section__actions">
{accounts?.length ? (
<div className="hero-section__actions-with-account">
<Select
value={`Account: ${activeAccount?.cloud_account_id}`}
options={selectOptions}
rootClassName="cloud-account-selector"
placeholder="Select AWS Account"
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
optionRender={(option): JSX.Element =>
renderOption(option, activeAccount?.cloud_account_id)
}
onChange={(value): void => {
setActiveAccount(getAccountById(accounts, value));
urlQuery.set('accountId', value);
navigate({ search: urlQuery.toString() });
}}
/>
<div className="hero-section__action-buttons">
<Button
type="primary"
className="hero-section__action-button primary"
onClick={(): void => setIsIntegrationModalOpen(true)}
>
Add New AWS Account
</Button>
<Button
type="default"
className="hero-section__action-button secondary"
onClick={(): void => setIsAccountSettingsModalOpen(true)}
>
Account Settings
</Button>
</div>
</div>
) : (
<Button
className="hero-section__action-button primary"
onClick={(): void => setIsIntegrationModalOpen(true)}
>
Integrate Now
</Button>
<AccountActionsRenderer
accounts={accounts}
isLoading={isLoading}
activeAccount={activeAccount}
selectOptions={selectOptions}
onAccountChange={(value): void => {
if (accounts) {
setActiveAccount(getAccountById(accounts, value));
urlQuery.set('cloudAccountId', value);
navigate({ search: urlQuery.toString() });
}
}}
onIntegrationModalOpen={(): void => setIsIntegrationModalOpen(true)}
onAccountSettingsModalOpen={(): void => setIsAccountSettingsModalOpen(true)}
/>
{isIntegrationModalOpen && (
<CloudAccountSetupModal
onClose={(): void => setIsIntegrationModalOpen(false)}
/>
)}
<CloudAccountSetupModal
isOpen={isIntegrationModalOpen}
onClose={(): void => setIsIntegrationModalOpen(false)}
/>
<AccountSettingsModal
isOpen={isAccountSettingsModalOpen}
onClose={(): void => setIsAccountSettingsModalOpen(false)}
account={activeAccount as CloudAccount}
setActiveAccount={setActiveAccount}
/>
{isAccountSettingsModalOpen && (
<AccountSettingsModal
onClose={(): void => setIsAccountSettingsModalOpen(false)}
account={activeAccount as CloudAccount}
setActiveAccount={setActiveAccount}
/>
)}
</div>
);
}

View File

@@ -2,27 +2,27 @@ import './AccountSettingsModal.style.scss';
import { Form, Select, Switch } from 'antd';
import SignozModal from 'components/SignozModal/SignozModal';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import {
getRegionPreviewText,
useAccountSettingsModal,
} from 'hooks/integrations/aws/useAccountSettingsModal';
import IntergrationsUninstallBar from 'pages/Integrations/IntegrationDetailPage/IntegrationsUninstallBar';
import { ConnectionStates } from 'pages/Integrations/IntegrationDetailPage/TestConnection';
import { AWS_INTEGRATION } from 'pages/Integrations/IntegrationsList';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { Dispatch, SetStateAction, useCallback } from 'react';
import { useQueryClient } from 'react-query';
import { CloudAccount } from '../../ServicesSection/types';
import { RegionSelector } from './RegionSelector';
import RemoveIntegrationAccount from './RemoveIntegrationAccount';
interface AccountSettingsModalProps {
isOpen: boolean;
onClose: () => void;
account: CloudAccount;
setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>;
}
function AccountSettingsModal({
isOpen,
onClose,
account,
setActiveAccount,
@@ -42,6 +42,16 @@ function AccountSettingsModal({
handleClose,
} = useAccountSettingsModal({ onClose, account, setActiveAccount });
const queryClient = useQueryClient();
const urlQuery = useUrlQuery();
const handleRemoveIntegrationAccountSuccess = (): void => {
queryClient.invalidateQueries([REACT_QUERY_KEY.AWS_ACCOUNTS]);
urlQuery.delete('cloudAccountId');
handleClose();
history.replace({ search: urlQuery.toString() });
};
const renderRegionSelector = useCallback(() => {
if (isRegionSelectOpen) {
return (
@@ -120,7 +130,7 @@ function AccountSettingsModal({
return (
<SignozModal
open={isOpen}
open
title={modalTitle}
onCancel={handleClose}
onOk={handleSubmit}
@@ -164,12 +174,9 @@ function AccountSettingsModal({
</Form.Item>
<div className="integration-detail-content">
<IntergrationsUninstallBar
integrationTitle={AWS_INTEGRATION.title}
integrationId={AWS_INTEGRATION.id}
onUnInstallSuccess={handleClose}
removeIntegrationTitle="Remove"
connectionStatus={ConnectionStates.Connected}
<RemoveIntegrationAccount
accountId={account?.id}
onRemoveIntegrationAccountSuccess={handleRemoveIntegrationAccountSuccess}
/>
</div>
</div>

View File

@@ -2,11 +2,9 @@ import './CloudAccountSetupModal.style.scss';
import { Color } from '@signozhq/design-tokens';
import SignozModal from 'components/SignozModal/SignozModal';
import ROUTES from 'constants/routes';
import { useIntegrationModal } from 'hooks/integrations/aws/useIntegrationModal';
import { SquareArrowOutUpRight } from 'lucide-react';
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom-v5-compat';
import {
ActiveViewEnum,
@@ -18,7 +16,6 @@ import { RegionSelector } from './RegionSelector';
import { SuccessView } from './SuccessView';
function CloudAccountSetupModal({
isOpen,
onClose,
}: IntegrationModalProps): JSX.Element {
const {
@@ -41,6 +38,8 @@ function CloudAccountSetupModal({
accountId,
selectedDeploymentRegion,
handleRegionChange,
connectionParams,
isConnectionParamsLoading,
} = useIntegrationModal({ onClose });
const renderContent = useCallback(() => {
@@ -71,6 +70,8 @@ function CloudAccountSetupModal({
accountId={accountId}
selectedDeploymentRegion={selectedDeploymentRegion}
handleRegionChange={handleRegionChange}
connectionParams={connectionParams}
isConnectionParamsLoading={isConnectionParamsLoading}
/>
);
}, [
@@ -86,6 +87,8 @@ function CloudAccountSetupModal({
accountId,
selectedDeploymentRegion,
handleRegionChange,
connectionParams,
isConnectionParamsLoading,
setSelectedRegions,
setIncludeAllRegions,
]);
@@ -96,11 +99,6 @@ function CloudAccountSetupModal({
[selectedRegions, allRegions],
);
const navigate = useNavigate();
const handleGoToDashboards = useCallback((): void => {
navigate(ROUTES.ALL_DASHBOARD);
}, [navigate]);
const getModalConfig = useCallback(() => {
// Handle success state first
if (modalState === ModalStateEnum.SUCCESS) {
@@ -108,11 +106,11 @@ function CloudAccountSetupModal({
title: 'AWS Webservice Integration',
okText: (
<div className="cloud-account-setup-success-view__footer-button">
Go to Dashboards
Continue
</div>
),
block: true,
onOk: handleGoToDashboards,
onOk: handleClose,
cancelButtonProps: { style: { display: 'none' } },
disabled: false,
};
@@ -151,7 +149,7 @@ function CloudAccountSetupModal({
isLoading,
isGeneratingUrl,
activeView,
handleGoToDashboards,
handleClose,
setActiveView,
]);
@@ -159,7 +157,7 @@ function CloudAccountSetupModal({
return (
<SignozModal
open={isOpen}
open
className="cloud-account-setup-modal"
title={modalConfig.title}
onCancel={handleClose}

View File

@@ -12,6 +12,7 @@ import {
MonitoringRegionsSection,
RegionDeploymentSection,
} from './IntegrateNowFormSections';
import RenderConnectionFields from './RenderConnectionParams';
const allRegions = (): string[] =>
regions.flatMap((r) => r.subRegions.map((sr) => sr.name));
@@ -35,6 +36,8 @@ export function RegionForm({
accountId,
selectedDeploymentRegion,
handleRegionChange,
connectionParams,
isConnectionParamsLoading,
}: RegionFormProps): JSX.Element {
const startTimeRef = useRef(Date.now());
const refetchInterval = 10 * 1000;
@@ -88,6 +91,11 @@ export function RegionForm({
isFormDisabled={isFormDisabled}
/>
<ComplianceNote />
<RenderConnectionFields
isConnectionParamsLoading={isConnectionParamsLoading}
connectionParams={connectionParams}
isFormDisabled={isFormDisabled}
/>
</div>
</Form>
);

View File

@@ -0,0 +1,48 @@
.remove-integration-account {
display: flex;
justify-content: space-between;
padding: 16px;
border-radius: 4px;
border: 1px solid rgba(218, 85, 101, 0.2);
background: rgba(218, 85, 101, 0.06);
&__header {
display: flex;
flex-direction: column;
gap: 6px;
}
&__title {
color: var(--bg-cherry-500);
font-size: 14px;
letter-spacing: -0.07px;
}
&__subtitle {
color: var(--bg-cherry-300);
font-size: 14px;
line-height: 22px;
letter-spacing: -0.07px;
}
&__button {
display: flex;
align-items: center;
background: var(--bg-cherry-500);
border: none;
color: var(--bg-vanilla-100);
font-size: 12px;
font-weight: 500;
line-height: 13.3px; /* 110.833% */
padding: 9px 13px;
.ant-btn-icon {
margin-inline-end: 4px !important;
}
&:hover {
&.ant-btn-default {
color: var(--bg-vanilla-300) !important;
}
}
}
}

View File

@@ -0,0 +1,94 @@
import './RemoveIntegrationAccount.scss';
import { Button, Modal } from 'antd';
import logEvent from 'api/common/logEvent';
import removeAwsIntegrationAccount from 'api/Integrations/removeAwsIntegrationAccount';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { useNotifications } from 'hooks/useNotifications';
import { X } from 'lucide-react';
import { INTEGRATION_TELEMETRY_EVENTS } from 'pages/Integrations/utils';
import { useState } from 'react';
import { useMutation } from 'react-query';
function RemoveIntegrationAccount({
accountId,
onRemoveIntegrationAccountSuccess,
}: {
accountId: string;
onRemoveIntegrationAccountSuccess: () => void;
}): JSX.Element {
const { notifications } = useNotifications();
const [isModalOpen, setIsModalOpen] = useState(false);
const showModal = (): void => {
setIsModalOpen(true);
};
const {
mutate: removeIntegration,
isLoading: isRemoveIntegrationLoading,
} = useMutation(removeAwsIntegrationAccount, {
onSuccess: () => {
onRemoveIntegrationAccountSuccess?.();
setIsModalOpen(false);
},
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
});
const handleOk = (): void => {
logEvent(INTEGRATION_TELEMETRY_EVENTS.AWS_INTEGRATION_ACCOUNT_REMOVED, {
accountId,
});
removeIntegration(accountId);
};
const handleCancel = (): void => {
setIsModalOpen(false);
};
return (
<div className="remove-integration-account">
<div className="remove-integration-account__header">
<div className="remove-integration-account__title">Remove Integration</div>
<div className="remove-integration-account__subtitle">
Removing this integration won&apos;t delete any existing data but will stop
collecting new data from AWS.
</div>
</div>
<Button
className="remove-integration-account__button"
icon={<X size={14} />}
onClick={(): void => showModal()}
>
Remove
</Button>
<Modal
className="remove-integration-modal"
open={isModalOpen}
title="Remove integration"
onOk={handleOk}
onCancel={handleCancel}
okText="Remove Integration"
okButtonProps={{
danger: true,
disabled: isRemoveIntegrationLoading,
}}
>
<div className="remove-integration-modal__text">
Removing this account will remove all components created for sending
telemetry to SigNoz in your AWS account within the next ~15 minutes
(cloudformation stacks named signoz-integration-telemetry-collection in
enabled regions). <br />
<br />
After that, you can delete the cloudformation stack that was created
manually when connecting this account.
</div>
</Modal>
</div>
);
}
export default RemoveIntegrationAccount;

View File

@@ -0,0 +1,71 @@
import { Form, Input } from 'antd';
import { ConnectionParams } from 'types/api/integrations/aws';
function RenderConnectionFields({
isConnectionParamsLoading,
connectionParams,
isFormDisabled,
}: {
isConnectionParamsLoading?: boolean;
connectionParams?: ConnectionParams | null;
isFormDisabled?: boolean;
}): JSX.Element | null {
if (
isConnectionParamsLoading ||
(!!connectionParams?.ingestion_url &&
!!connectionParams?.ingestion_key &&
!!connectionParams?.signoz_api_url &&
!!connectionParams?.signoz_api_key)
) {
return null;
}
return (
<Form.Item name="connection_params">
{!connectionParams?.ingestion_url && (
<Form.Item
name="ingestion_url"
label="Ingestion URL"
rules={[{ required: true, message: 'Please enter ingestion URL' }]}
>
<Input placeholder="Enter ingestion URL" disabled={isFormDisabled} />
</Form.Item>
)}
{!connectionParams?.ingestion_key && (
<Form.Item
name="ingestion_key"
label="Ingestion Key"
rules={[{ required: true, message: 'Please enter ingestion key' }]}
>
<Input placeholder="Enter ingestion key" disabled={isFormDisabled} />
</Form.Item>
)}
{!connectionParams?.signoz_api_url && (
<Form.Item
name="signoz_api_url"
label="SigNoz API URL"
rules={[{ required: true, message: 'Please enter SigNoz API URL' }]}
>
<Input placeholder="Enter SigNoz API URL" disabled={isFormDisabled} />
</Form.Item>
)}
{!connectionParams?.signoz_api_key && (
<Form.Item
name="signoz_api_key"
label="SigNoz API KEY"
rules={[{ required: true, message: 'Please enter SigNoz API Key' }]}
>
<Input placeholder="Enter SigNoz API Key" disabled={isFormDisabled} />
</Form.Item>
)}
</Form.Item>
);
}
RenderConnectionFields.defaultProps = {
connectionParams: null,
isFormDisabled: false,
isConnectionParamsLoading: false,
};
export default RenderConnectionFields;

View File

@@ -53,23 +53,18 @@ export function SuccessView(): JSX.Element {
WHAT NEXT
</h4>
<div className="what-next-items-wrapper">
{[
'Understand your AWS services with SigNozs out-of-the-box dashboards',
'Set up alerts for real-time monitoring.',
'Track logs and traces.',
].map((item) => (
<Alert
key={item}
message={
<div className="what-next-items-wrapper__item">
<div className="what-next-item-bullet-icon"></div>
<div className="what-next-item-text">{item}</div>
<Alert
message={
<div className="what-next-items-wrapper__item">
<div className="what-next-item-bullet-icon"></div>
<div className="what-next-item-text">
Set up your AWS services effortlessly under your enabled account.
</div>
}
type="info"
className="what-next-items-wrapper__item"
/>
))}
</div>
}
type="info"
className="what-next-items-wrapper__item"
/>
</div>
</div>
</div>

View File

@@ -1,5 +1,6 @@
import { FormInstance } from 'antd';
import { Dispatch, SetStateAction } from 'react';
import { ConnectionParams } from 'types/api/integrations/aws';
export enum ActiveViewEnum {
SELECT_REGIONS = 'select-regions',
@@ -25,9 +26,10 @@ export interface RegionFormProps {
accountId?: string;
selectedDeploymentRegion: string | undefined;
handleRegionChange: (value: string) => void;
connectionParams?: ConnectionParams;
isConnectionParamsLoading?: boolean;
}
export interface IntegrationModalProps {
isOpen: boolean;
onClose: () => void;
}

View File

@@ -1,3 +1,5 @@
import { Link } from 'react-router-dom';
import { ServiceData } from './types';
function DashboardItem({
@@ -5,8 +7,8 @@ function DashboardItem({
}: {
dashboard: ServiceData['assets']['dashboards'][number];
}): JSX.Element {
return (
<div className="cloud-service-dashboard-item">
const content = (
<>
<div className="cloud-service-dashboard-item__title">{dashboard.title}</div>
<div className="cloud-service-dashboard-item__preview">
<img
@@ -15,6 +17,18 @@ function DashboardItem({
className="cloud-service-dashboard-item__preview-image"
/>
</div>
</>
);
return (
<div className="cloud-service-dashboard-item">
{dashboard.url ? (
<Link to={dashboard.url} className="cloud-service-dashboard-item__link">
{content}
</Link>
) : (
content
)}
</div>
);
}

View File

@@ -1,4 +1,3 @@
import { Color } from '@signozhq/design-tokens';
import { Button, Tabs, TabsProps } from 'antd';
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
import Spinner from 'components/Spinner';
@@ -8,8 +7,7 @@ import { IServiceStatus } from 'container/CloudIntegrationPage/ServicesSection/t
import dayjs from 'dayjs';
import { useServiceDetails } from 'hooks/integrations/aws/useServiceDetails';
import useUrlQuery from 'hooks/useUrlQuery';
import { Wrench } from 'lucide-react';
import { useState } from 'react';
import { useMemo, useState } from 'react';
import ConfigureServiceModal from './ConfigureServiceModal';
@@ -38,7 +36,7 @@ const getStatus = (
function ServiceStatus({
serviceStatus,
}: {
serviceStatus: IServiceStatus | null;
serviceStatus: IServiceStatus | undefined;
}): JSX.Element {
const logsLastReceivedTimestamp = serviceStatus?.logs?.last_received_ts_ms;
const metricsLastReceivedTimestamp =
@@ -54,7 +52,7 @@ function ServiceStatus({
function ServiceDetails(): JSX.Element | null {
const urlQuery = useUrlQuery();
const accountId = urlQuery.get('accountId');
const cloudAccountId = urlQuery.get('cloudAccountId');
const serviceId = urlQuery.get('service');
const [isConfigureServiceModalOpen, setIsConfigureServiceModalOpen] = useState(
false,
@@ -62,7 +60,24 @@ function ServiceDetails(): JSX.Element | null {
const { data: serviceDetailsData, isLoading } = useServiceDetails(
serviceId || '',
accountId || undefined,
cloudAccountId || undefined,
);
// eslint-disable-next-line @typescript-eslint/naming-convention
const { config, supported_signals } = serviceDetailsData ?? {};
const totalSupportedSignals = Object.entries(supported_signals || {}).filter(
([, value]) => !!value,
).length;
const enabledSignals = useMemo(
() =>
Object.values(config || {}).filter((item) => item && item.enabled).length,
[config],
);
const isAnySignalConfigured = useMemo(
() => !!config?.logs?.enabled || !!config?.metrics?.enabled,
[config],
);
if (isLoading) {
@@ -96,16 +111,22 @@ function ServiceDetails(): JSX.Element | null {
<div className="service-details__title-bar">
<div className="service-details__details-title">Details</div>
<div className="service-details__right-actions">
{serviceDetailsData?.status && (
<ServiceStatus serviceStatus={serviceDetailsData.status} />
)}
{!!accountId && (
<ServiceStatus serviceStatus={serviceDetailsData.status} />
{!!cloudAccountId && isAnySignalConfigured ? (
<Button
className="configure-button"
className="configure-button configure-button--default"
onClick={(): void => setIsConfigureServiceModalOpen(true)}
>
<Wrench size={12} color={Color.BG_VANILLA_400} />
Configure
Configure ({enabledSignals}/{totalSupportedSignals})
</Button>
) : (
<Button
type="primary"
className="configure-button configure-button--primary"
onClick={(): void => setIsConfigureServiceModalOpen(true)}
>
Enable Service
</Button>
)}
</div>
@@ -119,15 +140,17 @@ function ServiceDetails(): JSX.Element | null {
<div className="service-details__tabs">
<Tabs items={tabItems} />
</div>
<ConfigureServiceModal
isOpen={isConfigureServiceModalOpen}
onClose={(): void => setIsConfigureServiceModalOpen(false)}
serviceName={serviceDetailsData.title}
serviceId={serviceId || ''}
cloudAccountId={accountId || ''}
initialConfig={serviceDetailsData.config}
supportedSignals={serviceDetailsData.supported_signals || {}}
/>
{isConfigureServiceModalOpen && (
<ConfigureServiceModal
isOpen
onClose={(): void => setIsConfigureServiceModalOpen(false)}
serviceName={serviceDetailsData.title}
serviceId={serviceId || ''}
cloudAccountId={cloudAccountId || ''}
initialConfig={serviceDetailsData.config}
supportedSignals={serviceDetailsData.supported_signals || {}}
/>
)}
</div>
);
}

View File

@@ -1,26 +1,34 @@
import Spinner from 'components/Spinner';
import { useGetAccountServices } from 'hooks/integrations/aws/useGetAccountServices';
import useUrlQuery from 'hooks/useUrlQuery';
import { useMemo } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom-v5-compat';
import ServiceItem from './ServiceItem';
interface ServicesListProps {
accountId: string;
cloudAccountId: string;
filter: 'all_services' | 'enabled' | 'available';
}
function ServicesList({ accountId, filter }: ServicesListProps): JSX.Element {
function ServicesList({
cloudAccountId,
filter,
}: ServicesListProps): JSX.Element {
const urlQuery = useUrlQuery();
const navigate = useNavigate();
const { data: services = [], isLoading } = useGetAccountServices(accountId);
const { data: services = [], isLoading } = useGetAccountServices(
cloudAccountId,
);
const activeService = urlQuery.get('service');
const handleServiceClick = (serviceId: string): void => {
urlQuery.set('service', serviceId);
navigate({ search: urlQuery.toString() });
};
const handleActiveService = useCallback(
(serviceId: string): void => {
urlQuery.set('service', serviceId);
navigate({ search: urlQuery.toString() });
},
[navigate, urlQuery],
);
const filteredServices = useMemo(() => {
if (filter === 'all_services') return services;
@@ -32,6 +40,12 @@ function ServicesList({ accountId, filter }: ServicesListProps): JSX.Element {
});
}, [services, filter]);
useEffect(() => {
if (activeService || !services?.length) return;
handleActiveService(services[0].id);
}, [services, activeService, handleActiveService]);
if (isLoading) return <Spinner size="large" height="25vh" />;
if (!services) return <div>No services found</div>;
@@ -41,7 +55,7 @@ function ServicesList({ accountId, filter }: ServicesListProps): JSX.Element {
<ServiceItem
key={service.id}
service={service}
onClick={handleServiceClick}
onClick={handleActiveService}
isActive={service.id === activeService}
/>
))}

View File

@@ -135,18 +135,25 @@
color: var(--bg-cherry-400);
}
}
.configure-button {
display: flex;
align-items: center;
gap: 6px;
color: var(--bg-vanilla-400);
background: var(--bg-ink-300);
border: 1px solid var(--bg-slate-400);
border-radius: 2px;
font-size: 12px;
font-weight: 400;
line-height: 10px; /* 83.333% */
letter-spacing: 0.12px;
font-weight: 500;
width: 116px;
box-shadow: none;
&--default {
color: var(--bg-vanilla-400);
background: var(--bg-slate-400);
border: 1px solid var(--bg-slate-400);
}
&--primary {
background-color: var(--bg-robin-500);
color: var(--bg-vanilla-100);
font-weight: 500;
color: var(--Vanilla-100, #fff);
}
}
}
}

View File

@@ -20,17 +20,17 @@ export enum ServiceFilterType {
}
interface ServicesFilterProps {
accountId: string;
cloudAccountId: string;
onFilterChange: (value: ServiceFilterType) => void;
}
function ServicesFilter({
accountId,
cloudAccountId,
onFilterChange,
}: ServicesFilterProps): JSX.Element | null {
const { data: services, isLoading } = useQuery(
[REACT_QUERY_KEY.AWS_SERVICES, accountId],
() => getAwsServices(accountId),
[REACT_QUERY_KEY.AWS_SERVICES, cloudAccountId],
() => getAwsServices(cloudAccountId),
);
const { enabledCount, availableCount } = useMemo(() => {
@@ -77,7 +77,7 @@ function ServicesFilter({
function ServicesSection(): JSX.Element {
const urlQuery = useUrlQuery();
const accountId = urlQuery.get('accountId') || '';
const cloudAccountId = urlQuery.get('cloudAccountId') || '';
const [activeFilter, setActiveFilter] = useState<
'all_services' | 'enabled' | 'available'
@@ -86,8 +86,11 @@ function ServicesSection(): JSX.Element {
return (
<div className="services-section">
<div className="services-section__sidebar">
<ServicesFilter accountId={accountId} onFilterChange={setActiveFilter} />
<ServicesList accountId={accountId} filter={activeFilter} />
<ServicesFilter
cloudAccountId={cloudAccountId}
onFilterChange={setActiveFilter}
/>
<ServicesList cloudAccountId={cloudAccountId} filter={activeFilter} />
</div>
<div className="services-section__content">
<ServiceDetails />

View File

@@ -0,0 +1,233 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { AppProvider } from 'providers/App/App';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { Provider } from 'react-redux';
import store from 'store';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import {
MENUITEM_KEYS_VS_LABELS,
MenuItemKeys,
} from '../WidgetHeader/contants';
import { WidgetGraphComponentProps } from './types';
import WidgetGraphComponent from './WidgetGraphComponent';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.DASHBOARD}/624652db-6097-42f5-bbca-e9012901db00`,
}),
}));
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
// Mock data
const mockProps: WidgetGraphComponentProps = {
widget: {
bucketCount: 30,
bucketWidth: 0,
columnUnits: {},
description: '',
fillSpans: false,
id: '17f905f6-d355-46bd-a78e-cbc87e6f58cc',
isStacked: false,
mergeAllActiveQueries: false,
nullZeroValues: 'zero',
opacity: '1',
panelTypes: PANEL_TYPES.VALUE,
query: {
builder: {
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.String,
id: 'span_id--string----true',
isColumn: true,
isJSON: false,
key: 'span_id',
type: '',
},
aggregateOperator: 'count_distinct',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [],
op: 'AND',
},
functions: [],
groupBy: [],
having: [],
legend: '',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'last',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'count_distinct',
},
],
queryFormulas: [],
},
clickhouse_sql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
id: '47449208-2c76-4465-9c62-a37fb4f5f11f',
promql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
queryType: EQueryType.QUERY_BUILDER,
},
selectedLogFields: [
{
dataType: 'string',
name: 'body',
type: '',
},
{
dataType: 'string',
name: 'timestamp',
type: '',
},
],
selectedTracesFields: [],
softMax: 0,
softMin: 0,
stackedBarChart: false,
thresholds: [],
timePreferance: 'GLOBAL_TIME',
title: 'Test Dashboard',
yAxisUnit: 'none',
},
queryResponse: {
status: 'loading',
isLoading: true,
isSuccess: false,
isError: false,
isIdle: false,
dataUpdatedAt: 0,
error: null,
errorUpdatedAt: 0,
failureCount: 0,
errorUpdateCount: 0,
isFetched: false,
isFetchedAfterMount: false,
isFetching: true,
isRefetching: false,
isLoadingError: false,
isPlaceholderData: false,
isPreviousData: false,
isRefetchError: false,
isStale: true,
data: undefined,
refetch: jest.fn(),
remove: jest.fn(),
},
errorMessage: '',
version: 'v4',
headerMenuList: [
MenuItemKeys.View,
MenuItemKeys.Clone,
MenuItemKeys.Delete,
MenuItemKeys.Edit,
MenuItemKeys.CreateAlerts,
],
isWarning: false,
isFetchingResponse: false,
setRequestData: jest.fn(),
onClickHandler: jest.fn(),
onDragSelect: jest.fn(),
openTracesButton: false,
onOpenTraceBtnClick: jest.fn(),
};
// Mock useDashabord hook
jest.mock('providers/Dashboard/Dashboard', () => ({
useDashboard: (): any => ({
selectedDashboard: {
data: {
variables: [],
},
},
}),
}));
describe('WidgetGraphComponent', () => {
it('should show correct menu items when hovering over more options while loading', async () => {
const { getByTestId, findByRole, getByText, container } = render(
<MockQueryClientProvider>
<Provider store={store}>
<AppProvider>
<WidgetGraphComponent
widget={mockProps.widget}
queryResponse={mockProps.queryResponse}
errorMessage={mockProps.errorMessage}
version={mockProps.version}
headerMenuList={mockProps.headerMenuList}
isWarning={mockProps.isWarning}
isFetchingResponse={mockProps.isFetchingResponse}
setRequestData={mockProps.setRequestData}
onClickHandler={mockProps.onClickHandler}
onDragSelect={mockProps.onDragSelect}
openTracesButton={mockProps.openTracesButton}
onOpenTraceBtnClick={mockProps.onOpenTraceBtnClick}
/>
</AppProvider>
</Provider>
</MockQueryClientProvider>,
);
expect(getByText('Test Dashboard')).toBeInTheDocument();
// check if skeleton is rendered
const skeleton = container.querySelector('.ant-skeleton');
expect(skeleton).toBeInTheDocument();
const moreOptionsButton = getByTestId('widget-header-options');
fireEvent.mouseEnter(moreOptionsButton);
const menu = await findByRole('menu');
expect(menu).toBeInTheDocument();
// Check if all menu items are present
const expectedMenuItems = [
MENUITEM_KEYS_VS_LABELS[MenuItemKeys.View],
MENUITEM_KEYS_VS_LABELS[MenuItemKeys.Clone],
MENUITEM_KEYS_VS_LABELS[MenuItemKeys.Delete],
MENUITEM_KEYS_VS_LABELS[MenuItemKeys.Edit],
MENUITEM_KEYS_VS_LABELS[MenuItemKeys.CreateAlerts],
];
// check that menu is visible
expectedMenuItems.forEach((item) => {
expect(screen.getByText(item)).toBeInTheDocument();
});
});
});

View File

@@ -6,6 +6,7 @@ import { ToggleGraphProps } from 'components/Graph/types';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { placeWidgetAtBottom } from 'container/NewWidget/utils';
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useNotifications } from 'hooks/useNotifications';
@@ -48,6 +49,7 @@ function WidgetGraphComponent({
openTracesButton,
onOpenTraceBtnClick,
customSeries,
customErrorMessage,
}: WidgetGraphComponentProps): JSX.Element {
const [deleteModal, setDeleteModal] = useState(false);
const [hovered, setHovered] = useState(false);
@@ -132,18 +134,14 @@ function WidgetGraphComponent({
(l) => l.i === widget.id,
);
// added the cloned panel on the top as it is given most priority when arranging
// in the layout. React_grid_layout assigns priority from top, hence no random position for cloned panel
const layout = [
{
i: uuid,
w: originalPanelLayout?.w || 6,
x: 0,
h: originalPanelLayout?.h || 6,
y: 0,
},
...(selectedDashboard.data.layout || []),
];
const newLayoutItem = placeWidgetAtBottom(
uuid,
selectedDashboard?.data.layout || [],
originalPanelLayout?.w || 6,
originalPanelLayout?.h || 6,
);
const layout = [...(selectedDashboard.data.layout || []), newLayoutItem];
updateDashboardMutation.mutateAsync(
{
@@ -231,21 +229,6 @@ function WidgetGraphComponent({
const [searchTerm, setSearchTerm] = useState<string>('');
const loadingState =
(queryResponse.isLoading || queryResponse.status === 'idle') &&
widget.panelTypes !== PANEL_TYPES.LIST;
if (loadingState) {
return (
<Skeleton
style={{
height: '100%',
padding: '16px',
}}
/>
);
}
return (
<div
style={{
@@ -317,6 +300,13 @@ function WidgetGraphComponent({
setSearchTerm={setSearchTerm}
/>
</div>
{queryResponse.error && customErrorMessage && (
<div className="error-message-container">
<Typography.Text type="warning">{customErrorMessage}</Typography.Text>
</div>
)}
{queryResponse.isLoading && widget.panelTypes !== PANEL_TYPES.LIST && (
<Skeleton />
)}

View File

@@ -41,9 +41,15 @@ function GridCardGraph({
openTracesButton,
onOpenTraceBtnClick,
customSeries,
customErrorMessage,
start,
end,
}: GridCardGraphProps): JSX.Element {
const dispatch = useDispatch();
const [errorMessage, setErrorMessage] = useState<string>();
const [isInternalServerError, setIsInternalServerError] = useState<boolean>(
false,
);
const {
toScrollWidgetId,
setToScrollWidgetId,
@@ -178,6 +184,8 @@ function GridCardGraph({
variables: getDashboardVariables(variables),
selectedTime: widget.timePreferance || 'GLOBAL_TIME',
globalSelectedInterval,
start,
end,
},
version || DEFAULT_ENTITY_VERSION,
{
@@ -207,6 +215,11 @@ function GridCardGraph({
refetchOnMount: false,
onError: (error) => {
setErrorMessage(error.message);
if (customErrorMessage) {
setIsInternalServerError(
String(error.message).includes('API responded with 500'),
);
}
setDashboardQueryRangeCalled(true);
},
onSettled: (data) => {
@@ -256,6 +269,7 @@ function GridCardGraph({
openTracesButton={openTracesButton}
onOpenTraceBtnClick={onOpenTraceBtnClick}
customSeries={customSeries}
customErrorMessage={isInternalServerError ? customErrorMessage : undefined}
/>
)}
</div>

View File

@@ -37,6 +37,7 @@ export interface WidgetGraphComponentProps {
openTracesButton?: boolean;
onOpenTraceBtnClick?: (record: RowData) => void;
customSeries?: (data: QueryData[]) => uPlot.Series[];
customErrorMessage?: string;
}
export interface GridCardGraphProps {
@@ -54,6 +55,9 @@ export interface GridCardGraphProps {
openTracesButton?: boolean;
onOpenTraceBtnClick?: (record: RowData) => void;
customSeries?: (data: QueryData[]) => uPlot.Series[];
customErrorMessage?: string;
start?: number;
end?: number;
}
export interface GetGraphVisibilityStateOnLegendClickProps {

View File

@@ -106,6 +106,16 @@
}
}
.error-message-container {
display: flex;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
padding-top: 0;
padding-bottom: 32px;
}
.row-settings {
.ant-popover-inner {
width: 191px;

View File

@@ -45,6 +45,7 @@ import DashboardEmptyState from './DashboardEmptyState/DashboardEmptyState';
import GridCard from './GridCard';
import { Card, CardContainer, ReactGridLayout } from './styles';
import { removeUndefinedValuesFromLayout } from './utils';
import { MenuItemKeys } from './WidgetHeader/contants';
import { WidgetRowHeader } from './WidgetRow';
interface GraphLayoutProps {
@@ -190,7 +191,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
const widgetActions = !isDashboardLocked
? [...ViewMenuAction, ...EditMenuAction]
: [...ViewMenuAction];
: [...ViewMenuAction, MenuItemKeys.CreateAlerts];
const handleLayoutChange = (layout: Layout[]): void => {
const filterLayout = removeUndefinedValuesFromLayout(layout);

View File

@@ -1,18 +1,19 @@
import './InfraMonitoring.styles.scss';
import { initialQueriesMap } from 'constants/queryBuilder';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useCallback, useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
function HostsListControls({
handleFiltersChange,
}: {
handleFiltersChange: (value: IBuilderQuery['filters']) => void;
}): JSX.Element {
const { currentQuery } = useQueryBuilder();
const currentQuery = initialQueriesMap[DataSource.METRICS];
const updatedCurrentQuery = useMemo(
() => ({
...currentQuery,

View File

@@ -141,13 +141,9 @@ function ClusterDetails({
[cluster?.meta.k8s_cluster_name],
);
const [logFilters, setLogFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [tracesFilters, setTracesFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [logsAndTracesFilters, setLogsAndTracesFilters] = useState<
IBuilderQuery['filters']
>(initialFilters);
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
initialEventsFilters,
@@ -161,8 +157,7 @@ function ClusterDetails({
}, []);
useEffect(() => {
setLogFilters(initialFilters);
setTracesFilters(initialFilters);
setLogsAndTracesFilters(initialFilters);
setEventsFilters(initialEventsFilters);
}, [initialFilters, initialEventsFilters]);
@@ -181,6 +176,10 @@ function ClusterDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
logEvent('Infra Monitoring: Clusters list details tab changed', {
cluster: cluster?.clusterUID,
view: e.target.value,
});
};
const handleTimeChange = useCallback(
@@ -204,6 +203,7 @@ function ClusterDetails({
logEvent('Infra Monitoring: Clusters list details time updated', {
cluster: cluster?.clusterUID,
interval,
view: selectedView,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -212,7 +212,7 @@ function ClusterDetails({
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setLogFilters((prevFilters) => {
setLogsAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_CLUSTER_NAME].includes(item.key?.key ?? ''),
);
@@ -244,7 +244,7 @@ function ClusterDetails({
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setTracesFilters((prevFilters) => {
setLogsAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_CLUSTER_NAME].includes(item.key?.key ?? ''),
);
@@ -320,8 +320,8 @@ function ClusterDetails({
if (selectedView === VIEW_TYPES.LOGS) {
const filtersWithoutPagination = {
...logFilters,
items: logFilters.items.filter((item) => item.key?.key !== 'id'),
...logsAndTracesFilters,
items: logsAndTracesFilters.items.filter((item) => item.key?.key !== 'id'),
};
const compositeQuery = {
@@ -355,7 +355,7 @@ function ClusterDetails({
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: tracesFilters,
filters: logsAndTracesFilters,
},
],
},
@@ -519,7 +519,7 @@ function ClusterDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logFilters}
logFilters={logsAndTracesFilters}
selectedInterval={selectedInterval}
queryKey="clusterLogs"
category={K8sCategory.CLUSTERS}
@@ -532,9 +532,10 @@ function ClusterDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeTracesFilters={handleChangeTracesFilters}
tracesFilters={tracesFilters}
tracesFilters={logsAndTracesFilters}
selectedInterval={selectedInterval}
queryKey="clusterTraces"
queryKeyFilters={[QUERY_KEYS.K8S_CLUSTER_NAME]}
/>
)}
{selectedView === VIEW_TYPES.EVENTS && (

View File

@@ -240,6 +240,11 @@ function K8sClustersList({
}
}, [selectedRowData, fetchGroupedByRowData]);
const numberOfPages = useMemo(() => Math.ceil(totalCount / pageSize), [
totalCount,
pageSize,
]);
const handleTableChange: TableProps<K8sClustersRowData>['onChange'] = useCallback(
(
pagination: TablePaginationConfig,
@@ -250,6 +255,11 @@ function K8sClustersList({
): void => {
if (pagination.current) {
setCurrentPage(pagination.current);
logEvent('Infra Monitoring: K8s clusters list page number changed', {
page: pagination.current,
pageSize,
numberOfPages,
});
}
if ('field' in sorter && sorter.order) {
@@ -261,7 +271,7 @@ function K8sClustersList({
setOrderBy(null);
}
},
[],
[numberOfPages, pageSize],
);
const { handleChangeQueryData } = useQueryOperations({
@@ -275,15 +285,13 @@ function K8sClustersList({
handleChangeQueryData('filters', value);
setCurrentPage(1);
logEvent('Infra Monitoring: K8s list filters applied', {
filters: value,
});
logEvent('Infra Monitoring: K8s clusters list filters applied', {});
},
[handleChangeQueryData],
);
useEffect(() => {
logEvent('Infra Monitoring: K8s list page visited', {});
logEvent('Infra Monitoring: K8s clusters list page visited', {});
}, []);
const selectedClusterData = useMemo(() => {
@@ -442,6 +450,7 @@ function K8sClustersList({
setCurrentPage(1);
setGroupBy(groupBy);
setExpandedRowKeys([]);
logEvent('Infra Monitoring: K8s clusters list group by changed', {});
},
[groupByFiltersData],
);
@@ -457,6 +466,16 @@ function K8sClustersList({
}
}, [groupByFiltersData]);
const onPaginationChange = (page: number, pageSize: number): void => {
setCurrentPage(page);
setPageSize(pageSize);
logEvent('Infra Monitoring: K8s clusters list page number changed', {
page,
pageSize,
numberOfPages,
});
};
return (
<div className="k8s-list">
<K8sHeader
@@ -482,10 +501,7 @@ function K8sClustersList({
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: (page, pageSize): void => {
setCurrentPage(page);
setPageSize(pageSize);
},
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{

View File

@@ -155,13 +155,9 @@ function DaemonSetDetails({
[daemonSet?.meta.k8s_daemonset_name],
);
const [logFilters, setLogFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [tracesFilters, setTracesFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
IBuilderQuery['filters']
>(initialFilters);
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
initialEventsFilters,
@@ -175,8 +171,7 @@ function DaemonSetDetails({
}, []);
useEffect(() => {
setLogFilters(initialFilters);
setTracesFilters(initialFilters);
setLogAndTracesFilters(initialFilters);
setEventsFilters(initialEventsFilters);
}, [initialFilters, initialEventsFilters]);
@@ -195,6 +190,10 @@ function DaemonSetDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
logEvent('Infra Monitoring: DaemonSets list details tab changed', {
daemonSet: daemonSet?.daemonSetName,
view: e.target.value,
});
};
const handleTimeChange = useCallback(
@@ -218,6 +217,7 @@ function DaemonSetDetails({
logEvent('Infra Monitoring: DaemonSets list details time updated', {
daemonSet: daemonSet?.daemonSetName,
interval,
view: selectedView,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -226,7 +226,7 @@ function DaemonSetDetails({
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setLogFilters((prevFilters) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_DAEMON_SET_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
item.key?.key ?? '',
@@ -259,7 +259,7 @@ function DaemonSetDetails({
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setTracesFilters((prevFilters) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_DAEMON_SET_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
item.key?.key ?? '',
@@ -339,8 +339,8 @@ function DaemonSetDetails({
if (selectedView === VIEW_TYPES.LOGS) {
const filtersWithoutPagination = {
...logFilters,
items: logFilters.items.filter((item) => item.key?.key !== 'id'),
...logAndTracesFilters,
items: logAndTracesFilters.items.filter((item) => item.key?.key !== 'id'),
};
const compositeQuery = {
@@ -374,7 +374,7 @@ function DaemonSetDetails({
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: tracesFilters,
filters: logAndTracesFilters,
},
],
},
@@ -551,7 +551,7 @@ function DaemonSetDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logFilters}
logFilters={logAndTracesFilters}
selectedInterval={selectedInterval}
category={K8sCategory.DAEMONSETS}
queryKey="daemonsetLogs"
@@ -567,9 +567,13 @@ function DaemonSetDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeTracesFilters={handleChangeTracesFilters}
tracesFilters={tracesFilters}
tracesFilters={logAndTracesFilters}
selectedInterval={selectedInterval}
queryKey="daemonsetTraces"
queryKeyFilters={[
QUERY_KEYS.K8S_DAEMON_SET_NAME,
QUERY_KEYS.K8S_NAMESPACE_NAME,
]}
/>
)}
{selectedView === VIEW_TYPES.EVENTS && (

View File

@@ -243,6 +243,11 @@ function K8sDaemonSetsList({
}
}, [selectedRowData, fetchGroupedByRowData]);
const numberOfPages = useMemo(() => Math.ceil(totalCount / pageSize), [
totalCount,
pageSize,
]);
const handleTableChange: TableProps<K8sDaemonSetsRowData>['onChange'] = useCallback(
(
pagination: TablePaginationConfig,
@@ -253,6 +258,11 @@ function K8sDaemonSetsList({
): void => {
if (pagination.current) {
setCurrentPage(pagination.current);
logEvent('Infra Monitoring: K8s daemonSets list page number changed', {
page: pagination.current,
pageSize,
numberOfPages,
});
}
if ('field' in sorter && sorter.order) {
@@ -264,7 +274,7 @@ function K8sDaemonSetsList({
setOrderBy(null);
}
},
[],
[numberOfPages, pageSize],
);
const { handleChangeQueryData } = useQueryOperations({
@@ -278,15 +288,13 @@ function K8sDaemonSetsList({
handleChangeQueryData('filters', value);
setCurrentPage(1);
logEvent('Infra Monitoring: K8s list filters applied', {
filters: value,
});
logEvent('Infra Monitoring: K8s daemonSets list filters applied', {});
},
[handleChangeQueryData],
);
useEffect(() => {
logEvent('Infra Monitoring: K8s list page visited', {});
logEvent('Infra Monitoring: K8s daemonSets list page visited', {});
}, []);
const selectedDaemonSetData = useMemo(() => {
@@ -448,6 +456,8 @@ function K8sDaemonSetsList({
setCurrentPage(1);
setGroupBy(groupBy);
setExpandedRowKeys([]);
logEvent('Infra Monitoring: K8s daemonSets list group by changed', {});
},
[groupByFiltersData],
);
@@ -463,6 +473,16 @@ function K8sDaemonSetsList({
}
}, [groupByFiltersData]);
const onPaginationChange = (page: number, pageSize: number): void => {
setCurrentPage(page);
setPageSize(pageSize);
logEvent('Infra Monitoring: K8s daemonSets list page number changed', {
page,
pageSize,
numberOfPages,
});
};
return (
<div className="k8s-list">
<K8sHeader
@@ -490,10 +510,7 @@ function K8sDaemonSetsList({
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: (page, pageSize): void => {
setCurrentPage(page);
setPageSize(pageSize);
},
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{

View File

@@ -157,13 +157,9 @@ function DeploymentDetails({
[deployment?.meta.k8s_deployment_name],
);
const [logFilters, setLogFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [tracesFilters, setTracesFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
IBuilderQuery['filters']
>(initialFilters);
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
initialEventsFilters,
@@ -177,8 +173,7 @@ function DeploymentDetails({
}, []);
useEffect(() => {
setLogFilters(initialFilters);
setTracesFilters(initialFilters);
setLogAndTracesFilters(initialFilters);
setEventsFilters(initialEventsFilters);
}, [initialFilters, initialEventsFilters]);
@@ -197,6 +192,10 @@ function DeploymentDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
logEvent('Infra Monitoring: Deployments list details tab changed', {
deployment: deployment?.deploymentName,
view: e.target.value,
});
};
const handleTimeChange = useCallback(
@@ -220,6 +219,7 @@ function DeploymentDetails({
logEvent('Infra Monitoring: Deployments list details time updated', {
deployment: deployment?.deploymentName,
interval,
view: selectedView,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -228,7 +228,7 @@ function DeploymentDetails({
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setLogFilters((prevFilters) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_DEPLOYMENT_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
item.key?.key ?? '',
@@ -266,7 +266,7 @@ function DeploymentDetails({
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setTracesFilters((prevFilters) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_DEPLOYMENT_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
item.key?.key ?? '',
@@ -350,8 +350,8 @@ function DeploymentDetails({
if (selectedView === VIEW_TYPES.LOGS) {
const filtersWithoutPagination = {
...logFilters,
items: logFilters.items.filter((item) => item.key?.key !== 'id'),
...logAndTracesFilters,
items: logAndTracesFilters.items.filter((item) => item.key?.key !== 'id'),
};
const compositeQuery = {
@@ -385,7 +385,7 @@ function DeploymentDetails({
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: tracesFilters,
filters: logAndTracesFilters,
},
],
},
@@ -562,7 +562,7 @@ function DeploymentDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logFilters}
logFilters={logAndTracesFilters}
selectedInterval={selectedInterval}
queryKeyFilters={[
QUERY_KEYS.K8S_DEPLOYMENT_NAME,
@@ -578,9 +578,13 @@ function DeploymentDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeTracesFilters={handleChangeTracesFilters}
tracesFilters={tracesFilters}
tracesFilters={logAndTracesFilters}
selectedInterval={selectedInterval}
queryKey="deploymentTraces"
queryKeyFilters={[
QUERY_KEYS.K8S_DEPLOYMENT_NAME,
QUERY_KEYS.K8S_NAMESPACE_NAME,
]}
/>
)}
{selectedView === VIEW_TYPES.EVENTS && (

View File

@@ -245,6 +245,11 @@ function K8sDeploymentsList({
}
}, [selectedRowData, fetchGroupedByRowData]);
const numberOfPages = useMemo(() => Math.ceil(totalCount / pageSize), [
totalCount,
pageSize,
]);
const handleTableChange: TableProps<K8sDeploymentsRowData>['onChange'] = useCallback(
(
pagination: TablePaginationConfig,
@@ -255,6 +260,11 @@ function K8sDeploymentsList({
): void => {
if (pagination.current) {
setCurrentPage(pagination.current);
logEvent('Infra Monitoring: K8s deployments list page number changed', {
page: pagination.current,
pageSize,
numberOfPages,
});
}
if ('field' in sorter && sorter.order) {
@@ -266,7 +276,7 @@ function K8sDeploymentsList({
setOrderBy(null);
}
},
[],
[numberOfPages, pageSize],
);
const { handleChangeQueryData } = useQueryOperations({
@@ -280,15 +290,13 @@ function K8sDeploymentsList({
handleChangeQueryData('filters', value);
setCurrentPage(1);
logEvent('Infra Monitoring: K8s list filters applied', {
filters: value,
});
logEvent('Infra Monitoring: K8s deployments list filters applied', {});
},
[handleChangeQueryData],
);
useEffect(() => {
logEvent('Infra Monitoring: K8s list page visited', {});
logEvent('Infra Monitoring: K8s deployments list page visited', {});
}, []);
const selectedDeploymentData = useMemo(() => {
@@ -452,6 +460,8 @@ function K8sDeploymentsList({
setCurrentPage(1);
setGroupBy(groupBy);
setExpandedRowKeys([]);
logEvent('Infra Monitoring: K8s deployments list group by changed', {});
},
[groupByFiltersData],
);
@@ -467,6 +477,16 @@ function K8sDeploymentsList({
}
}, [groupByFiltersData]);
const onPaginationChange = (page: number, pageSize: number): void => {
setCurrentPage(page);
setPageSize(pageSize);
logEvent('Infra Monitoring: K8s deployments list page number changed', {
page,
pageSize,
numberOfPages,
});
};
return (
<div className="k8s-list">
<K8sHeader
@@ -494,10 +514,7 @@ function K8sDeploymentsList({
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: (page, pageSize): void => {
setCurrentPage(page);
setPageSize(pageSize);
},
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{

View File

@@ -12,6 +12,7 @@ import { useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { filterOutPrimaryFilters } from '../utils';
import EntityLogs from './EntityLogs';
interface Props {
@@ -58,14 +59,14 @@ function EntityLogsDetailedView({
...currentQuery.builder.queryData[0].aggregateAttribute,
},
filters: {
items: [],
items: filterOutPrimaryFilters(logFilters.items, queryKeyFilters),
op: 'AND',
},
},
],
},
}),
[currentQuery],
[currentQuery, logFilters.items, queryKeyFilters],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;

View File

@@ -26,6 +26,7 @@ import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import {
filterOutPrimaryFilters,
getEntityTracesQueryPayload,
selectedEntityTracesColumns,
} from '../utils';
@@ -44,6 +45,7 @@ interface Props {
tracesFilters: IBuilderQuery['filters'];
selectedInterval: Time;
queryKey: string;
queryKeyFilters: string[];
}
function EntityTraces({
@@ -54,6 +56,7 @@ function EntityTraces({
tracesFilters,
selectedInterval,
queryKey,
queryKeyFilters,
}: Props): JSX.Element {
const [traces, setTraces] = useState<any[]>([]);
const [offset] = useState<number>(0);
@@ -73,14 +76,14 @@ function EntityTraces({
...currentQuery.builder.queryData[0].aggregateAttribute,
},
filters: {
items: [],
items: filterOutPrimaryFilters(tracesFilters.items, queryKeyFilters),
op: 'AND',
},
},
],
},
}),
[currentQuery],
[currentQuery, queryKeyFilters, tracesFilters.items],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;

View File

@@ -7,7 +7,10 @@ import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import {
IBuilderQuery,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { nanoToMilli } from 'utils/timeUtils';
@@ -301,3 +304,12 @@ export const getEntityTracesQueryPayload = (
],
},
});
export const filterOutPrimaryFilters = (
filters: TagFilterItem[],
primaryKeys: string[],
): TagFilterItem[] =>
filters.filter(
(filter) =>
!primaryKeys.includes(filter.key?.key ?? '') && filter.key?.key !== 'id',
);

View File

@@ -4,6 +4,7 @@ import { VerticalAlignTopOutlined } from '@ant-design/icons';
import * as Sentry from '@sentry/react';
import type { CollapseProps } from 'antd';
import { Collapse, Tooltip, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
@@ -68,6 +69,11 @@ export default function InfraMonitoringK8s(): JSX.Element {
// in infra monitoring k8s, we are using only one query, hence updating the 0th index of queryData
handleChangeQueryData('filters', query.builder.queryData[0].filters);
setQuickFiltersLastUpdated(Date.now());
logEvent(
`Infra Monitoring: K8s ${selectedCategory} list quick filters applied`,
{},
);
};
const items: CollapseProps['items'] = [

View File

@@ -152,13 +152,9 @@ function JobDetails({
[job?.meta.k8s_job_name],
);
const [logFilters, setLogFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [tracesFilters, setTracesFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
IBuilderQuery['filters']
>(initialFilters);
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
initialEventsFilters,
@@ -172,8 +168,7 @@ function JobDetails({
}, []);
useEffect(() => {
setLogFilters(initialFilters);
setTracesFilters(initialFilters);
setLogAndTracesFilters(initialFilters);
setEventsFilters(initialEventsFilters);
}, [initialFilters, initialEventsFilters]);
@@ -192,6 +187,10 @@ function JobDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
logEvent('Infra Monitoring: Jobs list details tab changed', {
job: job?.jobName,
view: e.target.value,
});
};
const handleTimeChange = useCallback(
@@ -215,6 +214,7 @@ function JobDetails({
logEvent('Infra Monitoring: Jobs list details time updated', {
job: job?.jobName,
interval,
view: selectedView,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -223,17 +223,16 @@ function JobDetails({
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setLogFilters((prevFilters) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_STATEFUL_SET_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
[QUERY_KEYS.K8S_JOB_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
item.key?.key ?? '',
),
);
const paginationFilter = value.items.find((item) => item.key?.key === 'id');
const newFilters = value.items.filter(
(item) =>
item.key?.key !== 'id' &&
item.key?.key !== QUERY_KEYS.K8S_STATEFUL_SET_NAME,
item.key?.key !== 'id' && item.key?.key !== QUERY_KEYS.K8S_JOB_NAME,
);
logEvent('Infra Monitoring: Jobs list details logs filters applied', {
@@ -256,9 +255,9 @@ function JobDetails({
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setTracesFilters((prevFilters) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_STATEFUL_SET_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
[QUERY_KEYS.K8S_JOB_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
item.key?.key ?? '',
),
);
@@ -272,7 +271,7 @@ function JobDetails({
items: [
...primaryFilters,
...value.items.filter(
(item) => item.key?.key !== QUERY_KEYS.K8S_STATEFUL_SET_NAME,
(item) => item.key?.key !== QUERY_KEYS.K8S_JOB_NAME,
),
].filter((item): item is TagFilterItem => item !== undefined),
};
@@ -330,8 +329,8 @@ function JobDetails({
if (selectedView === VIEW_TYPES.LOGS) {
const filtersWithoutPagination = {
...logFilters,
items: logFilters.items.filter((item) => item.key?.key !== 'id'),
...logAndTracesFilters,
items: logAndTracesFilters.items.filter((item) => item.key?.key !== 'id'),
};
const compositeQuery = {
@@ -365,7 +364,7 @@ function JobDetails({
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: tracesFilters,
filters: logAndTracesFilters,
},
],
},
@@ -531,7 +530,7 @@ function JobDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logFilters}
logFilters={logAndTracesFilters}
selectedInterval={selectedInterval}
category={K8sCategory.JOBS}
queryKey="jobLogs"
@@ -547,9 +546,13 @@ function JobDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeTracesFilters={handleChangeTracesFilters}
tracesFilters={tracesFilters}
tracesFilters={logAndTracesFilters}
selectedInterval={selectedInterval}
queryKey="jobTraces"
queryKeyFilters={[
QUERY_KEYS.K8S_JOB_NAME,
QUERY_KEYS.K8S_NAMESPACE_NAME,
]}
/>
)}
{selectedView === VIEW_TYPES.EVENTS && (

View File

@@ -234,6 +234,11 @@ function K8sJobsList({
}
}, [selectedRowData, fetchGroupedByRowData]);
const numberOfPages = useMemo(() => Math.ceil(totalCount / pageSize), [
totalCount,
pageSize,
]);
const handleTableChange: TableProps<K8sJobsRowData>['onChange'] = useCallback(
(
pagination: TablePaginationConfig,
@@ -242,6 +247,11 @@ function K8sJobsList({
): void => {
if (pagination.current) {
setCurrentPage(pagination.current);
logEvent('Infra Monitoring: K8s jobs list page number changed', {
page: pagination.current,
pageSize,
numberOfPages,
});
}
if ('field' in sorter && sorter.order) {
@@ -253,7 +263,7 @@ function K8sJobsList({
setOrderBy(null);
}
},
[],
[numberOfPages, pageSize],
);
const { handleChangeQueryData } = useQueryOperations({
@@ -267,15 +277,13 @@ function K8sJobsList({
handleChangeQueryData('filters', value);
setCurrentPage(1);
logEvent('Infra Monitoring: K8s list filters applied', {
filters: value,
});
logEvent('Infra Monitoring: K8s jobs list filters applied', {});
},
[handleChangeQueryData],
);
useEffect(() => {
logEvent('Infra Monitoring: K8s list page visited', {});
logEvent('Infra Monitoring: K8s jobs list page visited', {});
}, []);
const selectedJobData = useMemo(() => {
@@ -424,6 +432,8 @@ function K8sJobsList({
setCurrentPage(1);
setGroupBy(groupBy);
setExpandedRowKeys([]);
logEvent('Infra Monitoring: K8s jobs list group by changed', {});
},
[groupByFiltersData],
);
@@ -439,6 +449,16 @@ function K8sJobsList({
}
}, [groupByFiltersData]);
const onPaginationChange = (page: number, pageSize: number): void => {
setCurrentPage(page);
setPageSize(pageSize);
logEvent('Infra Monitoring: K8s jobs list page number changed', {
page,
pageSize,
numberOfPages,
});
};
return (
<div className="k8s-list">
<K8sHeader
@@ -466,10 +486,7 @@ function K8sJobsList({
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: (page, pageSize): void => {
setCurrentPage(page);
setPageSize(pageSize);
},
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{

View File

@@ -2,13 +2,14 @@
import './InfraMonitoringK8s.styles.scss';
import { Button, Select } from 'antd';
import { initialQueriesMap } from 'constants/queryBuilder';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { Filter, SlidersHorizontal } from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { K8sCategory } from './constants';
import K8sFiltersSidePanel from './K8sFiltersSidePanel/K8sFiltersSidePanel';
@@ -47,7 +48,7 @@ function K8sHeader({
}: K8sHeaderProps): JSX.Element {
const [isFiltersSidePanelOpen, setIsFiltersSidePanelOpen] = useState(false);
const { currentQuery } = useQueryBuilder();
const currentQuery = initialQueriesMap[DataSource.METRICS];
const updatedCurrentQuery = useMemo(
() => ({

View File

@@ -242,6 +242,11 @@ function K8sNamespacesList({
}
}, [selectedRowData, fetchGroupedByRowData]);
const numberOfPages = useMemo(() => Math.ceil(totalCount / pageSize), [
totalCount,
pageSize,
]);
const handleTableChange: TableProps<K8sNamespacesRowData>['onChange'] = useCallback(
(
pagination: TablePaginationConfig,
@@ -252,6 +257,11 @@ function K8sNamespacesList({
): void => {
if (pagination.current) {
setCurrentPage(pagination.current);
logEvent('Infra Monitoring: K8s namespaces list page number changed', {
page: pagination.current,
pageSize,
numberOfPages,
});
}
if ('field' in sorter && sorter.order) {
@@ -263,7 +273,7 @@ function K8sNamespacesList({
setOrderBy(null);
}
},
[],
[numberOfPages, pageSize],
);
const { handleChangeQueryData } = useQueryOperations({
@@ -277,15 +287,13 @@ function K8sNamespacesList({
handleChangeQueryData('filters', value);
setCurrentPage(1);
logEvent('Infra Monitoring: K8s list filters applied', {
filters: value,
});
logEvent('Infra Monitoring: K8s namespaces list filters applied', {});
},
[handleChangeQueryData],
);
useEffect(() => {
logEvent('Infra Monitoring: K8s list page visited', {});
logEvent('Infra Monitoring: K8s namespaces list page visited', {});
}, []);
const selectedNamespaceData = useMemo(() => {
@@ -449,6 +457,8 @@ function K8sNamespacesList({
setCurrentPage(1);
setGroupBy(groupBy);
setExpandedRowKeys([]);
logEvent('Infra Monitoring: K8s namespaces list group by changed', {});
},
[groupByFiltersData],
);
@@ -464,6 +474,16 @@ function K8sNamespacesList({
}
}, [groupByFiltersData]);
const onPaginationChange = (page: number, pageSize: number): void => {
setCurrentPage(page);
setPageSize(pageSize);
logEvent('Infra Monitoring: K8s namespaces list page number changed', {
page,
pageSize,
numberOfPages,
});
};
return (
<div className="k8s-list">
<K8sHeader
@@ -489,10 +509,7 @@ function K8sNamespacesList({
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: (page, pageSize): void => {
setCurrentPage(page);
setPageSize(pageSize);
},
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{

View File

@@ -143,13 +143,9 @@ function NamespaceDetails({
[namespace?.namespaceName],
);
const [logFilters, setLogFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [tracesFilters, setTracesFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
IBuilderQuery['filters']
>(initialFilters);
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
initialEventsFilters,
@@ -163,8 +159,7 @@ function NamespaceDetails({
}, []);
useEffect(() => {
setLogFilters(initialFilters);
setTracesFilters(initialFilters);
setLogAndTracesFilters(initialFilters);
setEventsFilters(initialEventsFilters);
}, [initialFilters, initialEventsFilters]);
@@ -183,6 +178,10 @@ function NamespaceDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
logEvent('Infra Monitoring: Namespaces list details tab changed', {
namespace: namespace?.namespaceName,
view: e.target.value,
});
};
const handleTimeChange = useCallback(
@@ -206,6 +205,7 @@ function NamespaceDetails({
logEvent('Infra Monitoring: Namespaces list details time updated', {
namespace: namespace?.namespaceName,
interval,
view: selectedView,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -214,7 +214,7 @@ function NamespaceDetails({
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setLogFilters((prevFilters) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_NAMESPACE_NAME, QUERY_KEYS.K8S_CLUSTER_NAME].includes(
item.key?.key ?? '',
@@ -246,7 +246,7 @@ function NamespaceDetails({
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setTracesFilters((prevFilters) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_NAMESPACE_NAME, QUERY_KEYS.K8S_CLUSTER_NAME].includes(
item.key?.key ?? '',
@@ -326,8 +326,8 @@ function NamespaceDetails({
if (selectedView === VIEW_TYPES.LOGS) {
const filtersWithoutPagination = {
...logFilters,
items: logFilters.items.filter((item) => item.key?.key !== 'id'),
...logAndTracesFilters,
items: logAndTracesFilters.items.filter((item) => item.key?.key !== 'id'),
};
const compositeQuery = {
@@ -361,7 +361,7 @@ function NamespaceDetails({
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: tracesFilters,
filters: logAndTracesFilters,
},
],
},
@@ -527,7 +527,7 @@ function NamespaceDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logFilters}
logFilters={logAndTracesFilters}
selectedInterval={selectedInterval}
queryKey="namespaceLogs"
category={K8sCategory.NAMESPACES}
@@ -540,9 +540,10 @@ function NamespaceDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeTracesFilters={handleChangeTracesFilters}
tracesFilters={tracesFilters}
tracesFilters={logAndTracesFilters}
selectedInterval={selectedInterval}
queryKey="namespaceTraces"
queryKeyFilters={[QUERY_KEYS.K8S_NAMESPACE_NAME]}
/>
)}
{selectedView === VIEW_TYPES.EVENTS && (

View File

@@ -233,6 +233,11 @@ function K8sNodesList({
}
}, [selectedRowData, fetchGroupedByRowData]);
const numberOfPages = useMemo(() => Math.ceil(totalCount / pageSize), [
totalCount,
pageSize,
]);
const handleTableChange: TableProps<K8sNodesRowData>['onChange'] = useCallback(
(
pagination: TablePaginationConfig,
@@ -241,6 +246,11 @@ function K8sNodesList({
): void => {
if (pagination.current) {
setCurrentPage(pagination.current);
logEvent('Infra Monitoring: K8s nodes list page number changed', {
page: pagination.current,
pageSize,
numberOfPages,
});
}
if ('field' in sorter && sorter.order) {
@@ -252,7 +262,7 @@ function K8sNodesList({
setOrderBy(null);
}
},
[],
[numberOfPages, pageSize],
);
const { handleChangeQueryData } = useQueryOperations({
@@ -266,15 +276,13 @@ function K8sNodesList({
handleChangeQueryData('filters', value);
setCurrentPage(1);
logEvent('Infra Monitoring: K8s list filters applied', {
filters: value,
});
logEvent('Infra Monitoring: K8s nodes list filters applied', {});
},
[handleChangeQueryData],
);
useEffect(() => {
logEvent('Infra Monitoring: K8s list page visited', {});
logEvent('Infra Monitoring: K8s nodes list page visited', {});
}, []);
const selectedNodeData = useMemo(() => {
@@ -427,6 +435,8 @@ function K8sNodesList({
setCurrentPage(1);
setGroupBy(groupBy);
setExpandedRowKeys([]);
logEvent('Infra Monitoring: K8s nodes list group by changed', {});
},
[groupByFiltersData],
);
@@ -442,6 +452,16 @@ function K8sNodesList({
}
}, [groupByFiltersData]);
const onPaginationChange = (page: number, pageSize: number): void => {
setCurrentPage(page);
setPageSize(pageSize);
logEvent('Infra Monitoring: K8s nodes list page number changed', {
page,
pageSize,
numberOfPages,
});
};
return (
<div className="k8s-list">
<K8sHeader
@@ -467,10 +487,7 @@ function K8sNodesList({
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: (page, pageSize): void => {
setCurrentPage(page);
setPageSize(pageSize);
},
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{

View File

@@ -141,13 +141,9 @@ function NodeDetails({
[node?.meta.k8s_node_name],
);
const [logFilters, setLogFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [tracesFilters, setTracesFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
IBuilderQuery['filters']
>(initialFilters);
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
initialEventsFilters,
@@ -161,8 +157,7 @@ function NodeDetails({
}, []);
useEffect(() => {
setLogFilters(initialFilters);
setTracesFilters(initialFilters);
setLogAndTracesFilters(initialFilters);
setEventsFilters(initialEventsFilters);
}, [initialFilters, initialEventsFilters]);
@@ -181,6 +176,10 @@ function NodeDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
logEvent('Infra Monitoring: Nodes list details tab changed', {
node: node?.nodeUID,
view: e.target.value,
});
};
const handleTimeChange = useCallback(
@@ -204,6 +203,7 @@ function NodeDetails({
logEvent('Infra Monitoring: Nodes list details time updated', {
node: node?.nodeUID,
interval,
view: selectedView,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -212,7 +212,7 @@ function NodeDetails({
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setLogFilters((prevFilters) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_NODE_NAME, QUERY_KEYS.K8S_CLUSTER_NAME].includes(
item.key?.key ?? '',
@@ -246,7 +246,7 @@ function NodeDetails({
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setTracesFilters((prevFilters) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_NODE_NAME, QUERY_KEYS.K8S_CLUSTER_NAME].includes(
item.key?.key ?? '',
@@ -322,8 +322,8 @@ function NodeDetails({
if (selectedView === VIEW_TYPES.LOGS) {
const filtersWithoutPagination = {
...logFilters,
items: logFilters.items.filter((item) => item.key?.key !== 'id'),
...logAndTracesFilters,
items: logAndTracesFilters.items.filter((item) => item.key?.key !== 'id'),
};
const compositeQuery = {
@@ -357,7 +357,7 @@ function NodeDetails({
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: tracesFilters,
filters: logAndTracesFilters,
},
],
},
@@ -521,7 +521,7 @@ function NodeDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logFilters}
logFilters={logAndTracesFilters}
selectedInterval={selectedInterval}
queryKeyFilters={[QUERY_KEYS.K8S_NODE_NAME, QUERY_KEYS.K8S_CLUSTER_NAME]}
queryKey="nodeLogs"
@@ -534,8 +534,9 @@ function NodeDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeTracesFilters={handleChangeTracesFilters}
tracesFilters={tracesFilters}
tracesFilters={logAndTracesFilters}
selectedInterval={selectedInterval}
queryKeyFilters={[QUERY_KEYS.K8S_NODE_NAME, QUERY_KEYS.K8S_CLUSTER_NAME]}
queryKey="nodeTraces"
/>
)}

View File

@@ -248,6 +248,11 @@ function K8sPodsList({
groupBy,
]);
const numberOfPages = useMemo(() => Math.ceil(totalCount / pageSize), [
totalCount,
pageSize,
]);
const handleTableChange: TableProps<K8sPodsRowData>['onChange'] = useCallback(
(
pagination: TablePaginationConfig,
@@ -256,6 +261,11 @@ function K8sPodsList({
): void => {
if (pagination.current) {
setCurrentPage(pagination.current);
logEvent('Infra Monitoring: K8s pods list page number changed', {
page: pagination.current,
pageSize,
numberOfPages,
});
}
if ('field' in sorter && sorter.order) {
@@ -267,7 +277,7 @@ function K8sPodsList({
setOrderBy(null);
}
},
[],
[numberOfPages, pageSize],
);
const { handleChangeQueryData } = useQueryOperations({
@@ -281,9 +291,7 @@ function K8sPodsList({
handleChangeQueryData('filters', value);
setCurrentPage(1);
logEvent('Infra Monitoring: K8s list filters applied', {
filters: value,
});
logEvent('Infra Monitoring: K8s pods list filters applied', {});
},
[handleChangeQueryData],
);
@@ -308,12 +316,14 @@ function K8sPodsList({
setCurrentPage(1);
setGroupBy(groupBy);
setExpandedRowKeys([]);
logEvent('Infra Monitoring: K8s pods list group by changed', {});
},
[groupByFiltersData],
);
useEffect(() => {
logEvent('Infra Monitoring: K8s list page visited', {});
logEvent('Infra Monitoring: K8s pods list page visited', {});
}, []);
const selectedPodData = useMemo(() => {
@@ -350,7 +360,7 @@ function K8sPodsList({
handleGroupByRowClick(record);
}
logEvent('Infra Monitoring: K8s list item clicked', {
logEvent('Infra Monitoring: K8s pods list item clicked', {
podUID: record.podUID,
});
};
@@ -499,6 +509,16 @@ function K8sPodsList({
);
};
const onPaginationChange = (page: number, pageSize: number): void => {
setCurrentPage(page);
setPageSize(pageSize);
logEvent('Infra Monitoring: K8s pods list page number changed', {
page,
pageSize,
numberOfPages,
});
};
return (
<div className="k8s-list">
<K8sHeader
@@ -530,10 +550,7 @@ function K8sPodsList({
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: (page, pageSize): void => {
setCurrentPage(page);
setPageSize(pageSize);
},
onChange: onPaginationChange,
}}
loading={{
spinning: isFetching || isLoading,

View File

@@ -158,13 +158,9 @@ function PodDetails({
[pod?.meta.k8s_pod_name],
);
const [logFilters, setLogFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [tracesFilters, setTracesFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [logsAndTracesFilters, setLogsAndTracesFilters] = useState<
IBuilderQuery['filters']
>(initialFilters);
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
initialEventsFilters,
@@ -178,8 +174,7 @@ function PodDetails({
}, []);
useEffect(() => {
setLogFilters(initialFilters);
setTracesFilters(initialFilters);
setLogsAndTracesFilters(initialFilters);
setEventsFilters(initialEventsFilters);
}, [initialFilters, initialEventsFilters]);
@@ -198,6 +193,10 @@ function PodDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
logEvent('Infra Monitoring: Pods list details tab changed', {
pod: pod?.podUID,
view: e.target.value,
});
};
const handleTimeChange = useCallback(
@@ -221,6 +220,7 @@ function PodDetails({
logEvent('Infra Monitoring: Pods list details time updated', {
pod: pod?.podUID,
interval,
view: selectedView,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -229,7 +229,7 @@ function PodDetails({
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setLogFilters((prevFilters) => {
setLogsAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[
QUERY_KEYS.K8S_POD_NAME,
@@ -265,7 +265,7 @@ function PodDetails({
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setTracesFilters((prevFilters) => {
setLogsAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[
QUERY_KEYS.K8S_POD_NAME,
@@ -343,8 +343,8 @@ function PodDetails({
if (selectedView === VIEW_TYPES.LOGS) {
const filtersWithoutPagination = {
...logFilters,
items: logFilters.items.filter((item) => item.key?.key !== 'id'),
...logsAndTracesFilters,
items: logsAndTracesFilters.items.filter((item) => item.key?.key !== 'id'),
};
const compositeQuery = {
@@ -378,7 +378,7 @@ function PodDetails({
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: tracesFilters,
filters: logsAndTracesFilters,
},
],
},
@@ -559,7 +559,7 @@ function PodDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logFilters}
logFilters={logsAndTracesFilters}
selectedInterval={selectedInterval}
queryKeyFilters={[
QUERY_KEYS.K8S_POD_NAME,
@@ -576,9 +576,14 @@ function PodDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeTracesFilters={handleChangeTracesFilters}
tracesFilters={tracesFilters}
tracesFilters={logsAndTracesFilters}
selectedInterval={selectedInterval}
queryKey="podTraces"
queryKeyFilters={[
QUERY_KEYS.K8S_POD_NAME,
QUERY_KEYS.K8S_CLUSTER_NAME,
QUERY_KEYS.K8S_NAMESPACE_NAME,
]}
/>
)}

View File

@@ -245,6 +245,11 @@ function K8sStatefulSetsList({
}
}, [selectedRowData, fetchGroupedByRowData]);
const numberOfPages = useMemo(() => Math.ceil(totalCount / pageSize), [
totalCount,
pageSize,
]);
const handleTableChange: TableProps<K8sStatefulSetsRowData>['onChange'] = useCallback(
(
pagination: TablePaginationConfig,
@@ -255,6 +260,11 @@ function K8sStatefulSetsList({
): void => {
if (pagination.current) {
setCurrentPage(pagination.current);
logEvent('Infra Monitoring: K8s statefulSets list page number changed', {
page: pagination.current,
pageSize,
numberOfPages,
});
}
if ('field' in sorter && sorter.order) {
@@ -266,7 +276,7 @@ function K8sStatefulSetsList({
setOrderBy(null);
}
},
[],
[numberOfPages, pageSize],
);
const { handleChangeQueryData } = useQueryOperations({
@@ -280,15 +290,13 @@ function K8sStatefulSetsList({
handleChangeQueryData('filters', value);
setCurrentPage(1);
logEvent('Infra Monitoring: K8s list filters applied', {
filters: value,
});
logEvent('Infra Monitoring: K8s statefulSets list filters applied', {});
},
[handleChangeQueryData],
);
useEffect(() => {
logEvent('Infra Monitoring: K8s list page visited', {});
logEvent('Infra Monitoring: K8s statefulSets list page visited', {});
}, []);
const selectedStatefulSetData = useMemo(() => {
@@ -449,6 +457,8 @@ function K8sStatefulSetsList({
setCurrentPage(1);
setGroupBy(groupBy);
setExpandedRowKeys([]);
logEvent('Infra Monitoring: K8s statefulSets list group by changed', {});
},
[groupByFiltersData],
);
@@ -464,6 +474,16 @@ function K8sStatefulSetsList({
}
}, [groupByFiltersData]);
const onPaginationChange = (page: number, pageSize: number): void => {
setCurrentPage(page);
setPageSize(pageSize);
logEvent('Infra Monitoring: K8s statefulSets list page number changed', {
page,
pageSize,
numberOfPages,
});
};
return (
<div className="k8s-list">
<K8sHeader
@@ -491,10 +511,7 @@ function K8sStatefulSetsList({
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: (page, pageSize): void => {
setCurrentPage(page);
setPageSize(pageSize);
},
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{

View File

@@ -158,13 +158,9 @@ function StatefulSetDetails({
[statefulSet?.meta.k8s_statefulset_name],
);
const [logFilters, setLogFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [tracesFilters, setTracesFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
IBuilderQuery['filters']
>(initialFilters);
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
initialEventsFilters,
@@ -178,8 +174,7 @@ function StatefulSetDetails({
}, []);
useEffect(() => {
setLogFilters(initialFilters);
setTracesFilters(initialFilters);
setLogAndTracesFilters(initialFilters);
setEventsFilters(initialEventsFilters);
}, [initialFilters, initialEventsFilters]);
@@ -198,6 +193,10 @@ function StatefulSetDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
logEvent('Infra Monitoring: StatefulSets list details tab changed', {
statefulSet: statefulSet?.statefulSetName,
view: e.target.value,
});
};
const handleTimeChange = useCallback(
@@ -221,6 +220,7 @@ function StatefulSetDetails({
logEvent('Infra Monitoring: StatefulSets list details time updated', {
statefulSet: statefulSet?.statefulSetName,
interval,
view: selectedView,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -229,7 +229,7 @@ function StatefulSetDetails({
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setLogFilters((prevFilters) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_STATEFUL_SET_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
item.key?.key ?? '',
@@ -265,7 +265,7 @@ function StatefulSetDetails({
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setTracesFilters((prevFilters) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_STATEFUL_SET_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
item.key?.key ?? '',
@@ -345,8 +345,8 @@ function StatefulSetDetails({
if (selectedView === VIEW_TYPES.LOGS) {
const filtersWithoutPagination = {
...logFilters,
items: logFilters.items.filter((item) => item.key?.key !== 'id'),
...logAndTracesFilters,
items: logAndTracesFilters.items.filter((item) => item.key?.key !== 'id'),
};
const compositeQuery = {
@@ -380,7 +380,7 @@ function StatefulSetDetails({
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: tracesFilters,
filters: logAndTracesFilters,
},
],
},
@@ -546,7 +546,7 @@ function StatefulSetDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logFilters}
logFilters={logAndTracesFilters}
selectedInterval={selectedInterval}
queryKey="statefulsetLogs"
category={K8sCategory.STATEFULSETS}
@@ -562,9 +562,13 @@ function StatefulSetDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeTracesFilters={handleChangeTracesFilters}
tracesFilters={tracesFilters}
tracesFilters={logAndTracesFilters}
selectedInterval={selectedInterval}
queryKey="statefulsetTraces"
queryKeyFilters={[
QUERY_KEYS.K8S_STATEFUL_SET_NAME,
QUERY_KEYS.K8S_NAMESPACE_NAME,
]}
/>
)}
{selectedView === VIEW_TYPES.EVENTS && (

View File

@@ -237,6 +237,11 @@ function K8sVolumesList({
}
}, [selectedRowData, fetchGroupedByRowData]);
const numberOfPages = useMemo(() => Math.ceil(totalCount / pageSize), [
totalCount,
pageSize,
]);
const handleTableChange: TableProps<K8sVolumesRowData>['onChange'] = useCallback(
(
pagination: TablePaginationConfig,
@@ -245,6 +250,11 @@ function K8sVolumesList({
): void => {
if (pagination.current) {
setCurrentPage(pagination.current);
logEvent('Infra Monitoring: K8s volumes list page number changed', {
page: pagination.current,
pageSize,
numberOfPages,
});
}
if ('field' in sorter && sorter.order) {
@@ -256,7 +266,7 @@ function K8sVolumesList({
setOrderBy(null);
}
},
[],
[numberOfPages, pageSize],
);
const { handleChangeQueryData } = useQueryOperations({
@@ -270,15 +280,13 @@ function K8sVolumesList({
handleChangeQueryData('filters', value);
setCurrentPage(1);
logEvent('Infra Monitoring: K8s list filters applied', {
filters: value,
});
logEvent('Infra Monitoring: K8s volumes list filters applied', {});
},
[handleChangeQueryData],
);
useEffect(() => {
logEvent('Infra Monitoring: K8s list page visited', {});
logEvent('Infra Monitoring: K8s volumes list page visited', {});
}, []);
const selectedVolumeData = useMemo(() => {
@@ -434,6 +442,8 @@ function K8sVolumesList({
setCurrentPage(1);
setGroupBy(groupBy);
setExpandedRowKeys([]);
logEvent('Infra Monitoring: K8s volumes list group by changed', {});
},
[groupByFiltersData],
);
@@ -449,6 +459,16 @@ function K8sVolumesList({
}
}, [groupByFiltersData]);
const onPaginationChange = (page: number, pageSize: number): void => {
setCurrentPage(page);
setPageSize(pageSize);
logEvent('Infra Monitoring: K8s volumes list page number changed', {
page,
pageSize,
numberOfPages,
});
};
return (
<div className="k8s-list">
<K8sHeader
@@ -476,10 +496,7 @@ function K8sVolumesList({
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: (page, pageSize): void => {
setCurrentPage(page);
setPageSize(pageSize);
},
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{

View File

@@ -0,0 +1,12 @@
import * as Sentry from '@sentry/react';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
function Explorer(): JSX.Element {
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
Explorer
</Sentry.ErrorBoundary>
);
}
export default Explorer;

View File

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

View File

@@ -0,0 +1,12 @@
import * as Sentry from '@sentry/react';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
function Summary(): JSX.Element {
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
Summary
</Sentry.ErrorBoundary>
);
}
export default Summary;

View File

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

View File

@@ -0,0 +1,12 @@
import * as Sentry from '@sentry/react';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
function Views(): JSX.Element {
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
Views
</Sentry.ErrorBoundary>
);
}
export default Views;

View File

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

View File

@@ -0,0 +1,92 @@
// This test suite covers several important scenarios:
// - Empty layout - widget should be placed at origin (0,0)
// - Empty layout with custom dimensions
// - Placing widget next to an existing widget when there's space in the last row
// - Placing widget at bottom when the last row is full
// - Handling multiple rows correctly
// - Handling widgets with different heights
import { placeWidgetAtBottom } from '../utils';
describe('placeWidgetAtBottom', () => {
it('should place widget at (0,0) when layout is empty', () => {
const result = placeWidgetAtBottom('widget1', []);
expect(result).toEqual({
i: 'widget1',
x: 0,
y: 0,
w: 6,
h: 6,
});
});
it('should place widget at (0,0) with custom dimensions when layout is empty', () => {
const result = placeWidgetAtBottom('widget1', [], 4, 8);
expect(result).toEqual({
i: 'widget1',
x: 0,
y: 0,
w: 4,
h: 8,
});
});
it('should place widget next to existing widget in last row if space available', () => {
const existingLayout = [{ i: 'widget1', x: 0, y: 0, w: 6, h: 6 }];
const result = placeWidgetAtBottom('widget2', existingLayout);
expect(result).toEqual({
i: 'widget2',
x: 6,
y: 0,
w: 6,
h: 6,
});
});
it('should place widget at bottom when last row is full', () => {
const existingLayout = [
{ i: 'widget1', x: 0, y: 0, w: 6, h: 6 },
{ i: 'widget2', x: 6, y: 0, w: 6, h: 6 },
];
const result = placeWidgetAtBottom('widget3', existingLayout);
expect(result).toEqual({
i: 'widget3',
x: 0,
y: 6,
w: 6,
h: 6,
});
});
it('should handle multiple rows correctly', () => {
const existingLayout = [
{ i: 'widget1', x: 0, y: 0, w: 6, h: 6 },
{ i: 'widget2', x: 6, y: 0, w: 6, h: 6 },
{ i: 'widget3', x: 0, y: 6, w: 6, h: 6 },
];
const result = placeWidgetAtBottom('widget4', existingLayout);
expect(result).toEqual({
i: 'widget4',
x: 6,
y: 6,
w: 6,
h: 6,
});
});
it('should handle widgets with different heights', () => {
const existingLayout = [
{ i: 'widget1', x: 0, y: 0, w: 6, h: 8 },
{ i: 'widget2', x: 6, y: 0, w: 6, h: 4 },
];
const result = placeWidgetAtBottom('widget3', existingLayout);
// y = 2 here as later the react-grid-layout will add 2px to the y value while adjusting the layout
expect(result).toEqual({
i: 'widget3',
x: 6,
y: 2,
w: 6,
h: 6,
});
});
});

View File

@@ -58,6 +58,7 @@ import {
getDefaultWidgetData,
getIsQueryModified,
handleQueryChange,
placeWidgetAtBottom,
} from './utils';
function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
@@ -363,20 +364,14 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
return;
}
const widgetId = query.get('widgetId');
const widgetId = query.get('widgetId') || '';
let updatedLayout = selectedDashboard.data.layout || [];
if (isNewDashboard) {
updatedLayout = [
{
i: widgetId || '',
w: 6,
x: 0,
h: 6,
y: 0,
},
...updatedLayout,
];
const newLayoutItem = placeWidgetAtBottom(widgetId, updatedLayout);
updatedLayout = [...updatedLayout, newLayoutItem];
}
const dashboard: Dashboard = {
...selectedDashboard,
uuid: selectedDashboard.uuid,

View File

@@ -10,7 +10,8 @@ import {
PANEL_TYPES_INITIAL_QUERY,
} from 'container/NewDashboard/ComponentsSlider/constants';
import { categoryToSupport } from 'container/QueryBuilder/filters/BuilderUnitsFilter/config';
import { cloneDeep, isEmpty, isEqual, set, unset } from 'lodash-es';
import { cloneDeep, defaultTo, isEmpty, isEqual, set, unset } from 'lodash-es';
import { Layout } from 'react-grid-layout';
import { Widgets } from 'types/api/dashboard/getAll';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
@@ -575,3 +576,58 @@ export const unitOptions = (columnUnit: string): DefaultOptionType[] => {
options: getCategorySelectOptionByName(filteredCategory),
}));
};
export const placeWidgetAtBottom = (
widgetId: string,
layout: Layout[],
widgetWidth?: number,
widgetHeight?: number,
): Layout => {
if (layout.length === 0) {
return { i: widgetId, x: 0, y: 0, w: widgetWidth || 6, h: widgetHeight || 6 };
}
// Find the maximum Y coordinate and height
const { maxY } = layout.reduce(
(acc, curr) => ({
maxY: Math.max(acc.maxY, curr.y + curr.h),
}),
{ maxY: 0 },
);
// Check for available space in the last row
const lastRowWidgets = layout.filter((item) => item.y + item.h === maxY);
const occupiedXInLastRow = lastRowWidgets.reduce(
(acc, widget) => acc + widget.w,
0,
);
// If there's space in the last row (total width < 12)
if (occupiedXInLastRow < 12) {
// Find the rightmost X coordinate in the last row
const maxXInLastRow = lastRowWidgets.reduce(
(acc, widget) => Math.max(acc, widget.x + widget.w),
0,
);
// If there's enough space for a 6-width widget
if (maxXInLastRow + defaultTo(widgetWidth, 6) <= 12) {
return {
i: widgetId,
x: maxXInLastRow,
y: maxY - (widgetHeight || 6), // Align with the last row
w: widgetWidth || 6,
h: widgetHeight || 6,
};
}
}
// If no space in last row, place at the bottom
return {
i: widgetId,
x: 0,
y: maxY,
w: widgetWidth || 6,
h: widgetHeight || 6,
};
};

View File

@@ -266,7 +266,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
>
<div>
<div
class="line-clamped-text"
class="line-clamped-wrapper__text"
>
demo-app
</div>
@@ -277,7 +277,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
>
<div>
<div
class="line-clamped-text"
class="line-clamped-wrapper__text"
>
4.35 s
</div>
@@ -292,7 +292,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
>
<div>
<div
class="line-clamped-text"
class="line-clamped-wrapper__text"
>
customer
</div>
@@ -303,7 +303,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
>
<div>
<div
class="line-clamped-text"
class="line-clamped-wrapper__text"
>
431 ms
</div>
@@ -318,7 +318,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
>
<div>
<div
class="line-clamped-text"
class="line-clamped-wrapper__text"
>
mysql
</div>
@@ -329,7 +329,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
>
<div>
<div
class="line-clamped-text"
class="line-clamped-wrapper__text"
>
431 ms
</div>
@@ -344,7 +344,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
>
<div>
<div
class="line-clamped-text"
class="line-clamped-wrapper__text"
>
frontend
</div>
@@ -355,7 +355,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
>
<div>
<div
class="line-clamped-text"
class="line-clamped-wrapper__text"
>
287 ms
</div>
@@ -370,7 +370,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
>
<div>
<div
class="line-clamped-text"
class="line-clamped-wrapper__text"
>
driver
</div>
@@ -381,7 +381,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
>
<div>
<div
class="line-clamped-text"
class="line-clamped-wrapper__text"
>
230 ms
</div>
@@ -396,7 +396,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
>
<div>
<div
class="line-clamped-text"
class="line-clamped-wrapper__text"
>
route
</div>
@@ -407,7 +407,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
>
<div>
<div
class="line-clamped-text"
class="line-clamped-wrapper__text"
>
66.4 ms
</div>
@@ -422,7 +422,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
>
<div>
<div
class="line-clamped-text"
class="line-clamped-wrapper__text"
>
redis
</div>
@@ -433,7 +433,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
>
<div>
<div
class="line-clamped-text"
class="line-clamped-wrapper__text"
>
31.3 ms
</div>

View File

@@ -82,6 +82,13 @@ const menuItems: SidebarItem[] = [
label: 'Logs',
icon: <ScrollText size={16} />,
},
// TODO - Enable this when the metrics explorer feature is read for release
// {
// key: ROUTES.METRICS_EXPLORER,
// label: 'Metrics',
// icon: <BarChart2 size={16} />,
// isNew: true,
// },
{
key: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
label: 'Infra Monitoring',

View File

@@ -219,6 +219,9 @@ export const routesToSkip = [
ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
ROUTES.SOMETHING_WENT_WRONG,
ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES,
ROUTES.METRICS_EXPLORER,
ROUTES.METRICS_EXPLORER_EXPLORER,
ROUTES.METRICS_EXPLORER_VIEWS,
];
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];

View File

@@ -6,6 +6,7 @@ import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import { formUrlParams } from 'container/TraceDetail/utils';
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
import { Link } from 'react-router-dom';
import { ILog } from 'types/api/logs/log';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
@@ -113,7 +114,9 @@ export const getListColumns = (
return (
<BlockLink to={getTraceLink(item)} openInNewTab={false}>
<Typography data-testid={key}>{value}</Typography>
<Typography data-testid={key}>
<LineClampedText text={value} lines={3} />
</Typography>
</BlockLink>
);
},

View File

@@ -1,5 +1,6 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { convertKeysToColumnFields } from 'container/LogsExplorerList/utils';
import { placeWidgetAtBottom } from 'container/NewWidget/utils';
import { Dashboard } from 'types/api/dashboard/getAll';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -22,20 +23,16 @@ export const addEmptyWidgetInDashboardJSONWithQuery = (
...convertKeysToColumnFields(selectedColumns || []),
];
const newLayoutItem = placeWidgetAtBottom(
widgetId,
dashboard?.data?.layout || [],
);
return {
...dashboard,
data: {
...dashboard.data,
layout: [
{
i: widgetId,
w: 6,
x: 0,
h: 6,
y: 0,
},
...(dashboard?.data?.layout || []),
],
layout: [...(dashboard?.data?.layout || []), newLayoutItem],
widgets: [
...(dashboard?.data?.widgets || []),
{

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