Compare commits

..

42 Commits

Author SHA1 Message Date
Vikrant Gupta
0401c27dbc chore: remove the base URL from the ws config (#5708) 2024-08-16 17:41:19 +05:30
Vibhu Pandey
57c45f22d6 feat(premium-support): add premium-support feature (#5707) 2024-08-16 16:50:40 +05:30
Srikanth Chekuri
29f1883edd chore: add telemetry for dashboards/alerts with tsv2 table (#5677) 2024-08-16 16:16:12 +05:30
Shaheer Kochai
5d903b5487 NOOP to Count in alert creation from logs (#5464)
* fix: change NOOP to count on creating alert from Logs and traces

* fix: change 'count' back to 'noop' in Traces page, in case there is a single query

* fix: handle the query modification in useGetCompositeQueryParam instead of Filter

* chore: use values StringOperators enum instead of hard coded strings

* Revert "fix: handle the query modification in useGetCompositeQueryParam instead of Filter"

This reverts commit 5bb837ec27.

* Revert "fix: change 'count' back to 'noop' in Traces page, in case there is a single query"

This reverts commit 5e506dbd35.
2024-08-16 14:12:22 +04:30
Vikrant Gupta
1b9683d699 feat: client changes for query stats (#5706)
* feat: changes for the query stats websockets

* chore: remove unwanted files

* fix: work on random id rather than hash

* fix: improve the icons and design

* feat: webpack and docker file changes

* fix: test cases

* chore: format the units

* chore: address review comments

* chore: update the id to uuid package

* fix: build issues

* chore: remove docker file changes

* chore: remove docker file changes
2024-08-16 15:07:06 +05:30
Vikrant Gupta
65280cf4e1 feat: support for attribute key suggestions and example queries in logs explorer query builder (#5608)
* feat: qb-suggestions base setup

* chore: make the dropdown a little similar to the designs

* chore: move out example queries from og and add to renderer

* chore: added the handlers for example queries

* chore: hide the example queries as soon as the user starts typing

* feat: handle changes for cancel query

* chore: remove stupid concept of option group

* chore: show only first 3 items and add option to show all filters

* chore: minor css changes and remove transitions

* feat: integrate suggestions api and control re-renders

* feat: added keyboard shortcuts for the dropdown

* fix: design cleanups and touchups

* fix: build issues and tests

* chore: extra safety check for base64 and fix tests

* fix: qs doesn't handle padding in base64 strings, added client logic

* chore: some code comments

* chore: some code comments

* chore: increase the height of the bar when key is set

* chore: address minor designs

* chore: update the keyboard shortcut to cmd+/

* feat: correct the option render for logs for tooltip

* chore: search bar to not loose focus on btn click

* fix: update the spacing and icon for search bar

* chore: address review comments
2024-08-16 13:11:39 +05:30
Yunus M
1308f0f15f feat: move chat support behind paywall (#5673)
* feat: move chat support behind paywall

* feat: wire up chat support paywall

* feat: move chat support code from app layout to separate component

* feat: add log events
2024-08-14 20:50:35 +05:30
Raj Kamal Singh
6c634b99d0 Feat: QS: query range progress api (#5671)
* feat: get query progress tracker started

* feat: flesh out query progress test some more and get first few assertions passing

* chore: flesh out query tracker tests and impl some more

* feat: add impl for QueryTracker.Subscribe

* feat: send latest update if available on subscription

* feat: broadcast query progress to all subscribers on update

* feat: finish plumbing query tracker happy path

* feat: finish with v0 impl for query progress tracker

* chore: some cleanup of query progress tracker

* feat: hook up query progress tracking for queryRangeV3

* feat: impl for query progress websocket API handler

* feat: implement Hijacker iface for loggingResponseWriter for websocket upgrades

* chore: some cleanup to query progress websocket API handler

* chore: some cleanup

* chore: move query progress impl into its own subpackage

* chore: separate in-memory tracker impl from interface

* chore: some more cleanup of in memory tracker

* chore: some more cleanup of query progress tracker

* chore: some final cleanups
2024-08-14 19:53:36 +05:30
Yunus M
9856335840 fix: hide beta icon in sidebar collapsed view (#5693) 2024-08-14 18:01:29 +05:30
Yunus M
e85b405396 fix: dashboards listing and details page css fixes (#5672)
* fix: dashboards listing and details page css fixes

* chore: remove commented code
2024-08-14 12:22:15 +05:30
Vishal Sharma
e2e965bc7f chore: zeus features (#5686)
* chore: zeus features

* chore: add tests and improve logging
2024-08-14 10:10:33 +05:30
Srikanth Chekuri
7811fdd17a fix: nil pointer dereference for empty payload (#5680) 2024-08-12 21:43:38 +05:30
Yunus M
0dca1237b9 feat: add beta tag for service map and light mode (#5674)
* feat: add beta tag for service map and light mode

* chore: update test case
2024-08-12 16:41:47 +05:30
Vikrant Gupta
f3d73f6d44 feat: use selected columns as pinned attributes (#5601)
* feat: use selected columns as pinned attributes

* chore: handle nested json structs

* chore: refactor and fix build issues

* feat: handle changes for dashboard list panel

* chore: remove console logs
2024-08-12 16:34:43 +05:30
Vikrant Gupta
187927403a fix: clean out the panel type change attribute dependency (#5648)
* fix: clean out the panel type change attribute dependency

* fix: clean out the updater function as well

* fix: issue with rendering list panel as first and then moving around
2024-08-11 16:46:18 +05:30
Srikanth Chekuri
0157b47424 chore: add empty labels to response (#5668) 2024-08-09 18:19:15 +05:30
Srikanth Chekuri
156905afc7 fix: send alert default annotations for missing data alert (#5315) 2024-08-09 15:31:39 +05:30
CheetoDa
a4878f6430 chore: updated k8s instructions (#5665)
Co-authored-by: Prashant Shahi <prashant@signoz.io>
2024-08-09 14:47:31 +05:30
Srikanth Chekuri
4489df6f39 feat: add runningDiff function (#5667) 2024-08-09 14:04:29 +05:30
Srikanth Chekuri
06c075466b chore: add eval tests for threshold rule (#5398) 2024-08-09 12:34:40 +05:30
Srikanth Chekuri
62be3e7c13 chore: enable caching for all panel types in metrics v4 (#5651) 2024-08-09 12:32:11 +05:30
Srikanth Chekuri
bb84960442 chore: add alerts state history query service impl (#5255) 2024-08-09 12:11:05 +05:30
Kobe Cai
52199361d5 chore: fix error message typo on update log field api (#5660) 2024-08-09 10:08:40 +05:30
Srikanth Chekuri
f031845300 chore: make eval delay configurable (#5649) 2024-08-08 17:34:25 +05:30
Shaheer Kochai
6f73bb6eca feat: login flow tests (#5540) 2024-08-08 09:18:23 +04:30
Shaheer Kochai
fe398bcc49 feat: my settings page tests (#5499)
* feat: my settings page tests

* chore: improve mysettings test names

* chore: remove commented code and console.log

* chore: add missing parentheses
2024-08-08 09:17:38 +04:30
Shaheer Kochai
6781c29082 feat: tests for alert channels settings (#5563)
* feat: tests for alert channels settings

* chore: overall improvements to alert channel settings tests

* chore: improve alerts dummy data
2024-08-08 08:52:15 +04:30
Raj Kamal Singh
eb146491f2 Feat: QS: query builder suggestions api v0 (#5634)
* chore: stash initial work with API signature

* chore: put together setup for integration testing filter suggestions

* feat: filter suggestions: suggest attribs using existing autocomplete logic

* chore: filter suggestions test: add expectation for example queries

* feat: filter suggestions: default suggestions when data yet to be received

* feat: finish plumbing basic example queries

* chore: add test for filter suggestions with an existing query

* feat: filter suggestions: don't suggest attribs already included in existing filter

* chore: generate example queries by including existing filter first

* chore: upgrade ClickHouse-go-mock

* chore: some cleanup of reader.GetQBFilterSuggestionsForLogs

* chore: some cleanup of filter suggestion tests

* chore: some cleanup to http handler and request parsing logic for filter suggestions

* chore: remove expectation that attrib suggestions won't contain attribs already used in filter
2024-08-08 09:27:41 +05:30
Vishal Sharma
ae325ec1ca chore: handle traceID search 404 performance issue (#5654)
By setting max and min timestamp filter same as current timestamp when traceIDs are not found
2024-08-08 08:32:11 +05:30
Srikanth Chekuri
fd6f0574f5 fix: make timeshift work with cache (#5646) 2024-08-06 20:24:06 +05:30
rahulkeswani101
b819a90c80 feat: added links to integrations page in onboarding section (#5606)
* feat: added links to integrations page in onboarding section

* feat: removed box shadow for button

* refactor: added routes object to navigate to integrations page

* feat: added new styles for data source name
2024-08-06 19:18:48 +05:30
rahulkeswani101
a6848f6abd fix: added card to show message for deleted alert id (#5565)
* fix: added card to show message for deleted alert id

* refactor: added new constants for handling error message when alert is deleted

* refactor: added error response to error message field

* refactor: removed console statement

* refactor: renamed the variables
2024-08-06 19:09:49 +05:30
Shivanshu Raj Shrivastava
abe65975c9 Merge pull request #5542 from shivanshuraj1333/api-kafka
messaging queue, consumer lag APIs
2024-08-06 17:58:01 +05:30
Shivanshu Raj Shrivastava
5cedd57aa2 Merge branch 'develop' into api-kafka 2024-08-06 16:30:30 +05:30
rahulkeswani101
80a7b9d16d feat: added link for dashboard name (#5544)
* feat: added link for dashboard name

* refactor: added getLink function to get the link of dashboard details page

* refactor: changed the color for dashboard name

* refactor: updated the classname for dashboard name

* fix: update css tokens and light mode design

---------

Co-authored-by: vikrantgupta25 <vikrant.thomso@gmail.com>
2024-08-06 13:33:51 +05:30
Shivanshu Raj Shrivastava
9f7b2542ec Merge branch 'develop' into api-kafka 2024-08-06 10:13:28 +05:30
Srikanth Chekuri
4a4c9f26a2 chore: add Reduce To for pie chart (#5629) 2024-08-05 20:53:52 +05:30
shivanshu
c957c0f757 chore: addressing review comments 2024-08-05 18:14:40 +05:30
shivanshu
3ff0aa4b4b chore: consumer group filtering 2024-08-05 18:09:58 +05:30
shivanshu
063c9adba6 chore: pr-reviews 2024-08-05 18:09:58 +05:30
shivanshu
5c3ce146fa chore: add queue type 2024-08-05 18:09:58 +05:30
shivanshu
481bb6e8b8 feat: add consumer and producer APIs 2024-08-05 18:09:58 +05:30
178 changed files with 8821 additions and 900 deletions

View File

@@ -1,17 +1,48 @@
package api
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
"go.signoz.io/signoz/ee/query-service/constants"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.uber.org/zap"
)
func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
featureSet, err := ah.FF().GetFeatureFlags()
if err != nil {
ah.HandleError(w, err, http.StatusInternalServerError)
return
}
if constants.FetchFeatures == "true" {
zap.L().Debug("fetching license")
license, err := ah.LM().GetRepo().GetActiveLicense(ctx)
if err != nil {
zap.L().Error("failed to fetch license", zap.Error(err))
} else if license == nil {
zap.L().Debug("no active license found")
} else {
licenseKey := license.Key
zap.L().Debug("fetching zeus features")
zeusFeatures, err := fetchZeusFeatures(constants.ZeusFeaturesURL, licenseKey)
if err == nil {
zap.L().Debug("fetched zeus features", zap.Any("features", zeusFeatures))
// merge featureSet and zeusFeatures in featureSet with higher priority to zeusFeatures
featureSet = MergeFeatureSets(zeusFeatures, featureSet)
} else {
zap.L().Error("failed to fetch zeus features", zap.Error(err))
}
}
}
if ah.opts.PreferSpanMetrics {
for idx := range featureSet {
feature := &featureSet[idx]
@@ -20,5 +51,96 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
}
}
}
ah.Respond(w, featureSet)
}
// fetchZeusFeatures makes an HTTP GET request to the /zeusFeatures endpoint
// and returns the FeatureSet.
func fetchZeusFeatures(url, licenseKey string) (basemodel.FeatureSet, error) {
// Check if the URL is empty
if url == "" {
return nil, fmt.Errorf("url is empty")
}
// Check if the licenseKey is empty
if licenseKey == "" {
return nil, fmt.Errorf("licenseKey is empty")
}
// Creating an HTTP client with a timeout for better control
client := &http.Client{
Timeout: 10 * time.Second,
}
// Creating a new GET request
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Setting the custom header
req.Header.Set("X-Signoz-Cloud-Api-Key", licenseKey)
// Making the GET request
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to make GET request: %w", err)
}
defer func() {
if resp != nil {
resp.Body.Close()
}
}()
// Check for non-OK status code
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d %s", errors.New("received non-OK HTTP status code"), resp.StatusCode, http.StatusText(resp.StatusCode))
}
// Reading and decoding the response body
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
var zeusResponse ZeusFeaturesResponse
if err := json.Unmarshal(body, &zeusResponse); err != nil {
return nil, fmt.Errorf("%w: %v", errors.New("failed to decode response body"), err)
}
if zeusResponse.Status != "success" {
return nil, fmt.Errorf("%w: %s", errors.New("failed to fetch zeus features"), zeusResponse.Status)
}
return zeusResponse.Data, nil
}
type ZeusFeaturesResponse struct {
Status string `json:"status"`
Data basemodel.FeatureSet `json:"data"`
}
// MergeFeatureSets merges two FeatureSet arrays with precedence to zeusFeatures.
func MergeFeatureSets(zeusFeatures, internalFeatures basemodel.FeatureSet) basemodel.FeatureSet {
// Create a map to store the merged features
featureMap := make(map[string]basemodel.Feature)
// Add all features from the otherFeatures set to the map
for _, feature := range internalFeatures {
featureMap[feature.Name] = feature
}
// Add all features from the zeusFeatures set to the map
// If a feature already exists (i.e., same name), the zeusFeature will overwrite it
for _, feature := range zeusFeatures {
featureMap[feature.Name] = feature
}
// Convert the map back to a FeatureSet slice
var mergedFeatures basemodel.FeatureSet
for _, feature := range featureMap {
mergedFeatures = append(mergedFeatures, feature)
}
return mergedFeatures
}

View File

@@ -0,0 +1,88 @@
package api
import (
"testing"
"github.com/stretchr/testify/assert"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
)
func TestMergeFeatureSets(t *testing.T) {
tests := []struct {
name string
zeusFeatures basemodel.FeatureSet
internalFeatures basemodel.FeatureSet
expected basemodel.FeatureSet
}{
{
name: "empty zeusFeatures and internalFeatures",
zeusFeatures: basemodel.FeatureSet{},
internalFeatures: basemodel.FeatureSet{},
expected: basemodel.FeatureSet{},
},
{
name: "non-empty zeusFeatures and empty internalFeatures",
zeusFeatures: basemodel.FeatureSet{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
internalFeatures: basemodel.FeatureSet{},
expected: basemodel.FeatureSet{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
},
{
name: "empty zeusFeatures and non-empty internalFeatures",
zeusFeatures: basemodel.FeatureSet{},
internalFeatures: basemodel.FeatureSet{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
expected: basemodel.FeatureSet{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
},
{
name: "non-empty zeusFeatures and non-empty internalFeatures with no conflicts",
zeusFeatures: basemodel.FeatureSet{
{Name: "Feature1", Active: true},
{Name: "Feature3", Active: false},
},
internalFeatures: basemodel.FeatureSet{
{Name: "Feature2", Active: true},
{Name: "Feature4", Active: false},
},
expected: basemodel.FeatureSet{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: true},
{Name: "Feature3", Active: false},
{Name: "Feature4", Active: false},
},
},
{
name: "non-empty zeusFeatures and non-empty internalFeatures with conflicts",
zeusFeatures: basemodel.FeatureSet{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
internalFeatures: basemodel.FeatureSet{
{Name: "Feature1", Active: false},
{Name: "Feature3", Active: true},
},
expected: basemodel.FeatureSet{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
{Name: "Feature3", Active: true},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual := MergeFeatureSets(test.zeusFeatures, test.internalFeatures)
assert.ElementsMatch(t, test.expected, actual)
})
}
}

View File

@@ -1,6 +1,7 @@
package app
import (
"bufio"
"bytes"
"context"
"encoding/json"
@@ -28,6 +29,7 @@ import (
"go.signoz.io/signoz/ee/query-service/integrations/gateway"
"go.signoz.io/signoz/ee/query-service/interfaces"
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/migrate"
"go.signoz.io/signoz/pkg/query-service/model"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
@@ -179,6 +181,13 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
return nil, err
}
go func() {
err = migrate.ClickHouseMigrate(reader.GetConn(), serverOptions.Cluster)
if err != nil {
zap.L().Error("error while running clickhouse migrations", zap.Error(err))
}
}()
// initiate opamp
_, err = opAmpModel.InitDB(localDB)
if err != nil {
@@ -309,7 +318,7 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
// ip here for alert manager
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "DELETE", "POST", "PUT", "PATCH"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "SIGNOZ-API-KEY"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "SIGNOZ-API-KEY", "X-SIGNOZ-QUERY-ID", "Sec-WebSocket-Protocol"},
})
handler := c.Handler(r)
@@ -354,7 +363,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
c := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "DELETE", "POST", "PUT", "PATCH", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "cache-control"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "cache-control", "X-SIGNOZ-QUERY-ID", "Sec-WebSocket-Protocol"},
})
handler := c.Handler(r)
@@ -410,6 +419,15 @@ func (lrw *loggingResponseWriter) Flush() {
lrw.ResponseWriter.(http.Flusher).Flush()
}
// Support websockets
func (lrw *loggingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
h, ok := lrw.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, errors.New("hijack not supported")
}
return h.Hijack()
}
func extractQueryRangeData(path string, r *http.Request) (map[string]interface{}, bool) {
pathToExtractBodyFromV3 := "/api/v3/query_range"
pathToExtractBodyFromV4 := "/api/v4/query_range"
@@ -728,6 +746,7 @@ func makeRulesManager(
DisableRules: disableRules,
FeatureFlags: fm,
Reader: ch,
EvalDelay: baseconst.GetEvalDelay(),
}
// create Manager

View File

@@ -13,6 +13,8 @@ var LicenseAPIKey = GetOrDefaultEnv("SIGNOZ_LICENSE_API_KEY", "")
var SaasSegmentKey = GetOrDefaultEnv("SIGNOZ_SAAS_SEGMENT_KEY", "")
var SpanRenderLimitStr = GetOrDefaultEnv("SPAN_RENDER_LIMIT", "2500")
var MaxSpansInTraceStr = GetOrDefaultEnv("MAX_SPANS_IN_TRACE", "250000")
var FetchFeatures = GetOrDefaultEnv("FETCH_FEATURES", "false")
var ZeusFeaturesURL = GetOrDefaultEnv("ZEUS_FEATURES_URL", "ZeusFeaturesURL")
func GetOrDefaultEnv(key string, fallback string) string {
v := os.Getenv(key)

View File

@@ -147,7 +147,7 @@ func (lm *Manager) GetLicenses(ctx context.Context) (response []model.License, a
for _, l := range licenses {
l.ParsePlan()
if l.Key == lm.activeLicense.Key {
if lm.activeLicense != nil && l.Key == lm.activeLicense.Key {
l.IsCurrent = true
}

View File

@@ -12,6 +12,7 @@ const DisableUpsell = "DISABLE_UPSELL"
const Onboarding = "ONBOARDING"
const ChatSupport = "CHAT_SUPPORT"
const Gateway = "GATEWAY"
const PremiumSupport = "PREMIUM_SUPPORT"
var BasicPlan = basemodel.FeatureSet{
basemodel.Feature{
@@ -119,6 +120,13 @@ var BasicPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: PremiumSupport,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}
var ProPlan = basemodel.FeatureSet{
@@ -220,6 +228,13 @@ var ProPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: PremiumSupport,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}
var EnterprisePlan = basemodel.FeatureSet{
@@ -335,4 +350,11 @@ var EnterprisePlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: PremiumSupport,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}

View File

@@ -0,0 +1 @@
<svg width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#prefix__clip0_4062_7291)" stroke-width="1.167" stroke-linecap="round" stroke-linejoin="round"><path d="M7 12.833A5.833 5.833 0 107 1.167a5.833 5.833 0 000 11.666z" fill="#E5484D" stroke="#E5484D"/><path d="M8.75 5.25l-3.5 3.5M5.25 5.25l3.5 3.5" stroke="#121317"/></g><defs><clipPath id="prefix__clip0_4062_7291"><path fill="#fff" d="M0 0h14v14H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 467 B

View File

@@ -66,6 +66,14 @@ function App(): JSX.Element {
allFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)?.active ||
false;
const isPremiumSupportEnabled =
allFlags.find((flag) => flag.name === FeatureKeys.PREMIUM_SUPPORT)?.active ||
false;
const showAddCreditCardModal =
!isPremiumSupportEnabled &&
!licenseData?.payload?.trialConvertedToSubscription;
dispatch({
type: UPDATE_FEATURE_FLAG_RESPONSE,
payload: {
@@ -82,7 +90,7 @@ function App(): JSX.Element {
setRoutes(newRoutes);
}
if (isLoggedInState && isChatSupportEnabled) {
if (isLoggedInState && isChatSupportEnabled && !showAddCreditCardModal) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.Intercom('boot', {

View File

@@ -9,9 +9,9 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
// making the error status code as standard Error Status Code
const statusCode = response.status as ErrorStatusCode;
if (statusCode >= 400 && statusCode < 500) {
const { data } = response as AxiosResponse;
const { data } = response as AxiosResponse;
if (statusCode >= 400 && statusCode < 500) {
if (statusCode === 404) {
return {
statusCode,
@@ -34,12 +34,11 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
body: JSON.stringify((response.data as any).data),
};
}
return {
statusCode,
payload: null,
error: 'Something went wrong',
message: null,
message: data?.error,
};
}
if (request) {

View File

@@ -0,0 +1,32 @@
import getLocalStorageApi from 'api/browser/localstorage/get';
import { ENVIRONMENT } from 'constants/env';
import { LOCALSTORAGE } from 'constants/localStorage';
export interface WsDataEvent {
read_rows: number;
read_bytes: number;
elapsed_ms: number;
}
interface GetQueryStatsProps {
queryId: string;
setData: React.Dispatch<React.SetStateAction<WsDataEvent | undefined>>;
}
export function getQueryStats(props: GetQueryStatsProps): void {
const { queryId, setData } = props;
const token = getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN) || '';
const socket = new WebSocket(
`${ENVIRONMENT.wsURL}api/v3/query_progress?q=${queryId}`,
token,
);
socket.addEventListener('message', (event) => {
try {
const parsedData = JSON.parse(event?.data);
setData(parsedData);
} catch {
setData(event?.data);
}
});
}

View File

@@ -12,10 +12,13 @@ export const getMetricsQueryRange = async (
props: QueryRangePayload,
version: string,
signal: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => {
try {
if (version && version === ENTITY_VERSION_V4) {
const response = await ApiV4Instance.post('/query_range', props, { signal });
const response = await ApiV4Instance.post('/query_range', props, {
signal,
});
return {
statusCode: 200,
@@ -26,7 +29,10 @@ export const getMetricsQueryRange = async (
};
}
const response = await ApiV3Instance.post('/query_range', props, { signal });
const response = await ApiV3Instance.post('/query_range', props, {
signal,
headers,
});
return {
statusCode: 200,

View File

@@ -0,0 +1,63 @@
import { ApiV3Instance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError, AxiosResponse } from 'axios';
import { baseAutoCompleteIdKeysOrder } from 'constants/queryBuilder';
import { encode } from 'js-base64';
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
import createQueryParams from 'lib/createQueryParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
IGetAttributeSuggestionsPayload,
IGetAttributeSuggestionsSuccessResponse,
} from 'types/api/queryBuilder/getAttributeSuggestions';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
export const getAttributeSuggestions = async ({
searchText,
dataSource,
filters,
}: IGetAttributeSuggestionsPayload): Promise<
SuccessResponse<IGetAttributeSuggestionsSuccessResponse> | ErrorResponse
> => {
try {
let base64EncodedFiltersString;
try {
// the replace function is to remove the padding at the end of base64 encoded string which is auto added to make it a multiple of 4
// why ? because the current working of qs doesn't work well with padding
base64EncodedFiltersString = encode(JSON.stringify(filters)).replace(
/=+$/,
'',
);
} catch {
// default base64 encoded string for empty filters object
base64EncodedFiltersString = 'eyJpdGVtcyI6W10sIm9wIjoiQU5EIn0';
}
const response: AxiosResponse<{
data: IGetAttributeSuggestionsSuccessResponse;
}> = await ApiV3Instance.get(
`/filter_suggestions?${createQueryParams({
searchText,
dataSource,
existingFilter: base64EncodedFiltersString,
})}`,
);
const payload: BaseAutocompleteData[] =
response.data.data.attributes?.map(({ id: _, ...item }) => ({
...item,
id: createIdFromObjectFields(item, baseAutoCompleteIdKeysOrder),
})) || [];
return {
statusCode: 200,
error: null,
message: response.statusText,
payload: {
attributes: payload,
example_queries: response.data.data.example_queries,
},
};
} catch (e) {
return ErrorResponseHandler(e as AxiosError);
}
};

View File

@@ -0,0 +1,136 @@
import { Button, Modal, Typography } from 'antd';
import updateCreditCardApi from 'api/billing/checkout';
import logEvent from 'api/common/logEvent';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import { CreditCard, X } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useMutation } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { License } from 'types/api/licenses/def';
export default function ChatSupportGateway(): JSX.Element {
const { notifications } = useNotifications();
const [activeLicense, setActiveLicense] = useState<License | null>(null);
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] = useState(
false,
);
const { data: licenseData, isFetching } = useLicense();
useEffect(() => {
const activeValidLicense =
licenseData?.payload?.licenses?.find(
(license) => license.isCurrent === true,
) || null;
setActiveLicense(activeValidLicense);
}, [licenseData, isFetching]);
const handleBillingOnSuccess = (
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
): void => {
if (data?.payload?.redirectURL) {
const newTab = document.createElement('a');
newTab.href = data.payload.redirectURL;
newTab.target = '_blank';
newTab.rel = 'noopener noreferrer';
newTab.click();
}
};
const handleBillingOnError = (): void => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
};
const { mutate: updateCreditCard, isLoading: isLoadingBilling } = useMutation(
updateCreditCardApi,
{
onSuccess: (data) => {
handleBillingOnSuccess(data);
},
onError: handleBillingOnError,
},
);
const handleAddCreditCard = (): void => {
logEvent('Add Credit card modal: Clicked', {
source: `intercom icon`,
page: '',
});
updateCreditCard({
licenseKey: activeLicense?.key || '',
successURL: window.location.href,
cancelURL: window.location.href,
});
};
return (
<>
<div className="chat-support-gateway">
<Button
className="chat-support-gateway-btn"
onClick={(): void => {
logEvent('Disabled Chat Support: Clicked', {
source: `intercom icon`,
page: '',
});
setIsAddCreditCardModalOpen(true);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 28 32"
className="chat-support-gateway-btn-icon"
>
<path d="M28 32s-4.714-1.855-8.527-3.34H3.437C1.54 28.66 0 27.026 0 25.013V3.644C0 1.633 1.54 0 3.437 0h21.125c1.898 0 3.437 1.632 3.437 3.645v18.404H28V32zm-4.139-11.982a.88.88 0 00-1.292-.105c-.03.026-3.015 2.681-8.57 2.681-5.486 0-8.517-2.636-8.571-2.684a.88.88 0 00-1.29.107 1.01 1.01 0 00-.219.708.992.992 0 00.318.664c.142.128 3.537 3.15 9.762 3.15 6.226 0 9.621-3.022 9.763-3.15a.992.992 0 00.317-.664 1.01 1.01 0 00-.218-.707z" />
</svg>
</Button>
</div>
{/* Add Credit Card Modal */}
<Modal
className="add-credit-card-modal"
title={<span className="title">Add Credit Card for Chat Support</span>}
open={isAddCreditCardModalOpen}
closable
onCancel={(): void => setIsAddCreditCardModalOpen(false)}
destroyOnClose
footer={[
<Button
key="cancel"
onClick={(): void => setIsAddCreditCardModalOpen(false)}
className="cancel-btn"
icon={<X size={16} />}
>
Cancel
</Button>,
<Button
key="submit"
type="primary"
icon={<CreditCard size={16} />}
size="middle"
loading={isLoadingBilling}
disabled={isLoadingBilling}
onClick={handleAddCreditCard}
className="add-credit-card-btn"
>
Add Credit Card
</Button>,
]}
>
<Typography.Text className="add-credit-card-text">
You&apos;re currently on <span className="highlight-text">Trial plan</span>
. Add a credit card to access SigNoz chat support to your workspace.
</Typography.Text>
</Modal>
</>
);
}

View File

@@ -0,0 +1,184 @@
import './LaunchChatSupport.styles.scss';
import { Button, Modal, Tooltip, Typography } from 'antd';
import updateCreditCardApi from 'api/billing/checkout';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { FeatureKeys } from 'constants/features';
import useFeatureFlags from 'hooks/useFeatureFlag';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import { defaultTo } from 'lodash-es';
import { CreditCard, HelpCircle, X } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useMutation } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { License } from 'types/api/licenses/def';
import { isCloudUser } from 'utils/app';
export interface LaunchChatSupportProps {
eventName: string;
attributes: Record<string, unknown>;
message?: string;
buttonText?: string;
className?: string;
onHoverText?: string;
intercomMessageDisabled?: boolean;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
function LaunchChatSupport({
attributes,
eventName,
message = '',
buttonText = '',
className = '',
onHoverText = '',
intercomMessageDisabled = false,
}: LaunchChatSupportProps): JSX.Element | null {
const isChatSupportEnabled = useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active;
const isCloudUserVal = isCloudUser();
const { notifications } = useNotifications();
const { data: licenseData, isFetching } = useLicense();
const [activeLicense, setActiveLicense] = useState<License | null>(null);
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] = useState(
false,
);
const isPremiumChatSupportEnabled =
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
const showAddCreditCardModal =
!isPremiumChatSupportEnabled &&
!licenseData?.payload?.trialConvertedToSubscription;
useEffect(() => {
const activeValidLicense =
licenseData?.payload?.licenses?.find(
(license) => license.isCurrent === true,
) || null;
setActiveLicense(activeValidLicense);
}, [licenseData, isFetching]);
const handleFacingIssuesClick = (): void => {
if (showAddCreditCardModal) {
setIsAddCreditCardModalOpen(true);
} else {
logEvent(eventName, attributes);
if (window.Intercom && !intercomMessageDisabled) {
window.Intercom('showNewMessage', defaultTo(message, ''));
}
}
};
const handleBillingOnSuccess = (
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
): void => {
if (data?.payload?.redirectURL) {
const newTab = document.createElement('a');
newTab.href = data.payload.redirectURL;
newTab.target = '_blank';
newTab.rel = 'noopener noreferrer';
newTab.click();
}
};
const handleBillingOnError = (): void => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
};
const { mutate: updateCreditCard, isLoading: isLoadingBilling } = useMutation(
updateCreditCardApi,
{
onSuccess: (data) => {
handleBillingOnSuccess(data);
},
onError: handleBillingOnError,
},
);
const handleAddCreditCard = (): void => {
logEvent('Add Credit card modal: Clicked', {
source: `facing issues button`,
page: '',
...attributes,
});
updateCreditCard({
licenseKey: activeLicense?.key || '',
successURL: window.location.href,
cancelURL: window.location.href,
});
};
return isCloudUserVal && isChatSupportEnabled ? ( // Note: we would need to move this condition to license based in future
<div className="facing-issue-button">
<Tooltip
title={onHoverText}
autoAdjustOverflow
style={{ padding: 8 }}
overlayClassName="tooltip-overlay"
>
<Button
className={cx('periscope-btn', 'facing-issue-button', className)}
onClick={handleFacingIssuesClick}
icon={<HelpCircle size={14} />}
>
{buttonText || 'Facing issues?'}
</Button>
</Tooltip>
{/* Add Credit Card Modal */}
<Modal
className="add-credit-card-modal"
title={<span className="title">Add Credit Card for Chat Support</span>}
open={isAddCreditCardModalOpen}
closable
onCancel={(): void => setIsAddCreditCardModalOpen(false)}
destroyOnClose
footer={[
<Button
key="cancel"
onClick={(): void => setIsAddCreditCardModalOpen(false)}
className="cancel-btn"
icon={<X size={16} />}
>
Cancel
</Button>,
<Button
key="submit"
type="primary"
icon={<CreditCard size={16} />}
size="middle"
loading={isLoadingBilling}
disabled={isLoadingBilling}
onClick={handleAddCreditCard}
className="add-credit-card-btn"
>
Add Credit Card
</Button>,
]}
>
<Typography.Text className="add-credit-card-text">
You&apos;re currently on <span className="highlight-text">Trial plan</span>
. Add a credit card to access SigNoz chat support to your workspace.
</Typography.Text>
</Modal>
</div>
) : null;
}
LaunchChatSupport.defaultProps = {
message: '',
buttonText: '',
className: '',
onHoverText: '',
intercomMessageDisabled: false,
};
export default LaunchChatSupport;

View File

@@ -1,6 +1,7 @@
import { DrawerProps } from 'antd';
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { VIEWS } from './constants';
@@ -9,6 +10,7 @@ export type LogDetailProps = {
log: ILog | null;
selectedTab: VIEWS;
isListViewPanel?: boolean;
listViewPanelSelectedFields?: IField[] | null;
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
Pick<DrawerProps, 'onClose'>;

View File

@@ -6,10 +6,13 @@ import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import cx from 'classnames';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
import { LOCALSTORAGE } from 'constants/localStorage';
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
import JSONView from 'container/LogDetailedView/JsonView';
import Overview from 'container/LogDetailedView/Overview';
import { aggregateAttributesResourcesToString } from 'container/LogDetailedView/utils';
import { useOptionsMenu } from 'container/OptionsMenu';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import {
@@ -21,9 +24,10 @@ import {
TextSelect,
X,
} from 'lucide-react';
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { VIEW_TYPES, VIEWS } from './constants';
import { LogDetailProps } from './LogDetail.interfaces';
@@ -36,6 +40,7 @@ function LogDetail({
onClickActionItem,
selectedTab,
isListViewPanel = false,
listViewPanelSelectedFields,
}: LogDetailProps): JSX.Element {
const [, copyToClipboard] = useCopyToClipboard();
const [selectedView, setSelectedView] = useState<VIEWS>(selectedTab);
@@ -45,6 +50,19 @@ function LogDetail({
const [contextQuery, setContextQuery] = useState<Query | undefined>();
const [filters, setFilters] = useState<TagFilter | null>(null);
const [isEdit, setIsEdit] = useState<boolean>(false);
const { initialDataSource, stagedQuery } = useQueryBuilder();
const listQuery = useMemo(() => {
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
return stagedQuery.builder.queryData.find((item) => !item.disabled) || null;
}, [stagedQuery]);
const { options } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: initialDataSource || DataSource.LOGS,
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
});
const isDarkMode = useIsDarkMode();
@@ -192,6 +210,8 @@ function LogDetail({
onAddToQuery={onAddToQuery}
onClickActionItem={onClickActionItem}
isListViewPanel={isListViewPanel}
selectedOptions={options}
listViewPanelSelectedFields={listViewPanelSelectedFields}
/>
)}
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}

View File

@@ -5,7 +5,7 @@ import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd';
import { ColumnGroupType, ColumnType } from 'antd/es/table';
import { ColumnsType } from 'antd/lib/table';
import logEvent from 'api/common/logEvent';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { SlidersHorizontal } from 'lucide-react';
import { memo, useEffect, useState } from 'react';
import { popupContainer } from 'utils/selectPopupContainer';
@@ -96,7 +96,7 @@ function DynamicColumnTable({
return (
<div className="DynamicColumnTable">
<Flex justify="flex-end" align="center" gap={8}>
{facingIssueBtn && <FacingIssueBtn {...facingIssueBtn} />}
{facingIssueBtn && <LaunchChatSupport {...facingIssueBtn} />}
{dynamicColumns && (
<Dropdown
getPopupContainer={popupContainer}

View File

@@ -2,7 +2,7 @@
import { TableProps } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { ColumnGroupType, ColumnType } from 'antd/lib/table';
import { FacingIssueBtnProps } from 'components/facingIssueBtn/FacingIssueBtn';
import { LaunchChatSupportProps } from 'components/LaunchChatSupport/LaunchChatSupport';
import { TableDataSource } from './contants';
@@ -13,7 +13,7 @@ export interface DynamicColumnTableProps extends TableProps<any> {
tablesource: typeof TableDataSource[keyof typeof TableDataSource];
dynamicColumns: TableProps<any>['columns'];
onDragColumn?: (fromIndex: number, toIndex: number) => void;
facingIssueBtn?: FacingIssueBtnProps;
facingIssueBtn?: LaunchChatSupportProps;
shouldSendAlertsLogEvent?: boolean;
}

View File

@@ -1,70 +0,0 @@
import './FacingIssueBtn.style.scss';
import { Button, Tooltip } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { FeatureKeys } from 'constants/features';
import useFeatureFlags from 'hooks/useFeatureFlag';
import { defaultTo } from 'lodash-es';
import { HelpCircle } from 'lucide-react';
import { isCloudUser } from 'utils/app';
export interface FacingIssueBtnProps {
eventName: string;
attributes: Record<string, unknown>;
message?: string;
buttonText?: string;
className?: string;
onHoverText?: string;
intercomMessageDisabled?: boolean;
}
function FacingIssueBtn({
attributes,
eventName,
message = '',
buttonText = '',
className = '',
onHoverText = '',
intercomMessageDisabled = false,
}: FacingIssueBtnProps): JSX.Element | null {
const handleFacingIssuesClick = (): void => {
logEvent(eventName, attributes);
if (window.Intercom && !intercomMessageDisabled) {
window.Intercom('showNewMessage', defaultTo(message, ''));
}
};
const isChatSupportEnabled = useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active;
const isCloudUserVal = isCloudUser();
return isCloudUserVal && isChatSupportEnabled ? ( // Note: we would need to move this condition to license based in future
<div className="facing-issue-button">
<Tooltip
title={onHoverText}
autoAdjustOverflow
style={{ padding: 8 }}
overlayClassName="tooltip-overlay"
>
<Button
className={cx('periscope-btn', 'facing-issue-button', className)}
onClick={handleFacingIssuesClick}
icon={<HelpCircle size={14} />}
>
{buttonText || 'Facing issues?'}
</Button>
</Tooltip>
</div>
) : null;
}
FacingIssueBtn.defaultProps = {
message: '',
buttonText: '',
className: '',
onHoverText: '',
intercomMessageDisabled: false,
};
export default FacingIssueBtn;

View File

@@ -3,4 +3,5 @@ export const ENVIRONMENT = {
process?.env?.FRONTEND_API_ENDPOINT ||
process?.env?.GITPOD_WORKSPACE_URL?.replace('://', '://8080-') ||
'',
wsURL: process?.env?.WEBSOCKET_API_ENDPOINT || '',
};

View File

@@ -20,4 +20,5 @@ export enum FeatureKeys {
ONBOARDING = 'ONBOARDING',
CHAT_SUPPORT = 'CHAT_SUPPORT',
GATEWAY = 'GATEWAY',
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
}

View File

@@ -52,7 +52,7 @@ export const selectValueDivider = '__';
export const baseAutoCompleteIdKeysOrder: (keyof Omit<
BaseAutocompleteData,
'id' | 'isJSON'
'id' | 'isJSON' | 'isIndexed'
>)[] = ['key', 'dataType', 'type', 'isColumn'];
export const autocompleteType: Record<AutocompleteType, AutocompleteType> = {
@@ -71,6 +71,7 @@ export const alphabet: string[] = alpha.map((str) => String.fromCharCode(str));
export enum QueryBuilderKeys {
GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE',
GET_AGGREGATE_KEYS = 'GET_AGGREGATE_KEYS',
GET_ATTRIBUTE_SUGGESTIONS = 'GET_ATTRIBUTE_SUGGESTIONS',
}
export const mapOfOperators = {

View File

@@ -23,6 +23,10 @@ export const metricQueryFunctionOptions: SelectOption<string, string>[] = [
value: QueryFunctionsTypes.ABSOLUTE,
label: 'Absolute',
},
{
value: QueryFunctionsTypes.RUNNING_DIFF,
label: 'Running Diff',
},
{
value: QueryFunctionsTypes.LOG_2,
label: 'Log2',
@@ -103,6 +107,9 @@ export const queryFunctionsTypesConfig: QueryFunctionConfigType = {
absolute: {
showInput: false,
},
runningDiff: {
showInput: false,
},
log2: {
showInput: false,
},

View File

@@ -4,6 +4,7 @@ const userOS = getUserOperatingSystem();
export const LogsExplorerShortcuts = {
StageAndRunQuery: 'enter+meta',
FocusTheSearchBar: 's',
ShowAllFilters: '/+meta',
};
export const LogsExplorerShortcutsName = {
@@ -11,9 +12,11 @@ export const LogsExplorerShortcutsName = {
userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'
}+enter`,
FocusTheSearchBar: 's',
ShowAllFilters: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'}+/`,
};
export const LogsExplorerShortcutsDescription = {
StageAndRunQuery: 'Stage and Run the current query',
FocusTheSearchBar: 'Shift the focus to the last query filter bar',
ShowAllFilters: 'Toggle all filters in the filters dropdown',
};

View File

@@ -0,0 +1,78 @@
import AlertChannels from 'container/AllAlertChannels';
import { allAlertChannels } from 'mocks-server/__mockdata__/alerts';
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
jest.mock('hooks/useFetch', () => ({
__esModule: true,
default: jest.fn().mockImplementation(() => ({
payload: allAlertChannels,
})),
}));
const successNotification = jest.fn();
jest.mock('hooks/useNotifications', () => ({
__esModule: true,
useNotifications: jest.fn(() => ({
notifications: {
success: successNotification,
error: jest.fn(),
},
})),
}));
describe('Alert Channels Settings List page', () => {
beforeEach(() => {
render(<AlertChannels />);
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('Should display the Alert Channels page properly', () => {
it('Should check if "The alerts will be sent to all the configured channels." is visible ', () => {
expect(screen.getByText('sending_channels_note')).toBeInTheDocument();
});
it('Should check if "New Alert Channel" Button is visble ', () => {
expect(screen.getByText('button_new_channel')).toBeInTheDocument();
});
it('Should check if the help icon is visible and displays "tooltip_notification_channels ', async () => {
const helpIcon = screen.getByLabelText('question-circle');
fireEvent.mouseOver(helpIcon);
await waitFor(() => {
const tooltip = screen.getByText('tooltip_notification_channels');
expect(tooltip).toBeInTheDocument();
});
});
});
describe('Should check if the channels table is properly displayed', () => {
it('Should check if the table columns are properly displayed', () => {
expect(screen.getByText('column_channel_name')).toBeInTheDocument();
expect(screen.getByText('column_channel_type')).toBeInTheDocument();
expect(screen.getByText('column_channel_action')).toBeInTheDocument();
});
it('Should check if the data in the table is displayed properly', () => {
expect(screen.getByText('Dummy-Channel')).toBeInTheDocument();
expect(screen.getAllByText('slack')[0]).toBeInTheDocument();
expect(screen.getAllByText('column_channel_edit')[0]).toBeInTheDocument();
expect(screen.getAllByText('Delete')[0]).toBeInTheDocument();
});
it('Should check if clicking on Delete displays Success Toast "Channel Deleted Successfully"', async () => {
const deleteButton = screen.getAllByRole('button', { name: 'Delete' })[0];
expect(deleteButton).toBeInTheDocument();
act(() => {
fireEvent.click(deleteButton);
});
await waitFor(() => {
expect(successNotification).toBeCalledWith({
message: 'Success',
description: 'channel_delete_success',
});
});
});
});
});

View File

@@ -0,0 +1,72 @@
import AlertChannels from 'container/AllAlertChannels';
import { allAlertChannels } from 'mocks-server/__mockdata__/alerts';
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
jest.mock('hooks/useFetch', () => ({
__esModule: true,
default: jest.fn().mockImplementation(() => ({
payload: allAlertChannels,
})),
}));
const successNotification = jest.fn();
jest.mock('hooks/useNotifications', () => ({
__esModule: true,
useNotifications: jest.fn(() => ({
notifications: {
success: successNotification,
error: jest.fn(),
},
})),
}));
jest.mock('hooks/useComponentPermission', () => ({
__esModule: true,
default: jest.fn().mockImplementation(() => [false]),
}));
describe('Alert Channels Settings List page (Normal User)', () => {
beforeEach(() => {
render(<AlertChannels />);
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('Should display the Alert Channels page properly', () => {
it('Should check if "The alerts will be sent to all the configured channels." is visible ', () => {
expect(screen.getByText('sending_channels_note')).toBeInTheDocument();
});
it('Should check if "New Alert Channel" Button is visble and disabled', () => {
const newAlertButton = screen.getByRole('button', {
name: 'plus button_new_channel',
});
expect(newAlertButton).toBeInTheDocument();
expect(newAlertButton).toBeDisabled();
});
it('Should check if the help icon is visible and displays "tooltip_notification_channels ', async () => {
const helpIcon = screen.getByLabelText('question-circle');
fireEvent.mouseOver(helpIcon);
await waitFor(() => {
const tooltip = screen.getByText('tooltip_notification_channels');
expect(tooltip).toBeInTheDocument();
});
});
});
describe('Should check if the channels table is properly displayed', () => {
it('Should check if the table columns are properly displayed', () => {
expect(screen.getByText('column_channel_name')).toBeInTheDocument();
expect(screen.getByText('column_channel_type')).toBeInTheDocument();
expect(screen.queryByText('column_channel_action')).not.toBeInTheDocument();
});
it('Should check if the data in the table is displayed properly', () => {
expect(screen.getByText('Dummy-Channel')).toBeInTheDocument();
expect(screen.getAllByText('slack')[0]).toBeInTheDocument();
expect(screen.queryByText('column_channel_edit')).not.toBeInTheDocument();
expect(screen.queryByText('Delete')).not.toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,424 @@
/* eslint-disable sonarjs/no-duplicate-string */
/* eslint-disable sonarjs/no-identical-functions */
import CreateAlertChannels from 'container/CreateAlertChannels';
import { ChannelType } from 'container/CreateAlertChannels/config';
import {
opsGenieDescriptionDefaultValue,
opsGenieMessageDefaultValue,
opsGeniePriorityDefaultValue,
pagerDutyAdditionalDetailsDefaultValue,
pagerDutyDescriptionDefaultVaule,
pagerDutySeverityTextDefaultValue,
slackDescriptionDefaultValue,
slackTitleDefaultValue,
} from 'mocks-server/__mockdata__/alerts';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
import { testLabelInputAndHelpValue } from './testUtils';
const successNotification = jest.fn();
const errorNotification = jest.fn();
jest.mock('hooks/useNotifications', () => ({
__esModule: true,
useNotifications: jest.fn(() => ({
notifications: {
success: successNotification,
error: errorNotification,
},
})),
}));
jest.mock('hooks/useFeatureFlag', () => ({
__esModule: true,
default: jest.fn().mockImplementation(() => ({
active: true,
})),
}));
describe('Create Alert Channel', () => {
afterEach(() => {
jest.clearAllMocks();
});
describe('Should check if the new alert channel is properly displayed with the cascading fields of slack channel ', () => {
beforeEach(() => {
render(<CreateAlertChannels preType={ChannelType.Slack} />);
});
afterEach(() => {
jest.clearAllMocks();
});
it('Should check if the title is "New Notification Channels"', () => {
expect(screen.getByText('page_title_create')).toBeInTheDocument();
});
it('Should check if the name label and textbox are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_channel_name',
testId: 'channel-name-textbox',
});
});
it('Should check if Send resolved alerts label and checkbox are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_send_resolved',
testId: 'field-send-resolved-checkbox',
});
});
it('Should check if channel type label and dropdown are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_channel_type',
testId: 'channel-type-select',
});
});
// Default Channel type (Slack) fields
it('Should check if the selected item in the type dropdown has text "Slack"', () => {
expect(screen.getByText('Slack')).toBeInTheDocument();
});
it('Should check if Webhook URL label and input are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_webhook_url',
testId: 'webhook-url-textbox',
});
});
it('Should check if Recepient label, input, and help text are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_slack_recipient',
testId: 'slack-channel-textbox',
helpText: 'slack_channel_help',
});
});
it('Should check if Title label and text area are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_slack_title',
testId: 'title-textarea',
});
});
it('Should check if Title contains template', () => {
const titleTextArea = screen.getByTestId('title-textarea');
expect(titleTextArea).toHaveTextContent(slackTitleDefaultValue);
});
it('Should check if Description label and text area are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_slack_description',
testId: 'description-textarea',
});
});
it('Should check if Description contains template', () => {
const descriptionTextArea = screen.getByTestId('description-textarea');
expect(descriptionTextArea).toHaveTextContent(slackDescriptionDefaultValue);
});
it('Should check if the form buttons are displayed properly (Save, Test, Back)', () => {
expect(screen.getByText('button_save_channel')).toBeInTheDocument();
expect(screen.getByText('button_test_channel')).toBeInTheDocument();
expect(screen.getByText('button_return')).toBeInTheDocument();
});
it('Should check if saving the form without filling the name displays "Something went wrong"', async () => {
const saveButton = screen.getByRole('button', {
name: 'button_save_channel',
});
fireEvent.click(saveButton);
await waitFor(() =>
expect(errorNotification).toHaveBeenCalledWith({
description: 'Something went wrong',
message: 'Error',
}),
);
});
it('Should check if clicking on Test button shows "An alert has been sent to this channel" success message if testing passes', async () => {
server.use(
rest.post('http://localhost/api/v1/testChannel', (req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
status: 'success',
data: 'test alert sent',
}),
),
),
);
const testButton = screen.getByRole('button', {
name: 'button_test_channel',
});
fireEvent.click(testButton);
await waitFor(() =>
expect(successNotification).toHaveBeenCalledWith({
message: 'Success',
description: 'channel_test_done',
}),
);
});
it('Should check if clicking on Test button shows "Something went wrong" error message if testing fails', async () => {
const testButton = screen.getByRole('button', {
name: 'button_test_channel',
});
fireEvent.click(testButton);
await waitFor(() =>
expect(errorNotification).toHaveBeenCalledWith({
message: 'Error',
description: 'channel_test_failed',
}),
);
});
});
describe('New Alert Channel Cascading Fields Based on Channel Type', () => {
describe('Webhook', () => {
beforeEach(() => {
render(<CreateAlertChannels preType={ChannelType.Webhook} />);
});
it('Should check if the selected item in the type dropdown has text "Webhook"', () => {
expect(screen.getByText('Webhook')).toBeInTheDocument();
});
it('Should check if Webhook URL label and input are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_webhook_url',
testId: 'webhook-url-textbox',
});
});
it('Should check if Webhook User Name label, input, and help text are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_webhook_username',
testId: 'webhook-username-textbox',
helpText: 'help_webhook_username',
});
});
it('Should check if Password label and textbox, and help text are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'Password (optional)',
testId: 'webhook-password-textbox',
helpText: 'help_webhook_password',
});
});
});
describe('PagerDuty', () => {
beforeEach(() => {
render(<CreateAlertChannels preType={ChannelType.Pagerduty} />);
});
it('Should check if the selected item in the type dropdown has text "Pagerduty"', () => {
expect(screen.getByText('Pagerduty')).toBeInTheDocument();
});
it('Should check if Routing key label, required, and textbox are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_pager_routing_key',
testId: 'pager-routing-key-textbox',
});
});
it('Should check if Description label, required, info (Shows up as description in pagerduty), and text area are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_pager_description',
testId: 'pager-description-textarea',
helpText: 'help_pager_description',
});
});
it('Should check if the description contains default template', () => {
const descriptionTextArea = screen.getByTestId(
'pager-description-textarea',
);
expect(descriptionTextArea).toHaveTextContent(
pagerDutyDescriptionDefaultVaule,
);
});
it('Should check if Severity label, info (help_pager_severity), and textbox are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_pager_severity',
testId: 'pager-severity-textbox',
helpText: 'help_pager_severity',
});
});
it('Should check if Severity contains the default template', () => {
const severityTextbox = screen.getByTestId('pager-severity-textbox');
expect(severityTextbox).toHaveValue(pagerDutySeverityTextDefaultValue);
});
it('Should check if Additional Information label, text area, and help text (help_pager_details) are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_pager_details',
testId: 'pager-additional-details-textarea',
helpText: 'help_pager_details',
});
});
it('Should check if Additional Information contains the default template', () => {
const detailsTextArea = screen.getByTestId(
'pager-additional-details-textarea',
);
expect(detailsTextArea).toHaveValue(pagerDutyAdditionalDetailsDefaultValue);
});
it('Should check if Group label, text area, and info (help_pager_group) are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_pager_group',
testId: 'pager-group-textarea',
helpText: 'help_pager_group',
});
});
it('Should check if Class label, text area, and info (help_pager_class) are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_pager_class',
testId: 'pager-class-textarea',
helpText: 'help_pager_class',
});
});
it('Should check if Client label, text area, and info (Shows up as event source in Pagerduty) are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_pager_client',
testId: 'pager-client-textarea',
helpText: 'help_pager_client',
});
});
it('Should check if Client input contains the default value "SigNoz Alert Manager"', () => {
const clientTextArea = screen.getByTestId('pager-client-textarea');
expect(clientTextArea).toHaveValue('SigNoz Alert Manager');
});
it('Should check if Client URL label, text area, and info (Shows up as event source link in Pagerduty) are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_pager_client_url',
testId: 'pager-client-url-textarea',
helpText: 'help_pager_client_url',
});
});
it('Should check if Client URL contains the default value "https://enter-signoz-host-n-port-here/alerts"', () => {
const clientUrlTextArea = screen.getByTestId('pager-client-url-textarea');
expect(clientUrlTextArea).toHaveValue(
'https://enter-signoz-host-n-port-here/alerts',
);
});
});
describe('Opsgenie', () => {
beforeEach(() => {
render(<CreateAlertChannels preType={ChannelType.Opsgenie} />);
});
it('Should check if the selected item in the type dropdown has text "Opsgenie"', () => {
expect(screen.getByText('Opsgenie')).toBeInTheDocument();
});
it('Should check if API key label, required, and textbox are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_opsgenie_api_key',
testId: 'opsgenie-api-key-textbox',
required: true,
});
});
it('Should check if Message label, required, info (Shows up as message in opsgenie), and text area are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_opsgenie_message',
testId: 'opsgenie-message-textarea',
helpText: 'help_opsgenie_message',
required: true,
});
});
it('Should check if Message contains the default template ', () => {
const messageTextArea = screen.getByTestId('opsgenie-message-textarea');
expect(messageTextArea).toHaveValue(opsGenieMessageDefaultValue);
});
it('Should check if Description label, required, info (Shows up as description in opsgenie), and text area are displayed properly `{{ if gt (len .Alerts.Firing) 0 -}}', () => {
testLabelInputAndHelpValue({
labelText: 'field_opsgenie_description',
testId: 'opsgenie-description-textarea',
helpText: 'help_opsgenie_description',
required: true,
});
});
it('Should check if Description label, required, info (Shows up as description in opsgenie), and text area are displayed properly `{{ if gt (len .Alerts.Firing) 0 -}}', () => {
const descriptionTextArea = screen.getByTestId(
'opsgenie-description-textarea',
);
expect(descriptionTextArea).toHaveTextContent(
opsGenieDescriptionDefaultValue,
);
});
it('Should check if Priority label, required, info (help_opsgenie_priority), and text area are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_opsgenie_priority',
testId: 'opsgenie-priority-textarea',
helpText: 'help_opsgenie_priority',
required: true,
});
});
it('Should check if Message contains the default template', () => {
const priorityTextArea = screen.getByTestId('opsgenie-priority-textarea');
expect(priorityTextArea).toHaveValue(opsGeniePriorityDefaultValue);
});
});
describe('Opsgenie', () => {
beforeEach(() => {
render(<CreateAlertChannels preType={ChannelType.Email} />);
});
it('Should check if the selected item in the type dropdown has text "Email"', () => {
expect(screen.getByText('Email')).toBeInTheDocument();
});
it('Should check if API key label, required, info(help_email_to), and textbox are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_email_to',
testId: 'email-to-textbox',
helpText: 'help_email_to',
required: true,
});
});
});
describe('Microsoft Teams', () => {
beforeEach(() => {
render(<CreateAlertChannels preType={ChannelType.MsTeams} />);
});
it('Should check if the selected item in the type dropdown has text "msteams"', () => {
expect(screen.getByText('msteams')).toBeInTheDocument();
});
it('Should check if Webhook URL label and input are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_webhook_url',
testId: 'webhook-url-textbox',
});
});
it('Should check if Title label and text area are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_slack_title',
testId: 'title-textarea',
});
});
it('Should check if Title contains template', () => {
const titleTextArea = screen.getByTestId('title-textarea');
expect(titleTextArea).toHaveTextContent(slackTitleDefaultValue);
});
it('Should check if Description label and text area are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_slack_description',
testId: 'description-textarea',
});
});
it('Should check if Description contains template', () => {
const descriptionTextArea = screen.getByTestId('description-textarea');
expect(descriptionTextArea).toHaveTextContent(slackDescriptionDefaultValue);
});
});
});
});

View File

@@ -0,0 +1,348 @@
/* eslint-disable sonarjs/no-duplicate-string */
/* eslint-disable sonarjs/no-identical-functions */
import { SIGNOZ_UPGRADE_PLAN_URL } from 'constants/app';
import CreateAlertChannels from 'container/CreateAlertChannels';
import { ChannelType } from 'container/CreateAlertChannels/config';
import {
opsGenieDescriptionDefaultValue,
opsGenieMessageDefaultValue,
opsGeniePriorityDefaultValue,
pagerDutyAdditionalDetailsDefaultValue,
pagerDutyDescriptionDefaultVaule,
pagerDutySeverityTextDefaultValue,
slackDescriptionDefaultValue,
slackTitleDefaultValue,
} from 'mocks-server/__mockdata__/alerts';
import { render, screen } from 'tests/test-utils';
import { testLabelInputAndHelpValue } from './testUtils';
describe('Create Alert Channel (Normal User)', () => {
afterEach(() => {
jest.clearAllMocks();
});
describe('Should check if the new alert channel is properly displayed with the cascading fields of slack channel ', () => {
beforeEach(() => {
render(<CreateAlertChannels preType={ChannelType.Slack} />);
});
it('Should check if the title is "New Notification Channels"', () => {
expect(screen.getByText('page_title_create')).toBeInTheDocument();
});
it('Should check if the name label and textbox are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_channel_name',
testId: 'channel-name-textbox',
});
});
it('Should check if Send resolved alerts label and checkbox are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_send_resolved',
testId: 'field-send-resolved-checkbox',
});
});
it('Should check if channel type label and dropdown are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_channel_type',
testId: 'channel-type-select',
});
});
// Default Channel type (Slack) fields
it('Should check if the selected item in the type dropdown has text "Slack"', () => {
expect(screen.getByText('Slack')).toBeInTheDocument();
});
it('Should check if Webhook URL label and input are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_webhook_url',
testId: 'webhook-url-textbox',
});
});
it('Should check if Recepient label, input, and help text are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_slack_recipient',
testId: 'slack-channel-textbox',
helpText: 'slack_channel_help',
});
});
it('Should check if Title label and text area are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_slack_title',
testId: 'title-textarea',
});
});
it('Should check if Title contains template', () => {
const titleTextArea = screen.getByTestId('title-textarea');
expect(titleTextArea).toHaveTextContent(slackTitleDefaultValue);
});
it('Should check if Description label and text area are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_slack_description',
testId: 'description-textarea',
});
});
it('Should check if Description contains template', () => {
const descriptionTextArea = screen.getByTestId('description-textarea');
expect(descriptionTextArea).toHaveTextContent(slackDescriptionDefaultValue);
});
it('Should check if the form buttons are displayed properly (Save, Test, Back)', () => {
expect(screen.getByText('button_save_channel')).toBeInTheDocument();
expect(screen.getByText('button_test_channel')).toBeInTheDocument();
expect(screen.getByText('button_return')).toBeInTheDocument();
});
});
describe('New Alert Channel Cascading Fields Based on Channel Type', () => {
describe('Webhook', () => {
beforeEach(() => {
render(<CreateAlertChannels preType={ChannelType.Webhook} />);
});
it('Should check if the selected item in the type dropdown has text "Webhook"', () => {
expect(screen.getByText('Webhook')).toBeInTheDocument();
});
it('Should check if Webhook URL label and input are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_webhook_url',
testId: 'webhook-url-textbox',
});
});
it('Should check if Webhook User Name label, input, and help text are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_webhook_username',
testId: 'webhook-username-textbox',
helpText: 'help_webhook_username',
});
});
it('Should check if Password label and textbox, and help text are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'Password (optional)',
testId: 'webhook-password-textbox',
helpText: 'help_webhook_password',
});
});
});
describe('PagerDuty', () => {
beforeEach(() => {
render(<CreateAlertChannels preType={ChannelType.Pagerduty} />);
});
it('Should check if the selected item in the type dropdown has text "Pagerduty"', () => {
expect(screen.getByText('Pagerduty')).toBeInTheDocument();
});
it('Should check if Routing key label, required, and textbox are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_pager_routing_key',
testId: 'pager-routing-key-textbox',
});
});
it('Should check if Description label, required, info (Shows up as description in pagerduty), and text area are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_pager_description',
testId: 'pager-description-textarea',
helpText: 'help_pager_description',
});
});
it('Should check if the description contains default template', () => {
const descriptionTextArea = screen.getByTestId(
'pager-description-textarea',
);
expect(descriptionTextArea).toHaveTextContent(
pagerDutyDescriptionDefaultVaule,
);
});
it('Should check if Severity label, info (help_pager_severity), and textbox are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_pager_severity',
testId: 'pager-severity-textbox',
helpText: 'help_pager_severity',
});
});
it('Should check if Severity contains the default template', () => {
const severityTextbox = screen.getByTestId('pager-severity-textbox');
expect(severityTextbox).toHaveValue(pagerDutySeverityTextDefaultValue);
});
it('Should check if Additional Information label, text area, and help text (help_pager_details) are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_pager_details',
testId: 'pager-additional-details-textarea',
helpText: 'help_pager_details',
});
});
it('Should check if Additional Information contains the default template', () => {
const detailsTextArea = screen.getByTestId(
'pager-additional-details-textarea',
);
expect(detailsTextArea).toHaveValue(pagerDutyAdditionalDetailsDefaultValue);
});
it('Should check if Group label, text area, and info (help_pager_group) are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_pager_group',
testId: 'pager-group-textarea',
helpText: 'help_pager_group',
});
});
it('Should check if Class label, text area, and info (help_pager_class) are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_pager_class',
testId: 'pager-class-textarea',
helpText: 'help_pager_class',
});
});
it('Should check if Client label, text area, and info (Shows up as event source in Pagerduty) are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_pager_client',
testId: 'pager-client-textarea',
helpText: 'help_pager_client',
});
});
it('Should check if Client input contains the default value "SigNoz Alert Manager"', () => {
const clientTextArea = screen.getByTestId('pager-client-textarea');
expect(clientTextArea).toHaveValue('SigNoz Alert Manager');
});
it('Should check if Client URL label, text area, and info (Shows up as event source link in Pagerduty) are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_pager_client_url',
testId: 'pager-client-url-textarea',
helpText: 'help_pager_client_url',
});
});
it('Should check if Client URL contains the default value "https://enter-signoz-host-n-port-here/alerts"', () => {
const clientUrlTextArea = screen.getByTestId('pager-client-url-textarea');
expect(clientUrlTextArea).toHaveValue(
'https://enter-signoz-host-n-port-here/alerts',
);
});
});
describe('Opsgenie', () => {
beforeEach(() => {
render(<CreateAlertChannels preType={ChannelType.Opsgenie} />);
});
it('Should check if the selected item in the type dropdown has text "Opsgenie"', () => {
expect(screen.getByText('Opsgenie')).toBeInTheDocument();
});
it('Should check if API key label, required, and textbox are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_opsgenie_api_key',
testId: 'opsgenie-api-key-textbox',
required: true,
});
});
it('Should check if Message label, required, info (Shows up as message in opsgenie), and text area are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_opsgenie_message',
testId: 'opsgenie-message-textarea',
helpText: 'help_opsgenie_message',
required: true,
});
});
it('Should check if Message contains the default template ', () => {
const messageTextArea = screen.getByTestId('opsgenie-message-textarea');
expect(messageTextArea).toHaveValue(opsGenieMessageDefaultValue);
});
it('Should check if Description label, required, info (Shows up as description in opsgenie), and text area are displayed properly `{{ if gt (len .Alerts.Firing) 0 -}}', () => {
testLabelInputAndHelpValue({
labelText: 'field_opsgenie_description',
testId: 'opsgenie-description-textarea',
helpText: 'help_opsgenie_description',
required: true,
});
});
it('Should check if Description label, required, info (Shows up as description in opsgenie), and text area are displayed properly `{{ if gt (len .Alerts.Firing) 0 -}}', () => {
const descriptionTextArea = screen.getByTestId(
'opsgenie-description-textarea',
);
expect(descriptionTextArea).toHaveTextContent(
opsGenieDescriptionDefaultValue,
);
});
it('Should check if Priority label, required, info (help_opsgenie_priority), and text area are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_opsgenie_priority',
testId: 'opsgenie-priority-textarea',
helpText: 'help_opsgenie_priority',
required: true,
});
});
it('Should check if Message contains the default template', () => {
const priorityTextArea = screen.getByTestId('opsgenie-priority-textarea');
expect(priorityTextArea).toHaveValue(opsGeniePriorityDefaultValue);
});
});
describe('Opsgenie', () => {
beforeEach(() => {
render(<CreateAlertChannels preType={ChannelType.Email} />);
});
it('Should check if the selected item in the type dropdown has text "Email"', () => {
expect(screen.getByText('Email')).toBeInTheDocument();
});
it('Should check if API key label, required, info(help_email_to), and textbox are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_email_to',
testId: 'email-to-textbox',
helpText: 'help_email_to',
required: true,
});
});
});
describe('Microsoft Teams', () => {
beforeEach(() => {
render(<CreateAlertChannels preType={ChannelType.MsTeams} />);
});
it('Should check if the selected item in the type dropdown has text "Microsoft Teams (Supported in Paid Plans Only)"', () => {
expect(
screen.getByText('Microsoft Teams (Supported in Paid Plans Only)'),
).toBeInTheDocument();
});
it('Should check if the upgrade plan message is shown', () => {
expect(screen.getByText('Upgrade to a Paid Plan')).toBeInTheDocument();
expect(
screen.getByText(/This feature is available for paid plans only./),
).toBeInTheDocument();
const link = screen.getByRole('link', { name: 'Click here' });
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute('href', SIGNOZ_UPGRADE_PLAN_URL);
expect(screen.getByText(/to Upgrade/)).toBeInTheDocument();
});
it('Should check if the form buttons are displayed properly (Save, Test, Back)', () => {
expect(
screen.getByRole('button', { name: 'button_save_channel' }),
).toBeInTheDocument();
expect(
screen.getByRole('button', { name: 'button_test_channel' }),
).toBeInTheDocument();
expect(
screen.getByRole('button', { name: 'button_return' }),
).toBeInTheDocument();
});
it('Should check if save and test buttons are disabled', () => {
expect(
screen.getByRole('button', { name: 'button_save_channel' }),
).toBeDisabled();
expect(
screen.getByRole('button', { name: 'button_test_channel' }),
).toBeDisabled();
});
});
});
});

View File

@@ -0,0 +1,118 @@
import EditAlertChannels from 'container/EditAlertChannels';
import {
editAlertChannelInitialValue,
editSlackDescriptionDefaultValue,
slackTitleDefaultValue,
} from 'mocks-server/__mockdata__/alerts';
import { render, screen } from 'tests/test-utils';
import { testLabelInputAndHelpValue } from './testUtils';
const successNotification = jest.fn();
const errorNotification = jest.fn();
jest.mock('hooks/useNotifications', () => ({
__esModule: true,
useNotifications: jest.fn(() => ({
notifications: {
success: successNotification,
error: errorNotification,
},
})),
}));
jest.mock('hooks/useFeatureFlag', () => ({
__esModule: true,
default: jest.fn().mockImplementation(() => ({
active: true,
})),
}));
describe('Should check if the edit alert channel is properly displayed ', () => {
beforeEach(() => {
render(<EditAlertChannels initialValue={editAlertChannelInitialValue} />);
});
afterEach(() => {
jest.clearAllMocks();
});
it('Should check if the title is "Edit Notification Channels"', () => {
expect(screen.getByText('page_title_edit')).toBeInTheDocument();
});
it('Should check if the name label and textbox are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_channel_name',
testId: 'channel-name-textbox',
value: 'Dummy-Channel',
});
});
it('Should check if Send resolved alerts label and checkbox are displayed properly and the checkbox is checked ', () => {
testLabelInputAndHelpValue({
labelText: 'field_send_resolved',
testId: 'field-send-resolved-checkbox',
});
expect(screen.getByTestId('field-send-resolved-checkbox')).toBeChecked();
});
it('Should check if channel type label and dropdown are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_channel_type',
testId: 'channel-type-select',
});
});
it('Should check if the selected item in the type dropdown has text "Slack"', () => {
expect(screen.getByText('Slack')).toBeInTheDocument();
});
it('Should check if Webhook URL label and input are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_webhook_url',
testId: 'webhook-url-textbox',
value:
'https://discord.com/api/webhooks/dummy_webhook_id/dummy_webhook_token/slack',
});
});
it('Should check if Recepient label, input, and help text are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_slack_recipient',
testId: 'slack-channel-textbox',
helpText: 'slack_channel_help',
value: '#dummy_channel',
});
});
it('Should check if Title label and text area are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_slack_title',
testId: 'title-textarea',
});
});
it('Should check if Title contains template', () => {
const titleTextArea = screen.getByTestId('title-textarea');
expect(titleTextArea).toHaveTextContent(slackTitleDefaultValue);
});
it('Should check if Description label and text area are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_slack_description',
testId: 'description-textarea',
});
});
it('Should check if Description contains template', () => {
const descriptionTextArea = screen.getByTestId('description-textarea');
expect(descriptionTextArea).toHaveTextContent(
editSlackDescriptionDefaultValue,
);
});
it('Should check if the form buttons are displayed properly (Save, Test, Back)', () => {
expect(screen.getByText('button_save_channel')).toBeInTheDocument();
expect(screen.getByText('button_test_channel')).toBeInTheDocument();
expect(screen.getByText('button_return')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,31 @@
import { screen } from 'tests/test-utils';
export const testLabelInputAndHelpValue = ({
labelText,
testId,
helpText,
required = false,
value,
}: {
labelText: string;
testId: string;
helpText?: string;
required?: boolean;
value?: string;
}): void => {
const label = screen.getByText(labelText);
expect(label).toBeInTheDocument();
const input = screen.getByTestId(testId);
expect(input).toBeInTheDocument();
if (helpText !== undefined) {
expect(screen.getByText(helpText)).toBeInTheDocument();
}
if (required) {
expect(input).toBeRequired();
}
if (value) {
expect(input).toHaveValue(value);
}
};

View File

@@ -24,6 +24,71 @@
}
}
.chat-support-gateway {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
.chat-support-gateway-btn {
max-width: 48px;
width: 48px;
max-height: 48px;
height: 48px;
padding: 12px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
background-color: #f25733;
border: none;
&:hover {
color: white !important;
border-color: white !important;
}
.chat-support-gateway-btn-icon {
fill: white;
}
}
}
.add-credit-card-btn,
.cancel-btn {
display: inline-flex;
justify-content: center;
align-items: center;
}
.highlight-text {
border-radius: 2px;
background: rgba(78, 116, 248, 0.1);
padding-right: 4px;
font-family: 'Geist Mono';
color: var(--bg-robin-500);
}
.add-credit-card-modal {
.ant-modal-footer {
display: flex;
justify-content: flex-end;
}
.cancel-btn {
border-radius: 2px;
border: none;
background: var(--bg-slate-500, #161922);
box-shadow: none;
}
.add-credit-card-btn {
box-shadow: none;
}
}
.isDarkMode {
.app-layout {
.app-content {

View File

@@ -9,12 +9,15 @@ import getLocalStorageKey from 'api/browser/localstorage/get';
import getUserLatestVersion from 'api/user/getLatestVersion';
import getUserVersion from 'api/user/getVersion';
import cx from 'classnames';
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
import { FeatureKeys } from 'constants/features';
import ROUTES from 'constants/routes';
import SideNav from 'container/SideNav';
import TopNav from 'container/TopNav';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useFeatureFlags from 'hooks/useFeatureFlag';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
@@ -49,6 +52,7 @@ import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
import { ChildrenContainer, Layout, LayoutContent } from './styles';
import { getRouteKey } from './utils';
// eslint-disable-next-line sonarjs/cognitive-complexity
function AppLayout(props: AppLayoutProps): JSX.Element {
const { isLoggedIn, user, role } = useSelector<AppState, AppReducer>(
(state) => state.app,
@@ -58,10 +62,19 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true',
);
const { notifications } = useNotifications();
const isDarkMode = useIsDarkMode();
const { data: licenseData, isFetching } = useLicense();
const isPremiumChatSupportEnabled =
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
const showAddCreditCardModal =
!isPremiumChatSupportEnabled &&
!licenseData?.payload?.trialConvertedToSubscription;
const { pathname } = useLocation();
const { t } = useTranslation(['titles']);
@@ -95,8 +108,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const latestCurrentCounter = useRef(0);
const latestVersionCounter = useRef(0);
const { notifications } = useNotifications();
const onCollapse = useCallback(() => {
setCollapsed((collapsed) => !collapsed);
}, []);
@@ -331,6 +342,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
</Sentry.ErrorBoundary>
</div>
</Flex>
{showAddCreditCardModal && <ChatSupportGateway />}
</Layout>
);
}

View File

@@ -33,6 +33,7 @@ import useErrorNotification from 'hooks/useErrorNotification';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications';
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
import { cloneDeep } from 'lodash-es';
import {
Check,
ConciergeBell,
@@ -56,7 +57,7 @@ import { useHistory } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
@@ -120,6 +121,21 @@ function ExplorerOptions({
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const handleConditionalQueryModification = useCallback((): string => {
if (
query?.builder?.queryData?.[0]?.aggregateOperator !== StringOperators.NOOP
) {
return JSON.stringify(query);
}
// Modify aggregateOperator to count, as noop is not supported in alerts
const modifiedQuery = cloneDeep(query);
modifiedQuery.builder.queryData[0].aggregateOperator = StringOperators.COUNT;
return JSON.stringify(modifiedQuery);
}, [query]);
const onCreateAlertsHandler = useCallback(() => {
if (sourcepage === DataSource.TRACES) {
logEvent('Traces Explorer: Create alert', {
@@ -130,13 +146,16 @@ function ExplorerOptions({
panelType,
});
}
const stringifiedQuery = handleConditionalQueryModification();
history.push(
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
JSON.stringify(query),
stringifiedQuery,
)}`,
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [history, query]);
}, [handleConditionalQueryModification, history]);
const onCancel = (value: boolean) => (): void => {
onModalToggle(value);

View File

@@ -27,6 +27,7 @@ function EmailForm({ setSelectedConfig }: EmailFormProps): JSX.Element {
<Input
onChange={handleInputChange('to')}
placeholder={t('placeholder_email_to')}
data-testid="email-to-textbox"
/>
</Form.Item>

View File

@@ -17,6 +17,7 @@ function MsTeams({ setSelectedConfig }: MsTeamsProps): JSX.Element {
webhook_url: event.target.value,
}));
}}
data-testid="webhook-url-textbox"
/>
</Form.Item>
@@ -30,6 +31,7 @@ function MsTeams({ setSelectedConfig }: MsTeamsProps): JSX.Element {
title: event.target.value,
}))
}
data-testid="title-textarea"
/>
</Form.Item>
@@ -41,6 +43,7 @@ function MsTeams({ setSelectedConfig }: MsTeamsProps): JSX.Element {
text: event.target.value,
}))
}
data-testid="description-textarea"
placeholder={t('placeholder_slack_description')}
/>
</Form.Item>

View File

@@ -20,7 +20,10 @@ function OpsgenieForm({ setSelectedConfig }: OpsgenieFormProps): JSX.Element {
return (
<>
<Form.Item name="api_key" label={t('field_opsgenie_api_key')} required>
<Input onChange={handleInputChange('api_key')} />
<Input
onChange={handleInputChange('api_key')}
data-testid="opsgenie-api-key-textbox"
/>
</Form.Item>
<Form.Item
@@ -33,6 +36,7 @@ function OpsgenieForm({ setSelectedConfig }: OpsgenieFormProps): JSX.Element {
rows={4}
onChange={handleInputChange('message')}
placeholder={t('placeholder_opsgenie_message')}
data-testid="opsgenie-message-textarea"
/>
</Form.Item>
@@ -46,6 +50,7 @@ function OpsgenieForm({ setSelectedConfig }: OpsgenieFormProps): JSX.Element {
rows={4}
onChange={handleInputChange('description')}
placeholder={t('placeholder_opsgenie_description')}
data-testid="opsgenie-description-textarea"
/>
</Form.Item>
@@ -59,6 +64,7 @@ function OpsgenieForm({ setSelectedConfig }: OpsgenieFormProps): JSX.Element {
rows={4}
onChange={handleInputChange('priority')}
placeholder={t('placeholder_opsgenie_priority')}
data-testid="opsgenie-priority-textarea"
/>
</Form.Item>
</>

View File

@@ -18,6 +18,7 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
routing_key: event.target.value,
}));
}}
data-testid="pager-routing-key-textbox"
/>
</Form.Item>
@@ -36,6 +37,7 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
}))
}
placeholder={t('placeholder_pager_description')}
data-testid="pager-description-textarea"
/>
</Form.Item>
@@ -51,6 +53,7 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
severity: event.target.value,
}))
}
data-testid="pager-severity-textbox"
/>
</Form.Item>
@@ -67,6 +70,7 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
details: event.target.value,
}))
}
data-testid="pager-additional-details-textarea"
/>
</Form.Item>
@@ -97,6 +101,7 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
group: event.target.value,
}))
}
data-testid="pager-group-textarea"
/>
</Form.Item>
@@ -112,6 +117,7 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
class: event.target.value,
}))
}
data-testid="pager-class-textarea"
/>
</Form.Item>
<Form.Item
@@ -126,6 +132,7 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
client: event.target.value,
}))
}
data-testid="pager-client-textarea"
/>
</Form.Item>
@@ -141,6 +148,7 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
client_url: event.target.value,
}))
}
data-testid="pager-client-url-textarea"
/>
</Form.Item>
</>

View File

@@ -19,6 +19,7 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
api_url: event.target.value,
}));
}}
data-testid="webhook-url-textbox"
/>
</Form.Item>
@@ -34,11 +35,13 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
channel: event.target.value,
}))
}
data-testid="slack-channel-textbox"
/>
</Form.Item>
<Form.Item name="title" label={t('field_slack_title')}>
<TextArea
data-testid="title-textarea"
rows={4}
// value={`[{{ .Status | toUpper }}{{ if eq .Status \"firing\" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }}\n{{- if gt (len .CommonLabels) (len .GroupLabels) -}}\n{{\" \"}}(\n{{- with .CommonLabels.Remove .GroupLabels.Names }}\n {{- range $index, $label := .SortedPairs -}}\n {{ if $index }}, {{ end }}\n {{- $label.Name }}=\"{{ $label.Value -}}\"\n {{- end }}\n{{- end -}}\n)\n{{- end }}`}
onChange={(event): void =>
@@ -59,6 +62,7 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
}))
}
placeholder={t('placeholder_slack_description')}
data-testid="description-textarea"
/>
</Form.Item>
</>

View File

@@ -17,6 +17,7 @@ function WebhookSettings({ setSelectedConfig }: WebhookProps): JSX.Element {
api_url: event.target.value,
}));
}}
data-testid="webhook-url-textbox"
/>
</Form.Item>
<Form.Item
@@ -31,6 +32,7 @@ function WebhookSettings({ setSelectedConfig }: WebhookProps): JSX.Element {
username: event.target.value,
}));
}}
data-testid="webhook-username-textbox"
/>
</Form.Item>
<Form.Item
@@ -46,6 +48,7 @@ function WebhookSettings({ setSelectedConfig }: WebhookProps): JSX.Element {
password: event.target.value,
}));
}}
data-testid="webhook-password-textbox"
/>
</Form.Item>
</>

View File

@@ -85,6 +85,7 @@ function FormAlertChannels({
<Form initialValues={initialValue} layout="vertical" form={formInstance}>
<Form.Item label={t('field_channel_name')} labelAlign="left" name="name">
<Input
data-testid="channel-name-textbox"
disabled={editing}
onChange={(event): void => {
setSelectedConfig((state) => ({
@@ -102,6 +103,7 @@ function FormAlertChannels({
>
<Switch
defaultChecked={initialValue?.send_resolved}
data-testid="field-send-resolved-checkbox"
onChange={(value): void => {
setSelectedConfig((state) => ({
...state,
@@ -112,24 +114,37 @@ function FormAlertChannels({
</Form.Item>
<Form.Item label={t('field_channel_type')} labelAlign="left" name="type">
<Select disabled={editing} onChange={onTypeChangeHandler} value={type}>
<Select.Option value="slack" key="slack">
<Select
disabled={editing}
onChange={onTypeChangeHandler}
value={type}
data-testid="channel-type-select"
>
<Select.Option value="slack" key="slack" data-testid="select-option">
Slack
</Select.Option>
<Select.Option value="webhook" key="webhook">
<Select.Option value="webhook" key="webhook" data-testid="select-option">
Webhook
</Select.Option>
<Select.Option value="pagerduty" key="pagerduty">
<Select.Option
value="pagerduty"
key="pagerduty"
data-testid="select-option"
>
Pagerduty
</Select.Option>
<Select.Option value="opsgenie" key="opsgenie">
<Select.Option
value="opsgenie"
key="opsgenie"
data-testid="select-option"
>
Opsgenie
</Select.Option>
<Select.Option value="email" key="email">
<Select.Option value="email" key="email" data-testid="select-option">
Email
</Select.Option>
{!isOssFeature?.active && (
<Select.Option value="msteams" key="msteams">
<Select.Option value="msteams" key="msteams" data-testid="select-option">
<div>
Microsoft Teams {!isUserOnEEPlan && '(Supported in Paid Plans Only)'}{' '}
</div>

View File

@@ -13,8 +13,8 @@ import {
import saveAlertApi from 'api/alerts/save';
import testAlertApi from 'api/alerts/testAlert';
import logEvent from 'api/common/logEvent';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import { alertHelpMessage } from 'components/facingIssueBtn/util';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { alertHelpMessage } from 'components/LaunchChatSupport/util';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
@@ -712,7 +712,7 @@ function FormAlertRules({
>
Check an example alert
</Button>
<FacingIssueBtn
<LaunchChatSupport
attributes={{
alert: alertDef?.alert,
alertType: alertDef?.alertType,

View File

@@ -5,7 +5,7 @@ import type { ColumnsType } from 'antd/es/table/interface';
import saveAlertApi from 'api/alerts/save';
import logEvent from 'api/common/logEvent';
import DropDown from 'components/DropDown/DropDown';
import { listAlertMessage } from 'components/facingIssueBtn/util';
import { listAlertMessage } from 'components/LaunchChatSupport/util';
import {
DynamicColumnsKey,
TableDataSource,

View File

@@ -43,10 +43,15 @@
background: var(--bg-ink-400);
cursor: pointer;
.dashboard-title {
color: var(--bg-vanilla-100);
}
.title-with-action {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
min-height: 24px;
@@ -55,6 +60,14 @@
align-items: center;
gap: 6px;
line-height: 20px;
width: 60%;
.dashboard-icon {
display: inline-block;
margin-top: 4px;
margin-right: 4px;
line-height: 20px;
}
.dot {
min-height: 6px;
@@ -62,6 +75,20 @@
border-radius: 50%;
}
.title {
color: var(--bg-vanilla-100);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: 20px;
letter-spacing: -0.07px;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.ant-typography {
color: var(--bg-vanilla-100);
font-size: var(--font-size-sm);
@@ -82,6 +109,45 @@
display: none;
}
}
.tags-with-actions {
display: flex;
align-items: center;
width: 40%;
justify-content: flex-end;
.dashboard-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-end;
.tag {
display: flex;
padding: 4px 8px;
justify-content: center;
align-items: center;
gap: 4px;
height: 28px;
border-radius: 20px;
border: 1px solid rgba(173, 127, 88, 0.2);
background: rgba(173, 127, 88, 0.1);
color: var(--bg-sienna-400);
text-align: center;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
margin-inline-end: 0px !important;
}
}
}
}
.dashboard-action-icon {
margin-left: 16px;
}
.dashboard-details {
@@ -521,35 +587,6 @@
cursor: pointer;
}
}
.tags-with-actions {
display: flex;
align-items: center;
.dashboard-tags {
display: flex;
.tag {
display: flex;
padding: 4px 8px;
justify-content: center;
align-items: center;
gap: 4px;
height: 28px;
border-radius: 20px;
border: 1px solid rgba(173, 127, 88, 0.2);
background: rgba(173, 127, 88, 0.1);
color: var(--bg-sienna-400);
text-align: center;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
}
}
.new-dashboard-menu {
@@ -677,13 +714,13 @@
.action-btn {
display: flex;
padding: 14px;
padding: 8px;
height: unset;
align-items: center;
gap: 6px;
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: normal;
@@ -702,12 +739,12 @@
.ant-typography {
display: flex;
padding: 14px;
padding: 12px 8px;
align-items: center;
gap: 6px;
color: var(--bg-cherry-400) !important;
font-family: Inter;
font-size: 14px;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: normal;
@@ -1048,6 +1085,10 @@
border: 1px solid var(--bg-vanilla-200);
background: var(--bg-vanilla-100);
.dashboard-title {
color: var(--bg-slate-300);
}
.title-with-action {
.dashboard-title {
.ant-typography {
@@ -1313,3 +1354,12 @@
}
}
}
.title-toolip {
.ant-tooltip-content {
.ant-tooltip-inner {
height: 400px;
overflow: auto;
}
}
}

View File

@@ -25,8 +25,8 @@ import logEvent from 'api/common/logEvent';
import createDashboard from 'api/dashboard/create';
import { AxiosError } from 'axios';
import cx from 'classnames';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import { dashboardListMessage } from 'components/facingIssueBtn/util';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { dashboardListMessage } from 'components/LaunchChatSupport/util';
import { ENTITY_VERSION_V4 } from 'constants/app';
import ROUTES from 'constants/routes';
import { Base64Icons } from 'container/NewDashboard/DashboardSettings/General/utils';
@@ -66,7 +66,7 @@ import {
} from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { generatePath } from 'react-router-dom';
import { generatePath, Link } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { AppState } from 'store/reducers';
import { Dashboard } from 'types/api/dashboard/getAll';
@@ -449,77 +449,94 @@ function DashboardsList(): JSX.Element {
<div className="dashboard-list-item" onClick={onClickHandler}>
<div className="title-with-action">
<div className="dashboard-title">
<img
src={dashboard?.image || Base64Icons[0]}
style={{ height: '14px', width: '14px' }}
alt="dashboard-image"
/>
<Typography.Text data-testid={`dashboard-title-${index}`}>
{dashboard.name}
</Typography.Text>
<Tooltip
title={dashboard?.name?.length > 50 ? dashboard?.name : ''}
placement="left"
overlayClassName="title-toolip"
>
<Typography.Text data-testid={`dashboard-title-${index}`}>
<Link to={getLink()} className="title">
<img
src={dashboard?.image || Base64Icons[0]}
style={{ height: '14px', width: '14px' }}
alt="dashboard-image"
className="dashboard-icon"
/>
{dashboard.name}
</Link>
</Typography.Text>
</Tooltip>
</div>
<div className="tags-with-actions">
{dashboard?.tags && dashboard.tags.length > 0 && (
<div className="dashboard-tags">
{dashboard.tags.map((tag) => (
{dashboard.tags.slice(0, 3).map((tag) => (
<Tag className="tag" key={tag}>
{tag}
</Tag>
))}
{dashboard.tags.length > 3 && (
<Tag className="tag" key={dashboard.tags[3]}>
+ <span> {dashboard.tags.length - 3} </span>
</Tag>
)}
</div>
)}
{action && (
<Popover
trigger="click"
content={
<div className="dashboard-action-content">
<section className="section-1">
<Button
type="text"
className="action-btn"
icon={<Expand size={14} />}
onClick={onClickHandler}
>
View
</Button>
<Button
type="text"
className="action-btn"
icon={<Link2 size={14} />}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
setCopy(`${window.location.origin}${getLink()}`);
}}
>
Copy Link
</Button>
</section>
<section className="section-2">
<DeleteButton
name={dashboard.name}
id={dashboard.id}
isLocked={dashboard.isLocked}
createdBy={dashboard.createdBy}
/>
</section>
</div>
}
placement="bottomRight"
arrow={false}
rootClassName="dashboard-actions"
>
<EllipsisVertical
size={14}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
}}
/>
</Popover>
)}
</div>
{action && (
<Popover
trigger="click"
content={
<div className="dashboard-action-content">
<section className="section-1">
<Button
type="text"
className="action-btn"
icon={<Expand size={12} />}
onClick={onClickHandler}
>
View
</Button>
<Button
type="text"
className="action-btn"
icon={<Link2 size={12} />}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
setCopy(`${window.location.origin}${getLink()}`);
}}
>
Copy Link
</Button>
</section>
<section className="section-2">
<DeleteButton
name={dashboard.name}
id={dashboard.id}
isLocked={dashboard.isLocked}
createdBy={dashboard.createdBy}
/>
</section>
</div>
}
placement="bottomRight"
arrow={false}
rootClassName="dashboard-actions"
>
<EllipsisVertical
className="dashboard-action-icon"
size={14}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
}}
/>
</Popover>
)}
</div>
<div className="dashboard-details">
<div className="dashboard-created-at">
@@ -647,7 +664,7 @@ function DashboardsList(): JSX.Element {
<Typography.Text className="subtitle">
Create and manage dashboards for your workspace.
</Typography.Text>
<FacingIssueBtn
<LaunchChatSupport
attributes={{
screen: 'Dashboard list page',
}}

View File

@@ -12,9 +12,11 @@ import {
Typography,
} from 'antd';
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { OptionsQuery } from 'container/OptionsMenu/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { ChevronDown, ChevronRight, Search } from 'lucide-react';
import { ReactNode, useState } from 'react';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { ActionItemProps } from './ActionItem';
@@ -23,6 +25,8 @@ import TableView from './TableView';
interface OverviewProps {
logData: ILog;
isListViewPanel?: boolean;
selectedOptions: OptionsQuery;
listViewPanelSelectedFields?: IField[] | null;
}
type Props = OverviewProps &
@@ -34,6 +38,8 @@ function Overview({
onAddToQuery,
onClickActionItem,
isListViewPanel = false,
selectedOptions,
listViewPanelSelectedFields,
}: Props): JSX.Element {
const [isWrapWord, setIsWrapWord] = useState<boolean>(true);
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(false);
@@ -200,6 +206,8 @@ function Overview({
fieldSearchInput={fieldSearchInput}
onClickActionItem={onClickActionItem}
isListViewPanel={isListViewPanel}
selectedOptions={selectedOptions}
listViewPanelSelectedFields={listViewPanelSelectedFields}
/>
</>
),
@@ -213,6 +221,7 @@ function Overview({
Overview.defaultProps = {
isListViewPanel: false,
listViewPanelSelectedFields: null,
};
export default Overview;

View File

@@ -6,17 +6,15 @@ import { LinkOutlined } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Button, Space, Spin, Tooltip, Tree, Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import cx from 'classnames';
import AddToQueryHOC, {
AddToQueryHOCProps,
} from 'components/Logs/AddToQueryHOC';
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
import { ResizeTable } from 'components/ResizeTable';
import { LOCALSTORAGE } from 'constants/localStorage';
import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { OptionsQuery } from 'container/OptionsMenu/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import history from 'lib/history';
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
@@ -29,12 +27,14 @@ import { generatePath } from 'react-router-dom';
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { ActionItemProps } from './ActionItem';
import FieldRenderer from './FieldRenderer';
import {
filterKeyForField,
findKeyPath,
flattenObject,
jsonToDataNodes,
recursiveParseJSON,
@@ -47,7 +47,9 @@ const RESTRICTED_FIELDS = ['timestamp'];
interface TableViewProps {
logData: ILog;
fieldSearchInput: string;
selectedOptions: OptionsQuery;
isListViewPanel?: boolean;
listViewPanelSelectedFields?: IField[] | null;
}
type Props = TableViewProps &
@@ -60,6 +62,8 @@ function TableView({
onAddToQuery,
onClickActionItem,
isListViewPanel = false,
selectedOptions,
listViewPanelSelectedFields,
}: Props): JSX.Element | null {
const dispatch = useDispatch<Dispatch<AppActions>>();
const [isfilterInLoading, setIsFilterInLoading] = useState<boolean>(false);
@@ -71,21 +75,31 @@ function TableView({
>({});
useEffect(() => {
const pinnedAttributesFromLocalStorage = getLocalStorageApi(
LOCALSTORAGE.PINNED_ATTRIBUTES,
);
const pinnedAttributes: Record<string, boolean> = {};
if (pinnedAttributesFromLocalStorage) {
try {
const parsedPinnedAttributes = JSON.parse(pinnedAttributesFromLocalStorage);
setPinnedAttributes(parsedPinnedAttributes);
} catch (e) {
console.error('Error parsing pinned attributes from local storgage');
}
if (isListViewPanel) {
listViewPanelSelectedFields?.forEach((val) => {
const path = findKeyPath(logData, val.name, '');
if (path) {
pinnedAttributes[path] = true;
}
});
} else {
setPinnedAttributes({});
selectedOptions.selectColumns.forEach((val) => {
const path = findKeyPath(logData, val.key, '');
if (path) {
pinnedAttributes[path] = true;
}
});
}
}, []);
setPinnedAttributes(pinnedAttributes);
}, [
logData,
selectedOptions.selectColumns,
listViewPanelSelectedFields,
isListViewPanel,
]);
const flattenLogData: Record<string, string> | null = useMemo(
() => (logData ? flattenObject(logData) : null),
@@ -103,19 +117,6 @@ function TableView({
}
};
const togglePinAttribute = (record: DataType): void => {
if (record) {
const newPinnedAttributes = { ...pinnedAttributes };
newPinnedAttributes[record.key] = !newPinnedAttributes[record.key];
setPinnedAttributes(newPinnedAttributes);
setLocalStorageApi(
LOCALSTORAGE.PINNED_ATTRIBUTES,
JSON.stringify(newPinnedAttributes),
);
}
};
const onClickHandler = (
operator: string,
fieldKey: string,
@@ -201,11 +202,8 @@ function TableView({
'pin-attribute-icon',
pinnedAttributes[record?.key] ? 'pinned' : '',
)}
onClick={(): void => {
togglePinAttribute(record);
}}
>
<Pin size={14} color={pinColor} />
{pinnedAttributes[record?.key] && <Pin size={14} color={pinColor} />}
</div>
</div>
);
@@ -380,6 +378,7 @@ function TableView({
TableView.defaultProps = {
isListViewPanel: false,
listViewPanelSelectedFields: null,
};
interface DataType {

View File

@@ -267,3 +267,27 @@ export const removeEscapeCharacters = (str: string): string =>
export function removeExtraSpaces(input: string): string {
return input.replace(/\s+/g, ' ').trim();
}
export function findKeyPath(
obj: AnyObject,
targetKey: string,
currentPath = '',
): string | null {
let finalPath = null;
Object.keys(obj).forEach((key) => {
const value = obj[key];
const newPath = currentPath ? `${currentPath}.${key}` : key;
if (key === targetKey) {
finalPath = newPath;
}
if (typeof value === 'object' && value !== null) {
const result = findKeyPath(value, targetKey, newPath);
if (result) {
finalPath = result;
}
}
});
return finalPath;
}

View File

@@ -0,0 +1,112 @@
import Login from 'container/Login';
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
const errorNotification = jest.fn();
jest.mock('hooks/useNotifications', () => ({
__esModule: true,
useNotifications: jest.fn(() => ({
notifications: {
error: errorNotification,
},
})),
}));
describe('Login Flow', () => {
test('Login form is rendered correctly', async () => {
render(<Login ssoerror="" jwt="" refreshjwt="" userId="" withPassword="" />);
const headingElement = screen.getByRole('heading', {
name: 'login_page_title',
});
expect(headingElement).toBeInTheDocument();
const textboxElement = screen.getByRole('textbox');
expect(textboxElement).toBeInTheDocument();
const buttonElement = screen.getByRole('button', {
name: 'button_initiate_login',
});
expect(buttonElement).toBeInTheDocument();
const noAccountPromptElement = screen.getByText('prompt_no_account');
expect(noAccountPromptElement).toBeInTheDocument();
});
test(`Display "invalid_email" if email is not provided`, async () => {
render(<Login ssoerror="" jwt="" refreshjwt="" userId="" withPassword="" />);
const buttonElement = screen.getByText('button_initiate_login');
fireEvent.click(buttonElement);
await waitFor(() =>
expect(errorNotification).toHaveBeenCalledWith({
message: 'invalid_email',
}),
);
});
test('Display invalid_config if invalid email is provided and next clicked', async () => {
render(<Login ssoerror="" jwt="" refreshjwt="" userId="" withPassword="" />);
const textboxElement = screen.getByRole('textbox');
fireEvent.change(textboxElement, {
target: { value: 'failEmail@signoz.io' },
});
const buttonElement = screen.getByRole('button', {
name: 'button_initiate_login',
});
fireEvent.click(buttonElement);
await waitFor(() =>
expect(errorNotification).toHaveBeenCalledWith({
message: 'invalid_config',
}),
);
});
test('providing shaheer@signoz.io as email and pressing next, should make the login_with_sso button visible', async () => {
render(<Login ssoerror="" jwt="" refreshjwt="" userId="" withPassword="" />);
act(() => {
fireEvent.change(screen.getByTestId('email'), {
target: { value: 'shaheer@signoz.io' },
});
fireEvent.click(screen.getByTestId('initiate_login'));
});
await waitFor(() => {
expect(screen.getByText('login_with_sso')).toBeInTheDocument();
});
});
test('Display email, password, forgot password if password=Y', () => {
render(<Login ssoerror="" jwt="" refreshjwt="" userId="" withPassword="Y" />);
const emailTextBox = screen.getByTestId('email');
expect(emailTextBox).toBeInTheDocument();
const passwordTextBox = screen.getByTestId('password');
expect(passwordTextBox).toBeInTheDocument();
const forgotPasswordLink = screen.getByText('forgot_password');
expect(forgotPasswordLink).toBeInTheDocument();
});
test('Display tooltip with "prompt_forgot_password" if forgot password is clicked while password=Y', async () => {
render(<Login ssoerror="" jwt="" refreshjwt="" userId="" withPassword="Y" />);
const forgotPasswordLink = screen.getByText('forgot_password');
act(() => {
fireEvent.mouseOver(forgotPasswordLink);
});
await waitFor(() => {
const forgotPasswordTooltip = screen.getByRole('tooltip', {
name: 'prompt_forgot_password',
});
expect(forgotPasswordLink).toBeInTheDocument();
expect(forgotPasswordTooltip).toBeInTheDocument();
});
});
});

View File

@@ -220,6 +220,7 @@ function Login({
<Input
type="email"
id="loginEmail"
data-testid="email"
required
placeholder={t('placeholder_email')}
autoFocus
@@ -231,7 +232,12 @@ function Login({
<ParentContainer>
<Label htmlFor="Password">{t('label_password')}</Label>
<FormContainer.Item name="password">
<Input.Password required id="currentPassword" disabled={isLoading} />
<Input.Password
required
id="currentPassword"
data-testid="password"
disabled={isLoading}
/>
</FormContainer.Item>
<Tooltip title={t('prompt_forgot_password')}>
<Typography.Link>{t('forgot_password')}</Typography.Link>
@@ -250,6 +256,7 @@ function Login({
loading={precheckInProcess}
type="primary"
onClick={onNextHandler}
data-testid="initiate_login"
>
{t('button_initiate_login')}
</Button>

View File

@@ -80,6 +80,36 @@
position: relative;
}
}
.query-stats {
display: flex;
align-items: center;
gap: 12px;
.rows {
color: var(--bg-vanilla-400);
font-family: 'Geist Mono';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
letter-spacing: 0.36px;
}
.divider {
width: 1px;
height: 14px;
background: #242834;
}
.time {
color: var(--bg-vanilla-400);
font-family: 'Geist Mono';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
letter-spacing: 0.36px;
}
}
}
.logs-actions-container {
@@ -149,6 +179,15 @@
background: var(--bg-robin-400);
}
}
.query-stats {
.rows {
color: var(--bg-ink-400);
}
.time {
color: var(--bg-ink-400);
}
}
}
}
}

View File

@@ -0,0 +1,4 @@
.query-status {
display: flex;
align-items: center;
}

View File

@@ -0,0 +1,42 @@
import './QueryStatus.styles.scss';
import { LoadingOutlined } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Spin } from 'antd';
import { CircleCheck } from 'lucide-react';
import React, { useMemo } from 'react';
interface IQueryStatusProps {
loading: boolean;
error: boolean;
success: boolean;
}
export default function QueryStatus(
props: IQueryStatusProps,
): React.ReactElement {
const { loading, error, success } = props;
const content = useMemo((): React.ReactElement => {
if (loading) {
return <Spin spinning size="small" indicator={<LoadingOutlined spin />} />;
}
if (error) {
return (
<img
src="/Icons/solid-x-circle.svg"
alt="header"
className="error"
style={{ height: '14px', width: '14px' }}
/>
);
}
if (success) {
return (
<CircleCheck className="success" size={14} fill={Color.BG_ROBIN_500} />
);
}
return <div />;
}, [error, loading, success]);
return <div className="query-status">{content}</div>;
}

View File

@@ -1,8 +1,10 @@
/* eslint-disable sonarjs/cognitive-complexity */
import './LogsExplorerViews.styles.scss';
import { Button } from 'antd';
import { Button, Typography } from 'antd';
import { getQueryStats, WsDataEvent } from 'api/common/getQueryStats';
import logEvent from 'api/common/logEvent';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { LOCALSTORAGE } from 'constants/localStorage';
@@ -48,7 +50,15 @@ import {
} from 'lodash-es';
import { Sliders } from 'lucide-react';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
memo,
MutableRefObject,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { AppState } from 'store/reducers';
@@ -69,12 +79,20 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
import { v4 } from 'uuid';
import QueryStatus from './QueryStatus';
function LogsExplorerViews({
selectedView,
showFrequencyChart,
setIsLoadingQueries,
listQueryKeyRef,
chartQueryKeyRef,
}: {
selectedView: SELECTED_VIEWS;
showFrequencyChart: boolean;
setIsLoadingQueries: React.Dispatch<React.SetStateAction<boolean>>;
listQueryKeyRef: MutableRefObject<any>;
chartQueryKeyRef: MutableRefObject<any>;
}): JSX.Element {
const { notifications } = useNotifications();
const history = useHistory();
@@ -116,6 +134,8 @@ function LogsExplorerViews({
const [logs, setLogs] = useState<ILog[]>([]);
const [requestData, setRequestData] = useState<Query | null>(null);
const [showFormatMenuItems, setShowFormatMenuItems] = useState(false);
const [queryId, setQueryId] = useState<string>(v4());
const [queryStats, setQueryStats] = useState<WsDataEvent>();
const handleAxisError = useAxiosError();
@@ -214,9 +234,18 @@ function LogsExplorerViews({
{
enabled: !!listChartQuery && panelType === PANEL_TYPES.LIST,
},
{},
undefined,
chartQueryKeyRef,
);
const { data, isLoading, isFetching, isError } = useGetExplorerQueryRange(
const {
data,
isLoading,
isFetching,
isError,
isSuccess,
} = useGetExplorerQueryRange(
requestData,
panelType,
DEFAULT_ENTITY_VERSION,
@@ -232,6 +261,11 @@ function LogsExplorerViews({
end: timeRange.end,
}),
},
undefined,
listQueryKeyRef,
{
...(!isEmpty(queryId) && { 'X-SIGNOZ-QUERY-ID': queryId }),
},
);
const getRequestData = useCallback(
@@ -318,6 +352,23 @@ function LogsExplorerViews({
],
);
useEffect(() => {
setQueryId(v4());
}, [isError, isSuccess]);
useEffect(() => {
if (
!isEmpty(queryId) &&
(isLoading || isFetching) &&
selectedPanelType !== PANEL_TYPES.LIST
) {
setQueryStats(undefined);
setTimeout(() => {
getQueryStats({ queryId, setData: setQueryStats });
}, 500);
}
}, [queryId, isLoading, isFetching, selectedPanelType]);
const logEventCalledRef = useRef(false);
useEffect(() => {
if (!logEventCalledRef.current && !isUndefined(data?.payload)) {
@@ -569,6 +620,25 @@ function LogsExplorerViews({
},
});
useEffect(() => {
if (
isLoading ||
isFetching ||
isLoadingListChartData ||
isFetchingListChartData
) {
setIsLoadingQueries(true);
} else {
setIsLoadingQueries(false);
}
}, [
isLoading,
isFetching,
isFetchingListChartData,
isLoadingListChartData,
setIsLoadingQueries,
]);
const flattenLogData = useMemo(
() =>
logs.map((log) => {
@@ -665,6 +735,30 @@ function LogsExplorerViews({
</div>
</div>
)}
{(selectedPanelType === PANEL_TYPES.TIME_SERIES ||
selectedPanelType === PANEL_TYPES.TABLE) && (
<div className="query-stats">
<QueryStatus
loading={isLoading || isFetching}
error={isError}
success={isSuccess}
/>
{queryStats?.read_rows && (
<Typography.Text className="rows">
{getYAxisFormattedValue(queryStats.read_rows?.toString(), 'short')}{' '}
rows
</Typography.Text>
)}
{queryStats?.elapsed_ms && (
<>
<div className="divider" />
<Typography.Text className="time">
{getYAxisFormattedValue(queryStats?.elapsed_ms?.toString(), 'ms')}
</Typography.Text>
</>
)}
</div>
)}
</div>
</div>

View File

@@ -46,6 +46,8 @@ jest.mock(
},
);
jest.mock('api/common/getQueryStats', () => jest.fn());
jest.mock('constants/panelTypes', () => ({
AVAILABLE_EXPORT_PANEL_TYPES: ['graph', 'table'],
}));
@@ -79,6 +81,9 @@ const renderer = (): RenderResult =>
<LogsExplorerViews
selectedView={SELECTED_VIEWS.SEARCH}
showFrequencyChart
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
/>
</VirtuosoMockContext.Provider>
</QueryBuilderProvider>

View File

@@ -245,6 +245,7 @@ function LogsPanelComponent({
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
isListViewPanel
listViewPanelSelectedFields={widget?.selectedLogFields}
/>
</>
);

View File

@@ -90,18 +90,23 @@ function PasswordContainer(): JSX.Element {
return (
<Card>
<Space direction="vertical" size="small">
<Typography.Title level={4} style={{ marginTop: 0 }}>
<Typography.Title
level={4}
style={{ marginTop: 0 }}
data-testid="change-password-header"
>
{t('change_password', {
ns: 'settings',
})}
</Typography.Title>
<Space direction="vertical">
<Typography>
<Typography data-testid="current-password-label">
{t('current_password', {
ns: 'settings',
})}
</Typography>
<Password
data-testid="current-password-textbox"
disabled={isLoading}
placeholder={defaultPlaceHolder}
onChange={(event): void => {
@@ -111,12 +116,13 @@ function PasswordContainer(): JSX.Element {
/>
</Space>
<Space direction="vertical">
<Typography>
<Typography data-testid="new-password-label">
{t('new_password', {
ns: 'settings',
})}
</Typography>
<Password
data-testid="new-password-textbox"
disabled={isLoading}
placeholder={defaultPlaceHolder}
onChange={(event): void => {
@@ -129,6 +135,7 @@ function PasswordContainer(): JSX.Element {
<Space>
{isPasswordPolicyError && (
<Typography.Paragraph
data-testid="validation-message"
style={{
color: '#D89614',
marginTop: '0.50rem',
@@ -143,8 +150,13 @@ function PasswordContainer(): JSX.Element {
loading={isLoading}
onClick={onChangePasswordClickHandler}
type="primary"
data-testid="update-password-button"
>
<Save size={12} style={{ marginRight: '8px' }} />{' '}
<Save
size={12}
style={{ marginRight: '8px' }}
data-testid="update-password-icon"
/>{' '}
{t('change_password', {
ns: 'settings',
})}

View File

@@ -86,8 +86,11 @@ function UserInfo(): JSX.Element {
<Flex gap={16}>
<Space>
<Typography className="userInfo-label">Name</Typography>
<Typography className="userInfo-label" data-testid="name-label">
Name
</Typography>
<NameInput
data-testid="name-textbox"
placeholder="Your Name"
onChange={(event): void => {
setChangedName(event.target.value);
@@ -102,6 +105,7 @@ function UserInfo(): JSX.Element {
loading={loading}
disabled={loading}
onClick={onClickUpdateHandler}
data-testid="update-name-button"
type="primary"
>
<PencilIcon size={12} /> Update
@@ -109,13 +113,29 @@ function UserInfo(): JSX.Element {
</Flex>
<Space>
<Typography className="userInfo-label"> Email </Typography>
<Input className="userInfo-value" value={user.email} disabled />
<Typography className="userInfo-label" data-testid="email-label">
{' '}
Email{' '}
</Typography>
<Input
className="userInfo-value"
data-testid="email-textbox"
value={user.email}
disabled
/>
</Space>
<Space>
<Typography className="userInfo-label"> Role </Typography>
<Input className="userInfo-value" value={role || ''} disabled />
<Typography className="userInfo-label" data-testid="role-label">
{' '}
Role{' '}
</Typography>
<Input
className="userInfo-value"
value={role || ''}
disabled
data-testid="role-textbox"
/>
</Space>
</Space>
</Card>

View File

@@ -0,0 +1,219 @@
import MySettingsContainer from 'container/MySettings';
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
const toggleThemeFunction = jest.fn();
jest.mock('hooks/useDarkMode', () => ({
__esModule: true,
useIsDarkMode: jest.fn(() => ({
toggleTheme: toggleThemeFunction,
})),
default: jest.fn(() => ({
toggleTheme: toggleThemeFunction,
})),
}));
const errorNotification = jest.fn();
const successNotification = jest.fn();
jest.mock('hooks/useNotifications', () => ({
__esModule: true,
useNotifications: jest.fn(() => ({
notifications: {
error: errorNotification,
success: successNotification,
},
})),
}));
enum ThemeOptions {
Dark = 'Dark',
Light = 'Light Beta',
}
describe('MySettings Flows', () => {
beforeEach(() => {
jest.clearAllMocks();
render(<MySettingsContainer />);
});
describe('Dark/Light Theme Switch', () => {
it('Should display Dark and Light theme buttons properly', async () => {
expect(screen.getByText('Dark')).toBeInTheDocument();
const darkThemeIcon = screen.getByTestId('dark-theme-icon');
expect(darkThemeIcon).toBeInTheDocument();
expect(darkThemeIcon.tagName).toBe('svg');
expect(screen.getByText('Light')).toBeInTheDocument();
const lightThemeIcon = screen.getByTestId('light-theme-icon');
expect(lightThemeIcon).toBeInTheDocument();
expect(lightThemeIcon.tagName).toBe('svg');
});
it('Should activate Dark and Light buttons on click', async () => {
const initialSelectedOption = screen.getByRole('radio', {
name: ThemeOptions.Dark,
});
expect(initialSelectedOption).toBeChecked();
const newThemeOption = screen.getByRole('radio', {
name: ThemeOptions.Light,
});
fireEvent.click(newThemeOption);
expect(newThemeOption).toBeChecked();
});
it('Should switch the them on clicking Light theme', async () => {
const lightThemeOption = screen.getByRole('radio', {
name: /light/i,
});
fireEvent.click(lightThemeOption);
await waitFor(() => {
expect(toggleThemeFunction).toBeCalled();
});
});
});
describe('User Details Form', () => {
it('Should properly display the User Details Form', () => {
const userDetailsHeader = screen.getByRole('heading', {
name: /user details/i,
});
const nameLabel = screen.getByTestId('name-label');
const nameTextbox = screen.getByTestId('name-textbox');
const updateNameButton = screen.getByTestId('update-name-button');
const emailLabel = screen.getByTestId('email-label');
const emailTextbox = screen.getByTestId('email-textbox');
const roleLabel = screen.getByTestId('role-label');
const roleTextbox = screen.getByTestId('role-textbox');
expect(userDetailsHeader).toBeInTheDocument();
expect(nameLabel).toBeInTheDocument();
expect(nameTextbox).toBeInTheDocument();
expect(updateNameButton).toBeInTheDocument();
expect(emailLabel).toBeInTheDocument();
expect(emailTextbox).toBeInTheDocument();
expect(roleLabel).toBeInTheDocument();
expect(roleTextbox).toBeInTheDocument();
});
it('Should update the name on clicking Update button', async () => {
const nameTextbox = screen.getByTestId('name-textbox');
const updateNameButton = screen.getByTestId('update-name-button');
act(() => {
fireEvent.change(nameTextbox, { target: { value: 'New Name' } });
});
fireEvent.click(updateNameButton);
await waitFor(() =>
expect(successNotification).toHaveBeenCalledWith({
message: 'success',
}),
);
});
});
describe('Reset password', () => {
let currentPasswordTextbox: Node | Window;
let newPasswordTextbox: Node | Window;
let submitButtonElement: HTMLElement;
beforeEach(() => {
currentPasswordTextbox = screen.getByTestId('current-password-textbox');
newPasswordTextbox = screen.getByTestId('new-password-textbox');
submitButtonElement = screen.getByTestId('update-password-button');
});
it('Should properly display the Password Reset Form', () => {
const passwordResetHeader = screen.getByTestId('change-password-header');
expect(passwordResetHeader).toBeInTheDocument();
const currentPasswordLabel = screen.getByTestId('current-password-label');
expect(currentPasswordLabel).toBeInTheDocument();
expect(currentPasswordTextbox).toBeInTheDocument();
const newPasswordLabel = screen.getByTestId('new-password-label');
expect(newPasswordLabel).toBeInTheDocument();
expect(newPasswordTextbox).toBeInTheDocument();
expect(submitButtonElement).toBeInTheDocument();
const savePasswordIcon = screen.getByTestId('update-password-icon');
expect(savePasswordIcon).toBeInTheDocument();
expect(savePasswordIcon.tagName).toBe('svg');
});
it('Should display validation error if password is less than 8 characters', async () => {
const currentPasswordTextbox = screen.getByTestId(
'current-password-textbox',
);
act(() => {
fireEvent.change(currentPasswordTextbox, { target: { value: '123' } });
});
const validationMessage = await screen.findByTestId('validation-message');
await waitFor(() => {
expect(validationMessage).toHaveTextContent(
'Password must a have minimum of 8 characters',
);
});
});
test("Should display 'inavlid credentials' error if different current and new passwords are provided", async () => {
act(() => {
fireEvent.change(currentPasswordTextbox, {
target: { value: '123456879' },
});
fireEvent.change(newPasswordTextbox, { target: { value: '123456789' } });
});
fireEvent.click(submitButtonElement);
await waitFor(() => expect(errorNotification).toHaveBeenCalled());
});
it('Should check if the "Change Password" button is disabled in case current / new password is less than 8 characters', () => {
act(() => {
fireEvent.change(currentPasswordTextbox, {
target: { value: '123' },
});
fireEvent.change(newPasswordTextbox, { target: { value: '123' } });
});
expect(submitButtonElement).toBeDisabled();
});
test("Should check if 'Change Password' button is enabled when password is at least 8 characters ", async () => {
expect(submitButtonElement).toBeDisabled();
act(() => {
fireEvent.change(currentPasswordTextbox, {
target: { value: '123456789' },
});
fireEvent.change(newPasswordTextbox, { target: { value: '1234567890' } });
});
expect(submitButtonElement).toBeEnabled();
});
test("Should check if 'Change Password' button is disabled when current and new passwords are the same ", async () => {
expect(submitButtonElement).toBeDisabled();
act(() => {
fireEvent.change(currentPasswordTextbox, {
target: { value: '123456789' },
});
fireEvent.change(newPasswordTextbox, { target: { value: '123456789' } });
});
expect(submitButtonElement).toBeDisabled();
});
});
});

View File

@@ -1,6 +1,6 @@
import './MySettings.styles.scss';
import { Button, Radio, RadioChangeEvent, Space, Typography } from 'antd';
import { Button, Radio, RadioChangeEvent, Space, Tag, Typography } from 'antd';
import { Logout } from 'api/utils';
import useThemeMode, { useIsDarkMode } from 'hooks/useDarkMode';
import { LogOut, Moon, Sun } from 'lucide-react';
@@ -17,7 +17,7 @@ function MySettings(): JSX.Element {
{
label: (
<div className="theme-option">
<Moon size={12} /> Dark{' '}
<Moon data-testid="dark-theme-icon" size={12} /> Dark{' '}
</div>
),
value: 'dark',
@@ -25,7 +25,8 @@ function MySettings(): JSX.Element {
{
label: (
<div className="theme-option">
<Sun size={12} /> Light{' '}
<Sun size={12} data-testid="light-theme-icon" /> Light{' '}
<Tag color="magenta">Beta</Tag>
</div>
),
value: 'light',
@@ -63,6 +64,7 @@ function MySettings(): JSX.Element {
value={theme}
optionType="button"
buttonStyle="solid"
data-testid="theme-selector"
/>
</div>
@@ -74,7 +76,12 @@ function MySettings(): JSX.Element {
<Password />
</div>
<Button className="flexBtn" onClick={(): void => Logout()} type="primary">
<Button
className="flexBtn"
onClick={(): void => Logout()}
type="primary"
data-testid="logout-button"
>
<LogOut size={12} /> Logout
</Button>
</Space>

View File

@@ -59,13 +59,16 @@
justify-content: space-between;
align-items: center;
margin-right: 16px;
box-sizing: border-box;
.dashboard-breadcrumbs {
width: 100%;
height: 48px;
padding: 16px;
display: flex;
gap: 6px;
align-items: center;
max-width: 100%;
.dashboard-btn {
display: flex;
@@ -99,6 +102,14 @@
line-height: 20px; /* 142.857% */
height: 20px;
max-width: calc(100% - 120px);
span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ant-btn-icon {
margin-inline-end: 4px;
}
@@ -110,14 +121,20 @@
}
}
.dashbord-details {
.dashboard-details {
display: flex;
justify-content: space-between;
gap: 8px;
padding: 16px 16px 0px 16px;
align-items: flex-start;
.left-section {
display: flex;
padding: 10px 0px 0px 16px;
flex-wrap: wrap;
align-items: center;
gap: 8px;
width: 45%;
.dashboard-title {
color: #fff;
@@ -128,13 +145,23 @@
line-height: 24px; /* 150% */
letter-spacing: -0.08px;
flex-shrink: 0;
flex: 1;
min-width: fit-content;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
.right-section {
display: flex;
width: 55%;
justify-content: flex-end;
flex-wrap: wrap;
align-items: center;
padding: 10px 16px 0px 0px;
gap: 14px;
.icons {
@@ -199,6 +226,8 @@
display: flex;
gap: 6px;
padding: 16px 16px 0px 16px;
flex-wrap: wrap;
.tag {
display: flex;
padding: 4px 8px;
@@ -219,7 +248,6 @@
}
}
.dashboard-description-section {
max-width: 957px;
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
@@ -578,7 +606,7 @@
}
}
.dashbord-details {
.dashboard-details {
.left-section {
.dashboard-title {
color: var(--bg-ink-300);

View File

@@ -12,8 +12,8 @@ import {
Typography,
} from 'antd';
import logEvent from 'api/common/logEvent';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import { dashboardHelpMessage } from 'components/facingIssueBtn/util';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { dashboardHelpMessage } from 'components/LaunchChatSupport/util';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
@@ -294,36 +294,35 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
>
Dashboard /
</Button>
<Button
type="text"
className="id-btn"
icon={
// eslint-disable-next-line jsx-a11y/img-redundant-alt
<img
src={image}
alt="dashboard-image"
style={{ height: '14px', width: '14px' }}
/>
}
>
<Button type="text" className="id-btn dashboard-name-btn">
<img
src={image}
alt="dashboard-icon"
style={{ height: '14px', width: '14px' }}
/>
{title}
</Button>
</section>
</div>
<section className="dashbord-details">
<section className="dashboard-details">
<div className="left-section">
<img
src={image}
alt="dashboard-img"
style={{ width: '16px', height: '16px' }}
/>
<Typography.Text className="dashboard-title" data-testid="dashboard-title">
{title}
</Typography.Text>
<Tooltip title={title.length > 30 ? title : ''}>
<Typography.Text
className="dashboard-title"
data-testid="dashboard-title"
>
<img
src={image}
alt="dashboard-img"
style={{ width: '16px', height: '16px' }}
/>{' '}
{title}
</Typography.Text>
</Tooltip>
{isDashboardLocked && <LockKeyhole size={14} />}
</div>
<div className="right-section">
<FacingIssueBtn
<LaunchChatSupport
attributes={{
uuid: selectedDashboard?.uuid,
title: updatedTitle,

View File

@@ -4,7 +4,7 @@ import { Color } from '@signozhq/design-tokens';
import { Button, Tabs, Tooltip, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import PromQLIcon from 'assets/Dashboard/PromQl';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import TextToolTip from 'components/TextToolTip';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
@@ -237,7 +237,7 @@ function QuerySection({
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<FacingIssueBtn
<LaunchChatSupport
attributes={{
uuid: selectedDashboard?.uuid,
title: selectedDashboard?.data.title,

View File

@@ -8,7 +8,7 @@ import {
listViewInitialTraceQuery,
PANEL_TYPES_INITIAL_QUERY,
} from 'container/NewDashboard/ComponentsSlider/constants';
import { isEqual, set, unset } from 'lodash-es';
import { cloneDeep, isEqual, set, unset } from 'lodash-es';
import { Widgets } from 'types/api/dashboard/getAll';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
@@ -43,54 +43,59 @@ export const panelTypeDataSourceFormValuesMap: Record<
[DataSource.LOGS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'filters',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'queryName',
'expression',
'stepInterval',
'disabled',
'queryName',
'legend',
'expression',
],
},
},
[DataSource.METRICS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'timeAggregation',
'filters',
'spaceAggregation',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'spaceAggregation',
'queryName',
'expression',
'disabled',
'stepInterval',
'legend',
'queryName',
'disabled',
'functions',
'expression',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'filters',
'groupBy',
'limit',
'having',
'orderBy',
'queryName',
'expression',
'functions',
'stepInterval',
'disabled',
'queryName',
'legend',
'expression',
],
},
},
@@ -99,54 +104,59 @@ export const panelTypeDataSourceFormValuesMap: Record<
[DataSource.LOGS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'filters',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'queryName',
'expression',
'stepInterval',
'disabled',
'queryName',
'legend',
'expression',
],
},
},
[DataSource.METRICS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'timeAggregation',
'filters',
'spaceAggregation',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'spaceAggregation',
'queryName',
'expression',
'disabled',
'stepInterval',
'legend',
'queryName',
'disabled',
'functions',
'expression',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'filters',
'groupBy',
'limit',
'having',
'orderBy',
'queryName',
'expression',
'functions',
'stepInterval',
'disabled',
'queryName',
'legend',
'expression',
],
},
},
@@ -155,48 +165,59 @@ export const panelTypeDataSourceFormValuesMap: Record<
[DataSource.LOGS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'filters',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'stepInterval',
'disabled',
'queryName',
'legend',
'expression',
],
},
},
[DataSource.METRICS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'timeAggregation',
'filters',
'spaceAggregation',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'spaceAggregation',
'disabled',
'stepInterval',
'legend',
'queryName',
'disabled',
'functions',
'expression',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'filters',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'stepInterval',
'disabled',
'queryName',
'legend',
'expression',
],
},
},
@@ -205,18 +226,18 @@ export const panelTypeDataSourceFormValuesMap: Record<
[DataSource.LOGS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'filters',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'stepInterval',
'disabled',
'queryName',
'expression',
'disabled',
'reduceTo',
'legend',
],
},
@@ -224,37 +245,40 @@ export const panelTypeDataSourceFormValuesMap: Record<
[DataSource.METRICS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'timeAggregation',
'filters',
'spaceAggregation',
'groupBy',
'reduceTo',
'limit',
'having',
'orderBy',
'functions',
'spaceAggregation',
'stepInterval',
'legend',
'queryName',
'expression',
'disabled',
'reduceTo',
'legend',
'functions',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'filters',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'stepInterval',
'disabled',
'queryName',
'expression',
'disabled',
'reduceTo',
'legend',
],
},
@@ -264,17 +288,18 @@ export const panelTypeDataSourceFormValuesMap: Record<
[DataSource.LOGS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'filters',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'stepInterval',
'disabled',
'queryName',
'expression',
'disabled',
'legend',
],
},
@@ -282,35 +307,40 @@ export const panelTypeDataSourceFormValuesMap: Record<
[DataSource.METRICS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'timeAggregation',
'filters',
'spaceAggregation',
'groupBy',
'reduceTo',
'limit',
'having',
'orderBy',
'functions',
'spaceAggregation',
'stepInterval',
'legend',
'queryName',
'expression',
'disabled',
'legend',
'functions',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'filters',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'stepInterval',
'disabled',
'queryName',
'expression',
'disabled',
'legend',
],
},
@@ -319,7 +349,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
[PANEL_TYPES.LIST]: {
[DataSource.LOGS]: {
builder: {
queryData: ['filters', 'limit', 'orderBy'],
queryData: ['filters', 'limit', 'orderBy', 'functions'],
},
},
[DataSource.METRICS]: {
@@ -329,7 +359,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
},
[DataSource.TRACES]: {
builder: {
queryData: ['filters', 'limit', 'orderBy'],
queryData: ['filters', 'limit', 'orderBy', 'functions'],
},
},
},
@@ -337,12 +367,13 @@ export const panelTypeDataSourceFormValuesMap: Record<
[DataSource.LOGS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'filters',
'reduceTo',
'having',
'functions',
'stepInterval',
'queryName',
'expression',
'disabled',
@@ -353,30 +384,32 @@ export const panelTypeDataSourceFormValuesMap: Record<
[DataSource.METRICS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'aggregateOperator',
'timeAggregation',
'filters',
'spaceAggregation',
'having',
'reduceTo',
'functions',
'spaceAggregation',
'stepInterval',
'legend',
'queryName',
'expression',
'disabled',
'legend',
'functions',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'groupBy',
'limit',
'aggregateOperator',
'filters',
'reduceTo',
'having',
'orderBy',
'functions',
'stepInterval',
'queryName',
'expression',
'disabled',
@@ -396,12 +429,8 @@ export function handleQueryChange(
builder: {
...supersetQuery.builder,
queryData: supersetQuery.builder.queryData.map((query, index) => {
const { dataSource, expression, queryName } = query;
const tempQuery = {
...initialQueryBuilderFormValuesMap[dataSource],
expression,
queryName,
};
const { dataSource } = query;
const tempQuery = cloneDeep(initialQueryBuilderFormValuesMap[dataSource]);
const fieldsToSelect =
panelTypeDataSourceFormValuesMap[newPanelType][dataSource].builder
@@ -416,6 +445,8 @@ export function handleQueryChange(
set(tempQuery, 'offset', 0);
set(tempQuery, 'pageSize', 10);
} else if (tempQuery.aggregateOperator === 'noop') {
// this condition takes care of the part where we start with the list panel type and then shift to other panels
// because in other cases we never set list operator and other fields in superset query rather just update in the current / staged query
set(tempQuery, 'aggregateOperator', 'count');
unset(tempQuery, 'offset');
unset(tempQuery, 'pageSize');

View File

@@ -1,24 +1,43 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -1,24 +1,43 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -1,24 +1,43 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -1,24 +1,43 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -1,24 +1,43 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -1,24 +1,43 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -1,24 +1,43 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -1,24 +1,43 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -1,24 +1,43 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -1,24 +1,43 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -1,24 +1,43 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -1,24 +1,43 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -1,24 +1,43 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -1,24 +1,43 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -1,24 +1,43 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -1,24 +1,43 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -1,24 +1,43 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -1,24 +1,43 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -1,24 +1,43 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -1,24 +1,43 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -1,24 +1,43 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -1,24 +1,43 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -1,24 +1,47 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: aws
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{region}.signoz.cloud:443
otelInsecure: false
signozApiKey: <SIGNOZ_INGESTION_KEY>
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
resourceDetection:
detectors:
- eks
- system
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -5,17 +5,39 @@ Add the SigNoz Helm Chart repository
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
&nbsp;
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -1,24 +1,43 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -41,3 +41,37 @@ div[class*='-setup-instructions-container'] {
.service-name-container {
width: 100%;
}
.intgeration-page-container {
background-color: var(--bg-ink-400);
border-color: var(--bg-slate-500);
}
.intgeration-page-container-text {
color: var(--bg-vanilla-400);
}
.navigate-integrations-page-btn {
display: flex;
align-items: center;
color: var(--bg-vanilla-100);
background-color: var(--bg-slate-300);
box-shadow: none;
border: none;
}
.dataSourceName {
color: var(--bg-vanilla-100);
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
}
.lightMode {
.dataSourceName {
color: var(--bg-slate-500);
}
}

View File

@@ -6,6 +6,7 @@ import { LoadingOutlined } from '@ant-design/icons';
import { Button, Card, Form, Input, Select, Space, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import ROUTES from 'constants/routes';
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
import { useCases } from 'container/OnboardingContainer/OnboardingContainer';
import {
@@ -14,9 +15,10 @@ import {
hasFrameworks,
} from 'container/OnboardingContainer/utils/dataSourceUtils';
import { useNotifications } from 'hooks/useNotifications';
import { Check } from 'lucide-react';
import { Blocks, Check } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { popupContainer } from 'utils/selectPopupContainer';
export interface DataSourceType {
@@ -29,6 +31,7 @@ export interface DataSourceType {
export default function DataSource(): JSX.Element {
const [form] = Form.useForm();
const { t } = useTranslation(['common']);
const history = useHistory();
const {
serviceName,
@@ -127,6 +130,10 @@ export default function DataSource(): JSX.Element {
}
};
const goToIntegrationsPage = (): void => {
history.push(ROUTES.INTEGRATIONS);
};
return (
<div className="module-container">
<Typography.Text className="data-source-title">
@@ -156,7 +163,7 @@ export default function DataSource(): JSX.Element {
</div>
<div>
<Typography.Text className="serviceName">
<Typography.Text className="dataSourceName">
{dataSource.name}
</Typography.Text>
</div>
@@ -214,6 +221,20 @@ export default function DataSource(): JSX.Element {
</>
)}
<div className="request-entity-container intgeration-page-container">
<Typography.Text className="intgeration-page-container-text">
Not able to find datasources you are looking for, check our Integrations
page which allows more sources of sending data
</Typography.Text>
<Button
onClick={goToIntegrationsPage}
icon={<Blocks size={14} />}
className="navigate-integrations-page-btn"
>
Go to integrations
</Button>
</div>
<div className="request-entity-container">
<Typography.Text>
Cannot find what youre looking for? Request a data source

View File

@@ -18,6 +18,7 @@ export const defaultTraceSelectedColumns = [
isColumn: true,
isJSON: false,
id: 'serviceName--string--tag--true',
isIndexed: false,
},
{
key: 'name',
@@ -26,6 +27,7 @@ export const defaultTraceSelectedColumns = [
isColumn: true,
isJSON: false,
id: 'name--string--tag--true',
isIndexed: false,
},
{
key: 'durationNano',
@@ -34,6 +36,7 @@ export const defaultTraceSelectedColumns = [
isColumn: true,
isJSON: false,
id: 'durationNano--float64--tag--true',
isIndexed: false,
},
{
key: 'httpMethod',
@@ -42,6 +45,7 @@ export const defaultTraceSelectedColumns = [
isColumn: true,
isJSON: false,
id: 'httpMethod--string--tag--true',
isIndexed: false,
},
{
key: 'responseStatusCode',
@@ -50,5 +54,6 @@ export const defaultTraceSelectedColumns = [
isColumn: true,
isJSON: false,
id: 'responseStatusCode--string--tag--true',
isIndexed: false,
},
];

View File

@@ -544,19 +544,21 @@ export const Query = memo(function Query({
)}
</Col>
{isVersionV4 && isMetricsDataSource && panelType === PANEL_TYPES.TABLE && (
<Col flex="1 1 12.5rem">
<Row>
<Col span={6}>
<FilterLabel label="Reduce to" />
</Col>
{isVersionV4 &&
isMetricsDataSource &&
(panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.PIE) && (
<Col flex="1 1 12.5rem">
<Row>
<Col span={6}>
<FilterLabel label="Reduce to" />
</Col>
<Col span={18}>
<ReduceToFilter query={query} onChange={handleChangeReduceTo} />
</Col>
</Row>
</Col>
)}
<Col span={18}>
<ReduceToFilter query={query} onChange={handleChangeReduceTo} />
</Col>
</Row>
</Col>
)}
</Row>
</Col>
)}

View File

@@ -3,18 +3,27 @@ import './ToolbarActions.styles.scss';
import { Button } from 'antd';
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { Play } from 'lucide-react';
import { useEffect } from 'react';
import { Play, X } from 'lucide-react';
import { MutableRefObject, useEffect } from 'react';
import { useQueryClient } from 'react-query';
interface RightToolbarActionsProps {
onStageRunQuery: () => void;
isLoadingQueries?: boolean;
listQueryKeyRef?: MutableRefObject<any>;
chartQueryKeyRef?: MutableRefObject<any>;
}
export default function RightToolbarActions({
onStageRunQuery,
isLoadingQueries,
listQueryKeyRef,
chartQueryKeyRef,
}: RightToolbarActionsProps): JSX.Element {
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
const queryClient = useQueryClient();
useEffect(() => {
registerShortcut(LogsExplorerShortcuts.StageAndRunQuery, onStageRunQuery);
@@ -25,14 +34,41 @@ export default function RightToolbarActions({
}, [onStageRunQuery]);
return (
<div>
<Button
type="primary"
className="right-toolbar"
onClick={onStageRunQuery}
icon={<Play size={14} />}
>
Stage & Run Query
</Button>
{isLoadingQueries ? (
<div className="loading-container">
<Button className="loading-btn" loading={isLoadingQueries} />
<Button
icon={<X size={14} />}
className="cancel-run"
onClick={(): void => {
if (listQueryKeyRef?.current) {
queryClient.cancelQueries(listQueryKeyRef.current);
}
if (chartQueryKeyRef?.current) {
queryClient.cancelQueries(chartQueryKeyRef.current);
}
}}
>
Cancel Run
</Button>
</div>
) : (
<Button
type="primary"
className="right-toolbar"
disabled={isLoadingQueries}
onClick={onStageRunQuery}
icon={<Play size={14} />}
>
Stage & Run Query
</Button>
)}
</div>
);
}
RightToolbarActions.defaultProps = {
isLoadingQueries: false,
listQueryKeyRef: null,
chartQueryKeyRef: null,
};

View File

@@ -5,8 +5,8 @@
.left-toolbar-query-actions {
display: flex;
border-radius: 2px;
border: 1px solid var(--bg-slate-400, #1d212d);
background: var(--bg-ink-300, #16181d);
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
flex-direction: row;
.prom-ql-icon {
@@ -24,7 +24,7 @@
border-radius: 0;
&.active-tab {
background-color: #1d212d;
background-color: var(--bg-slate-400);
}
&:disabled {
@@ -33,7 +33,7 @@
}
}
.action-btn + .action-btn {
border-left: 1px solid var(--bg-slate-400, #1d212d);
border-left: 1px solid var(--bg-slate-400);
}
}
@@ -51,6 +51,50 @@
background-color: var(--bg-robin-600);
}
.right-actions {
display: flex;
align-items: center;
}
.loading-container {
display: flex;
gap: 8px;
align-items: center;
.loading-btn {
display: flex;
width: 32px;
height: 33px;
padding: 4px 10px;
justify-content: center;
align-items: center;
gap: 6px;
flex-shrink: 0;
border-radius: 2px;
background: var(--bg-slate-300);
box-shadow: none;
border: none;
}
.cancel-run {
display: flex;
height: 33px;
padding: 4px 10px;
justify-content: center;
align-items: center;
gap: 6px;
flex: 1 0 0;
border-radius: 2px;
background: var(--bg-cherry-500);
border-color: none;
}
.cancel-run:hover {
background-color: #ff7875 !important;
color: var(--bg-vanilla-100) !important;
border: none;
}
}
.lightMode {
.left-toolbar {
.left-toolbar-query-actions {
@@ -68,4 +112,17 @@
}
}
}
.loading-container {
.loading-btn {
background: var(--bg-vanilla-300);
}
.cancel-run {
color: var(--bg-vanilla-100);
}
.cancel-run:hover {
background-color: #ff7875;
}
}
}

View File

@@ -1,6 +1,7 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import LeftToolbarActions from '../LeftToolbarActions';
import RightToolbarActions from '../RightToolbarActions';
@@ -94,7 +95,9 @@ describe('ToolbarActions', () => {
it('RightToolbarActions - render correctly with props', async () => {
const onStageRunQuery = jest.fn();
const { queryByText } = render(
<RightToolbarActions onStageRunQuery={onStageRunQuery} />,
<MockQueryClientProvider>
<RightToolbarActions onStageRunQuery={onStageRunQuery} />,
</MockQueryClientProvider>,
);
const stageNRunBtn = queryByText('Stage & Run Query');

View File

@@ -0,0 +1,30 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './QueryBuilderSearch.styles.scss';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
function ExampleQueriesRendererForLogs({
label,
value,
handleAddTag,
}: ExampleQueriesRendererForLogsProps): JSX.Element {
return (
<div
className="example-query-container"
onClick={(): void => {
handleAddTag(value);
}}
>
<span className="example-query">{label}</span>
</div>
);
}
interface ExampleQueriesRendererForLogsProps {
label: string;
value: TagFilter;
handleAddTag: (value: TagFilter) => void;
}
export default ExampleQueriesRendererForLogs;

View File

@@ -0,0 +1,77 @@
import './QueryBuilderSearch.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Tooltip, Typography } from 'antd';
import cx from 'classnames';
import { Zap } from 'lucide-react';
import { useState } from 'react';
import { getOptionType } from './utils';
function OptionRendererForLogs({
label,
value,
dataType,
isIndexed,
setDynamicPlaceholder,
}: OptionRendererProps): JSX.Element {
const [truncated, setTruncated] = useState<boolean>(false);
const optionType = getOptionType(label);
return (
<span
className="option"
onMouseEnter={(): void => setDynamicPlaceholder(value)}
onFocus={(): void => setDynamicPlaceholder(value)}
>
{optionType ? (
<Tooltip title={truncated ? `${value}` : ''} placement="topLeft">
<div className="logs-options-select">
<section className="left-section">
{isIndexed ? (
<Zap size={12} fill={Color.BG_AMBER_500} />
) : (
<div className="dot" />
)}
<Typography.Text
className="text value"
ellipsis={{ onEllipsis: (ellipsis): void => setTruncated(ellipsis) }}
>
{value}
</Typography.Text>
</section>
<section className="right-section">
<div className="text tags data-type-tag">{dataType}</div>
<div className={cx('text tags option-type-tag', optionType)}>
<div className="dot" />
{optionType}
</div>
</section>
</div>
</Tooltip>
) : (
<Tooltip title={truncated ? `${label}` : ''} placement="topLeft">
<div className="without-option-type">
<div className="dot" />
<Typography.Text
className="text"
ellipsis={{ onEllipsis: (ellipsis): void => setTruncated(ellipsis) }}
>
{label}
</Typography.Text>
</div>
</Tooltip>
)}
</span>
);
}
interface OptionRendererProps {
label: string;
value: string;
dataType: string;
isIndexed: boolean;
setDynamicPlaceholder: React.Dispatch<React.SetStateAction<string>>;
}
export default OptionRendererForLogs;

View File

@@ -11,6 +11,290 @@
}
}
.logs-popup {
&.hide-scroll {
.rc-virtual-list-holder {
height: 100px;
}
}
}
.logs-explorer-popup {
padding: 0px;
.ant-select-item-group {
padding: 12px 14px 8px 14px;
color: var(--bg-slate-50);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 163.636% */
letter-spacing: 0.88px;
text-transform: uppercase;
}
.show-all-filter-props {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 13px;
width: 100%;
cursor: pointer;
.content {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.left-section {
display: flex;
align-items: center;
gap: 4px;
.text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.text:hover {
color: var(--bg-vanilla-100);
}
}
.right-section {
display: flex;
align-items: center;
gap: 4px;
.keyboard-shortcut-slash {
width: 16px;
height: 16px;
flex-shrink: 0;
border-radius: 2.286px;
border-top: 1.143px solid var(--bg-ink-200);
border-right: 1.143px solid var(--bg-ink-200);
border-bottom: 2.286px solid var(--bg-ink-200);
border-left: 1.143px solid var(--bg-ink-200);
background: var(--bg-ink-400);
}
}
}
}
.show-all-filter-props:hover {
background: rgba(255, 255, 255, 0.04) !important;
}
.example-queries {
cursor: default;
.heading {
padding: 12px 14px 8px 14px;
color: var(--bg-slate-50);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 163.636% */
letter-spacing: 0.88px;
text-transform: uppercase;
}
.query-container {
display: flex;
flex-direction: column;
gap: 12px;
padding: 0px 12px 12px 12px;
cursor: pointer;
.example-query {
display: flex;
padding: 4px 8px;
justify-content: center;
align-items: center;
gap: 10px;
border-radius: 2px;
background: var(--bg-ink-200);
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: -0.07px;
width: fit-content;
}
.example-query:hover {
color: var(--bg-vanilla-100);
}
}
}
.ant-select-item-option-grouped {
padding-inline-start: 0px;
padding: 7px 13px;
}
.keyboard-shortcuts {
display: flex;
align-items: center;
border-radius: 0px 0px 4px 4px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
padding: 11px 16px;
cursor: default;
.icons {
width: 16px;
height: 16px;
flex-shrink: 0;
border-radius: 2.286px;
border-top: 1.143px solid var(--Ink-200, #23262e);
border-right: 1.143px solid var(--Ink-200, #23262e);
border-bottom: 2.286px solid var(--Ink-200, #23262e);
border-left: 1.143px solid var(--Ink-200, #23262e);
background: var(--Ink-400, #121317);
}
.keyboard-text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 142.857% */
letter-spacing: -0.07px;
}
.navigate {
display: flex;
align-items: center;
padding-right: 12px;
gap: 4px;
border-right: 1px solid #1d212d;
}
.update-query {
display: flex;
align-items: center;
margin-left: 12px;
gap: 4px;
}
}
.without-option-type {
display: flex;
gap: 8px;
align-items: center;
.dot {
height: 5px;
width: 5px;
border-radius: 50%;
background-color: var(--bg-slate-300);
}
}
.logs-options-select {
display: flex;
align-items: center;
justify-content: space-between;
.text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.tags {
display: flex;
height: 20px;
padding: 4px 8px;
justify-content: center;
align-items: center;
gap: 4px;
border-radius: 20px;
}
.dot {
height: 5px;
width: 5px;
border-radius: 50%;
flex-shrink: 0;
}
.left-section {
display: flex;
align-items: center;
gap: 8px;
width: 90%;
.dot {
background-color: var(--bg-slate-300);
}
.value {
width: 100%;
}
}
.right-section {
display: flex;
align-items: center;
gap: 4px;
.data-type-tag {
background: rgba(255, 255, 255, 0.08);
}
.option-type-tag {
display: flex;
gap: 4px;
align-items: center;
padding: 0px 6px;
text-transform: capitalize;
}
.tag {
border-radius: 50px;
background: rgba(189, 153, 121, 0.1);
color: var(--bg-sienna-400);
.dot {
background-color: var(--bg-sienna-400);
}
}
.resource {
border-radius: 50px;
background: rgba(245, 108, 135, 0.1);
color: var(--bg-sakura-400);
.dot {
background-color: var(--bg-sakura-400);
}
}
}
}
.ant-select-item-option-active {
.logs-options-select {
.left-section {
.value {
color: var(--bg-vanilla-100);
}
}
}
}
}
.lightMode {
.query-builder-search {
.ant-select-dropdown {
@@ -21,4 +305,108 @@
background-color: var(--bg-vanilla-200) !important;
}
}
.logs-explorer-popup {
.ant-select-item-group {
color: var(--bg-slate-50);
}
.show-all-filter-props {
.content {
.left-section {
.text {
color: var(--bg-ink-400);
}
.text:hover {
color: var(--bg-slate-100);
}
}
.right-section {
.keyboard-shortcut-slash {
border-top: 1.143px solid var(--bg-ink-200);
border-right: 1.143px solid var(--bg-ink-200);
border-bottom: 2.286px solid var(--bg-ink-200);
border-left: 1.143px solid var(--bg-ink-200);
background: var(--bg-vanilla-200);
}
}
}
}
.show-all-filter-props:hover {
background: var(--bg-vanilla-200) !important;
}
.example-queries {
.heading {
color: var(--bg-slate-50);
}
.query-container {
.example-query-container {
.example-query {
background: var(--bg-vanilla-200);
color: var(--bg-ink-400);
}
.example-query:hover {
color: var(--bg-ink-400);
}
}
}
}
.keyboard-shortcuts {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-200);
.icons {
border-top: 1.143px solid var(--Ink-200, #23262e);
border-right: 1.143px solid var(--Ink-200, #23262e);
border-bottom: 2.286px solid var(--Ink-200, #23262e);
border-left: 1.143px solid var(--Ink-200, #23262e);
background: var(--bg-vanilla-200);
}
.keyboard-text {
color: var(--bg-ink-400);
}
.navigate {
border-right: 1px solid #1d212d;
}
}
.logs-options-select {
.text {
color: var(--bg-ink-400);
}
.right-section {
.data-type-tag {
background: var(--bg-vanilla-200);
}
.tag {
background: rgba(189, 153, 121, 0.1);
color: var(--bg-sienna-400);
}
.resource {
background: rgba(245, 108, 135, 0.1);
color: var(--bg-sakura-400);
}
}
}
.ant-select-item-option-active {
.logs-options-select {
.left-section {
.value {
color: var(--bg-ink-100);
}
}
}
}
}
}

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