Compare commits

..

1 Commits

Author SHA1 Message Date
Rahul Keswani
e31f58a338 chore: added unit tests for help and support section 2024-07-22 13:28:22 +05:30
402 changed files with 2033 additions and 18700 deletions

View File

@@ -30,7 +30,6 @@ jobs:
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
GCP_ZONE: ${{ secrets.GCP_ZONE }}
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
run: |
read -r -d '' COMMAND <<EOF || true
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
@@ -52,4 +51,4 @@ jobs:
make build-frontend-amd64
make run-testing
EOF
gcloud beta compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"
gcloud compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"

View File

@@ -30,7 +30,6 @@ jobs:
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
GCP_ZONE: ${{ secrets.GCP_ZONE }}
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
run: |
read -r -d '' COMMAND <<EOF || true
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
@@ -53,4 +52,4 @@ jobs:
make build-frontend-amd64
make run-testing
EOF
gcloud beta compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"
gcloud compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"

View File

@@ -211,7 +211,6 @@ services:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
- ./otel-collector-opamp-config.yaml:/etc/manager-config.yaml
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /:/hostfs:ro
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}},dockerswarm.service.name={{.Service.Name}},dockerswarm.task.name={{.Task.Name}}
- DOCKER_MULTI_NODE_CLUSTER=false

View File

@@ -36,7 +36,6 @@ receivers:
# endpoint: 0.0.0.0:6832
hostmetrics:
collection_interval: 30s
root_path: /hostfs
scrapers:
cpu: {}
load: {}

View File

@@ -93,8 +93,6 @@ services:
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
- ./otel-collector-opamp-config.yaml:/etc/manager-config.yaml
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /:/hostfs:ro
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
ports:

View File

@@ -244,7 +244,6 @@ services:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
- ./otel-collector-opamp-config.yaml:/etc/manager-config.yaml
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /:/hostfs:ro
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
- DOCKER_MULTI_NODE_CLUSTER=false

View File

@@ -243,7 +243,6 @@ services:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
- ./otel-collector-opamp-config.yaml:/etc/manager-config.yaml
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /:/hostfs:ro
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
- DOCKER_MULTI_NODE_CLUSTER=false

View File

@@ -36,7 +36,6 @@ receivers:
# endpoint: 0.0.0.0:6832
hostmetrics:
collection_interval: 30s
root_path: /hostfs
scrapers:
cpu: {}
load: {}

View File

@@ -1,9 +1,7 @@
package api
import (
"errors"
"net/http"
"strings"
"github.com/gorilla/mux"
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
@@ -31,10 +29,6 @@ func (ah *APIHandler) lockUnlockDashboard(w http.ResponseWriter, r *http.Request
// Get the dashboard UUID from the request
uuid := mux.Vars(r)["uuid"]
if strings.HasPrefix(uuid,"integration") {
RespondError(w, &model.ApiError{Typ: model.ErrorForbidden, Err: errors.New("dashboards created by integrations cannot be unlocked")}, "You are not authorized to lock/unlock this dashboard")
return
}
dashboard, err := dashboards.GetDashboard(r.Context(), uuid)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, err.Error())

View File

@@ -4,11 +4,11 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
_ "net/http/pprof" // http profiler
"os"
"regexp"
@@ -28,8 +28,6 @@ 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"
licensepkg "go.signoz.io/signoz/ee/query-service/license"
@@ -43,7 +41,6 @@ import (
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
"go.signoz.io/signoz/pkg/query-service/app/opamp"
opAmpModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model"
"go.signoz.io/signoz/pkg/query-service/app/preferences"
"go.signoz.io/signoz/pkg/query-service/cache"
baseconst "go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/healthcheck"
@@ -113,10 +110,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
baseexplorer.InitWithDSN(baseconst.RELATIONAL_DATASOURCE_PATH)
if err := preferences.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH); err != nil {
return nil, err
}
localDB, err := dashboards.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH)
if err != nil {
@@ -125,13 +118,33 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
localDB.SetMaxOpenConns(10)
gatewayProxy, err := gateway.NewProxy(serverOptions.GatewayUrl, gateway.RoutePrefix)
if err != nil {
return nil, err
gatewayFeature := basemodel.Feature{
Name: "GATEWAY",
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
}
//Activate this feature if the url is not empty
var gatewayProxy *httputil.ReverseProxy
if serverOptions.GatewayUrl == "" {
gatewayFeature.Active = false
gatewayProxy, err = gateway.NewNoopProxy()
if err != nil {
return nil, err
}
} else {
zap.L().Info("Enabling gateway feature flag ...")
gatewayFeature.Active = true
gatewayProxy, err = gateway.NewProxy(serverOptions.GatewayUrl, gateway.RoutePrefix)
if err != nil {
return nil, err
}
}
// initiate license manager
lm, err := licensepkg.StartManager("sqlite", localDB)
lm, err := licensepkg.StartManager("sqlite", localDB, gatewayFeature)
if err != nil {
return nil, err
}
@@ -180,13 +193,6 @@ 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 {
@@ -334,17 +340,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
// add auth middleware
getUserFromRequest := func(r *http.Request) (*basemodel.UserPayload, error) {
user, err := auth.GetUserFromRequest(r, apiHandler)
if err != nil {
return nil, err
}
if user.User.OrgId == "" {
return nil, model.UnauthorizedError(errors.New("orgId is missing in the claims"))
}
return user, nil
return auth.GetUserFromRequest(r, apiHandler)
}
am := baseapp.NewAuthMiddleware(getUserFromRequest)
@@ -736,7 +732,6 @@ func makeRulesManager(
DisableRules: disableRules,
FeatureFlags: fm,
Reader: ch,
EvalDelay: baseconst.GetEvalDelay(),
}
// create Manager

View File

@@ -20,14 +20,11 @@ import (
func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (*basemodel.User, basemodel.BaseApiError) {
// get auth domain from email domain
domain, apierr := m.GetDomainByEmail(ctx, email)
if apierr != nil {
zap.L().Error("failed to get domain from email", zap.Error(apierr))
return nil, model.InternalErrorStr("failed to get domain from email")
}
if domain == nil {
zap.L().Error("email domain does not match any authenticated domain", zap.String("email", email))
return nil, model.InternalErrorStr("email domain does not match any authenticated domain")
}
hash, err := baseauth.PasswordHash(utils.GeneratePassowrd())
if err != nil {

View File

@@ -5,5 +5,5 @@ import (
)
func NewNoopProxy() (*httputil.ReverseProxy, error) {
return &httputil.ReverseProxy{}, nil
return nil, nil
}

View File

@@ -11,7 +11,6 @@ const Enterprise = "ENTERPRISE_PLAN"
const DisableUpsell = "DISABLE_UPSELL"
const Onboarding = "ONBOARDING"
const ChatSupport = "CHAT_SUPPORT"
const Gateway = "GATEWAY"
var BasicPlan = basemodel.FeatureSet{
basemodel.Feature{
@@ -112,13 +111,6 @@ var BasicPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: Gateway,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}
var ProPlan = basemodel.FeatureSet{
@@ -213,13 +205,6 @@ var ProPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: Gateway,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}
var EnterprisePlan = basemodel.FeatureSet{
@@ -328,11 +313,4 @@ var EnterprisePlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: Gateway,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}

View File

@@ -110,8 +110,6 @@
"react-syntax-highlighter": "15.5.0",
"react-use": "^17.3.2",
"react-virtuoso": "4.0.3",
"overlayscrollbars-react": "^0.5.6",
"overlayscrollbars": "^2.8.1",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"rehype-raw": "7.0.0",

View File

@@ -76,8 +76,9 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
isUserFetching: false,
},
});
if (!isLoggedIn) {
history.push(ROUTES.LOGIN, { from: pathname });
history.push(ROUTES.LOGIN);
}
};

View File

@@ -1,7 +1,6 @@
import { ConfigProvider } from 'antd';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import NotFound from 'components/NotFound';
import Spinner from 'components/Spinner';
import { FeatureKeys } from 'constants/features';
@@ -49,7 +48,7 @@ function App(): JSX.Element {
const dispatch = useDispatch<Dispatch<AppActions>>();
const { trackPageView } = useAnalytics();
const { trackPageView, trackEvent } = useAnalytics();
const { hostname, pathname } = window.location;
@@ -200,7 +199,7 @@ function App(): JSX.Element {
LOCALSTORAGE.THEME_ANALYTICS_V1,
);
if (!isThemeAnalyticsSent) {
logEvent('Theme Analytics', {
trackEvent('Theme Analytics', {
theme: isDarkMode ? THEME_MODE.DARK : THEME_MODE.LIGHT,
user: pick(user, ['email', 'userId', 'name']),
org,

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;
const { data } = response as AxiosResponse;
if (statusCode >= 400 && statusCode < 500) {
const { data } = response as AxiosResponse;
if (statusCode === 404) {
return {
statusCode,
@@ -34,11 +34,12 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
body: JSON.stringify((response.data as any).data),
};
}
return {
statusCode,
payload: null,
error: 'Something went wrong',
message: data?.error,
message: null,
};
}
if (request) {

View File

@@ -3,7 +3,7 @@ const apiV1 = '/api/v1/';
export const apiV2 = '/api/v2/';
export const apiV3 = '/api/v3/';
export const apiV4 = '/api/v4/';
export const gatewayApiV1 = '/api/gateway/v1/';
export const apiAlertManager = '/api/alertmanager/';
export const gatewayApiV1 = '/api/gateway/v1';
export const apiAlertManager = '/api/alertmanager';
export default apiV1;

View File

@@ -1,4 +1,4 @@
import { ApiBaseInstance as axios } from 'api';
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
@@ -21,7 +21,6 @@ const logEvent = async (
payload: response.data.data,
};
} catch (error) {
console.error(error);
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -96,10 +96,6 @@ const interceptorRejected = async (
}
};
const interceptorRejectedBase = async (
value: AxiosResponse<any>,
): Promise<AxiosResponse<any>> => Promise.reject(value);
const instance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
});
@@ -144,18 +140,6 @@ ApiV4Instance.interceptors.response.use(
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
//
// axios Base
export const ApiBaseInstance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
});
ApiBaseInstance.interceptors.response.use(
interceptorsResponse,
interceptorRejectedBase,
);
ApiBaseInstance.interceptors.request.use(interceptorsRequestResponse);
//
// gateway Api V1
export const GatewayApiV1Instance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${gatewayApiV1}`,

View File

@@ -1,20 +1,7 @@
import axios from 'api';
import { isNil } from 'lodash-es';
interface GetTopLevelOperationsProps {
service?: string;
start?: number;
end?: number;
}
const getTopLevelOperations = async (
props: GetTopLevelOperationsProps,
): Promise<ServiceDataProps> => {
const response = await axios.post(`/service/top_level_operations`, {
start: !isNil(props.start) ? `${props.start}` : undefined,
end: !isNil(props.end) ? `${props.end}` : undefined,
service: props.service,
});
const getTopLevelOperations = async (): Promise<ServiceDataProps> => {
const response = await axios.post(`/service/top_level_operations`);
return response.data;
};

View File

@@ -1,7 +1,6 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { Dayjs } from 'dayjs';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { Recurrence } from './getAllDowntimeSchedules';
@@ -12,8 +11,8 @@ export interface DowntimeSchedulePayload {
alertIds: string[];
schedule: {
timezone?: string;
startTime?: string | Dayjs;
endTime?: string | Dayjs;
startTime?: string;
endTime?: string;
recurrence?: Recurrence;
};
}

View File

@@ -1,6 +1,6 @@
import axios from 'api';
import { AxiosError, AxiosResponse } from 'axios';
import { Option } from 'container/PlannedDowntime/PlannedDowntimeutils';
import { Option } from 'container/PlannedDowntime/DropdownWithSubMenu/DropdownWithSubMenu';
import { useQuery, UseQueryResult } from 'react-query';
export type Recurrence = {
@@ -28,7 +28,6 @@ export interface DowntimeSchedules {
createdBy: string | null;
updatedAt: string | null;
updatedBy: string | null;
kind: string | null;
}
export type PayloadProps = { data: DowntimeSchedules[] };

View File

@@ -28,17 +28,12 @@ export const SEVERITY_TEXT_TYPE = {
FATAL2: 'FATAL2',
FATAL3: 'FATAL3',
FATAL4: 'FATAL4',
UNKNOWN: 'UNKNOWN',
} as const;
export const LogType = {
TRACE: 'TRACE',
DEBUG: 'DEBUG',
INFO: 'INFO',
WARN: 'WARN',
WARNING: 'WARNING',
ERROR: 'ERROR',
FATAL: 'FATAL',
UNKNOWN: 'UNKNOWN',
} as const;
function LogStateIndicator({

View File

@@ -1,10 +1,9 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { ILog } from 'types/api/logs/log';
import { getLogIndicatorType, getLogIndicatorTypeForTable } from './utils';
describe('getLogIndicatorType', () => {
it('severity_number should be given priority over severity_text', () => {
it('should return severity type for valid log with severityText', () => {
const log = {
date: '2024-02-29T12:34:46Z',
timestamp: 1646115296,
@@ -21,57 +20,11 @@ describe('getLogIndicatorType', () => {
attributesInt: {},
attributesFloat: {},
severity_text: 'INFO',
severity_number: 2,
};
// severity_number should get priority over severity_text
expect(getLogIndicatorType(log)).toBe('TRACE');
expect(getLogIndicatorType(log)).toBe('INFO');
});
it('severity_text should be used when severity_number is absent ', () => {
const log = {
date: '2024-02-29T12:34:46Z',
timestamp: 1646115296,
id: '123456',
traceId: '987654',
spanId: '54321',
traceFlags: 0,
severityText: 'INFO',
severityNumber: 2,
body: 'Sample log Message',
resources_string: {},
attributesString: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
severity_text: 'FATAL',
severity_number: 0,
};
expect(getLogIndicatorType(log)).toBe('FATAL');
});
it('case insensitive severity_text should be valid', () => {
const log = {
date: '2024-02-29T12:34:46Z',
timestamp: 1646115296,
id: '123456',
traceId: '987654',
spanId: '54321',
traceFlags: 0,
severityText: 'INFO',
severityNumber: 2,
body: 'Sample log Message',
resources_string: {},
attributesString: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
severity_text: 'fatAl',
severity_number: 0,
};
expect(getLogIndicatorType(log)).toBe('FATAL');
});
it('should return log level if severityText and severityNumber is missing', () => {
it('should return log level if severityText is missing', () => {
const log: ILog = {
date: '2024-02-29T12:34:58Z',
timestamp: 1646115296,
@@ -83,16 +36,13 @@ describe('getLogIndicatorType', () => {
body: 'Sample log',
resources_string: {},
attributesString: {},
attributes_string: {
log_level: 'INFO' as never,
},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
severity_text: 'some_random',
severity_text: 'FATAL',
severityText: '',
severity_number: 0,
};
expect(getLogIndicatorType(log)).toBe('INFO');
expect(getLogIndicatorType(log)).toBe('FATAL');
});
});
@@ -105,7 +55,6 @@ describe('getLogIndicatorTypeForTable', () => {
traceId: '987654',
spanId: '54321',
traceFlags: 0,
severityNumber: 2,
severity_number: 2,
body: 'Sample log message',
resources_string: {},
@@ -115,7 +64,7 @@ describe('getLogIndicatorTypeForTable', () => {
attributesFloat: {},
severity_text: 'WARN',
};
expect(getLogIndicatorTypeForTable(log)).toBe('TRACE');
expect(getLogIndicatorTypeForTable(log)).toBe('WARN');
});
it('should return log level if severityText is missing', () => {
@@ -126,8 +75,7 @@ describe('getLogIndicatorTypeForTable', () => {
traceId: '987654',
spanId: '54321',
traceFlags: 0,
severityNumber: 0,
severity_number: 0,
severityNumber: 2,
body: 'Sample log message',
resources_string: {},
attributesString: {},
@@ -139,47 +87,3 @@ describe('getLogIndicatorTypeForTable', () => {
expect(getLogIndicatorTypeForTable(log)).toBe('INFO');
});
});
describe('logIndicatorBySeverityNumber', () => {
// https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber
const logLevelExpectations = [
{ minSevNumber: 1, maxSevNumber: 4, expectedIndicatorType: 'TRACE' },
{ minSevNumber: 5, maxSevNumber: 8, expectedIndicatorType: 'DEBUG' },
{ minSevNumber: 9, maxSevNumber: 12, expectedIndicatorType: 'INFO' },
{ minSevNumber: 13, maxSevNumber: 16, expectedIndicatorType: 'WARN' },
{ minSevNumber: 17, maxSevNumber: 20, expectedIndicatorType: 'ERROR' },
{ minSevNumber: 21, maxSevNumber: 24, expectedIndicatorType: 'FATAL' },
];
logLevelExpectations.forEach((e) => {
for (let sevNum = e.minSevNumber; sevNum <= e.maxSevNumber; sevNum++) {
const sevText = (Math.random() + 1).toString(36).substring(2);
const log = {
date: '2024-02-29T12:34:46Z',
timestamp: 1646115296,
id: '123456',
traceId: '987654',
spanId: '54321',
traceFlags: 0,
severityText: sevText,
severityNumber: sevNum,
body: 'Sample log Message',
resources_string: {},
attributesString: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
severity_text: sevText,
severity_number: sevNum,
};
it(`getLogIndicatorType should return ${e.expectedIndicatorType} for severity_text: ${sevText} and severity_number: ${sevNum}`, () => {
expect(getLogIndicatorType(log)).toBe(e.expectedIndicatorType);
});
it(`getLogIndicatorTypeForTable should return ${e.expectedIndicatorType} for severity_text: ${sevText} and severity_number: ${sevNum}`, () => {
expect(getLogIndicatorTypeForTable(log)).toBe(e.expectedIndicatorType);
});
}
});
});

View File

@@ -2,112 +2,56 @@ import { ILog } from 'types/api/logs/log';
import { LogType, SEVERITY_TEXT_TYPE } from './LogStateIndicator';
const getLogTypeBySeverityText = (severityText: string): string => {
const getSeverityType = (severityText: string): string => {
switch (severityText) {
case SEVERITY_TEXT_TYPE.TRACE:
case SEVERITY_TEXT_TYPE.TRACE2:
case SEVERITY_TEXT_TYPE.TRACE3:
case SEVERITY_TEXT_TYPE.TRACE4:
return LogType.TRACE;
return SEVERITY_TEXT_TYPE.TRACE;
case SEVERITY_TEXT_TYPE.DEBUG:
case SEVERITY_TEXT_TYPE.DEBUG2:
case SEVERITY_TEXT_TYPE.DEBUG3:
case SEVERITY_TEXT_TYPE.DEBUG4:
return LogType.DEBUG;
return SEVERITY_TEXT_TYPE.DEBUG;
case SEVERITY_TEXT_TYPE.INFO:
case SEVERITY_TEXT_TYPE.INFO2:
case SEVERITY_TEXT_TYPE.INFO3:
case SEVERITY_TEXT_TYPE.INFO4:
return LogType.INFO;
return SEVERITY_TEXT_TYPE.INFO;
case SEVERITY_TEXT_TYPE.WARN:
case SEVERITY_TEXT_TYPE.WARN2:
case SEVERITY_TEXT_TYPE.WARN3:
case SEVERITY_TEXT_TYPE.WARN4:
case SEVERITY_TEXT_TYPE.WARNING:
return LogType.WARN;
return SEVERITY_TEXT_TYPE.WARN;
case SEVERITY_TEXT_TYPE.ERROR:
case SEVERITY_TEXT_TYPE.ERROR2:
case SEVERITY_TEXT_TYPE.ERROR3:
case SEVERITY_TEXT_TYPE.ERROR4:
return LogType.ERROR;
return SEVERITY_TEXT_TYPE.ERROR;
case SEVERITY_TEXT_TYPE.FATAL:
case SEVERITY_TEXT_TYPE.FATAL2:
case SEVERITY_TEXT_TYPE.FATAL3:
case SEVERITY_TEXT_TYPE.FATAL4:
return LogType.FATAL;
return SEVERITY_TEXT_TYPE.FATAL;
default:
return LogType.UNKNOWN;
return SEVERITY_TEXT_TYPE.INFO;
}
};
// https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber
const getLogTypeBySeverityNumber = (severityNumber: number): string => {
if (severityNumber < 1) {
return LogType.UNKNOWN;
}
if (severityNumber < 5) {
return LogType.TRACE;
}
if (severityNumber < 9) {
return LogType.DEBUG;
}
if (severityNumber < 13) {
return LogType.INFO;
}
if (severityNumber < 17) {
return LogType.WARN;
}
if (severityNumber < 21) {
return LogType.ERROR;
}
if (severityNumber < 25) {
return LogType.FATAL;
}
return LogType.UNKNOWN;
};
const getLogType = (
severityText: string,
severityNumber: number,
defaultType: string,
): string => {
// give priority to the severityNumber
if (severityNumber) {
const logType = getLogTypeBySeverityNumber(severityNumber);
if (logType !== LogType.UNKNOWN) {
return logType;
}
}
// is severityNumber is not present then rely on the severityText
if (severityText) {
const logType = getLogTypeBySeverityText(severityText);
if (logType !== LogType.UNKNOWN) {
return logType;
}
}
return defaultType;
};
export const getLogIndicatorType = (logData: ILog): string => {
const defaultType = logData.attributes_string?.log_level || LogType.INFO;
// convert the severity_text to upper case for the comparison to support case insensitive values
return getLogType(
logData?.severity_text?.toUpperCase(),
logData?.severity_number || 0,
defaultType,
);
if (logData.severity_text) {
return getSeverityType(logData.severity_text);
}
return logData.attributes_string?.log_level || LogType.INFO;
};
export const getLogIndicatorTypeForTable = (
log: Record<string, unknown>,
): string => {
const defaultType = (log.log_level as string) || LogType.INFO;
// convert the severity_text to upper case for the comparison to support case insensitive values
return getLogType(
(log?.severity_text as string)?.toUpperCase(),
(log?.severity_number as number) || 0,
defaultType,
);
if (log.severity_text) {
return getSeverityType(log.severity_text as string);
}
return (log.log_level as string) || LogType.INFO;
};

View File

@@ -1,54 +0,0 @@
import TypicalOverlayScrollbar from 'components/TypicalOverlayScrollbar/TypicalOverlayScrollbar';
import VirtuosoOverlayScrollbar from 'components/VirtuosoOverlayScrollbar/VirtuosoOverlayScrollbar';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { PartialOptions } from 'overlayscrollbars';
import { CSSProperties, ReactElement, useMemo } from 'react';
type Props = {
children: ReactElement;
isVirtuoso?: boolean;
style?: CSSProperties;
options?: PartialOptions;
};
function OverlayScrollbar({
children,
isVirtuoso,
style,
options: customOptions,
}: Props): any {
const isDarkMode = useIsDarkMode();
const options = useMemo(
() =>
({
scrollbars: {
autoHide: 'scroll',
theme: isDarkMode ? 'os-theme-light' : 'os-theme-dark',
},
...(customOptions || {}),
} as PartialOptions),
[customOptions, isDarkMode],
);
if (isVirtuoso) {
return (
<VirtuosoOverlayScrollbar style={style} options={options}>
{children}
</VirtuosoOverlayScrollbar>
);
}
return (
<TypicalOverlayScrollbar style={style} options={options}>
{children}
</TypicalOverlayScrollbar>
);
}
OverlayScrollbar.defaultProps = {
isVirtuoso: false,
style: {},
options: {},
};
export default OverlayScrollbar;

View File

@@ -5,6 +5,7 @@ import { Button } from 'antd';
import { Tag } from 'antd/lib';
import Input from 'components/Input';
import { Check, X } from 'lucide-react';
import { TweenOneGroup } from 'rc-tween-one';
import React, { Dispatch, SetStateAction, useState } from 'react';
function Tags({ tags, setTags }: AddTagsProps): JSX.Element {
@@ -45,19 +46,41 @@ function Tags({ tags, setTags }: AddTagsProps): JSX.Element {
func(value);
};
const forMap = (tag: string): React.ReactElement => (
<span key={tag} style={{ display: 'inline-block' }}>
<Tag
closable
onClose={(e): void => {
e.preventDefault();
handleClose(tag);
}}
>
{tag}
</Tag>
</span>
);
const tagChild = tags.map(forMap);
const renderTagsAnimated = (): React.ReactElement => (
<TweenOneGroup
appear={false}
className="tags"
enter={{ scale: 0.8, opacity: 0, type: 'from', duration: 100 }}
leave={{ opacity: 0, width: 0, scale: 0, duration: 200 }}
onEnd={(e): void => {
if (e.type === 'appear' || e.type === 'enter') {
(e.target as any).style = 'display: inline-block';
}
}}
>
{tagChild}
</TweenOneGroup>
);
return (
<div className="tags-container">
{tags.map<React.ReactNode>((tag) => (
<Tag
key={tag}
closable
style={{ userSelect: 'none' }}
onClose={(): void => handleClose(tag)}
>
<span>{tag}</span>
</Tag>
))}
{renderTagsAnimated()}
{inputVisible && (
<div className="add-tag-container">
<Input

View File

@@ -1,31 +0,0 @@
import './typicalOverlayScrollbar.scss';
import { PartialOptions } from 'overlayscrollbars';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { CSSProperties, ReactElement } from 'react';
interface Props {
children: ReactElement;
style?: CSSProperties;
options?: PartialOptions;
}
export default function TypicalOverlayScrollbar({
children,
style,
options,
}: Props): ReturnType<typeof OverlayScrollbarsComponent> {
return (
<OverlayScrollbarsComponent
defer
options={options}
style={style}
className="overlay-scrollbar"
data-overlayscrollbars-initialize
>
{children}
</OverlayScrollbarsComponent>
);
}
TypicalOverlayScrollbar.defaultProps = { style: {}, options: {} };

View File

@@ -1,3 +0,0 @@
.overlay-scrollbar {
height: 100%;
}

View File

@@ -49,10 +49,7 @@ function ValueGraph({
}
>
<Tooltip title={t('this_value_satisfies_multiple_thresholds')}>
<ExclamationCircleFilled
className="value-graph-icon"
data-testid="conflicting-thresholds"
/>
<ExclamationCircleFilled className="value-graph-icon" />
</Tooltip>
</div>
)}

View File

@@ -1,37 +0,0 @@
import './virtuosoOverlayScrollbar.scss';
import useInitializeOverlayScrollbar from 'hooks/useInitializeOverlayScrollbar/useInitializeOverlayScrollbar';
import { PartialOptions } from 'overlayscrollbars';
import React, { CSSProperties, ReactElement } from 'react';
interface VirtuosoOverlayScrollbarProps {
children: ReactElement;
style?: CSSProperties;
options: PartialOptions;
}
export default function VirtuosoOverlayScrollbar({
children,
style,
options,
}: VirtuosoOverlayScrollbarProps): JSX.Element {
const { rootRef, setScroller } = useInitializeOverlayScrollbar(options);
const enhancedChild = React.cloneElement(children, {
scrollerRef: setScroller,
'data-overlayscrollbars-initialize': true,
});
return (
<div
data-overlayscrollbars-initialize
ref={rootRef}
className="overlay-scroll-wrapper"
style={style}
>
{enhancedChild}
</div>
);
}
VirtuosoOverlayScrollbar.defaultProps = { style: {} };

View File

@@ -1,5 +0,0 @@
.overlay-scroll-wrapper {
height: 100%;
width: 100%;
overflow: auto;
}

View File

@@ -16,7 +16,6 @@ export interface FacingIssueBtnProps {
buttonText?: string;
className?: string;
onHoverText?: string;
intercomMessageDisabled?: boolean;
}
function FacingIssueBtn({
@@ -26,12 +25,11 @@ function FacingIssueBtn({
buttonText = '',
className = '',
onHoverText = '',
intercomMessageDisabled = false,
}: FacingIssueBtnProps): JSX.Element | null {
const handleFacingIssuesClick = (): void => {
logEvent(eventName, attributes);
if (window.Intercom && !intercomMessageDisabled) {
if (window.Intercom) {
window.Intercom('showNewMessage', defaultTo(message, ''));
}
};
@@ -64,7 +62,6 @@ FacingIssueBtn.defaultProps = {
buttonText: '',
className: '',
onHoverText: '',
intercomMessageDisabled: false,
};
export default FacingIssueBtn;

View File

@@ -19,5 +19,6 @@ export enum FeatureKeys {
OSS = 'OSS',
ONBOARDING = 'ONBOARDING',
CHAT_SUPPORT = 'CHAT_SUPPORT',
PLANNED_MAINTENANCE = 'PLANNED_MAINTENANCE',
GATEWAY = 'GATEWAY',
}

View File

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

View File

@@ -1,78 +0,0 @@
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

@@ -1,72 +0,0 @@
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

@@ -1,424 +0,0 @@
/* 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

@@ -1,348 +0,0 @@
/* 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

@@ -1,118 +0,0 @@
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

@@ -1,31 +0,0 @@
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

@@ -12,7 +12,6 @@ import { ColumnType, TablePaginationConfig } from 'antd/es/table';
import { FilterValue, SorterResult } from 'antd/es/table/interface';
import { ColumnsType } from 'antd/lib/table';
import { FilterConfirmProps } from 'antd/lib/table/interface';
import logEvent from 'api/common/logEvent';
import getAll from 'api/errors/getAll';
import getErrorCounts from 'api/errors/getErrorCounts';
import { ResizeTable } from 'components/ResizeTable';
@@ -24,8 +23,7 @@ import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute
import useUrlQuery from 'hooks/useUrlQuery';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import { isUndefined } from 'lodash-es';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueries } from 'react-query';
import { useSelector } from 'react-redux';
@@ -412,26 +410,6 @@ function AllErrors(): JSX.Element {
[pathname],
);
const logEventCalledRef = useRef(false);
useEffect(() => {
if (
!logEventCalledRef.current &&
!isUndefined(errorCountResponse.data?.payload)
) {
const selectedEnvironments = queries.find(
(val) => val.tagKey === 'resource_deployment_environment',
)?.tagValue;
logEvent('Exception: List page visited', {
numberOfExceptions: errorCountResponse?.data?.payload,
selectedEnvironments,
resourceAttributeUsed: !!queries?.length,
});
logEventCalledRef.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [errorCountResponse.data?.payload]);
return (
<ResizeTable
columns={columns}

View File

@@ -5,6 +5,7 @@
.app-content {
width: calc(100% - 64px);
overflow: auto;
z-index: 0;
.content-container {

View File

@@ -9,7 +9,6 @@ import getLocalStorageKey from 'api/browser/localstorage/get';
import getUserLatestVersion from 'api/user/getLatestVersion';
import getUserVersion from 'api/user/getVersion';
import cx from 'classnames';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
import ROUTES from 'constants/routes';
import SideNav from 'container/SideNav';
@@ -304,29 +303,24 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
collapsed={collapsed}
/>
)}
<div
className={cx('app-content', collapsed ? 'collapsed' : '')}
data-overlayscrollbars-initialize
>
<div className={cx('app-content', collapsed ? 'collapsed' : '')}>
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<LayoutContent data-overlayscrollbars-initialize>
<OverlayScrollbar>
<ChildrenContainer
style={{
margin:
isLogsView() ||
isTracesView() ||
isDashboardView() ||
isDashboardWidgetView() ||
isDashboardListView()
? 0
: '0 1rem',
}}
>
{isToDisplayLayout && !renderFullScreen && <TopNav />}
{children}
</ChildrenContainer>
</OverlayScrollbar>
<LayoutContent>
<ChildrenContainer
style={{
margin:
isLogsView() ||
isTracesView() ||
isDashboardView() ||
isDashboardWidgetView() ||
isDashboardListView()
? 0
: '0 1rem',
}}
>
{isToDisplayLayout && !renderFullScreen && <TopNav />}
{children}
</ChildrenContainer>
</LayoutContent>
</Sentry.ErrorBoundary>
</div>

View File

@@ -13,6 +13,7 @@ export const Layout = styled(LayoutComponent)`
`;
export const LayoutContent = styled(LayoutComponent.Content)`
overflow-y: auto;
height: 100%;
&::-webkit-scrollbar {
width: 0.1rem;

View File

@@ -19,10 +19,10 @@ import { ColumnsType } from 'antd/es/table';
import updateCreditCardApi from 'api/billing/checkout';
import getUsage, { UsageResponsePayloadProps } from 'api/billing/getUsage';
import manageCreditCardApi from 'api/billing/manage';
import logEvent from 'api/common/logEvent';
import Spinner from 'components/Spinner';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import useAnalytics from 'hooks/analytics/useAnalytics';
import useAxiosError from 'hooks/useAxiosError';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
@@ -137,6 +137,8 @@ export default function BillingContainer(): JSX.Element {
Partial<UsageResponsePayloadProps>
>({});
const { trackEvent } = useAnalytics();
const { isFetching, data: licensesData, error: licenseError } = useLicense();
const { user, org } = useSelector<AppState, AppReducer>((state) => state.app);
@@ -314,7 +316,7 @@ export default function BillingContainer(): JSX.Element {
const handleBilling = useCallback(async () => {
if (isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription) {
logEvent('Billing : Upgrade Plan', {
trackEvent('Billing : Upgrade Plan', {
user: pick(user, ['email', 'userId', 'name']),
org,
});
@@ -325,7 +327,7 @@ export default function BillingContainer(): JSX.Element {
cancelURL: window.location.href,
});
} else {
logEvent('Billing : Manage Billing', {
trackEvent('Billing : Manage Billing', {
user: pick(user, ['email', 'userId', 'name']),
org,
});

View File

@@ -449,8 +449,8 @@ function CreateAlertChannels({
const result = await functionToCall();
logEvent('Alert Channel: Save channel', {
type: value,
sendResolvedAlert: selectedConfig?.send_resolved,
name: selectedConfig?.name,
sendResolvedAlert: selectedConfig.send_resolved,
name: selectedConfig.name,
new: 'true',
status: result?.status,
statusMessage: result?.statusMessage,
@@ -530,8 +530,8 @@ function CreateAlertChannels({
logEvent('Alert Channel: Test notification', {
type: channelType,
sendResolvedAlert: selectedConfig?.send_resolved,
name: selectedConfig?.name,
sendResolvedAlert: selectedConfig.send_resolved,
name: selectedConfig.name,
new: 'true',
status:
response && response.statusCode === 200 ? 'Test success' : 'Test failed',

View File

@@ -370,8 +370,8 @@ function EditAlertChannels({
}
logEvent('Alert Channel: Save channel', {
type: value,
sendResolvedAlert: selectedConfig?.send_resolved,
name: selectedConfig?.name,
sendResolvedAlert: selectedConfig.send_resolved,
name: selectedConfig.name,
new: 'false',
status: result?.status,
statusMessage: result?.statusMessage,
@@ -441,8 +441,8 @@ function EditAlertChannels({
}
logEvent('Alert Channel: Test notification', {
type: channelType,
sendResolvedAlert: selectedConfig?.send_resolved,
name: selectedConfig?.name,
sendResolvedAlert: selectedConfig.send_resolved,
name: selectedConfig.name,
new: 'false',
status:
response && response.statusCode === 200 ? 'Test success' : 'Test failed',

View File

@@ -1,34 +1,8 @@
import './EmptyLogsSearch.styles.scss';
import { Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { useEffect, useRef } from 'react';
import { DataSource, PanelTypeKeys } from 'types/common/queryBuilder';
export default function EmptyLogsSearch({
dataSource,
panelType,
}: {
dataSource: DataSource;
panelType: PanelTypeKeys;
}): JSX.Element {
const logEventCalledRef = useRef(false);
useEffect(() => {
if (!logEventCalledRef.current) {
if (dataSource === DataSource.TRACES) {
logEvent('Traces Explorer: No results', {
panelType,
});
} else if (dataSource === DataSource.LOGS) {
logEvent('Logs Explorer: No results', {
panelType,
});
}
logEventCalledRef.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
export default function EmptyLogsSearch(): JSX.Element {
return (
<div className="empty-logs-search-container">
<div className="empty-logs-search-container-content">

View File

@@ -1,7 +1,6 @@
import './styles.scss';
import { Button, Divider, Space, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import getNextPrevId from 'api/errors/getNextPrevId';
import Editor from 'components/Editor';
import { ResizeTable } from 'components/ResizeTable';
@@ -10,9 +9,8 @@ import dayjs from 'dayjs';
import { useNotifications } from 'hooks/useNotifications';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import { isUndefined } from 'lodash-es';
import { urlKey } from 'pages/ErrorDetails/utils';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { useLocation } from 'react-router-dom';
@@ -113,29 +111,9 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
}));
const onClickTraceHandler = (): void => {
logEvent('Exception: Navigate to trace detail page', {
groupId: errorDetail?.groupID,
spanId: errorDetail.spanID,
traceId: errorDetail.traceID,
exceptionId: errorDetail?.errorId,
});
history.push(`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`);
};
const logEventCalledRef = useRef(false);
useEffect(() => {
if (!logEventCalledRef.current && !isUndefined(data)) {
logEvent('Exception: Detail page visited', {
groupId: errorDetail?.groupID,
spanId: errorDetail.spanID,
traceId: errorDetail.traceID,
exceptionId: errorDetail?.errorId,
});
logEventCalledRef.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
return (
<>
<Typography>{errorDetail.exceptionType}</Typography>

View File

@@ -14,7 +14,6 @@ import {
Tooltip,
Typography,
} from 'antd';
import logEvent from 'api/common/logEvent';
import axios from 'axios';
import cx from 'classnames';
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
@@ -94,23 +93,7 @@ function ExplorerOptions({
setIsExport(value);
}, []);
const {
currentQuery,
panelType,
isStagedQueryUpdated,
redirectWithQueryBuilderData,
} = useQueryBuilder();
const handleSaveViewModalToggle = (): void => {
if (sourcepage === DataSource.TRACES) {
logEvent('Traces Explorer: Save view clicked', {
panelType,
});
} else if (sourcepage === DataSource.LOGS) {
logEvent('Logs Explorer: Save view clicked', {
panelType,
});
}
setIsSaveModalOpen(!isSaveModalOpen);
};
@@ -121,21 +104,11 @@ function ExplorerOptions({
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const onCreateAlertsHandler = useCallback(() => {
if (sourcepage === DataSource.TRACES) {
logEvent('Traces Explorer: Create alert', {
panelType,
});
} else if (sourcepage === DataSource.LOGS) {
logEvent('Logs Explorer: Create alert', {
panelType,
});
}
history.push(
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
JSON.stringify(query),
)}`,
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [history, query]);
const onCancel = (value: boolean) => (): void => {
@@ -143,15 +116,6 @@ function ExplorerOptions({
};
const onAddToDashboard = (): void => {
if (sourcepage === DataSource.TRACES) {
logEvent('Traces Explorer: Add to dashboard clicked', {
panelType,
});
} else if (sourcepage === DataSource.LOGS) {
logEvent('Logs Explorer: Add to dashboard clicked', {
panelType,
});
}
setIsExport(true);
};
@@ -163,6 +127,13 @@ function ExplorerOptions({
refetch: refetchAllView,
} = useGetAllViews(sourcepage);
const {
currentQuery,
panelType,
isStagedQueryUpdated,
redirectWithQueryBuilderData,
} = useQueryBuilder();
const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType);
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
@@ -253,17 +224,6 @@ function ExplorerOptions({
onMenuItemSelectHandler({
key: option.key,
});
if (sourcepage === DataSource.TRACES) {
logEvent('Traces Explorer: Select view', {
panelType,
viewName: option?.value,
});
} else if (sourcepage === DataSource.LOGS) {
logEvent('Logs Explorer: Select view', {
panelType,
viewName: option?.value,
});
}
if (ref.current) {
ref.current.blur();
}
@@ -299,17 +259,6 @@ function ExplorerOptions({
viewName: newViewName,
setNewViewName,
});
if (sourcepage === DataSource.TRACES) {
logEvent('Traces Explorer: Save view successful', {
panelType,
viewName: newViewName,
});
} else if (sourcepage === DataSource.LOGS) {
logEvent('Logs Explorer: Save view successful', {
panelType,
viewName: newViewName,
});
}
};
// TODO: Remove this and move this to scss file
@@ -550,7 +499,7 @@ function ExplorerOptions({
export interface ExplorerOptionsProps {
isLoading?: boolean;
onExport: (dashboard: Dashboard | null, isNewDashboard?: boolean) => void;
onExport: (dashboard: Dashboard | null) => void;
query: Query | null;
disabled: boolean;
sourcepage: DataSource;

View File

@@ -41,7 +41,7 @@ function ExportPanelContainer({
} = useMutation(createDashboard, {
onSuccess: (data) => {
if (data.payload) {
onExport(data?.payload, true);
onExport(data?.payload);
}
refetch();
},
@@ -55,7 +55,7 @@ function ExportPanelContainer({
({ uuid }) => uuid === selectedDashboardId,
);
onExport(currentSelectedDashboard || null, false);
onExport(currentSelectedDashboard || null);
}, [data, selectedDashboardId, onExport]);
const handleSelect = useCallback(

View File

@@ -40,7 +40,7 @@ function ExportPanel({
export interface ExportPanelProps {
isLoading?: boolean;
onExport: (dashboard: Dashboard | null, isNewDashboard?: boolean) => void;
onExport: (dashboard: Dashboard | null) => void;
query: Query | null;
}

View File

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

View File

@@ -20,10 +20,7 @@ function OpsgenieForm({ setSelectedConfig }: OpsgenieFormProps): JSX.Element {
return (
<>
<Form.Item name="api_key" label={t('field_opsgenie_api_key')} required>
<Input
onChange={handleInputChange('api_key')}
data-testid="opsgenie-api-key-textbox"
/>
<Input onChange={handleInputChange('api_key')} />
</Form.Item>
<Form.Item
@@ -36,7 +33,6 @@ function OpsgenieForm({ setSelectedConfig }: OpsgenieFormProps): JSX.Element {
rows={4}
onChange={handleInputChange('message')}
placeholder={t('placeholder_opsgenie_message')}
data-testid="opsgenie-message-textarea"
/>
</Form.Item>
@@ -50,7 +46,6 @@ function OpsgenieForm({ setSelectedConfig }: OpsgenieFormProps): JSX.Element {
rows={4}
onChange={handleInputChange('description')}
placeholder={t('placeholder_opsgenie_description')}
data-testid="opsgenie-description-textarea"
/>
</Form.Item>
@@ -64,7 +59,6 @@ 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,7 +18,6 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
routing_key: event.target.value,
}));
}}
data-testid="pager-routing-key-textbox"
/>
</Form.Item>
@@ -37,7 +36,6 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
}))
}
placeholder={t('placeholder_pager_description')}
data-testid="pager-description-textarea"
/>
</Form.Item>
@@ -53,7 +51,6 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
severity: event.target.value,
}))
}
data-testid="pager-severity-textbox"
/>
</Form.Item>
@@ -70,7 +67,6 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
details: event.target.value,
}))
}
data-testid="pager-additional-details-textarea"
/>
</Form.Item>
@@ -101,7 +97,6 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
group: event.target.value,
}))
}
data-testid="pager-group-textarea"
/>
</Form.Item>
@@ -117,7 +112,6 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
class: event.target.value,
}))
}
data-testid="pager-class-textarea"
/>
</Form.Item>
<Form.Item
@@ -132,7 +126,6 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
client: event.target.value,
}))
}
data-testid="pager-client-textarea"
/>
</Form.Item>
@@ -148,7 +141,6 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
client_url: event.target.value,
}))
}
data-testid="pager-client-url-textarea"
/>
</Form.Item>
</>

View File

@@ -19,7 +19,6 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
api_url: event.target.value,
}));
}}
data-testid="webhook-url-textbox"
/>
</Form.Item>
@@ -35,13 +34,11 @@ 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 =>
@@ -62,7 +59,6 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
}))
}
placeholder={t('placeholder_slack_description')}
data-testid="description-textarea"
/>
</Form.Item>
</>

View File

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

View File

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

View File

@@ -88,7 +88,7 @@ function BasicInfo({
if (!channels.loading && isNewRule) {
logEvent('Alert: New alert creation page visited', {
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
numberOfChannels: channels?.payload?.length,
numberOfChannels: channels.payload?.length,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@@ -48,7 +48,6 @@ export interface ChartPreviewProps {
userQueryKey?: string;
allowSelectedIntervalForStepGen?: boolean;
yAxisUnit: string;
setQueryStatus?: (status: string) => void;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
@@ -63,7 +62,6 @@ function ChartPreview({
allowSelectedIntervalForStepGen = false,
alertDef,
yAxisUnit,
setQueryStatus,
}: ChartPreviewProps): JSX.Element | null {
const { t } = useTranslation('alerts');
const dispatch = useDispatch();
@@ -151,10 +149,10 @@ function ChartPreview({
useEffect((): void => {
const { startTime, endTime } = getTimeRange(queryResponse);
if (setQueryStatus) setQueryStatus(queryResponse.status);
setMinTimeScale(startTime);
setMaxTimeScale(endTime);
}, [maxTime, minTime, globalSelectedInterval, queryResponse, setQueryStatus]);
}, [maxTime, minTime, globalSelectedInterval, queryResponse]);
if (queryResponse.data && graphType === PANEL_TYPES.BAR) {
const sortedSeriesData = getSortedSeriesData(
@@ -286,7 +284,6 @@ ChartPreview.defaultProps = {
userQueryKey: '',
allowSelectedIntervalForStepGen: false,
alertDef: undefined,
setQueryStatus: (): void => {},
};
export default ChartPreview;

View File

@@ -101,7 +101,6 @@ function FormAlertRules({
const isNewRule = ruleId === 0;
const [loading, setLoading] = useState(false);
const [queryStatus, setQueryStatus] = useState<string>('');
// alertDef holds the form values to be posted
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
@@ -524,7 +523,6 @@ function FormAlertRules({
alertDef={alertDef}
yAxisUnit={yAxisUnit || ''}
graphType={panelType || PANEL_TYPES.TIME_SERIES}
setQueryStatus={setQueryStatus}
/>
);
@@ -542,7 +540,6 @@ function FormAlertRules({
selectedInterval={globalSelectedInterval}
yAxisUnit={yAxisUnit || ''}
graphType={panelType || PANEL_TYPES.TIME_SERIES}
setQueryStatus={setQueryStatus}
/>
);
@@ -668,8 +665,7 @@ function FormAlertRules({
disabled={
isAlertNameMissing ||
isAlertAvailableToSave ||
!isChannelConfigurationValid ||
queryStatus === 'error'
!isChannelConfigurationValid
}
>
{isNewRule ? t('button_createrule') : t('button_savechanges')}
@@ -678,11 +674,7 @@ function FormAlertRules({
<ActionButton
loading={loading || false}
disabled={
isAlertNameMissing ||
!isChannelConfigurationValid ||
queryStatus === 'error'
}
disabled={isAlertNameMissing || !isChannelConfigurationValid}
type="default"
onClick={onTestRuleHandler}
>

View File

@@ -124,9 +124,6 @@ const getSpanWithoutChildren = (
value: span.value,
event: span.event,
hasError: span.hasError,
spanKind: span.spanKind,
statusCodeString: span.statusCodeString,
statusMessage: span.statusMessage,
});
export const isSpanPresentInSearchString = (

View File

@@ -3,7 +3,6 @@ import './DashboardEmptyState.styles.scss';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import SettingsDrawer from 'container/NewDashboard/DashboardDescription/SettingsDrawer';
import useComponentPermission from 'hooks/useComponentPermission';
import { useDashboard } from 'providers/Dashboard/Dashboard';
@@ -37,12 +36,6 @@ export default function DashboardEmptyState(): JSX.Element {
const onEmptyWidgetHandler = useCallback(() => {
handleToggleDashboardSlider(true);
logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.uuid,
dashboardName: selectedDashboard?.data.title,
numberOfPanels: selectedDashboard?.data.widgets?.length,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [handleToggleDashboardSlider]);
return (
<section className="dashboard-empty-state">

View File

@@ -14,7 +14,6 @@ import { memo, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getGraphType } from 'utils/getGraphType';
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
@@ -114,7 +113,6 @@ function GridCardGraph({
};
}
updatedQuery.builder.queryData[0].pageSize = 10;
const initialDataSource = updatedQuery.builder.queryData[0].dataSource;
return {
query: updatedQuery,
graphType: PANEL_TYPES.LIST,
@@ -125,9 +123,6 @@ function GridCardGraph({
offset: 0,
limit: updatedQuery.builder.queryData[0].limit || 0,
},
// we do not need select columns in case of logs
selectColumns:
initialDataSource === DataSource.TRACES && widget.selectedTracesFields,
},
fillGaps: widget.fillSpans,
};

View File

@@ -1,7 +1,6 @@
.fullscreen-grid-container {
overflow: auto;
margin: 8px -8px;
margin-right: 0;
.react-grid-layout {
border: none !important;
@@ -50,7 +49,7 @@
.footer {
display: flex;
flex-direction: column;
position: fixed;
position: absolute;
bottom: 0;
width: -webkit-fill-available;

View File

@@ -3,7 +3,6 @@ import './GridCardLayout.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Form, Input, Modal, Typography } from 'antd';
import { useForm } from 'antd/es/form/Form';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
@@ -16,7 +15,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { defaultTo, isUndefined } from 'lodash-es';
import { defaultTo } from 'lodash-es';
import isEqual from 'lodash-es/isEqual';
import {
Check,
@@ -28,7 +27,7 @@ import {
} from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { sortLayout } from 'providers/Dashboard/util';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { FullScreen, FullScreenHandle } from 'react-full-screen';
import { ItemCallback, Layout } from 'react-grid-layout';
import { useDispatch, useSelector } from 'react-redux';
@@ -127,18 +126,6 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
setDashboardLayout(sortLayout(layouts));
}, [layouts]);
const logEventCalledRef = useRef(false);
useEffect(() => {
if (!logEventCalledRef.current && !isUndefined(data)) {
logEvent('Dashboard Detail: Opened', {
dashboardId: data.uuid,
dashboardName: data.title,
numberOfPanels: data.widgets?.length,
numberOfVariables: Object.keys(data?.variables || {}).length || 0,
});
logEventCalledRef.current = true;
}
}, [data]);
const onSaveHandler = (): void => {
if (!selectedDashboard) return;
@@ -441,11 +428,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
return isDashboardEmpty ? (
<DashboardEmptyState />
) : (
<FullScreen
handle={handle}
className="fullscreen-grid-container"
data-overlayscrollbars-initialize
>
<FullScreen handle={handle} className="fullscreen-grid-container">
<ReactGridLayout
cols={12}
rowHeight={45}

View File

@@ -79,7 +79,7 @@ function WidgetHeader({
);
}, [widget.id, widget.panelTypes, widget.query]);
const onCreateAlertsHandler = useCreateAlerts(widget, 'dashboardView');
const onCreateAlertsHandler = useCreateAlerts(widget);
const onDownloadHandler = useCallback((): void => {
const csv = unparse(tableProcessedDataRef.current);
@@ -234,7 +234,6 @@ function WidgetHeader({
)}
<Dropdown menu={menu} trigger={['hover']} placement="bottomRight">
<MoreOutlined
data-testid="widget-header-options"
className={`widget-header-more-options ${
parentHover ? 'widget-header-hover' : ''
}`}

View File

@@ -1,10 +1,6 @@
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import {
createColumnsAndDataSource,
getQueryLegend,
sortFunction,
} from '../utils';
import { createColumnsAndDataSource, getQueryLegend } from '../utils';
import {
expectedOutputWithLegends,
tableDataMultipleQueriesSuccessResponse,
@@ -43,88 +39,4 @@ describe('Table Panel utils', () => {
// should return undefined when legend not present
expect(getQueryLegend(query, 'B')).toBe(undefined);
});
it('sorter function for table sorting', () => {
let rowA: {
A: string | number;
timestamp: number;
key: string;
} = {
A: 22.4,
timestamp: 111111,
key: '1111',
};
let rowB: {
A: string | number;
timestamp: number;
key: string;
} = {
A: 'n/a',
timestamp: 111112,
key: '1112',
};
const item = {
isValueColumn: true,
name: 'A',
queryName: 'A',
};
// A has value and value is considered bigger than n/a hence 1
expect(sortFunction(rowA, rowB, item)).toBe(1);
rowA = {
A: 'n/a',
timestamp: 111111,
key: '1111',
};
rowB = {
A: 22.4,
timestamp: 111112,
key: '1112',
};
// B has value and value is considered bigger than n/a hence -1
expect(sortFunction(rowA, rowB, item)).toBe(-1);
rowA = {
A: 11,
timestamp: 111111,
key: '1111',
};
rowB = {
A: 22,
timestamp: 111112,
key: '1112',
};
// A and B has value , since B > A hence A-B
expect(sortFunction(rowA, rowB, item)).toBe(-11);
rowA = {
A: 'read',
timestamp: 111111,
key: '1111',
};
rowB = {
A: 'write',
timestamp: 111112,
key: '1112',
};
// A and B are strings so A is smaller than B because r comes before w hence -1
expect(sortFunction(rowA, rowB, item)).toBe(-1);
rowA = {
A: 'n/a',
timestamp: 111111,
key: '1111',
};
rowB = {
A: 'n/a',
timestamp: 111112,
key: '1112',
};
// A and B are strings n/a , since both of them are same hence 0
expect(sortFunction(rowA, rowB, item)).toBe(0);
});
});

View File

@@ -1,4 +1,3 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { ColumnsType, ColumnType } from 'antd/es/table';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { QUERY_TABLE_CONFIG } from 'container/QueryTable/config';
@@ -106,39 +105,6 @@ export function getQueryLegend(
return legend;
}
export function sortFunction(
a: RowData,
b: RowData,
item: {
name: string;
queryName: string;
isValueColumn: boolean;
},
): number {
// assumption :- number values is bigger than 'n/a'
const valueA = Number(a[`${item.name}_without_unit`] ?? a[item.name]);
const valueB = Number(b[`${item.name}_without_unit`] ?? b[item.name]);
// if both the values are numbers then return the difference here
if (!isNaN(valueA) && !isNaN(valueB)) {
return valueA - valueB;
}
// if valueB is a number then make it bigger value
if (isNaN(valueA) && !isNaN(valueB)) {
return -1;
}
// if valueA is number make it the bigger value
if (!isNaN(valueA) && isNaN(valueB)) {
return 1;
}
// if both of them are strings do the localecompare
return ((a[item.name] as string) || '').localeCompare(
(b[item.name] as string) || '',
);
}
export function createColumnsAndDataSource(
data: TableData,
currentQuery: Query,
@@ -157,7 +123,18 @@ export function createColumnsAndDataSource(
title: !isEmpty(legend) ? legend : item.name,
width: QUERY_TABLE_CONFIG.width,
render: renderColumnCell && renderColumnCell[item.name],
sorter: (a: RowData, b: RowData): number => sortFunction(a, b, item),
sorter: (a: RowData, b: RowData): number => {
const valueA = Number(a[`${item.name}_without_unit`] ?? a[item.name]);
const valueB = Number(b[`${item.name}_without_unit`] ?? b[item.name]);
if (!isNaN(valueA) && !isNaN(valueB)) {
return valueA - valueB;
}
return ((a[item.name] as string) || '').localeCompare(
(b[item.name] as string) || '',
);
},
};
return [...acc, column];

View File

@@ -940,50 +940,3 @@
border-color: var(--bg-vanilla-300) !important;
}
}
.mt-8 {
margin-top: 8px;
}
.mt-12 {
margin-top: 12px;
}
.mt-24 {
margin-top: 24px;
}
.mb-24 {
margin-bottom: 24px;
}
.ingestion-setup-details-links {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 24px;
padding: 12px;
border-radius: 4px;
background: rgba(113, 144, 249, 0.1);
color: var(--bg-robin-300, #95acfb);
.learn-more {
display: inline-flex;
justify-content: center;
align-items: center;
text-decoration: underline;
color: var(--bg-robin-300, #95acfb);
}
}
.lightMode {
.ingestion-setup-details-links {
background: rgba(113, 144, 249, 0.1);
color: var(--bg-robin-500);
.learn-more {
color: var(--bg-robin-500);
}
}
}

View File

@@ -34,14 +34,11 @@ import dayjs, { Dayjs } from 'dayjs';
import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useNotifications } from 'hooks/useNotifications';
import { isNil } from 'lodash-es';
import {
ArrowUpRight,
CalendarClock,
Check,
Copy,
Infinity,
Info,
Minus,
PenLine,
Plus,
@@ -606,250 +603,243 @@ function MultiIngestionSettings(): JSX.Element {
<div className="limits-data">
<div className="signals">
{SIGNALS.map((signal) => {
const hasValidDayLimit = !isNil(limits[signal]?.config?.day?.size);
const hasValidSecondLimit = !isNil(
limits[signal]?.config?.second?.size,
);
return (
<div className="signal" key={signal}>
<div className="header">
<div className="signal-name">{signal}</div>
<div className="actions">
{hasLimits(signal) ? (
<>
<Button
className="periscope-btn ghost"
icon={<PenLine size={14} />}
disabled={!!(activeAPIKey?.id === APIKey.id && activeSignal)}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
enableEditLimitMode(APIKey, limits[signal]);
}}
/>
<Button
className="periscope-btn ghost"
icon={<Trash2 color={Color.BG_CHERRY_500} size={14} />}
disabled={!!(activeAPIKey?.id === APIKey.id && activeSignal)}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
showDeleteLimitModal(APIKey, limits[signal]);
}}
/>
</>
) : (
{SIGNALS.map((signal) => (
<div className="signal" key={signal}>
<div className="header">
<div className="signal-name">{signal}</div>
<div className="actions">
{hasLimits(signal) ? (
<>
<Button
className="periscope-btn"
size="small"
shape="round"
icon={<PlusIcon size={14} />}
className="periscope-btn ghost"
icon={<PenLine size={14} />}
disabled={!!(activeAPIKey?.id === APIKey.id && activeSignal)}
// eslint-disable-next-line sonarjs/no-identical-functions
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
enableEditLimitMode(APIKey, {
id: signal,
signal,
config: {},
});
enableEditLimitMode(APIKey, limits[signal]);
}}
>
Limits
</Button>
)}
</div>
</div>
/>
<div className="signal-limit-values">
{activeAPIKey?.id === APIKey.id &&
activeSignal?.signal === signal &&
isEditAddLimitOpen ? (
<Form
name="edit-ingestion-key-limit-form"
key="addEditLimitForm"
form={addEditLimitForm}
autoComplete="off"
initialValues={{
dailyLimit: bytesToGb(limits[signal]?.config?.day?.size),
secondsLimit: bytesToGb(limits[signal]?.config?.second?.size),
}}
className="edit-ingestion-key-limit-form"
>
<div className="signal-limit-edit-mode">
<div className="daily-limit">
<div className="heading">
<div className="title"> Daily limit </div>
<div className="subtitle">
Add a limit for data ingested daily{' '}
</div>
</div>
<div className="size">
<Form.Item name="dailyLimit">
<InputNumber
addonAfter={
<Select defaultValue="GiB" disabled>
<Option value="TiB"> TiB</Option>
<Option value="GiB"> GiB</Option>
<Option value="MiB"> MiB </Option>
<Option value="KiB"> KiB </Option>
</Select>
}
/>
</Form.Item>
</div>
</div>
<div className="second-limit">
<div className="heading">
<div className="title"> Per Second limit </div>
<div className="subtitle">
{' '}
Add a limit for data ingested every second{' '}
</div>
</div>
<div className="size">
<Form.Item name="secondsLimit">
<InputNumber
addonAfter={
<Select defaultValue="GiB" disabled>
<Option value="TiB"> TiB</Option>
<Option value="GiB"> GiB</Option>
<Option value="MiB"> MiB </Option>
<Option value="KiB"> KiB </Option>
</Select>
}
/>
</Form.Item>
</div>
</div>
</div>
{activeAPIKey?.id === APIKey.id &&
activeSignal.signal === signal &&
!isLoadingLimitForKey &&
hasCreateLimitForIngestionKeyError &&
createLimitForIngestionKeyError &&
createLimitForIngestionKeyError?.error && (
<div className="error">
{createLimitForIngestionKeyError?.error}
</div>
)}
{activeAPIKey?.id === APIKey.id &&
activeSignal.signal === signal &&
!isLoadingLimitForKey &&
hasUpdateLimitForIngestionKeyError &&
updateLimitForIngestionKeyError && (
<div className="error">
{updateLimitForIngestionKeyError?.error}
</div>
)}
{activeAPIKey?.id === APIKey.id &&
activeSignal.signal === signal &&
isEditAddLimitOpen && (
<div className="signal-limit-save-discard">
<Button
type="primary"
className="periscope-btn primary"
size="small"
disabled={
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
}
loading={
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
}
onClick={(): void => {
if (!hasLimits(signal)) {
handleAddLimit(APIKey, signal);
} else {
handleUpdateLimit(APIKey, limits[signal]);
}
}}
>
Save
</Button>
<Button
type="default"
className="periscope-btn"
size="small"
disabled={
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
}
onClick={handleDiscardSaveLimit}
>
Discard
</Button>
</div>
)}
</Form>
<Button
className="periscope-btn ghost"
icon={<Trash2 color={Color.BG_CHERRY_500} size={14} />}
disabled={!!(activeAPIKey?.id === APIKey.id && activeSignal)}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
showDeleteLimitModal(APIKey, limits[signal]);
}}
/>
</>
) : (
<div className="signal-limit-view-mode">
<div className="signal-limit-value">
<div className="limit-type">
Daily <Minus size={16} />{' '}
</div>
<Button
className="periscope-btn"
size="small"
shape="round"
icon={<PlusIcon size={14} />}
disabled={!!(activeAPIKey?.id === APIKey.id && activeSignal)}
// eslint-disable-next-line sonarjs/no-identical-functions
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
<div className="limit-value">
{hasValidDayLimit ? (
<>
{getYAxisFormattedValue(
(limits[signal]?.metric?.day?.size || 0).toString(),
'bytes',
)}{' '}
/{' '}
{getYAxisFormattedValue(
(limits[signal]?.config?.day?.size || 0).toString(),
'bytes',
)}
</>
) : (
<>
<Infinity size={16} /> NO LIMIT
</>
)}
</div>
</div>
<div className="signal-limit-value">
<div className="limit-type">
Seconds <Minus size={16} />
</div>
<div className="limit-value">
{hasValidSecondLimit ? (
<>
{getYAxisFormattedValue(
(limits[signal]?.metric?.second?.size || 0).toString(),
'bytes',
)}{' '}
/{' '}
{getYAxisFormattedValue(
(limits[signal]?.config?.second?.size || 0).toString(),
'bytes',
)}
</>
) : (
<>
<Infinity size={16} /> NO LIMIT
</>
)}
</div>
</div>
</div>
enableEditLimitMode(APIKey, {
id: signal,
signal,
config: {},
});
}}
>
Limits
</Button>
)}
</div>
</div>
);
})}
<div className="signal-limit-values">
{activeAPIKey?.id === APIKey.id &&
activeSignal?.signal === signal &&
isEditAddLimitOpen ? (
<Form
name="edit-ingestion-key-limit-form"
key="addEditLimitForm"
form={addEditLimitForm}
autoComplete="off"
initialValues={{
dailyLimit: bytesToGb(limits[signal]?.config?.day?.size),
secondsLimit: bytesToGb(limits[signal]?.config?.second?.size),
}}
className="edit-ingestion-key-limit-form"
>
<div className="signal-limit-edit-mode">
<div className="daily-limit">
<div className="heading">
<div className="title"> Daily limit </div>
<div className="subtitle">
Add a limit for data ingested daily{' '}
</div>
</div>
<div className="size">
<Form.Item name="dailyLimit">
<InputNumber
addonAfter={
<Select defaultValue="GiB" disabled>
<Option value="TiB"> TiB</Option>
<Option value="GiB"> GiB</Option>
<Option value="MiB"> MiB </Option>
<Option value="KiB"> KiB </Option>
</Select>
}
/>
</Form.Item>
</div>
</div>
<div className="second-limit">
<div className="heading">
<div className="title"> Per Second limit </div>
<div className="subtitle">
{' '}
Add a limit for data ingested every second{' '}
</div>
</div>
<div className="size">
<Form.Item name="secondsLimit">
<InputNumber
addonAfter={
<Select defaultValue="GiB" disabled>
<Option value="TiB"> TiB</Option>
<Option value="GiB"> GiB</Option>
<Option value="MiB"> MiB </Option>
<Option value="KiB"> KiB </Option>
</Select>
}
/>
</Form.Item>
</div>
</div>
</div>
{activeAPIKey?.id === APIKey.id &&
activeSignal.signal === signal &&
!isLoadingLimitForKey &&
hasCreateLimitForIngestionKeyError &&
createLimitForIngestionKeyError &&
createLimitForIngestionKeyError?.error && (
<div className="error">
{createLimitForIngestionKeyError?.error}
</div>
)}
{activeAPIKey?.id === APIKey.id &&
activeSignal.signal === signal &&
!isLoadingLimitForKey &&
hasUpdateLimitForIngestionKeyError &&
updateLimitForIngestionKeyError && (
<div className="error">
{updateLimitForIngestionKeyError?.error}
</div>
)}
{activeAPIKey?.id === APIKey.id &&
activeSignal.signal === signal &&
isEditAddLimitOpen && (
<div className="signal-limit-save-discard">
<Button
type="primary"
className="periscope-btn primary"
size="small"
disabled={
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
}
loading={
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
}
onClick={(): void => {
if (!hasLimits(signal)) {
handleAddLimit(APIKey, signal);
} else {
handleUpdateLimit(APIKey, limits[signal]);
}
}}
>
Save
</Button>
<Button
type="default"
className="periscope-btn"
size="small"
disabled={
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
}
onClick={handleDiscardSaveLimit}
>
Discard
</Button>
</div>
)}
</Form>
) : (
<div className="signal-limit-view-mode">
<div className="signal-limit-value">
<div className="limit-type">
Daily <Minus size={16} />{' '}
</div>
<div className="limit-value">
{limits[signal]?.config?.day?.size ? (
<>
{getYAxisFormattedValue(
(limits[signal]?.metric?.day?.size || 0).toString(),
'bytes',
)}{' '}
/{' '}
{getYAxisFormattedValue(
(limits[signal]?.config?.day?.size || 0).toString(),
'bytes',
)}
</>
) : (
<>
<Infinity size={16} /> NO LIMIT
</>
)}
</div>
</div>
<div className="signal-limit-value">
<div className="limit-type">
Seconds <Minus size={16} />
</div>
<div className="limit-value">
{limits[signal]?.config?.second?.size ? (
<>
{getYAxisFormattedValue(
(limits[signal]?.metric?.second?.size || 0).toString(),
'bytes',
)}{' '}
/{' '}
{getYAxisFormattedValue(
(limits[signal]?.config?.second?.size || 0).toString(),
'bytes',
)}
</>
) : (
<>
<Infinity size={16} /> NO LIMIT
</>
)}
</div>
</div>
</div>
)}
</div>
</div>
))}
</div>
</div>
</div>
@@ -885,35 +875,10 @@ function MultiIngestionSettings(): JSX.Element {
return (
<div className="ingestion-key-container">
<div className="ingestion-key-content">
<div className="ingestion-setup-details-links">
<Info size={14} />
<span>
Find your ingestion URL and learn more about sending data to SigNoz{' '}
<a
href="https://signoz.io/docs/ingestion/signoz-cloud/overview/"
target="_blank"
className="learn-more"
rel="noreferrer"
>
here <ArrowUpRight size={14} />
</a>
</span>
</div>
<header>
<Typography.Title className="title"> Ingestion Keys </Typography.Title>
<Typography.Text className="subtitle">
Create and manage ingestion keys for the SigNoz Cloud{' '}
<a
href="https://signoz.io/docs/ingestion/signoz-cloud/keys/"
target="_blank"
className="learn-more"
rel="noreferrer"
>
{' '}
Learn more <ArrowUpRight size={14} />
</a>
Create and manage ingestion keys for the SigNoz Cloud
</Typography.Text>
</header>

View File

@@ -1,45 +0,0 @@
import { render, screen } from 'tests/test-utils';
import MultiIngestionSettings from '../MultiIngestionSettings';
describe('MultiIngestionSettings Page', () => {
beforeEach(() => {
render(<MultiIngestionSettings />);
});
afterEach(() => {
jest.clearAllMocks();
});
it('renders MultiIngestionSettings page without crashing', () => {
expect(
screen.getByText(
'Find your ingestion URL and learn more about sending data to SigNoz',
),
).toBeInTheDocument();
expect(screen.getByText('Ingestion Keys')).toBeInTheDocument();
expect(
screen.getByText('Create and manage ingestion keys for the SigNoz Cloud'),
).toBeInTheDocument();
const overviewLink = screen.getByRole('link', { name: /here/i });
expect(overviewLink).toHaveAttribute(
'href',
'https://signoz.io/docs/ingestion/signoz-cloud/overview/',
);
expect(overviewLink).toHaveAttribute('target', '_blank');
expect(overviewLink).toHaveClass('learn-more');
expect(overviewLink).toHaveAttribute('rel', 'noreferrer');
const aboutKeyslink = screen.getByRole('link', { name: /Learn more/i });
expect(aboutKeyslink).toHaveAttribute(
'href',
'https://signoz.io/docs/ingestion/signoz-cloud/keys/',
);
expect(aboutKeyslink).toHaveAttribute('target', '_blank');
expect(aboutKeyslink).toHaveClass('learn-more');
expect(aboutKeyslink).toHaveAttribute('rel', 'noreferrer');
});
});

View File

@@ -49,9 +49,9 @@ export const alertActionLogEvent = (
break;
}
logEvent('Alert: Action', {
ruleId: record?.id,
ruleId: record.id,
dataSource: ALERTS_DATA_SOURCE_MAP[record.alertType as AlertTypes],
name: record?.alert,
name: record.alert,
action: actionValue,
});
};

View File

@@ -43,10 +43,6 @@
background: var(--bg-ink-400);
cursor: pointer;
.dashboard-title {
color: var(--bg-vanilla-100);
}
.title-with-action {
display: flex;
justify-content: space-between;
@@ -1052,10 +1048,6 @@
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 {

View File

@@ -21,7 +21,6 @@ import {
Typography,
} from 'antd';
import { TableProps } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import createDashboard from 'api/dashboard/create';
import { AxiosError } from 'axios';
import cx from 'classnames';
@@ -35,7 +34,7 @@ import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
import useComponentPermission from 'hooks/useComponentPermission';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { get, isEmpty, isUndefined } from 'lodash-es';
import { get, isEmpty } from 'lodash-es';
import {
ArrowDownWideNarrow,
ArrowUpRight,
@@ -61,18 +60,18 @@ import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { generatePath, Link } from 'react-router-dom';
import { generatePath } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { AppState } from 'store/reducers';
import { Dashboard } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import { isCloudUser } from 'utils/app';
import useUrlQuery from '../../hooks/useUrlQuery';
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
import ImportJSON from './ImportJSON';
import { DeleteButton } from './TableComponents/DeleteButton';
@@ -85,7 +84,7 @@ import {
// eslint-disable-next-line sonarjs/cognitive-complexity
function DashboardsList(): JSX.Element {
const {
data: dashboardListResponse,
data: dashboardListResponse = [],
isLoading: isDashboardListLoading,
error: dashboardFetchError,
refetch: refetchDashboardList,
@@ -98,14 +97,12 @@ function DashboardsList(): JSX.Element {
setListSortOrder: setSortOrder,
} = useDashboard();
const [searchString, setSearchString] = useState<string>(
sortOrder.search || '',
);
const [action, createNewDashboard] = useComponentPermission(
['action', 'create_new_dashboards'],
role,
);
const [searchValue, setSearchValue] = useState<string>('');
const [
showNewDashboardTemplatesModal,
setShowNewDashboardTemplatesModal,
@@ -124,6 +121,10 @@ function DashboardsList(): JSX.Element {
false,
);
const params = useUrlQuery();
const searchParams = params.get('search');
const [searchString, setSearchString] = useState<string>(searchParams || '');
const getLocalStorageDynamicColumns = (): DashboardDynamicColumns => {
const dashboardDynamicColumnsString = localStorage.getItem('dashboard');
let dashboardDynamicColumns: DashboardDynamicColumns = {
@@ -185,6 +186,14 @@ function DashboardsList(): JSX.Element {
setDashboards(sortedDashboards);
};
useEffect(() => {
params.set('columnKey', sortOrder.columnKey as string);
params.set('order', sortOrder.order as string);
params.set('page', sortOrder.pagination || '1');
history.replace({ search: params.toString() });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sortOrder]);
const sortHandle = (key: string): void => {
if (!dashboards) return;
if (key === 'createdAt') {
@@ -193,7 +202,6 @@ function DashboardsList(): JSX.Element {
columnKey: 'createdAt',
order: 'descend',
pagination: sortOrder.pagination || '1',
search: sortOrder.search || '',
});
} else if (key === 'updatedAt') {
sortDashboardsByUpdatedAt(dashboards);
@@ -201,19 +209,21 @@ function DashboardsList(): JSX.Element {
columnKey: 'updatedAt',
order: 'descend',
pagination: sortOrder.pagination || '1',
search: sortOrder.search || '',
});
}
};
function handlePageSizeUpdate(page: number): void {
setSortOrder({ ...sortOrder, pagination: String(page) });
setSortOrder((order) => ({
...order,
pagination: String(page),
}));
}
useEffect(() => {
const filteredDashboards = filterDashboard(
searchString,
dashboardListResponse || [],
dashboardListResponse,
);
if (sortOrder.columnKey === 'updatedAt') {
sortDashboardsByUpdatedAt(filteredDashboards || []);
@@ -224,7 +234,6 @@ function DashboardsList(): JSX.Element {
columnKey: 'updatedAt',
order: 'descend',
pagination: sortOrder.pagination || '1',
search: sortOrder.search || '',
});
sortDashboardsByUpdatedAt(filteredDashboards || []);
}
@@ -234,7 +243,6 @@ function DashboardsList(): JSX.Element {
setSortOrder,
sortOrder.columnKey,
sortOrder.pagination,
sortOrder.search,
]);
const [newDashboardState, setNewDashboardState] = useState({
@@ -261,7 +269,6 @@ function DashboardsList(): JSX.Element {
const onNewDashboardHandler = useCallback(async () => {
try {
logEvent('Dashboard List: Create dashboard clicked', {});
setNewDashboardState({
...newDashboardState,
loading: true,
@@ -298,23 +305,18 @@ function DashboardsList(): JSX.Element {
}, [newDashboardState, t]);
const onModalHandler = (uploadedGrafana: boolean): void => {
logEvent('Dashboard List: Import JSON clicked', {});
setIsImportJSONModalVisible((state) => !state);
setUploadedGrafana(uploadedGrafana);
};
const handleSearch = (event: ChangeEvent<HTMLInputElement>): void => {
setIsFilteringDashboards(true);
setSearchValue(event.target.value);
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
const filteredDashboards = filterDashboard(
searchText,
dashboardListResponse || [],
);
const filteredDashboards = filterDashboard(searchText, dashboardListResponse);
setDashboards(filteredDashboards);
setIsFilteringDashboards(false);
setSearchString(searchText);
setSortOrder({ ...sortOrder, search: searchText });
};
const [state, setCopy] = useCopyToClipboard();
@@ -405,7 +407,7 @@ function DashboardsList(): JSX.Element {
{
title: 'Dashboards',
key: 'dashboard',
render: (dashboard: Data, _, index): JSX.Element => {
render: (dashboard: Data): JSX.Element => {
const timeOptions: Intl.DateTimeFormatOptions = {
hour: '2-digit',
minute: '2-digit',
@@ -439,10 +441,6 @@ function DashboardsList(): JSX.Element {
} else {
history.push(getLink());
}
logEvent('Dashboard List: Clicked on dashboard', {
dashboardId: dashboard.id,
dashboardName: dashboard.name,
});
};
return (
@@ -454,11 +452,7 @@ function DashboardsList(): JSX.Element {
style={{ height: '14px', width: '14px' }}
alt="dashboard-image"
/>
<Typography.Text data-testid={`dashboard-title-${index}`}>
<Link to={getLink()} className="dashboard-title">
{dashboard.name}
</Link>
</Typography.Text>
<Typography.Text>{dashboard.name}</Typography.Text>
</div>
<div className="tags-with-actions">
@@ -625,21 +619,6 @@ function DashboardsList(): JSX.Element {
hideOnSinglePage: true,
};
const logEventCalledRef = useRef(false);
useEffect(() => {
if (
!logEventCalledRef.current &&
!isDashboardListLoading &&
!isUndefined(dashboardListResponse)
) {
logEvent('Dashboard List: Page visited', {
number: dashboardListResponse?.length,
});
logEventCalledRef.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isDashboardListLoading]);
return (
<div className="dashboards-list-container">
<div className="dashboards-list-view-content">
@@ -655,9 +634,8 @@ function DashboardsList(): JSX.Element {
}}
eventName="Dashboard: Facing Issues in dashboard"
message={dashboardListMessage}
buttonText="Need help with dashboards?"
buttonText="Facing issues with dashboards?"
onHoverText="Click here to get help with dashboards"
intercomMessageDisabled
/>
</Flex>
</div>
@@ -699,7 +677,7 @@ function DashboardsList(): JSX.Element {
<ArrowUpRight size={16} className="learn-more-arrow" />
</section>
</div>
) : dashboards?.length === 0 && !searchString ? (
) : dashboards?.length === 0 && !searchValue ? (
<div className="dashboard-empty-state">
<img
src="/Icons/dashboards.svg"
@@ -727,9 +705,6 @@ function DashboardsList(): JSX.Element {
type="text"
className="new-dashboard"
icon={<Plus size={14} />}
onClick={(): void => {
logEvent('Dashboard List: New dashboard clicked', {});
}}
>
New Dashboard
</Button>
@@ -737,7 +712,6 @@ function DashboardsList(): JSX.Element {
<Button
type="text"
className="learn-more"
data-testid="learn-more"
onClick={(): void => {
window.open(
'https://signoz.io/docs/userguide/manage-dashboards?utm_source=product&utm_medium=dashboard-list-empty-state',
@@ -757,7 +731,7 @@ function DashboardsList(): JSX.Element {
<Input
placeholder="Search by name, description, or tags..."
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
value={searchString}
value={searchValue}
onChange={handleSearch}
/>
{createNewDashboard && (
@@ -771,9 +745,6 @@ function DashboardsList(): JSX.Element {
type="primary"
className="periscope-btn primary btn"
icon={<Plus size={14} />}
onClick={(): void => {
logEvent('Dashboard List: New dashboard clicked', {});
}}
>
New dashboard
</Button>
@@ -785,7 +756,7 @@ function DashboardsList(): JSX.Element {
<div className="no-search">
<img src="/Icons/emptyState.svg" alt="img" className="img" />
<Typography.Text className="text">
No dashboards found for {searchString}. Create a new dashboard?
No dashboards found for {searchValue}. Create a new dashboard?
</Typography.Text>
</div>
) : (
@@ -807,7 +778,6 @@ function DashboardsList(): JSX.Element {
type="text"
className={cx('sort-btns')}
onClick={(): void => sortHandle('createdAt')}
data-testid="sort-by-last-created"
>
Last created
{sortOrder.columnKey === 'createdAt' && <Check size={14} />}
@@ -816,7 +786,6 @@ function DashboardsList(): JSX.Element {
type="text"
className={cx('sort-btns')}
onClick={(): void => sortHandle('updatedAt')}
data-testid="sort-by-last-updated"
>
Last updated
{sortOrder.columnKey === 'updatedAt' && <Check size={14} />}
@@ -827,7 +796,7 @@ function DashboardsList(): JSX.Element {
placement="bottomRight"
arrow={false}
>
<ArrowDownWideNarrow size={14} data-testid="sort-by" />
<ArrowDownWideNarrow size={14} />
</Popover>
</Tooltip>
<Popover

View File

@@ -5,7 +5,6 @@ import { ExclamationCircleTwoTone } from '@ant-design/icons';
import MEditor, { Monaco } from '@monaco-editor/react';
import { Color } from '@signozhq/design-tokens';
import { Button, Modal, Space, Typography, Upload, UploadProps } from 'antd';
import logEvent from 'api/common/logEvent';
import createDashboard from 'api/dashboard/create';
import ROUTES from 'constants/routes';
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -68,8 +67,6 @@ function ImportJSON({
const onClickLoadJsonHandler = async (): Promise<void> => {
try {
setDashboardCreating(true);
logEvent('Dashboard List: Import and next clicked', {});
const dashboardData = JSON.parse(editorValue) as DashboardData;
if (dashboardData?.layout) {
@@ -89,10 +86,6 @@ function ImportJSON({
dashboardId: response.payload.uuid,
}),
);
logEvent('Dashboard List: New dashboard imported successfully', {
dashboardId: response.payload?.uuid,
dashboardName: response.payload?.data?.title,
});
} else if (response.error === 'feature usage exceeded') {
setIsFeatureAlert(true);
notifications.error({
@@ -187,9 +180,6 @@ function ImportJSON({
type="default"
className="periscope-btn"
icon={<MonitorDot size={14} />}
onClick={(): void => {
logEvent('Dashboard List: Upload JSON file clicked', {});
}}
>
{' '}
{t('upload_json_file')}

View File

@@ -3,7 +3,6 @@ import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import ListLogView from 'components/Logs/ListLogView';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import Spinner from 'components/Spinner';
import { CARD_BODY_STYLE } from 'constants/card';
import { LOCALSTORAGE } from 'constants/localStorage';
@@ -129,15 +128,13 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
/>
) : (
<Card style={{ width: '100%' }} bodyStyle={CARD_BODY_STYLE}>
<OverlayScrollbar isVirtuoso>
<Virtuoso
ref={ref}
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
data={logs}
totalCount={logs.length}
itemContent={getItemContent}
/>
</OverlayScrollbar>
<Virtuoso
ref={ref}
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
data={logs}
totalCount={logs.length}
itemContent={getItemContent}
/>
</Card>
)}
</InfinityWrapperStyled>

View File

@@ -2,7 +2,6 @@ import './ContextLogRenderer.styles.scss';
import { Skeleton } from 'antd';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import ShowButton from 'container/LogsContextList/ShowButton';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { useCallback, useEffect, useState } from 'react';
@@ -95,15 +94,13 @@ function ContextLogRenderer({
}}
/>
)}
<OverlayScrollbar isVirtuoso>
<Virtuoso
className="virtuoso-list"
initialTopMostItemIndex={0}
data={logs}
itemContent={getItemContent}
style={{ height: `calc(${logs.length} * 32px)` }}
/>
</OverlayScrollbar>
<Virtuoso
className="virtuoso-list"
initialTopMostItemIndex={0}
data={logs}
itemContent={getItemContent}
style={{ height: `calc(${logs.length} * 32px)` }}
/>
{isAfterLogsFetching && (
<Skeleton
style={{

View File

@@ -1,4 +1,3 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import {
@@ -47,8 +46,6 @@ export const useContextLogData = ({
} => {
const [logs, setLogs] = useState<ILog[]>([]);
const [lastLog, setLastLog] = useState<ILog>(log);
const orderByTimestamp = useMemo(() => getOrderByTimestamp(order), [order]);
const logsMorePageSize = useMemo(() => (page - 1) * LOGS_MORE_PAGE_SIZE, [
@@ -74,11 +71,11 @@ export const useContextLogData = ({
getRequestData({
stagedQueryData: currentStagedQueryData,
query,
log: lastLog,
log,
orderByTimestamp,
page,
}),
[currentStagedQueryData, query, lastLog, orderByTimestamp, page],
[currentStagedQueryData, page, log, query, orderByTimestamp],
);
const [requestData, setRequestData] = useState<Query | null>(
@@ -98,10 +95,8 @@ export const useContextLogData = ({
if (order === ORDERBY_FILTERS.ASC) {
const reversedCurrentLogs = currentLogs.reverse();
setLogs([...reversedCurrentLogs]);
setLastLog(reversedCurrentLogs[0]);
} else {
setLogs([...currentLogs]);
setLastLog(currentLogs[currentLogs.length - 1]);
}
}
},
@@ -123,7 +118,7 @@ export const useContextLogData = ({
const newRequestData = getRequestData({
stagedQueryData: currentStagedQueryData,
query,
log: lastLog,
log,
orderByTimestamp,
page: page + 1,
pageSize: LOGS_MORE_PAGE_SIZE,
@@ -136,7 +131,6 @@ export const useContextLogData = ({
query,
page,
order,
lastLog,
currentStagedQueryData,
isDisabledFetch,
orderByTimestamp,
@@ -148,7 +142,7 @@ export const useContextLogData = ({
const newRequestData = getRequestData({
stagedQueryData: currentStagedQueryData,
query,
log: lastLog,
log,
orderByTimestamp,
page: 1,
});

View File

@@ -188,7 +188,6 @@ export const aggregateAttributesResourcesToString = (logData: ILog): string => {
attributes: {},
resources: {},
severity_text: logData.severity_text,
severity_number: logData.severity_number,
};
Object.keys(logData).forEach((key) => {

View File

@@ -1,112 +0,0 @@
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

@@ -163,15 +163,8 @@ function Login({
response.payload.accessJwt,
response.payload.refreshJwt,
);
if (history?.location?.state) {
const historyState = history?.location?.state as any;
if (historyState?.from) {
history.push(historyState?.from);
} else {
history.push(ROUTES.APPLICATION);
}
}
history.push(ROUTES.APPLICATION);
} else {
notifications.error({
message: response.error || t('unexpected_error'),
@@ -220,7 +213,6 @@ function Login({
<Input
type="email"
id="loginEmail"
data-testid="email"
required
placeholder={t('placeholder_email')}
autoFocus
@@ -232,12 +224,7 @@ function Login({
<ParentContainer>
<Label htmlFor="Password">{t('label_password')}</Label>
<FormContainer.Item name="password">
<Input.Password
required
id="currentPassword"
data-testid="password"
disabled={isLoading}
/>
<Input.Password required id="currentPassword" disabled={isLoading} />
</FormContainer.Item>
<Tooltip title={t('prompt_forgot_password')}>
<Typography.Link>{t('forgot_password')}</Typography.Link>
@@ -256,7 +243,6 @@ function Login({
loading={precheckInProcess}
type="primary"
onClick={onNextHandler}
data-testid="initiate_login"
>
{t('button_initiate_login')}
</Button>

View File

@@ -1,7 +1,6 @@
import './LogsContextList.styles.scss';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import Spinner from 'components/Spinner';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -188,15 +187,14 @@ function LogsContextList({
<EmptyText>No Data</EmptyText>
)}
{isFetching && <Spinner size="large" height="10rem" />}
<OverlayScrollbar isVirtuoso>
<Virtuoso
className="virtuoso-list"
initialTopMostItemIndex={0}
data={logs}
itemContent={getItemContent}
followOutput={order === ORDERBY_FILTERS.DESC}
/>
</OverlayScrollbar>
<Virtuoso
className="virtuoso-list"
initialTopMostItemIndex={0}
data={logs}
itemContent={getItemContent}
followOutput={order === ORDERBY_FILTERS.DESC}
/>
</ListContainer>
{order === ORDERBY_FILTERS.DESC && (

View File

@@ -6,7 +6,6 @@ import { VIEW_TYPES } from 'components/LogDetail/constants';
// components
import ListLogView from 'components/Logs/ListLogView';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import Spinner from 'components/Spinner';
import { CARD_BODY_STYLE } from 'constants/card';
import { LOCALSTORAGE } from 'constants/localStorage';
@@ -134,17 +133,15 @@ function LogsExplorerList({
style={{ width: '100%', marginTop: '20px' }}
bodyStyle={CARD_BODY_STYLE}
>
<OverlayScrollbar isVirtuoso>
<Virtuoso
ref={ref}
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
data={logs}
endReached={onEndReached}
totalCount={logs.length}
itemContent={getItemContent}
components={components}
/>
</OverlayScrollbar>
<Virtuoso
ref={ref}
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
data={logs}
endReached={onEndReached}
totalCount={logs.length}
itemContent={getItemContent}
components={components}
/>
</Card>
);
}, [
@@ -172,9 +169,7 @@ function LogsExplorerList({
!isFetching &&
logs.length === 0 &&
!isError &&
isFilterApplied && (
<EmptyLogsSearch dataSource={DataSource.LOGS} panelType="LIST" />
)}
isFilterApplied && <EmptyLogsSearch />}
{isError && !isLoading && !isFetching && <LogsError />}

View File

@@ -2,7 +2,6 @@
import './LogsExplorerViews.styles.scss';
import { Button } from 'antd';
import logEvent from 'api/common/logEvent';
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { LOCALSTORAGE } from 'constants/localStorage';
@@ -38,14 +37,7 @@ import { useNotifications } from 'hooks/useNotifications';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { FlatLogData } from 'lib/logs/flatLogData';
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
import {
cloneDeep,
defaultTo,
isEmpty,
isUndefined,
omit,
set,
} from 'lodash-es';
import { cloneDeep, defaultTo, isEmpty, omit, set } from 'lodash-es';
import { Sliders } from 'lucide-react';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -318,19 +310,6 @@ function LogsExplorerViews({
],
);
const logEventCalledRef = useRef(false);
useEffect(() => {
if (!logEventCalledRef.current && !isUndefined(data?.payload)) {
const currentData = data?.payload?.data?.newResult?.data?.result || [];
logEvent('Logs Explorer: Page visited', {
panelType,
isEmpty: !currentData?.[0]?.list,
});
logEventCalledRef.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data?.payload]);
const {
mutate: updateDashboard,
isLoading: isUpdateDashboardLoading,
@@ -345,7 +324,7 @@ function LogsExplorerViews({
}, [currentQuery]);
const handleExport = useCallback(
(dashboard: Dashboard | null, isNewDashboard?: boolean): void => {
(dashboard: Dashboard | null): void => {
if (!dashboard || !panelType) return;
const panelTypeParam = AVAILABLE_EXPORT_PANEL_TYPES.includes(panelType)
@@ -367,12 +346,6 @@ function LogsExplorerViews({
options.selectColumns,
);
logEvent('Logs Explorer: Add to dashboard successful', {
panelType,
isNewDashboard,
dashboardName: dashboard?.data?.title,
});
updateDashboard(updatedDashboard, {
onSuccess: (data) => {
if (data.error) {

View File

@@ -3,7 +3,6 @@ import './LogsPanelComponent.styles.scss';
import { Table } from 'antd';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { PANEL_TYPES } from 'constants/queryBuilder';
import Controls from 'container/Controls';
@@ -208,19 +207,17 @@ function LogsPanelComponent({
<>
<div className="logs-table">
<div className="resize-table">
<OverlayScrollbar>
<Table
pagination={false}
tableLayout="fixed"
scroll={{ x: `calc(50vw - 10px)` }}
sticky
loading={queryResponse.isFetching}
style={tableStyles}
dataSource={flattenLogData}
columns={columns}
onRow={handleRow}
/>
</OverlayScrollbar>
<Table
pagination={false}
tableLayout="fixed"
scroll={{ x: `calc(50vw - 10px)` }}
sticky
loading={queryResponse.isFetching}
style={tableStyles}
dataSource={flattenLogData}
columns={columns}
onRow={handleRow}
/>
</div>
{!widget.query.builder.queryData[0].limit && (
<div className="controller">

View File

@@ -7,7 +7,6 @@ import { VIEW_TYPES } from 'components/LogDetail/constants';
import ListLogView from 'components/Logs/ListLogView';
import RawLogView from 'components/Logs/RawLogView';
import LogsTableView from 'components/Logs/TableView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import Spinner from 'components/Spinner';
import { CARD_BODY_STYLE } from 'constants/card';
import { useActiveLog } from 'hooks/logs/useActiveLog';
@@ -98,9 +97,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
return (
<Card className="logs-card" bodyStyle={CARD_BODY_STYLE}>
<OverlayScrollbar isVirtuoso>
<Virtuoso totalCount={logs.length} itemContent={getItemContent} />
</OverlayScrollbar>
<Virtuoso totalCount={logs.length} itemContent={getItemContent} />
</Card>
);
}, [getItemContent, linesPerRow, logs, onSetActiveLog, selected, viewMode]);

View File

@@ -1,5 +1,4 @@
import { Col } from 'antd';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import Graph from 'container/GridCardLayout/GridCard';
@@ -12,7 +11,7 @@ import {
convertRawQueriesToTraceSelectedTags,
resourceAttributesToTagFilterItems,
} from 'hooks/useResourceAttribute/utils';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
@@ -98,24 +97,6 @@ function DBCall(): JSX.Element {
[servicename, tagFilterItems],
);
const logEventCalledRef = useRef(false);
useEffect(() => {
if (!logEventCalledRef.current) {
const selectedEnvironments = queries.find(
(val) => val.tagKey === 'resource_deployment_environment',
)?.tagValue;
logEvent('APM: Service detail page visited', {
selectedEnvironments,
resourceAttributeUsed: !!queries?.length,
section: 'dbMetrics',
});
logEventCalledRef.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const apmToTraceQuery = useGetAPMToTracesQueries({
servicename,
isDBCall: true,

View File

@@ -1,5 +1,4 @@
import { Col } from 'antd';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import Graph from 'container/GridCardLayout/GridCard';
@@ -14,7 +13,7 @@ import {
convertRawQueriesToTraceSelectedTags,
resourceAttributesToTagFilterItems,
} from 'hooks/useResourceAttribute/utils';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { EQueryType } from 'types/common/dashboard';
@@ -115,23 +114,6 @@ function External(): JSX.Element {
],
});
const logEventCalledRef = useRef(false);
useEffect(() => {
if (!logEventCalledRef.current) {
const selectedEnvironments = queries.find(
(val) => val.tagKey === 'resource_deployment_environment',
)?.tagValue;
logEvent('APM: Service detail page visited', {
selectedEnvironments,
resourceAttributeUsed: !!queries?.length,
section: 'externalMetrics',
});
logEventCalledRef.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const externalCallRPSWidget = useMemo(
() =>
getWidgetQueryBuilder({

View File

@@ -1,4 +1,3 @@
import logEvent from 'api/common/logEvent';
import getTopLevelOperations, {
ServiceDataProps,
} from 'api/metrics/getTopLevelOperations';
@@ -18,16 +17,14 @@ import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { defaultTo } from 'lodash-es';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { useLocation, useParams } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuid } from 'uuid';
import { GraphTitle, SERVICE_CHART_ID } from '../constant';
@@ -54,11 +51,6 @@ import {
function Application(): JSX.Element {
const { servicename: encodedServiceName } = useParams<IServiceName>();
const servicename = decodeURIComponent(encodedServiceName);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
const { search, pathname } = useLocation();
const { queries } = useResourceAttribute();
@@ -89,36 +81,14 @@ function Application(): JSX.Element {
[handleSetTimeStamp],
);
const logEventCalledRef = useRef(false);
useEffect(() => {
if (!logEventCalledRef.current) {
const selectedEnvironments = queries.find(
(val) => val.tagKey === 'resource_deployment_environment',
)?.tagValue;
logEvent('APM: Service detail page visited', {
selectedEnvironments,
resourceAttributeUsed: !!queries?.length,
section: 'overview',
});
logEventCalledRef.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const {
data: topLevelOperations,
error: topLevelOperationsError,
isLoading: topLevelOperationsIsLoading,
isError: topLevelOperationsIsError,
} = useQuery<ServiceDataProps>({
queryKey: [servicename, minTime, maxTime],
queryFn: (): Promise<ServiceDataProps> =>
getTopLevelOperations({
service: servicename || '',
start: minTime,
end: maxTime,
}),
queryKey: [servicename],
queryFn: getTopLevelOperations,
});
const selectedTraceTags: string = JSON.stringify(

View File

@@ -164,12 +164,6 @@ function TopOperationsTable({
const downloadableData = convertedTracesToDownloadData(data);
const paginationConfig = {
pageSize: 10,
showSizeChanger: false,
hideOnSinglePage: true,
};
return (
<div className="top-operation">
<div className="top-operation--download">
@@ -187,7 +181,6 @@ function TopOperationsTable({
tableLayout="fixed"
dataSource={data}
rowKey="name"
pagination={paginationConfig}
/>
</div>
);

View File

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

View File

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

View File

@@ -1,219 +0,0 @@
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',
}
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

@@ -17,7 +17,7 @@ function MySettings(): JSX.Element {
{
label: (
<div className="theme-option">
<Moon data-testid="dark-theme-icon" size={12} /> Dark{' '}
<Moon size={12} /> Dark{' '}
</div>
),
value: 'dark',
@@ -25,7 +25,7 @@ function MySettings(): JSX.Element {
{
label: (
<div className="theme-option">
<Sun size={12} data-testid="light-theme-icon" /> Light{' '}
<Sun size={12} /> Light{' '}
</div>
),
value: 'light',
@@ -63,7 +63,6 @@ function MySettings(): JSX.Element {
value={theme}
optionType="button"
buttonStyle="solid"
data-testid="theme-selector"
/>
</div>
@@ -75,12 +74,7 @@ function MySettings(): JSX.Element {
<Password />
</div>
<Button
className="flexBtn"
onClick={(): void => Logout()}
type="primary"
data-testid="logout-button"
>
<Button className="flexBtn" onClick={(): void => Logout()} type="primary">
<LogOut size={12} /> Logout
</Button>
</Space>

View File

@@ -1,7 +1,6 @@
import './ComponentSlider.styles.scss';
import { Card, Modal } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import createQueryParams from 'lib/createQueryParams';
@@ -21,13 +20,6 @@ function DashboardGraphSlider(): JSX.Element {
const onClickHandler = (name: PANEL_TYPES) => (): void => {
const id = uuid();
handleToggleDashboardSlider(false);
logEvent('Dashboard Detail: New panel type selected', {
// dashboardId: '',
// dashboardName: '',
// numberOfPanels: 0, // todo - at this point we don't know these attributes
panelType: name,
widgetId: id,
});
const queryParamsLog = {
graphType: name,
widgetId: id,
@@ -55,6 +47,7 @@ function DashboardGraphSlider(): JSX.Element {
PANEL_TYPES_INITIAL_QUERY[name],
),
};
if (name === PANEL_TYPES.LIST) {
history.push(
`${history.location.pathname}/new?${createQueryParams(queryParamsLog)}`,

View File

@@ -2,7 +2,6 @@ import './Description.styles.scss';
import { Button } from 'antd';
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { useRef, useState } from 'react';
import DashboardSettingsContent from '../DashboardSettings';
@@ -42,9 +41,7 @@ function SettingsDrawer({ drawerTitle }: { drawerTitle: string }): JSX.Element {
open={visible}
rootClassName="settings-container-root"
>
<OverlayScrollbar>
<DashboardSettingsContent variableViewModeRef={variableViewModeRef} />
</OverlayScrollbar>
<DashboardSettingsContent variableViewModeRef={variableViewModeRef} />
</DrawerContainer>
</>
);

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