Compare commits

...

42 Commits

Author SHA1 Message Date
grandwizard28
d05e279bcd Merge branch 'main' into alertmanager 2025-02-12 22:58:29 +05:30
Vibhu Pandey
bcf7bf38fc feat(alertmanager): add alertmanagertypes (#7101)
add alertmanagertypes
2025-02-12 17:23:18 +00:00
Prashant Shahi
5511c0230b fix(otelcol): rename clickhousemetricswritev2 to signozclickhousemetrics (#7099)
### Summary

- rename exporter `clickhousemetricswritev2` to `signozclickhousemetrics` in collector config

Signed-off-by: Prashant Shahi <prashant@signoz.io>
2025-02-12 19:25:29 +05:30
Nityananda Gohain
7a03a09ac1 fix: add migration and postgres provider (#7089)
* fix: move migrations to bun

* fix: use anonymous structs and move modes to types package

* fix: minor changes after tests

* fix: remove bun relations and add foreign keys

* fix: minor changes

* Update pkg/types/agent.go

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* fix: add migration and postgres provider

* fix: address minor comments

* fix: use bun create index

* fix: add migration

* fix: support for postgres in migrations

* Update pkg/sqlstore/pgstore/provider.go

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update pkg/sqlmigration/001_add_organization.go

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* fix: address comments

* fix: move max connection to base config

* fix: update scope

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-02-12 13:23:40 +00:00
grandwizard28
f1c5d873f7 feat(alertmanager): first attempt at bootstrap 2025-02-12 18:46:28 +05:30
grandwizard28
aec239cc7c feat(alertmanager): first attempt at bootstrap 2025-02-12 18:46:28 +05:30
grandwizard28
59e26652dc feat(alertmanager): first attempt at bootstrap 2025-02-12 18:46:27 +05:30
grandwizard28
e02afc5e97 feat(alertmanager): first attempt at bootstrap 2025-02-12 18:46:27 +05:30
grandwizard28
3eac8ac30b feat(alertmanager): first attempt at bootstrap 2025-02-12 18:46:27 +05:30
grandwizard28
382c4f58e1 refactor(alertmanager): add alertmanager 2025-02-12 18:46:27 +05:30
grandwizard28
73ea632a3f refactor(alertmanager): move to types package 2025-02-12 18:46:27 +05:30
grandwizard28
00fa8810c0 feat(alertmanager): add support for multi org 2025-02-12 18:46:26 +05:30
grandwizard28
6cee330d44 feat(alertmanager): add support for testing receiver 2025-02-12 18:46:26 +05:30
grandwizard28
871c6e642c feat(alertmanager): first iteration of alertmanager 2025-02-12 18:46:25 +05:30
primus-bot[bot]
0d281e694d chore(release): bump to v0.72.0 (#7096)
#### Summary
 - Release SigNoz v0.72.0
 - Bump SigNoz OTel Collector to v0.111.27

 Created by [Primus-Bot](https://github.com/apps/primus-bot)

Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2025-02-12 15:52:55 +05:30
SagarRajput-7
b772a0fb56 feat: changed visible Panel name for Value Panel to Number (#7092)
* feat: changed visible Panel name for Value Panel to Number

* feat: added display values to enum
2025-02-12 11:29:07 +05:30
Nityananda Gohain
962e75c6d4 fix: move migrations to bun (#7055)
* fix: move migrations to bun

* fix: use anonymous structs and move modes to types package

* fix: minor changes after tests

* fix: remove bun relations and add foreign keys

* fix: minor changes

* Update pkg/types/agent.go

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* fix: address minor comments

* fix: use bun create index

* fix: remove extra comma

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-02-12 09:58:49 +05:30
SagarRajput-7
42fad23cb0 feat: added feat to add new panel in a section (#6999)
* feat: added common util and took possible space available in last row in account

* feat: added different test cases

* feat: remove console.log

* feat: added default value to widgetWidth

* feat: added feat to add new panel in a section

* feat: added different test cases
2025-02-11 22:55:42 +05:30
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
191 changed files with 6235 additions and 1079 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:

2
.gitignore vendored
View File

@@ -76,3 +76,5 @@ dist/
# ignore user_scripts that is fetched by init-clickhouse
deploy/common/clickhouse/user_scripts/
# queries.active
queries.active

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.72.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.72.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.27
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.72.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.72.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.27
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml

View File

@@ -66,7 +66,7 @@ exporters:
enabled: true
clickhousemetricswrite/prometheus:
endpoint: tcp://clickhouse:9000/signoz_metrics
clickhousemetricswritev2:
signozclickhousemetrics:
dsn: tcp://clickhouse:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/signoz_logs
@@ -90,11 +90,11 @@ service:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [clickhousemetricswrite, clickhousemetricswritev2]
exporters: [clickhousemetricswrite, signozclickhousemetrics]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [clickhousemetricswrite/prometheus, clickhousemetricswritev2]
exporters: [clickhousemetricswrite/prometheus, signozclickhousemetrics]
logs:
receivers: [otlp]
processors: [batch]

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.72.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.72.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.27}
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.72.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.72.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.27}
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.72.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.72.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.27}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml

View File

@@ -66,7 +66,7 @@ exporters:
enabled: true
clickhousemetricswrite/prometheus:
endpoint: tcp://clickhouse:9000/signoz_metrics
clickhousemetricswritev2:
signozclickhousemetrics:
dsn: tcp://clickhouse:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/signoz_logs
@@ -90,11 +90,11 @@ service:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [clickhousemetricswrite, clickhousemetricswritev2]
exporters: [clickhousemetricswrite, signozclickhousemetrics]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [clickhousemetricswrite/prometheus, clickhousemetricswritev2]
exporters: [clickhousemetricswrite/prometheus, signozclickhousemetrics]
logs:
receivers: [otlp]
processors: [batch]

View File

@@ -20,6 +20,7 @@ import (
basemodel "go.signoz.io/signoz/pkg/query-service/model"
rules "go.signoz.io/signoz/pkg/query-service/rules"
"go.signoz.io/signoz/pkg/query-service/version"
"go.signoz.io/signoz/pkg/signoz"
)
type APIHandlerOptions struct {
@@ -41,6 +42,7 @@ type APIHandlerOptions struct {
FluxInterval time.Duration
UseLogsNewSchema bool
UseTraceNewSchema bool
SigNoz *signoz.SigNoz
}
type APIHandler struct {
@@ -65,6 +67,7 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
FluxInterval: opts.FluxInterval,
UseLogsNewSchema: opts.UseLogsNewSchema,
UseTraceNewSchema: opts.UseTraceNewSchema,
SigNoz: opts.SigNoz,
})
if err != nil {

View File

@@ -269,6 +269,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
GatewayUrl: serverOptions.GatewayUrl,
UseLogsNewSchema: serverOptions.UseLogsNewSchema,
UseTraceNewSchema: serverOptions.UseTraceNewSchema,
SigNoz: serverOptions.SigNoz,
}
apiHandler, err := api.NewAPIHandler(apiOpts)

View File

@@ -7,7 +7,6 @@ import (
"os"
"os/signal"
"strconv"
"syscall"
"time"
"go.opentelemetry.io/otel/sdk/resource"
@@ -87,6 +86,8 @@ func init() {
}
func main() {
ctx := context.Background()
var promConfigPath, skipTopLvlOpsPath string
// disables rule execution but allows change to the rule definition
@@ -191,20 +192,20 @@ func main() {
zap.L().Fatal("Could not start server", zap.Error(err))
}
if err := auth.InitAuthCache(context.Background()); err != nil {
if err := auth.InitAuthCache(ctx); err != nil {
zap.L().Fatal("Failed to initialize auth cache", zap.Error(err))
}
signalsChannel := make(chan os.Signal, 1)
signal.Notify(signalsChannel, os.Interrupt, syscall.SIGTERM)
if err := signoz.Start(ctx); err != nil {
zap.L().Fatal("Failed to start signoz", zap.Error(err))
}
for {
select {
case status := <-server.HealthCheckStatus():
zap.L().Info("Received HealthCheck status: ", zap.Int("status", int(status)))
case <-signalsChannel:
zap.L().Fatal("Received OS Interrupt Signal ... ")
server.Stop()
}
if err := signoz.Wait(ctx); err != nil {
zap.L().Fatal("Failed to wait for signoz", zap.Error(err))
}
server.Stop()
if err := signoz.Stop(ctx); err != nil {
zap.L().Fatal("Failed to stop signoz", zap.Error(err))
}
}

View File

@@ -139,7 +139,7 @@ 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 license status is invalid then default it to basic
if status == LicenseStatusInvalid {
planName = PlanNameBasic
}

View File

@@ -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

@@ -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

@@ -244,7 +244,7 @@ function getColumns(data: RowData[]): TableColumnsType<RowData> {
},
},
{
title: 'THROUGHPUT',
title: 'THROUGHPUT (ops/s)',
dataIndex: 'throughput',
key: 'throughput',
ellipsis: {

View File

@@ -116,9 +116,8 @@
gap: 10px;
.metric-page-grid {
display: grid;
grid-template-columns: 30% 19%;
align-items: flex-start;
display: flex;
flex-direction: row;
gap: 10px;
width: 100%;

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

@@ -407,3 +407,13 @@ export const HAVING_OPERATORS: string[] = [
OPERATORS['<='],
OPERATORS['<'],
];
export enum PanelDisplay {
TIME_SERIES = 'Time Series',
VALUE = 'Number',
TABLE = 'Table',
LIST = 'List',
BAR = 'Bar',
PIE = 'Pie',
HISTOGRAM = 'Histogram',
}

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

@@ -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

@@ -17,6 +17,7 @@ export default function DashboardEmptyState(): JSX.Element {
selectedDashboard,
isDashboardLocked,
handleToggleDashboardSlider,
setSelectedRowWidgetId,
} = useDashboard();
const { user } = useAppContext();
@@ -34,6 +35,7 @@ export default function DashboardEmptyState(): JSX.Element {
const [addPanelPermission] = useComponentPermission(permissions, userRole);
const onEmptyWidgetHandler = useCallback(() => {
setSelectedRowWidgetId(null);
handleToggleDashboardSlider(true);
logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.uuid,

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';
@@ -133,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(
{
@@ -232,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={{

View File

@@ -133,7 +133,8 @@
.menu-content {
.section-1 {
.rename-btn {
.rename-btn,
.new-panel-btn {
display: flex;
align-items: center;
gap: 6px;
@@ -150,6 +151,10 @@
margin-inline-end: 0px;
}
}
.rename-btn {
padding-bottom: 10px;
}
}
.section-2 {

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 {
@@ -64,6 +65,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
isDashboardLocked,
dashboardQueryRangeCalled,
setDashboardQueryRangeCalled,
setSelectedRowWidgetId,
} = useDashboard();
const { data } = selectedDashboard || {};
const { pathname } = useLocation();
@@ -173,6 +175,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
updateDashboardMutation.mutate(updatedDashboard, {
onSuccess: (updatedDashboard) => {
setSelectedRowWidgetId(null);
if (updatedDashboard.payload) {
if (updatedDashboard.payload.data.layout)
setLayouts(sortLayout(updatedDashboard.payload.data.layout));
@@ -190,7 +193,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,7 +1,12 @@
import { Button, Popover } from 'antd';
import { EllipsisIcon, PenLine, X } from 'lucide-react';
import useComponentPermission from 'hooks/useComponentPermission';
import { EllipsisIcon, PenLine, Plus, X } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useState } from 'react';
import { Layout } from 'react-grid-layout';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
interface WidgetRowHeaderProps {
rowWidgetProperties: {
@@ -27,6 +32,23 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
id,
} = props;
const [isRowSettingsOpen, setIsRowSettingsOpen] = useState<boolean>(false);
const {
handleToggleDashboardSlider,
selectedDashboard,
isDashboardLocked,
setSelectedRowWidgetId,
} = useDashboard();
const permissions: ComponentTypes[] = ['add_panel'];
const { user } = useAppContext();
const userRole: ROLES | null =
selectedDashboard?.created_by === user?.email
? (USER_ROLES.AUTHOR as ROLES)
: user.role;
const [addPanelPermission] = useComponentPermission(permissions, userRole);
return (
<Popover
open={isRowSettingsOpen}
@@ -52,6 +74,20 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
Rename
</Button>
</section>
<section className="section-1">
<Button
className="new-panel-btn"
type="text"
disabled={!editWidget && addPanelPermission && !isDashboardLocked}
icon={<Plus size={14} />}
onClick={(): void => {
setSelectedRowWidgetId(id);
handleToggleDashboardSlider(true);
}}
>
New Panel
</Button>
</section>
{!rowWidgetProperties.collapsed && (
<section className="section-2">
<Button

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]);
@@ -217,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 ?? ''),
);
@@ -249,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 ?? ''),
);
@@ -325,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 = {
@@ -360,7 +355,7 @@ function ClusterDetails({
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: tracesFilters,
filters: logsAndTracesFilters,
},
],
},
@@ -524,7 +519,7 @@ function ClusterDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logFilters}
logFilters={logsAndTracesFilters}
selectedInterval={selectedInterval}
queryKey="clusterLogs"
category={K8sCategory.CLUSTERS}
@@ -537,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

@@ -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]);
@@ -231,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 ?? '',
@@ -264,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 ?? '',
@@ -344,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 = {
@@ -379,7 +374,7 @@ function DaemonSetDetails({
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: tracesFilters,
filters: logAndTracesFilters,
},
],
},
@@ -556,7 +551,7 @@ function DaemonSetDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logFilters}
logFilters={logAndTracesFilters}
selectedInterval={selectedInterval}
category={K8sCategory.DAEMONSETS}
queryKey="daemonsetLogs"
@@ -572,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

@@ -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]);
@@ -233,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 ?? '',
@@ -271,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 ?? '',
@@ -355,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 = {
@@ -390,7 +385,7 @@ function DeploymentDetails({
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: tracesFilters,
filters: logAndTracesFilters,
},
],
},
@@ -567,7 +562,7 @@ function DeploymentDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logFilters}
logFilters={logAndTracesFilters}
selectedInterval={selectedInterval}
queryKeyFilters={[
QUERY_KEYS.K8S_DEPLOYMENT_NAME,
@@ -583,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

@@ -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

@@ -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]);
@@ -228,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', {
@@ -261,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 ?? '',
),
);
@@ -277,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),
};
@@ -335,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 = {
@@ -370,7 +364,7 @@ function JobDetails({
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: tracesFilters,
filters: logAndTracesFilters,
},
],
},
@@ -536,7 +530,7 @@ function JobDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logFilters}
logFilters={logAndTracesFilters}
selectedInterval={selectedInterval}
category={K8sCategory.JOBS}
queryKey="jobLogs"
@@ -552,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

@@ -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]);
@@ -219,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 ?? '',
@@ -251,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 ?? '',
@@ -331,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 = {
@@ -366,7 +361,7 @@ function NamespaceDetails({
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: tracesFilters,
filters: logAndTracesFilters,
},
],
},
@@ -532,7 +527,7 @@ function NamespaceDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logFilters}
logFilters={logAndTracesFilters}
selectedInterval={selectedInterval}
queryKey="namespaceLogs"
category={K8sCategory.NAMESPACES}
@@ -545,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

@@ -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]);
@@ -217,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 ?? '',
@@ -251,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 ?? '',
@@ -327,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 = {
@@ -362,7 +357,7 @@ function NodeDetails({
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: tracesFilters,
filters: logAndTracesFilters,
},
],
},
@@ -526,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"
@@ -539,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

@@ -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]);
@@ -234,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,
@@ -270,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,
@@ -348,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 = {
@@ -383,7 +378,7 @@ function PodDetails({
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: tracesFilters,
filters: logsAndTracesFilters,
},
],
},
@@ -564,7 +559,7 @@ function PodDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logFilters}
logFilters={logsAndTracesFilters}
selectedInterval={selectedInterval}
queryKeyFilters={[
QUERY_KEYS.K8S_POD_NAME,
@@ -581,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

@@ -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]);
@@ -234,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 ?? '',
@@ -270,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 ?? '',
@@ -350,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 = {
@@ -385,7 +380,7 @@ function StatefulSetDetails({
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: tracesFilters,
filters: logAndTracesFilters,
},
],
},
@@ -551,7 +546,7 @@ function StatefulSetDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logFilters}
logFilters={logAndTracesFilters}
selectedInterval={selectedInterval}
queryKey="statefulsetLogs"
category={K8sCategory.STATEFULSETS}
@@ -567,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

@@ -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

@@ -1,5 +1,5 @@
import { Color } from '@signozhq/design-tokens';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
import {
BarChart3,
LineChart,
@@ -13,37 +13,37 @@ const Items: ItemsProps[] = [
{
name: PANEL_TYPES.TIME_SERIES,
icon: <LineChart size={16} color={Color.BG_ROBIN_400} />,
display: 'Time Series',
display: PanelDisplay.TIME_SERIES,
},
{
name: PANEL_TYPES.VALUE,
icon: <SigmaSquare size={16} color={Color.BG_ROBIN_400} />,
display: 'Value',
display: PanelDisplay.VALUE,
},
{
name: PANEL_TYPES.TABLE,
icon: <Table size={16} color={Color.BG_ROBIN_400} />,
display: 'Table',
display: PanelDisplay.TABLE,
},
{
name: PANEL_TYPES.LIST,
icon: <List size={16} color={Color.BG_ROBIN_400} />,
display: 'List',
display: PanelDisplay.LIST,
},
{
name: PANEL_TYPES.BAR,
icon: <BarChart3 size={16} color={Color.BG_ROBIN_400} />,
display: 'Bar',
display: PanelDisplay.BAR,
},
{
name: PANEL_TYPES.PIE,
icon: <PieChart size={16} color={Color.BG_ROBIN_400} />,
display: 'Pie',
display: PanelDisplay.PIE,
},
{
name: PANEL_TYPES.HISTOGRAM,
icon: <BarChart3 size={16} color={Color.BG_ROBIN_400} />,
display: 'Histogram',
display: PanelDisplay.HISTOGRAM,
},
];

View File

@@ -100,6 +100,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
listSortOrder,
setSelectedDashboard,
handleToggleDashboardSlider,
setSelectedRowWidgetId,
handleDashboardLockToggle,
} = useDashboard();
@@ -157,6 +158,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const [addPanelPermission] = useComponentPermission(permissions, userRole);
const onEmptyWidgetHandler = useCallback(() => {
setSelectedRowWidgetId(null);
handleToggleDashboardSlider(true);
logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.uuid,

View File

@@ -4,7 +4,7 @@ import './RightContainer.styles.scss';
import { Input, InputNumber, Select, Space, Switch, Typography } from 'antd';
import TimePreference from 'components/TimePreferenceDropDown';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
import GraphTypes, {
ItemsProps,
} from 'container/NewDashboard/ComponentsSlider/menuItems';
@@ -212,7 +212,8 @@ function RightContainer({
defaultValue={yAxisUnit}
onSelect={setYAxisUnit}
fieldLabel={
selectedGraphType === 'Value' || selectedGraphType === 'Pie'
selectedGraphType === PanelDisplay.VALUE ||
selectedGraphType === PanelDisplay.PIE
? 'Unit'
: 'Y Axis Unit'
}

View File

@@ -0,0 +1,218 @@
// 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, placeWidgetBetweenRows } 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,
});
});
});
describe('placeWidgetBetweenRows', () => {
it('should return single widget layout when layout is empty', () => {
const result = placeWidgetBetweenRows('widget1', [], 'currentRow');
expect(result).toEqual([
{
i: 'widget1',
x: 0,
y: 0,
w: 6,
h: 6,
},
]);
});
it('should place widget at the end of the layout when no nextRowId is provided', () => {
const existingLayout = [
{ i: 'widget1', x: 0, y: 0, w: 6, h: 6 },
{ i: 'widget2', x: 6, y: 0, w: 6, h: 6 },
];
const result = placeWidgetBetweenRows('widget3', existingLayout, 'widget2');
expect(result).toEqual([
{ 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 },
]);
});
it('should place widget between current and next row', () => {
const existingLayout = [
{
h: 1,
i: "'widget1'",
maxH: 1,
minH: 1,
minW: 12,
moved: false,
static: false,
w: 12,
x: 0,
y: 0,
},
{ i: 'widget2', x: 6, y: 0, w: 6, h: 6 },
{
h: 1,
i: 'widget3',
maxH: 1,
minH: 1,
minW: 12,
moved: false,
static: false,
w: 12,
x: 0,
y: 7,
},
];
const result = placeWidgetBetweenRows(
'widget4',
existingLayout,
'widget1',
'widget3',
);
expect(result).toEqual([
{
h: 1,
i: "'widget1'",
maxH: 1,
minH: 1,
minW: 12,
moved: false,
static: false,
w: 12,
x: 0,
y: 0,
},
{
h: 6,
i: 'widget2',
w: 6,
x: 6,
y: 0,
},
{
h: 6,
i: 'widget4',
w: 6,
x: 0,
y: 6,
},
{
h: 1,
i: 'widget3',
maxH: 1,
minH: 1,
minW: 12,
moved: false,
static: false,
w: 12,
x: 0,
y: 7,
},
]);
});
it('should respect custom widget dimensions', () => {
const existingLayout = [{ i: 'widget1', x: 0, y: 0, w: 12, h: 4 }];
const result = placeWidgetBetweenRows(
'widget2',
existingLayout,
'widget1',
null,
8,
3,
);
expect(result).toEqual([
{ i: 'widget1', x: 0, y: 0, w: 12, h: 4 },
{ i: 'widget2', x: 0, y: 4, w: 8, h: 3 },
]);
});
});

View File

@@ -7,7 +7,11 @@ import logEvent from 'api/common/logEvent';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import {
initialQueriesMap,
PANEL_GROUP_TYPES,
PANEL_TYPES,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { DashboardShortcuts } from 'constants/shortcuts/DashboardShortcuts';
import { DEFAULT_BUCKET_COUNT } from 'container/PanelWrapper/constants';
@@ -20,7 +24,7 @@ import useUrlQuery from 'hooks/useUrlQuery';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import history from 'lib/history';
import { defaultTo, isUndefined } from 'lodash-es';
import { defaultTo, isEmpty, isUndefined } from 'lodash-es';
import { Check, X } from 'lucide-react';
import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
import { useAppContext } from 'providers/App/App';
@@ -58,6 +62,8 @@ import {
getDefaultWidgetData,
getIsQueryModified,
handleQueryChange,
placeWidgetAtBottom,
placeWidgetBetweenRows,
} from './utils';
function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
@@ -65,6 +71,8 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
selectedDashboard,
setSelectedDashboard,
setToScrollWidgetId,
selectedRowWidgetId,
setSelectedRowWidgetId,
} = useDashboard();
const { t } = useTranslation(['dashboard']);
@@ -363,20 +371,36 @@ 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,
];
if (isNewDashboard && isEmpty(selectedRowWidgetId)) {
const newLayoutItem = placeWidgetAtBottom(widgetId, updatedLayout);
updatedLayout = [...updatedLayout, newLayoutItem];
}
if (isNewDashboard && selectedRowWidgetId) {
// Find the next row by looking through remaining layout items
const currentIndex = updatedLayout.findIndex(
(e) => e.i === selectedRowWidgetId,
);
const nextRowIndex = updatedLayout.findIndex(
(item, index) =>
index > currentIndex &&
widgets?.find((w) => w.id === item.i)?.panelTypes ===
PANEL_GROUP_TYPES.ROW,
);
const nextRowId = nextRowIndex !== -1 ? updatedLayout[nextRowIndex].i : null;
const newLayoutItem = placeWidgetBetweenRows(
widgetId,
updatedLayout,
selectedRowWidgetId,
nextRowId,
);
updatedLayout = newLayoutItem;
}
const dashboard: Dashboard = {
...selectedDashboard,
uuid: selectedDashboard.uuid,
@@ -442,6 +466,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
updateDashboardMutation.mutateAsync(dashboard, {
onSuccess: () => {
setSelectedRowWidgetId(null);
setSelectedDashboard(dashboard);
setToScrollWidgetId(selectedWidget?.id || '');
history.push({
@@ -454,16 +479,19 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
selectedDashboard,
query,
isNewDashboard,
preWidgets,
selectedRowWidgetId,
afterWidgets,
selectedWidget,
selectedTime.enum,
graphType,
currentQuery,
afterWidgets,
preWidgets,
updateDashboardMutation,
handleError,
widgets,
setSelectedDashboard,
setToScrollWidgetId,
setSelectedRowWidgetId,
dashboardId,
]);

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,98 @@ 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,
};
};
export const placeWidgetBetweenRows = (
widgetId: string,
layout: Layout[],
_currentRowId: string,
nextRowId?: string | null,
widgetWidth?: number,
widgetHeight?: number,
): Layout[] => {
if (layout.length === 0) {
return [
{
i: widgetId,
x: 0,
y: 0,
w: widgetWidth || 6,
h: widgetHeight || 6,
},
];
}
const nextRowIndex = nextRowId
? layout.findIndex((item) => item.i === nextRowId)
: -1;
// slice the layout from current row to next row
const sectionWidgets =
nextRowIndex === -1 ? layout : layout.slice(0, nextRowIndex);
const newWidgetLayout = placeWidgetAtBottom(
widgetId,
sectionWidgets,
widgetWidth,
widgetHeight,
);
const remainingWidgets = nextRowIndex === -1 ? [] : layout.slice(nextRowIndex);
// add new layout in between the sectionWidgets and the rest of the layout
return [...sectionWidgets, newWidgetLayout, ...remainingWidgets];
};

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 || []),
{

View File

@@ -0,0 +1,17 @@
import { getConnectionParams } from 'api/integrations/aws';
import { AxiosError } from 'axios';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { ConnectionParams } from 'types/api/integrations/aws';
export function useConnectionParams({
options,
}: {
options?: UseQueryOptions<ConnectionParams, AxiosError>;
}): UseQueryResult<ConnectionParams, AxiosError> {
return useQuery<ConnectionParams, AxiosError>(
[REACT_QUERY_KEY.AWS_GET_CONNECTION_PARAMS],
getConnectionParams,
options,
);
}

View File

@@ -4,6 +4,7 @@ import {
ActiveViewEnum,
ModalStateEnum,
} from 'container/CloudIntegrationPage/HeroSection/types';
import useAxiosError from 'hooks/useAxiosError';
import {
Dispatch,
SetStateAction,
@@ -13,11 +14,13 @@ import {
useState,
} from 'react';
import {
ConnectionParams,
ConnectionUrlResponse,
GenerateConnectionUrlPayload,
} from 'types/api/integrations/aws';
import { regions } from 'utils/regions';
import { useConnectionParams } from './useConnectionParams';
import { useGenerateConnectionUrl } from './useGenerateConnectionUrl';
interface UseIntegrationModalProps {
@@ -44,6 +47,8 @@ interface UseIntegrationModal {
accountId?: string;
selectedDeploymentRegion: string | undefined;
handleRegionChange: (value: string) => void;
connectionParams?: ConnectionParams;
isConnectionParamsLoading: boolean;
}
export function useIntegrationModal({
@@ -102,6 +107,12 @@ export function useIntegrationModal({
isLoading: isGeneratingUrl,
} = useGenerateConnectionUrl();
const handleError = useAxiosError();
const {
data: connectionParams,
isLoading: isConnectionParamsLoading,
} = useConnectionParams({ options: { onError: handleError } });
const handleGenerateUrl = useCallback(
(payload: GenerateConnectionUrlPayload): void => {
generateUrl(payload, {
@@ -126,6 +137,10 @@ export function useIntegrationModal({
const payload: GenerateConnectionUrlPayload = {
agent_config: {
region: values.region,
ingestion_url: connectionParams?.ingestion_url || values.ingestion_url,
ingestion_key: connectionParams?.ingestion_key || values.ingestion_key,
signoz_api_url: connectionParams?.signoz_api_url || values.signoz_api_url,
signoz_api_key: connectionParams?.signoz_api_key || values.signoz_api_key,
},
account_config: {
regions: includeAllRegions ? ['all'] : selectedRegions,
@@ -138,7 +153,13 @@ export function useIntegrationModal({
} finally {
setIsLoading(false);
}
}, [form, includeAllRegions, selectedRegions, handleGenerateUrl]);
}, [
form,
includeAllRegions,
selectedRegions,
handleGenerateUrl,
connectionParams,
]);
return {
form,
@@ -160,5 +181,7 @@ export function useIntegrationModal({
setModalState,
selectedDeploymentRegion,
handleRegionChange,
connectionParams,
isConnectionParamsLoading,
};
}

View File

@@ -5,11 +5,11 @@ import { useQuery, UseQueryResult } from 'react-query';
export function useServiceDetails(
serviceId: string,
accountId?: string,
cloudAccountId?: string,
): UseQueryResult<ServiceData> {
return useQuery(
[REACT_QUERY_KEY.AWS_SERVICE_DETAILS, serviceId, accountId],
() => getServiceDetails(serviceId, accountId),
[REACT_QUERY_KEY.AWS_SERVICE_DETAILS, serviceId, cloudAccountId],
() => getServiceDetails(serviceId, cloudAccountId),
{
enabled: !!serviceId,
},

View File

@@ -98,7 +98,23 @@
}
})();
</script>
<script>
const CUSTOMERIO_ID = '<%= htmlWebpackPlugin.options.CUSTOMERIO_ID %>';
const CUSTOMERIO_SITE_ID = '<%= htmlWebpackPlugin.options.CUSTOMERIO_SITE_ID %>';
!function(){var i="cioanalytics", analytics=(window[i]=window[i]||[]);if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","reset","group","track","ready","alias","debug","page","once","off","on","addSourceMiddleware","addIntegrationMiddleware","setAnonymousId","addDestinationMiddleware"];analytics.factory=function(e){return function(){var t=Array.prototype.slice.call(arguments);t.unshift(e);analytics.push(t);return analytics}};for(var e=0;e<analytics.methods.length;e++){var key=analytics.methods[e];analytics[key]=analytics.factory(key)}analytics.load=function(key,e){var t=document.createElement("script");t.type="text/javascript";t.async=!0;t.setAttribute('data-global-customerio-analytics-key', i);t.src="https://cdp.customer.io/v1/analytics-js/snippet/" + key + "/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(t,n);analytics._writeKey=key;analytics._loadOptions=e};analytics.SNIPPET_VERSION="4.15.3";
analytics.load(
CUSTOMERIO_ID,
{
"integrations": {
"Customer.io In-App Plugin": {
siteId: CUSTOMERIO_SITE_ID
}
}
}
);
analytics.page();
}}();
</script>
<script>
//Set your SEGMENT_ID
const SEGMENT_ID = '<%= htmlWebpackPlugin.options.SEGMENT_ID %>';

View File

@@ -116,8 +116,8 @@ export default function OverviewRightPanelGraph({
isDarkMode: false,
drawStyle: 'bars',
colorMapping: {
False: Color.BG_CHERRY_500,
True: Color.BG_FOREST_400,
True: Color.BG_CHERRY_500,
False: Color.BG_FOREST_400,
None: Color.BG_SLATE_200,
'Request Rate': Color.BG_ROBIN_500,
},

View File

@@ -4,8 +4,10 @@ import './Integrations.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, List, Typography } from 'antd';
import { FeatureKeys } from 'constants/features';
import { useGetAllIntegrations } from 'hooks/Integrations/useGetAllIntegrations';
import { MoveUpRight, RotateCw } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { Dispatch, SetStateAction, useMemo } from 'react';
import { IntegrationsProps } from 'types/api/integrations/types';
import { isCloudUser } from 'utils/app';
@@ -14,9 +16,15 @@ import { handleContactSupport, INTEGRATION_TYPES } from './utils';
export const AWS_INTEGRATION = {
id: INTEGRATION_TYPES.AWS_INTEGRATION,
title: 'AWS Web Services',
title: 'Amazon Web Services',
description: 'One-click setup for AWS monitoring with SigNoz',
author: {
name: 'SigNoz',
email: 'integrations@signoz.io',
homepage: 'https://signoz.io',
},
icon: `Logos/aws-dark.svg`,
is_installed: false,
is_new: true,
};
@@ -38,13 +46,20 @@ function IntegrationsList(props: IntegrationsListProps): JSX.Element {
refetch,
} = useGetAllIntegrations();
const { featureFlags } = useAppContext();
const isAwsIntegrationEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.AWS_INTEGRATION)
?.active || false;
const filteredDataList = useMemo(() => {
let integrationsList: IntegrationsProps[] = [];
// Temporarily hide AWS Integration from the list, uncomment when the BE changes are finalized
// if (AWS_INTEGRATION.title.toLowerCase().includes(searchTerm.toLowerCase())) {
// integrationsList.push(AWS_INTEGRATION);
// }
if (
isAwsIntegrationEnabled &&
AWS_INTEGRATION.title.toLowerCase().includes(searchTerm.toLowerCase())
) {
integrationsList.push(AWS_INTEGRATION);
}
// Add other integrations
if (data?.data.data.integrations) {
@@ -57,7 +72,7 @@ function IntegrationsList(props: IntegrationsListProps): JSX.Element {
}
return integrationsList;
}, [data?.data.data.integrations, searchTerm]);
}, [data?.data.data.integrations, isAwsIntegrationEnabled, searchTerm]);
const loading = isLoading || isFetching || isRefetching;

View File

@@ -19,6 +19,8 @@ export const INTEGRATION_TELEMETRY_EVENTS = {
'Integrations Detail Page: Clicked remove Integration button for integration',
INTEGRATIONS_DETAIL_CONFIGURE_INSTRUCTION:
'Integrations Detail Page: Navigated to configure an integration',
AWS_INTEGRATION_ACCOUNT_REMOVED:
'AWS Integration Detail page: Clicked remove Integration button for integration',
};
export const INTEGRATION_TYPES = {

View File

@@ -0,0 +1,52 @@
.metrics-explorer-page {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
.ant-tabs {
height: 100%;
}
.ant-tabs-nav {
padding: 0 8px;
margin-bottom: 0px;
&::before {
border-bottom: 1px solid var(--bg-slate-400) !important;
}
}
.ant-tabs-content-holder {
display: flex;
.ant-tabs-content {
flex: 1;
display: flex;
flex-direction: column;
.ant-tabs-tabpane {
flex: 1;
display: flex;
flex-direction: column;
}
}
}
.tab-item {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
}
.lightMode {
.metrics-explorer-page {
.ant-tabs-nav {
&::before {
border-bottom: 1px solid var(--bg-vanilla-300) !important;
}
}
}
}

View File

@@ -0,0 +1,22 @@
import './MetricsExplorerPage.styles.scss';
import RouteTab from 'components/RouteTab';
import { TabRoutes } from 'components/RouteTab/types';
import history from 'lib/history';
import { useLocation } from 'react-use';
import { Explorer, Summary, Views } from './constants';
function MetricsExplorerPage(): JSX.Element {
const { pathname } = useLocation();
const routes: TabRoutes[] = [Summary, Explorer, Views];
return (
<div className="metrics-explorer-page">
<RouteTab routes={routes} activeKey={pathname} history={history} />
</div>
);
}
export default MetricsExplorerPage;

View File

@@ -0,0 +1,39 @@
import { TabRoutes } from 'components/RouteTab/types';
import ROUTES from 'constants/routes';
import ExplorerPage from 'container/MetricsExplorer/Explorer';
import SummaryPage from 'container/MetricsExplorer/Summary';
import ViewsPage from 'container/MetricsExplorer/Views';
import { BarChart2, Compass, TowerControl } from 'lucide-react';
export const Summary: TabRoutes = {
Component: SummaryPage,
name: (
<div className="tab-item">
<BarChart2 size={16} /> Summary
</div>
),
route: ROUTES.METRICS_EXPLORER,
key: ROUTES.METRICS_EXPLORER,
};
export const Explorer: TabRoutes = {
Component: ExplorerPage,
name: (
<div className="tab-item">
<Compass size={16} /> Explorer
</div>
),
route: ROUTES.METRICS_EXPLORER_EXPLORER,
key: ROUTES.METRICS_EXPLORER_EXPLORER,
};
export const Views: TabRoutes = {
Component: ViewsPage,
name: (
<div className="tab-item">
<TowerControl size={16} /> Views
</div>
),
route: ROUTES.METRICS_EXPLORER_VIEWS,
key: ROUTES.METRICS_EXPLORER_VIEWS,
};

View File

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

View File

@@ -1,6 +1,10 @@
.line-clamped-text {
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
.line-clamped-wrapper {
max-height: 300px;
overflow-y: auto;
&__text {
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
}

View File

@@ -35,7 +35,7 @@ function LineClampedText({
const content = (
<div
ref={textRef}
className="line-clamped-text"
className="line-clamped-wrapper__text"
style={{
WebkitLineClamp: lines,
}}
@@ -46,7 +46,9 @@ function LineClampedText({
return isOverflowing ? (
<Tooltip
title={text}
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
title={<div onClick={(e): void => e.stopPropagation()}>{text}</div>}
overlayClassName="line-clamped-wrapper"
// eslint-disable-next-line react/jsx-props-no-spreading
{...tooltipProps}
>

View File

@@ -71,6 +71,8 @@ const DashboardContext = createContext<IDashboardContext>({
setVariablesToGetUpdated: () => {},
dashboardQueryRangeCalled: false,
setDashboardQueryRangeCalled: () => {},
selectedRowWidgetId: '',
setSelectedRowWidgetId: () => {},
});
interface Props {
@@ -87,6 +89,10 @@ export function DashboardProvider({
const [isDashboardLocked, setIsDashboardLocked] = useState<boolean>(false);
const [selectedRowWidgetId, setSelectedRowWidgetId] = useState<string | null>(
null,
);
const [
dashboardQueryRangeCalled,
setDashboardQueryRangeCalled,
@@ -416,6 +422,8 @@ export function DashboardProvider({
setVariablesToGetUpdated,
dashboardQueryRangeCalled,
setDashboardQueryRangeCalled,
selectedRowWidgetId,
setSelectedRowWidgetId,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[
@@ -435,6 +443,8 @@ export function DashboardProvider({
setVariablesToGetUpdated,
dashboardQueryRangeCalled,
setDashboardQueryRangeCalled,
selectedRowWidgetId,
setSelectedRowWidgetId,
],
);

View File

@@ -45,4 +45,6 @@ export interface IDashboardContext {
setVariablesToGetUpdated: React.Dispatch<React.SetStateAction<string[]>>;
dashboardQueryRangeCalled: boolean;
setDashboardQueryRangeCalled: (value: boolean) => void;
selectedRowWidgetId: string | null;
setSelectedRowWidgetId: React.Dispatch<React.SetStateAction<string | null>>;
}

View File

@@ -1,9 +1,16 @@
import { CloudAccount } from 'container/CloudIntegrationPage/ServicesSection/types';
export interface ConnectionParams {
ingestion_url?: string;
ingestion_key?: string;
signoz_api_url?: string;
signoz_api_key?: string;
}
export interface GenerateConnectionUrlPayload {
agent_config: {
region: string;
};
} & ConnectionParams;
account_config: {
regions: string[];
};

View File

@@ -5,6 +5,7 @@ declare global {
store: Store;
Intercom: any;
analytics: Record<string, any>;
cioanalytics: Record<string, any>;
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__: typeof compose;
}
}

View File

@@ -110,4 +110,7 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
INFRASTRUCTURE_MONITORING_KUBERNETES: ['ADMIN', 'EDITOR', 'VIEWER'],
MESSAGING_QUEUES_CELERY_TASK: ['ADMIN', 'EDITOR', 'VIEWER'],
MESSAGING_QUEUES_OVERVIEW: ['ADMIN', 'EDITOR', 'VIEWER'],
METRICS_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
METRICS_EXPLORER_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
METRICS_EXPLORER_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'],
};

View File

@@ -22,6 +22,8 @@ const plugins = [
template: 'src/index.html.ejs',
INTERCOM_APP_ID: process.env.INTERCOM_APP_ID,
SEGMENT_ID: process.env.SEGMENT_ID,
CUSTOMERIO_SITE_ID: process.env.CUSTOMERIO_SITE_ID,
CUSTOMERIO_ID: process.env.CUSTOMERIO_ID,
POSTHOG_KEY: process.env.POSTHOG_KEY,
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
SENTRY_ORG: process.env.SENTRY_ORG,
@@ -40,6 +42,8 @@ const plugins = [
WEBSOCKET_API_ENDPOINT: process.env.WEBSOCKET_API_ENDPOINT,
INTERCOM_APP_ID: process.env.INTERCOM_APP_ID,
SEGMENT_ID: process.env.SEGMENT_ID,
CUSTOMERIO_SITE_ID: process.env.CUSTOMERIO_SITE_ID,
CUSTOMERIO_ID: process.env.CUSTOMERIO_ID,
POSTHOG_KEY: process.env.POSTHOG_KEY,
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
SENTRY_ORG: process.env.SENTRY_ORG,

View File

@@ -27,6 +27,8 @@ const plugins = [
template: 'src/index.html.ejs',
INTERCOM_APP_ID: process.env.INTERCOM_APP_ID,
SEGMENT_ID: process.env.SEGMENT_ID,
CUSTOMERIO_SITE_ID: process.env.CUSTOMERIO_SITE_ID,
CUSTOMERIO_ID: process.env.CUSTOMERIO_ID,
POSTHOG_KEY: process.env.POSTHOG_KEY,
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
SENTRY_ORG: process.env.SENTRY_ORG,
@@ -50,6 +52,8 @@ const plugins = [
WEBSOCKET_API_ENDPOINT: process.env.WEBSOCKET_API_ENDPOINT,
INTERCOM_APP_ID: process.env.INTERCOM_APP_ID,
SEGMENT_ID: process.env.SEGMENT_ID,
CUSTOMERIO_SITE_ID: process.env.CUSTOMERIO_SITE_ID,
CUSTOMERIO_ID: process.env.CUSTOMERIO_ID,
POSTHOG_KEY: process.env.POSTHOG_KEY,
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
SENTRY_ORG: process.env.SENTRY_ORG,

View File

@@ -6706,6 +6706,11 @@ core-js@^2.4.0:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
core-js@^3.38.1:
version "3.40.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.40.0.tgz#2773f6b06877d8eda102fc42f828176437062476"
integrity sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==
core-util-is@~1.0.0:
version "1.0.3"
resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz"
@@ -13118,14 +13123,15 @@ postcss@8.4.38, postcss@^8.0.0, postcss@^8.1.1, postcss@^8.3.7, postcss@^8.4.21,
picocolors "^1.0.0"
source-map-js "^1.2.0"
posthog-js@1.160.3:
version "1.160.3"
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.160.3.tgz#17c8af4c9ffa2d795d925ca1e7146e61cd5ccabd"
integrity sha512-mGvxOIlWPtdPx8EI0MQ81wNKlnH2K0n4RqwQOl044b34BCKiFVzZ7Hc7geMuZNaRAvCi5/5zyGeWHcAYZQxiMQ==
posthog-js@1.215.5:
version "1.215.5"
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.215.5.tgz#0512cfdb919da960b809c5f686ca147f9c2922ba"
integrity sha512-42lPur+xvkp51pHz2FQ7Y+KHdZ4eQSNIhUO03EECvc2UsmnM0FiVTrF1bcLwHZMaWfR26gOeuOAAjTUV9tinJg==
dependencies:
core-js "^3.38.1"
fflate "^0.4.8"
preact "^10.19.3"
web-vitals "^4.0.1"
web-vitals "^4.2.0"
preact@^10.19.3:
version "10.22.0"
@@ -16465,10 +16471,10 @@ web-vitals@^0.2.4:
resolved "https://registry.npmjs.org/web-vitals/-/web-vitals-0.2.4.tgz"
integrity sha512-6BjspCO9VriYy12z356nL6JBS0GYeEcA457YyRzD+dD6XYCQ75NKhcOHUMHentOE7OcVCIXXDvOm0jKFfQG2Gg==
web-vitals@^4.0.1:
version "4.2.0"
resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-4.2.0.tgz#008949ab79717a68ccaaa3c4371cbc7bbbd78a92"
integrity sha512-ohj72kbtVWCpKYMxcbJ+xaOBV3En76hW47j52dG+tEGG36LZQgfFw5yHl9xyjmosy3XUMn8d/GBUAy4YPM839w==
web-vitals@^4.2.0:
version "4.2.4"
resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-4.2.4.tgz#1d20bc8590a37769bd0902b289550936069184b7"
integrity sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==
web-worker@^1.2.0:
version "1.2.0"

65
go.mod
View File

@@ -5,6 +5,7 @@ go 1.22.0
toolchain go1.22.7
require (
dario.cat/mergo v1.0.1
github.com/ClickHouse/clickhouse-go/v2 v2.25.0
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
@@ -18,6 +19,7 @@ require (
github.com/dustin/go-humanize v1.0.1
github.com/go-co-op/gocron v1.30.1
github.com/go-kit/log v0.2.1
github.com/go-openapi/runtime v0.28.0
github.com/go-redis/redis/v8 v8.11.5
github.com/go-redis/redismock/v8 v8.11.5
github.com/go-viper/mapstructure/v2 v2.1.0
@@ -27,6 +29,7 @@ require (
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.0
github.com/gosimple/slug v1.10.0
github.com/jackc/pgx/v5 v5.7.2
github.com/jmoiron/sqlx v1.3.4
github.com/json-iterator/go v1.1.12
github.com/knadh/koanf v1.5.0
@@ -39,8 +42,9 @@ require (
github.com/opentracing/opentracing-go v1.2.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.20.4
github.com/prometheus/common v0.60.0
github.com/prometheus/alertmanager v0.28.0
github.com/prometheus/client_golang v1.20.5
github.com/prometheus/common v0.61.0
github.com/prometheus/prometheus v2.5.0+incompatible
github.com/rs/cors v1.11.1
github.com/russellhaering/gosaml2 v0.9.0
@@ -51,8 +55,9 @@ require (
github.com/soheilhy/cmux v0.1.5
github.com/srikanthccv/ClickHouse-go-mock v0.9.0
github.com/stretchr/testify v1.10.0
github.com/uptrace/bun v1.2.8
github.com/uptrace/bun/dialect/sqlitedialect v1.2.8
github.com/uptrace/bun v1.2.9
github.com/uptrace/bun/dialect/pgdialect v1.2.9
github.com/uptrace/bun/dialect/sqlitedialect v1.2.9
go.opentelemetry.io/collector/confmap v1.17.0
go.opentelemetry.io/collector/pdata v1.17.0
go.opentelemetry.io/collector/processor v0.111.0
@@ -66,10 +71,10 @@ require (
golang.org/x/crypto v0.31.0
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/net v0.33.0
golang.org/x/oauth2 v0.23.0
golang.org/x/oauth2 v0.24.0
golang.org/x/text v0.21.0
google.golang.org/grpc v1.67.1
google.golang.org/protobuf v1.34.2
google.golang.org/protobuf v1.35.2
gopkg.in/segmentio/analytics-go.v3 v3.1.0
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
@@ -85,16 +90,21 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
github.com/ClickHouse/ch-go v0.61.5 // indirect
github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30 // indirect
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go v1.55.5 // indirect
github.com/beevik/etree v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/coder/quartz v0.1.2 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dennwc/varint v1.0.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.8.0 // indirect
github.com/edsrzf/mmap-go v1.1.0 // indirect
github.com/elastic/lunes v0.1.0 // indirect
@@ -110,19 +120,43 @@ require (
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-openapi/analysis v0.23.0 // indirect
github.com/go-openapi/errors v0.22.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/loads v0.22.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/strfmt v0.23.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-openapi/validate v0.24.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/gosimple/unidecode v1.0.0 // indirect
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-msgpack/v2 v2.1.1 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/memberlist v0.5.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jessevdk/go-flags v1.6.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect
@@ -136,6 +170,10 @@ require (
github.com/lufia/plan9stats v0.0.0-20240408141607-282e7b5d6b74 // indirect
github.com/magefile/mage v1.15.0 // indirect
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/mdlayher/vsock v1.2.1 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
@@ -144,6 +182,7 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.111.0 // indirect
github.com/paulmach/orb v0.11.1 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
@@ -152,13 +191,17 @@ require (
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common/sigv4 v0.1.0 // indirect
github.com/prometheus/exporter-toolkit v0.13.2 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/segmentio/backo-go v1.0.1 // indirect
github.com/shirou/gopsutil/v4 v4.24.9 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/smarty/assertions v1.15.0 // indirect
github.com/spf13/cobra v1.8.1 // indirect
@@ -166,12 +209,14 @@ require (
github.com/tklauser/go-sysconf v0.3.13 // indirect
github.com/tklauser/numcpus v0.7.0 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/trivago/tgo v1.0.7 // indirect
github.com/valyala/fastjson v1.6.4 // indirect
github.com/vjeantet/grok v1.0.1 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.mongodb.org/mongo-driver v1.17.1 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/collector v0.111.0 // indirect
@@ -220,12 +265,16 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.30.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/time v0.6.0 // indirect
golang.org/x/tools v0.28.0 // indirect
gonum.org/v1/gonum v0.15.1 // indirect
google.golang.org/api v0.199.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
gopkg.in/telebot.v3 v3.3.8 // indirect
k8s.io/client-go v0.31.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect

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