Compare commits
28 Commits
issue_2705
...
v0.51.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4192fd573d | ||
|
|
ca13d80205 | ||
|
|
59121bd932 | ||
|
|
aef935a817 | ||
|
|
f300518d61 | ||
|
|
18b608a1d8 | ||
|
|
738d62c9cf | ||
|
|
38e694cd36 | ||
|
|
1281330c52 | ||
|
|
7b7cca7db7 | ||
|
|
3134e8c1cf | ||
|
|
d00024b64a | ||
|
|
4360cd0397 | ||
|
|
a688b6c60e | ||
|
|
522e73b48e | ||
|
|
ba7e6fcf23 | ||
|
|
eefccafa5b | ||
|
|
05bd6d52f1 | ||
|
|
d60daef171 | ||
|
|
d50530f58c | ||
|
|
6957bd71ca | ||
|
|
ef8b50c19e | ||
|
|
1585065fff | ||
|
|
99c68ddbcd | ||
|
|
b08e859426 | ||
|
|
89fd3e4f55 | ||
|
|
8d84ce8f06 | ||
|
|
09ea7b9eb5 |
@@ -146,7 +146,7 @@ services:
|
||||
condition: on-failure
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.49.1
|
||||
image: signoz/query-service:0.51.0
|
||||
command:
|
||||
[
|
||||
"-config=/root/config/prometheus.yml",
|
||||
@@ -199,7 +199,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:0.102.2
|
||||
image: signoz/signoz-otel-collector:0.102.3
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-config.yaml",
|
||||
@@ -237,7 +237,7 @@ services:
|
||||
- query-service
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:0.102.2
|
||||
image: signoz/signoz-schema-migrator:0.102.3
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
||||
@@ -66,7 +66,7 @@ services:
|
||||
- --storage.path=/data
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.3}
|
||||
container_name: otel-migrator
|
||||
command:
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
@@ -81,7 +81,7 @@ services:
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
otel-collector:
|
||||
container_name: signoz-otel-collector
|
||||
image: signoz/signoz-otel-collector:0.102.2
|
||||
image: signoz/signoz-otel-collector:0.102.3
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-config.yaml",
|
||||
|
||||
@@ -164,7 +164,7 @@ services:
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.49.1}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.51.0}
|
||||
container_name: signoz-query-service
|
||||
command:
|
||||
[
|
||||
@@ -204,7 +204,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.49.1}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.51.0}
|
||||
container_name: signoz-frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
@@ -216,7 +216,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.3}
|
||||
container_name: otel-migrator
|
||||
command:
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
@@ -230,7 +230,7 @@ services:
|
||||
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.2}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.3}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
[
|
||||
|
||||
@@ -164,7 +164,7 @@ services:
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.49.1}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.51.0}
|
||||
container_name: signoz-query-service
|
||||
command:
|
||||
[
|
||||
@@ -203,7 +203,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.49.1}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.51.0}
|
||||
container_name: signoz-frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
@@ -215,7 +215,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.3}
|
||||
container_name: otel-migrator
|
||||
command:
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
@@ -229,7 +229,7 @@ services:
|
||||
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.2}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.3}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
[
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
|
||||
@@ -29,6 +31,10 @@ 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())
|
||||
|
||||
@@ -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,6 +28,7 @@ import (
|
||||
"go.signoz.io/signoz/ee/query-service/integrations/gateway"
|
||||
"go.signoz.io/signoz/ee/query-service/interfaces"
|
||||
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
|
||||
licensepkg "go.signoz.io/signoz/ee/query-service/license"
|
||||
@@ -41,6 +42,7 @@ 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"
|
||||
@@ -110,6 +112,10 @@ 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 {
|
||||
@@ -118,33 +124,13 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
|
||||
localDB.SetMaxOpenConns(10)
|
||||
|
||||
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
|
||||
}
|
||||
gatewayProxy, err := gateway.NewProxy(serverOptions.GatewayUrl, gateway.RoutePrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// initiate license manager
|
||||
lm, err := licensepkg.StartManager("sqlite", localDB, gatewayFeature)
|
||||
lm, err := licensepkg.StartManager("sqlite", localDB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -340,7 +326,17 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
|
||||
|
||||
// add auth middleware
|
||||
getUserFromRequest := func(r *http.Request) (*basemodel.UserPayload, error) {
|
||||
return auth.GetUserFromRequest(r, apiHandler)
|
||||
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
|
||||
}
|
||||
am := baseapp.NewAuthMiddleware(getUserFromRequest)
|
||||
|
||||
|
||||
@@ -20,11 +20,14 @@ 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 {
|
||||
|
||||
@@ -5,5 +5,5 @@ import (
|
||||
)
|
||||
|
||||
func NewNoopProxy() (*httputil.ReverseProxy, error) {
|
||||
return nil, nil
|
||||
return &httputil.ReverseProxy{}, nil
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ const Enterprise = "ENTERPRISE_PLAN"
|
||||
const DisableUpsell = "DISABLE_UPSELL"
|
||||
const Onboarding = "ONBOARDING"
|
||||
const ChatSupport = "CHAT_SUPPORT"
|
||||
const Gateway = "GATEWAY"
|
||||
|
||||
var BasicPlan = basemodel.FeatureSet{
|
||||
basemodel.Feature{
|
||||
@@ -111,6 +112,13 @@ var BasicPlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: Gateway,
|
||||
Active: false,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
}
|
||||
|
||||
var ProPlan = basemodel.FeatureSet{
|
||||
@@ -205,6 +213,13 @@ var ProPlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: Gateway,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
}
|
||||
|
||||
var EnterprisePlan = basemodel.FeatureSet{
|
||||
@@ -313,4 +328,11 @@ var EnterprisePlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: Gateway,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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';
|
||||
@@ -48,7 +49,7 @@ function App(): JSX.Element {
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
const { trackPageView, trackEvent } = useAnalytics();
|
||||
const { trackPageView } = useAnalytics();
|
||||
|
||||
const { hostname, pathname } = window.location;
|
||||
|
||||
@@ -199,7 +200,7 @@ function App(): JSX.Element {
|
||||
LOCALSTORAGE.THEME_ANALYTICS_V1,
|
||||
);
|
||||
if (!isThemeAnalyticsSent) {
|
||||
trackEvent('Theme Analytics', {
|
||||
logEvent('Theme Analytics', {
|
||||
theme: isDarkMode ? THEME_MODE.DARK : THEME_MODE.LIGHT,
|
||||
user: pick(user, ['email', 'userId', 'name']),
|
||||
org,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import axios from 'api';
|
||||
import { ApiBaseInstance as axios } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
@@ -21,6 +21,7 @@ const logEvent = async (
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -96,6 +96,10 @@ const interceptorRejected = async (
|
||||
}
|
||||
};
|
||||
|
||||
const interceptorRejectedBase = async (
|
||||
value: AxiosResponse<any>,
|
||||
): Promise<AxiosResponse<any>> => Promise.reject(value);
|
||||
|
||||
const instance = axios.create({
|
||||
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
|
||||
});
|
||||
@@ -140,6 +144,18 @@ 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}`,
|
||||
|
||||
@@ -5,7 +5,6 @@ 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 {
|
||||
@@ -46,41 +45,19 @@ 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">
|
||||
{renderTagsAnimated()}
|
||||
{tags.map<React.ReactNode>((tag) => (
|
||||
<Tag
|
||||
key={tag}
|
||||
closable
|
||||
style={{ userSelect: 'none' }}
|
||||
onClose={(): void => handleClose(tag)}
|
||||
>
|
||||
<span>{tag}</span>
|
||||
</Tag>
|
||||
))}
|
||||
|
||||
{inputVisible && (
|
||||
<div className="add-tag-container">
|
||||
<Input
|
||||
|
||||
@@ -49,7 +49,10 @@ function ValueGraph({
|
||||
}
|
||||
>
|
||||
<Tooltip title={t('this_value_satisfies_multiple_thresholds')}>
|
||||
<ExclamationCircleFilled className="value-graph-icon" />
|
||||
<ExclamationCircleFilled
|
||||
className="value-graph-icon"
|
||||
data-testid="conflicting-thresholds"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface FacingIssueBtnProps {
|
||||
buttonText?: string;
|
||||
className?: string;
|
||||
onHoverText?: string;
|
||||
intercomMessageDisabled?: boolean;
|
||||
}
|
||||
|
||||
function FacingIssueBtn({
|
||||
@@ -25,11 +26,12 @@ function FacingIssueBtn({
|
||||
buttonText = '',
|
||||
className = '',
|
||||
onHoverText = '',
|
||||
intercomMessageDisabled = false,
|
||||
}: FacingIssueBtnProps): JSX.Element | null {
|
||||
const handleFacingIssuesClick = (): void => {
|
||||
logEvent(eventName, attributes);
|
||||
|
||||
if (window.Intercom) {
|
||||
if (window.Intercom && !intercomMessageDisabled) {
|
||||
window.Intercom('showNewMessage', defaultTo(message, ''));
|
||||
}
|
||||
};
|
||||
@@ -62,6 +64,7 @@ FacingIssueBtn.defaultProps = {
|
||||
buttonText: '',
|
||||
className: '',
|
||||
onHoverText: '',
|
||||
intercomMessageDisabled: false,
|
||||
};
|
||||
|
||||
export default FacingIssueBtn;
|
||||
|
||||
@@ -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,8 +137,6 @@ 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);
|
||||
@@ -316,7 +314,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
|
||||
const handleBilling = useCallback(async () => {
|
||||
if (isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription) {
|
||||
trackEvent('Billing : Upgrade Plan', {
|
||||
logEvent('Billing : Upgrade Plan', {
|
||||
user: pick(user, ['email', 'userId', 'name']),
|
||||
org,
|
||||
});
|
||||
@@ -327,7 +325,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
cancelURL: window.location.href,
|
||||
});
|
||||
} else {
|
||||
trackEvent('Billing : Manage Billing', {
|
||||
logEvent('Billing : Manage Billing', {
|
||||
user: pick(user, ['email', 'userId', 'name']),
|
||||
org,
|
||||
});
|
||||
|
||||
@@ -125,10 +125,9 @@ function GridCardGraph({
|
||||
offset: 0,
|
||||
limit: updatedQuery.builder.queryData[0].limit || 0,
|
||||
},
|
||||
// we do not need select columns in case of logs
|
||||
selectColumns:
|
||||
initialDataSource === DataSource.LOGS
|
||||
? widget.selectedLogFields
|
||||
: widget.selectedTracesFields,
|
||||
initialDataSource === DataSource.TRACES && widget.selectedTracesFields,
|
||||
},
|
||||
fillGaps: widget.fillSpans,
|
||||
};
|
||||
|
||||
@@ -940,3 +940,50 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,11 +34,14 @@ 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,
|
||||
@@ -603,243 +606,250 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
|
||||
<div className="limits-data">
|
||||
<div className="signals">
|
||||
{SIGNALS.map((signal) => (
|
||||
<div className="signal" key={signal}>
|
||||
<div className="header">
|
||||
<div className="signal-name">{signal}</div>
|
||||
<div className="actions">
|
||||
{hasLimits(signal) ? (
|
||||
<>
|
||||
{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]);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
icon={<PenLine size={14} />}
|
||||
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();
|
||||
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]);
|
||||
enableEditLimitMode(APIKey, {
|
||||
id: signal,
|
||||
signal,
|
||||
config: {},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<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();
|
||||
>
|
||||
Limits
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
enableEditLimitMode(APIKey, {
|
||||
id: signal,
|
||||
signal,
|
||||
config: {},
|
||||
});
|
||||
<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"
|
||||
>
|
||||
Limits
|
||||
</Button>
|
||||
<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">
|
||||
{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>
|
||||
)}
|
||||
</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>
|
||||
@@ -875,10 +885,35 @@ 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
|
||||
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>
|
||||
</Typography.Text>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -653,8 +653,9 @@ function DashboardsList(): JSX.Element {
|
||||
}}
|
||||
eventName="Dashboard: Facing Issues in dashboard"
|
||||
message={dashboardListMessage}
|
||||
buttonText="Facing issues with dashboards?"
|
||||
buttonText="Need help with dashboards?"
|
||||
onHoverText="Click here to get help with dashboards"
|
||||
intercomMessageDisabled
|
||||
/>
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { getNonIntegrationDashboardById } from 'mocks-server/__mockdata__/dashboards';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||
import { MemoryRouter, useLocation } from 'react-router-dom';
|
||||
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
|
||||
import DashboardDescription from '..';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn(),
|
||||
useRouteMatch: jest.fn().mockReturnValue({
|
||||
params: {
|
||||
dashboardId: 4,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'container/TopNav/DateTimeSelectionV2/index.tsx',
|
||||
() =>
|
||||
function MockDateTimeSelection(): JSX.Element {
|
||||
return <div>MockDateTimeSelection</div>;
|
||||
},
|
||||
);
|
||||
|
||||
describe('Dashboard landing page actions header tests', () => {
|
||||
it('unlock dashboard should be disabled for integrations created dashboards', async () => {
|
||||
const mockLocation = {
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}/dashboard/4`,
|
||||
search: '',
|
||||
};
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
const { getByTestId } = render(
|
||||
<MemoryRouter initialEntries={['/dashboard/4']}>
|
||||
<DashboardProvider>
|
||||
<DashboardDescription
|
||||
handle={{
|
||||
active: false,
|
||||
enter: (): Promise<void> => Promise.resolve(),
|
||||
exit: (): Promise<void> => Promise.resolve(),
|
||||
node: { current: null },
|
||||
}}
|
||||
/>
|
||||
</DashboardProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getByTestId('dashboard-title')).toHaveTextContent('thor'),
|
||||
);
|
||||
|
||||
const dashboardSettingsTrigger = getByTestId('options');
|
||||
|
||||
await fireEvent.click(dashboardSettingsTrigger);
|
||||
|
||||
const lockUnlockButton = screen.getByTestId('lock-unlock-dashboard');
|
||||
|
||||
await waitFor(() => expect(lockUnlockButton).toBeDisabled());
|
||||
});
|
||||
it('unlock dashboard should not be disabled for non integration created dashboards', async () => {
|
||||
const mockLocation = {
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}/dashboard/4`,
|
||||
search: '',
|
||||
};
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
server.use(
|
||||
rest.get('http://localhost/api/v1/dashboards/4', (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(getNonIntegrationDashboardById)),
|
||||
),
|
||||
);
|
||||
const { getByTestId } = render(
|
||||
<MemoryRouter initialEntries={['/dashboard/4']}>
|
||||
<DashboardProvider>
|
||||
<DashboardDescription
|
||||
handle={{
|
||||
active: false,
|
||||
enter: (): Promise<void> => Promise.resolve(),
|
||||
exit: (): Promise<void> => Promise.resolve(),
|
||||
node: { current: null },
|
||||
}}
|
||||
/>
|
||||
</DashboardProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getByTestId('dashboard-title')).toHaveTextContent('thor'),
|
||||
);
|
||||
|
||||
const dashboardSettingsTrigger = getByTestId('options');
|
||||
|
||||
await fireEvent.click(dashboardSettingsTrigger);
|
||||
|
||||
const lockUnlockButton = screen.getByTestId('lock-unlock-dashboard');
|
||||
|
||||
await waitFor(() => expect(lockUnlockButton).not.toBeDisabled());
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,16 @@
|
||||
import './Description.styles.scss';
|
||||
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Input, Modal, Popover, Tag, Typography } from 'antd';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Input,
|
||||
Modal,
|
||||
Popover,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||
import { dashboardHelpMessage } from 'components/facingIssueBtn/util';
|
||||
@@ -300,17 +309,6 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
{title}
|
||||
</Button>
|
||||
</section>
|
||||
<FacingIssueBtn
|
||||
attributes={{
|
||||
uuid: selectedDashboard?.uuid,
|
||||
title: updatedTitle,
|
||||
screen: 'Dashboard Details',
|
||||
}}
|
||||
eventName="Dashboard: Facing Issues in dashboard"
|
||||
message={dashboardHelpMessage(selectedDashboard?.data, selectedDashboard)}
|
||||
buttonText="Facing issues with dashboards?"
|
||||
onHoverText="Click here to get help with dashboard details"
|
||||
/>
|
||||
</div>
|
||||
<section className="dashbord-details">
|
||||
<div className="left-section">
|
||||
@@ -319,10 +317,24 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
alt="dashboard-img"
|
||||
style={{ width: '16px', height: '16px' }}
|
||||
/>
|
||||
<Typography.Text className="dashboard-title">{title}</Typography.Text>
|
||||
<Typography.Text className="dashboard-title" data-testid="dashboard-title">
|
||||
{title}
|
||||
</Typography.Text>
|
||||
{isDashboardLocked && <LockKeyhole size={14} />}
|
||||
</div>
|
||||
<div className="right-section">
|
||||
<FacingIssueBtn
|
||||
attributes={{
|
||||
uuid: selectedDashboard?.uuid,
|
||||
title: updatedTitle,
|
||||
screen: 'Dashboard Details',
|
||||
}}
|
||||
eventName="Dashboard: Facing Issues in dashboard"
|
||||
message={dashboardHelpMessage(selectedDashboard?.data, selectedDashboard)}
|
||||
buttonText="Need help with this dashboard?"
|
||||
onHoverText="Click here to get help with dashboard"
|
||||
intercomMessageDisabled
|
||||
/>
|
||||
<DateTimeSelectionV2 showAutoRefresh hideShareModal />
|
||||
<Popover
|
||||
open={isDashboardSettingsOpen}
|
||||
@@ -333,13 +345,22 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
<div className="menu-content">
|
||||
<section className="section-1">
|
||||
{(isAuthor || role === USER_ROLES.ADMIN) && (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<LockKeyhole size={14} />}
|
||||
onClick={handleLockDashboardToggle}
|
||||
<Tooltip
|
||||
title={
|
||||
selectedDashboard?.created_by === 'integration' &&
|
||||
'Dashboards created by integrations cannot be unlocked'
|
||||
}
|
||||
>
|
||||
{isDashboardLocked ? 'Unlock Dashboard' : 'Lock Dashboard'}
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<LockKeyhole size={14} />}
|
||||
disabled={selectedDashboard?.created_by === 'integration'}
|
||||
onClick={handleLockDashboardToggle}
|
||||
data-testid="lock-unlock-dashboard"
|
||||
>
|
||||
{isDashboardLocked ? 'Unlock Dashboard' : 'Lock Dashboard'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{!isDashboardLocked && editDashboard && (
|
||||
|
||||
@@ -309,6 +309,7 @@ function ExplorerColumnsRenderer({
|
||||
>
|
||||
<Button
|
||||
className="action-btn"
|
||||
data-testid="add-columns-button"
|
||||
icon={
|
||||
<PlusCircle
|
||||
size={16}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Tabs, Tooltip, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import PromQLIcon from 'assets/Dashboard/PromQl';
|
||||
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
||||
@@ -236,6 +237,21 @@ function QuerySection({
|
||||
onChange={handleQueryCategoryChange}
|
||||
tabBarExtraContent={
|
||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||
<FacingIssueBtn
|
||||
attributes={{
|
||||
uuid: selectedDashboard?.uuid,
|
||||
title: selectedDashboard?.data.title,
|
||||
screen: 'Dashboard widget',
|
||||
panelType: selectedGraph,
|
||||
widgetId: query.id,
|
||||
queryType: currentQuery.queryType,
|
||||
}}
|
||||
eventName="Dashboard: Facing Issues in dashboard"
|
||||
buttonText="Need help with this chart?"
|
||||
// message={chartHelpMessage(selectedDashboard, graphType)}
|
||||
onHoverText="Click here to get help with this dashboard widget"
|
||||
intercomMessageDisabled
|
||||
/>
|
||||
<TextToolTip
|
||||
text="This will temporarily save the current query and graph state. This will persist across tab change"
|
||||
url="https://signoz.io/docs/userguide/query-builder?utm_source=product&utm_medium=query-builder"
|
||||
|
||||
@@ -4,8 +4,6 @@ import './NewWidget.styles.scss';
|
||||
import { WarningOutlined } from '@ant-design/icons';
|
||||
import { Button, Flex, Modal, Space, Tooltip, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||
import { chartHelpMessage } from 'components/facingIssueBtn/util';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@@ -79,6 +77,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
stagedQuery,
|
||||
redirectWithQueryBuilderData,
|
||||
supersetQuery,
|
||||
setSupersetQuery,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const isQueryModified = useMemo(
|
||||
@@ -548,6 +547,17 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
isNewTraceLogsAvailable,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* we need this extra handling for superset query because we cannot keep this in sync with current query
|
||||
* always.we do not sync superset query in the initQueryBuilderData because that function is called on all stage and run
|
||||
* actions. we do not want that as we loose out on superset functionalities if we do the same. hence initialising the superset query
|
||||
* on mount here with the currentQuery in the begining itself
|
||||
*/
|
||||
setSupersetQuery(currentQuery);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
registerShortcut(DashboardShortcuts.SaveChanges, onSaveDashboard);
|
||||
registerShortcut(DashboardShortcuts.DiscardChanges, onClickDiscardHandler);
|
||||
@@ -563,11 +573,11 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
if (selectedGraph === PANEL_TYPES.LIST) {
|
||||
const initialDataSource = currentQuery.builder.queryData[0].dataSource;
|
||||
if (initialDataSource === DataSource.LOGS) {
|
||||
// we do not need selected log columns in the request data as the entire response contains all the necessary data
|
||||
setRequestData((prev) => ({
|
||||
...prev,
|
||||
tableParams: {
|
||||
...prev.tableParams,
|
||||
selectColumns: selectedLogFields,
|
||||
},
|
||||
}));
|
||||
} else if (initialDataSource === DataSource.TRACES) {
|
||||
@@ -596,20 +606,6 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
<Typography.Text className="configure-panel">
|
||||
Configure panel
|
||||
</Typography.Text>
|
||||
<FacingIssueBtn
|
||||
attributes={{
|
||||
uuid: selectedDashboard?.uuid,
|
||||
title: selectedDashboard?.data.title,
|
||||
screen: 'Dashboard widget',
|
||||
panelType: graphType,
|
||||
widgetId: query.get('widgetId'),
|
||||
queryType: currentQuery.queryType,
|
||||
}}
|
||||
eventName="Dashboard: Facing Issues in dashboard"
|
||||
message={chartHelpMessage(selectedDashboard, graphType)}
|
||||
buttonText="Facing issues with dashboards?"
|
||||
onHoverText="Click here to get help with dashboard widget"
|
||||
/>
|
||||
</Flex>
|
||||
</div>
|
||||
{isSaveDisabled && (
|
||||
|
||||
@@ -11,7 +11,6 @@ import ROUTES from 'constants/routes';
|
||||
import FullScreenHeader from 'container/FullScreenHeader/FullScreenHeader';
|
||||
import InviteUserModal from 'container/OrganizationSettings/InviteUserModal/InviteUserModal';
|
||||
import { InviteMemberFormValues } from 'container/OrganizationSettings/PendingInvitesContainer';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import history from 'lib/history';
|
||||
import { UserPlus } from 'lucide-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
@@ -104,7 +103,6 @@ export default function Onboarding(): JSX.Element {
|
||||
const [selectedModuleSteps, setSelectedModuleSteps] = useState(APM_STEPS);
|
||||
const [activeStep, setActiveStep] = useState(1);
|
||||
const [current, setCurrent] = useState(0);
|
||||
const { trackEvent } = useAnalytics();
|
||||
const { location } = history;
|
||||
const { t } = useTranslation(['onboarding']);
|
||||
|
||||
@@ -120,7 +118,7 @@ export default function Onboarding(): JSX.Element {
|
||||
} = useOnboardingContext();
|
||||
|
||||
useEffectOnce(() => {
|
||||
trackEvent('Onboarding V2 Started');
|
||||
logEvent('Onboarding V2 Started', {});
|
||||
});
|
||||
|
||||
const { status, data: ingestionData } = useQuery({
|
||||
@@ -231,7 +229,7 @@ export default function Onboarding(): JSX.Element {
|
||||
const nextStep = activeStep + 1;
|
||||
|
||||
// on next
|
||||
trackEvent('Onboarding V2: Get Started', {
|
||||
logEvent('Onboarding V2: Get Started', {
|
||||
selectedModule: selectedModule.id,
|
||||
nextStepId: nextStep,
|
||||
});
|
||||
|
||||
@@ -5,9 +5,9 @@ import {
|
||||
CloseCircleTwoTone,
|
||||
LoadingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import Header from 'container/OnboardingContainer/common/Header/Header';
|
||||
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import { useQueryService } from 'hooks/useQueryService';
|
||||
import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
|
||||
@@ -41,8 +41,6 @@ export default function ConnectionStatus(): JSX.Element {
|
||||
[queries],
|
||||
);
|
||||
|
||||
const { trackEvent } = useAnalytics();
|
||||
|
||||
const [retryCount, setRetryCount] = useState(20); // Retry for 3 mins 20s
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isReceivingData, setIsReceivingData] = useState(false);
|
||||
@@ -155,7 +153,7 @@ export default function ConnectionStatus(): JSX.Element {
|
||||
if (data || isError) {
|
||||
setRetryCount(retryCount - 1);
|
||||
if (retryCount < 0) {
|
||||
trackEvent('Onboarding V2: Connection Status', {
|
||||
logEvent('Onboarding V2: Connection Status', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
framework: selectedFramework,
|
||||
environment: selectedEnvironment,
|
||||
@@ -174,7 +172,7 @@ export default function ConnectionStatus(): JSX.Element {
|
||||
setLoading(false);
|
||||
setIsReceivingData(true);
|
||||
|
||||
trackEvent('Onboarding V2: Connection Status', {
|
||||
logEvent('Onboarding V2: Connection Status', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
framework: selectedFramework,
|
||||
environment: selectedEnvironment,
|
||||
|
||||
@@ -5,11 +5,11 @@ import {
|
||||
CloseCircleTwoTone,
|
||||
LoadingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import Header from 'container/OnboardingContainer/common/Header/Header';
|
||||
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
@@ -32,7 +32,6 @@ export default function LogsConnectionStatus(): JSX.Element {
|
||||
activeStep,
|
||||
selectedEnvironment,
|
||||
} = useOnboardingContext();
|
||||
const { trackEvent } = useAnalytics();
|
||||
const [isReceivingData, setIsReceivingData] = useState(false);
|
||||
const [pollingInterval, setPollingInterval] = useState<number | false>(15000); // initial Polling interval of 15 secs , Set to false after 5 mins
|
||||
const [retryCount, setRetryCount] = useState(20); // Retry for 5 mins
|
||||
@@ -105,7 +104,7 @@ export default function LogsConnectionStatus(): JSX.Element {
|
||||
setRetryCount(retryCount - 1);
|
||||
|
||||
if (retryCount < 0) {
|
||||
trackEvent('Onboarding V2: Connection Status', {
|
||||
logEvent('Onboarding V2: Connection Status', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
environment: selectedEnvironment,
|
||||
module: activeStep?.module?.id,
|
||||
@@ -141,7 +140,7 @@ export default function LogsConnectionStatus(): JSX.Element {
|
||||
setRetryCount(-1);
|
||||
setPollingInterval(false);
|
||||
|
||||
trackEvent('Onboarding V2: Connection Status', {
|
||||
logEvent('Onboarding V2: Connection Status', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
environment: selectedEnvironment,
|
||||
module: activeStep?.module?.id,
|
||||
|
||||
@@ -15,7 +15,6 @@ import ROUTES from 'constants/routes';
|
||||
import { stepsMap } from 'container/OnboardingContainer/constants/stepsConfig';
|
||||
import { DataSourceType } from 'container/OnboardingContainer/Steps/DataSource/DataSource';
|
||||
import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUtils';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import history from 'lib/history';
|
||||
import { isEmpty, isNull } from 'lodash-es';
|
||||
import { HelpCircle, UserPlus } from 'lucide-react';
|
||||
@@ -79,7 +78,6 @@ export default function ModuleStepsContainer({
|
||||
} = useOnboardingContext();
|
||||
|
||||
const [current, setCurrent] = useState(0);
|
||||
const { trackEvent } = useAnalytics();
|
||||
const [metaData, setMetaData] = useState<MetaDataProps[]>(defaultMetaData);
|
||||
const lastStepIndex = selectedModuleSteps.length - 1;
|
||||
|
||||
@@ -143,7 +141,7 @@ export default function ModuleStepsContainer({
|
||||
};
|
||||
|
||||
const redirectToModules = (): void => {
|
||||
trackEvent('Onboarding V2 Complete', {
|
||||
logEvent('Onboarding V2 Complete', {
|
||||
module: selectedModule.id,
|
||||
dataSource: selectedDataSource?.id,
|
||||
framework: selectedFramework,
|
||||
@@ -186,14 +184,14 @@ export default function ModuleStepsContainer({
|
||||
// on next step click track events
|
||||
switch (selectedModuleSteps[current].id) {
|
||||
case stepsMap.dataSource:
|
||||
trackEvent('Onboarding V2: Data Source Selected', {
|
||||
logEvent('Onboarding V2: Data Source Selected', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
framework: selectedFramework,
|
||||
module: activeStep?.module?.id,
|
||||
});
|
||||
break;
|
||||
case stepsMap.environmentDetails:
|
||||
trackEvent('Onboarding V2: Environment Selected', {
|
||||
logEvent('Onboarding V2: Environment Selected', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
framework: selectedFramework,
|
||||
environment: selectedEnvironment,
|
||||
@@ -201,7 +199,7 @@ export default function ModuleStepsContainer({
|
||||
});
|
||||
break;
|
||||
case stepsMap.selectMethod:
|
||||
trackEvent('Onboarding V2: Method Selected', {
|
||||
logEvent('Onboarding V2: Method Selected', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
framework: selectedFramework,
|
||||
environment: selectedEnvironment,
|
||||
@@ -211,7 +209,7 @@ export default function ModuleStepsContainer({
|
||||
break;
|
||||
|
||||
case stepsMap.setupOtelCollector:
|
||||
trackEvent('Onboarding V2: Setup Otel Collector', {
|
||||
logEvent('Onboarding V2: Setup Otel Collector', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
framework: selectedFramework,
|
||||
environment: selectedEnvironment,
|
||||
@@ -220,7 +218,7 @@ export default function ModuleStepsContainer({
|
||||
});
|
||||
break;
|
||||
case stepsMap.instrumentApplication:
|
||||
trackEvent('Onboarding V2: Instrument Application', {
|
||||
logEvent('Onboarding V2: Instrument Application', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
framework: selectedFramework,
|
||||
environment: selectedEnvironment,
|
||||
@@ -229,13 +227,13 @@ export default function ModuleStepsContainer({
|
||||
});
|
||||
break;
|
||||
case stepsMap.cloneRepository:
|
||||
trackEvent('Onboarding V2: Clone Repository', {
|
||||
logEvent('Onboarding V2: Clone Repository', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
module: activeStep?.module?.id,
|
||||
});
|
||||
break;
|
||||
case stepsMap.runApplication:
|
||||
trackEvent('Onboarding V2: Run Application', {
|
||||
logEvent('Onboarding V2: Run Application', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
framework: selectedFramework,
|
||||
environment: selectedEnvironment,
|
||||
@@ -244,95 +242,95 @@ export default function ModuleStepsContainer({
|
||||
});
|
||||
break;
|
||||
case stepsMap.addHttpDrain:
|
||||
trackEvent('Onboarding V2: Add HTTP Drain', {
|
||||
logEvent('Onboarding V2: Add HTTP Drain', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
module: activeStep?.module?.id,
|
||||
});
|
||||
break;
|
||||
case stepsMap.startContainer:
|
||||
trackEvent('Onboarding V2: Start Container', {
|
||||
logEvent('Onboarding V2: Start Container', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
module: activeStep?.module?.id,
|
||||
});
|
||||
break;
|
||||
case stepsMap.setupLogDrains:
|
||||
trackEvent('Onboarding V2: Setup Log Drains', {
|
||||
logEvent('Onboarding V2: Setup Log Drains', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
module: activeStep?.module?.id,
|
||||
});
|
||||
break;
|
||||
case stepsMap.configureReceiver:
|
||||
trackEvent('Onboarding V2: Configure Receiver', {
|
||||
logEvent('Onboarding V2: Configure Receiver', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
environment: selectedEnvironment,
|
||||
module: activeStep?.module?.id,
|
||||
});
|
||||
break;
|
||||
case stepsMap.configureAws:
|
||||
trackEvent('Onboarding V2: Configure AWS', {
|
||||
logEvent('Onboarding V2: Configure AWS', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
environment: selectedEnvironment,
|
||||
module: activeStep?.module?.id,
|
||||
});
|
||||
break;
|
||||
case stepsMap.sendLogsCloudwatch:
|
||||
trackEvent('Onboarding V2: Send Logs Cloudwatch', {
|
||||
logEvent('Onboarding V2: Send Logs Cloudwatch', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
environment: selectedEnvironment,
|
||||
module: activeStep?.module?.id,
|
||||
});
|
||||
break;
|
||||
case stepsMap.setupDaemonService:
|
||||
trackEvent('Onboarding V2: Setup ECS Daemon Service', {
|
||||
logEvent('Onboarding V2: Setup ECS Daemon Service', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
environment: selectedEnvironment,
|
||||
module: activeStep?.module?.id,
|
||||
});
|
||||
break;
|
||||
case stepsMap.createOtelConfig:
|
||||
trackEvent('Onboarding V2: Create ECS OTel Config', {
|
||||
logEvent('Onboarding V2: Create ECS OTel Config', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
environment: selectedEnvironment,
|
||||
module: activeStep?.module?.id,
|
||||
});
|
||||
break;
|
||||
case stepsMap.createDaemonService:
|
||||
trackEvent('Onboarding V2: Create ECS Daemon Service', {
|
||||
logEvent('Onboarding V2: Create ECS Daemon Service', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
environment: selectedEnvironment,
|
||||
module: activeStep?.module?.id,
|
||||
});
|
||||
break;
|
||||
case stepsMap.ecsSendData:
|
||||
trackEvent('Onboarding V2: ECS send traces data', {
|
||||
logEvent('Onboarding V2: ECS send traces data', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
environment: selectedEnvironment,
|
||||
module: activeStep?.module?.id,
|
||||
});
|
||||
break;
|
||||
case stepsMap.createSidecarCollectorContainer:
|
||||
trackEvent('Onboarding V2: ECS create Sidecar Container', {
|
||||
logEvent('Onboarding V2: ECS create Sidecar Container', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
environment: selectedEnvironment,
|
||||
module: activeStep?.module?.id,
|
||||
});
|
||||
break;
|
||||
case stepsMap.deployTaskDefinition:
|
||||
trackEvent('Onboarding V2: ECS deploy task definition', {
|
||||
logEvent('Onboarding V2: ECS deploy task definition', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
environment: selectedEnvironment,
|
||||
module: activeStep?.module?.id,
|
||||
});
|
||||
break;
|
||||
case stepsMap.ecsSendLogsData:
|
||||
trackEvent('Onboarding V2: ECS Fargate send logs data', {
|
||||
logEvent('Onboarding V2: ECS Fargate send logs data', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
environment: selectedEnvironment,
|
||||
module: activeStep?.module?.id,
|
||||
});
|
||||
break;
|
||||
case stepsMap.monitorDashboard:
|
||||
trackEvent('Onboarding V2: EKS monitor dashboard', {
|
||||
logEvent('Onboarding V2: EKS monitor dashboard', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
environment: selectedEnvironment,
|
||||
module: activeStep?.module?.id,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { render } from 'tests/test-utils';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
import TablePanelWrapper from '../TablePanelWrapper';
|
||||
import {
|
||||
tablePanelQueryResponse,
|
||||
tablePanelWidgetQuery,
|
||||
} from './tablePanelWrapperHelper';
|
||||
|
||||
describe('Table panel wrappper tests', () => {
|
||||
it('table should render fine with the query response and column units', () => {
|
||||
const { container, getByText } = render(
|
||||
<TablePanelWrapper
|
||||
widget={(tablePanelWidgetQuery as unknown) as Widgets}
|
||||
queryResponse={(tablePanelQueryResponse as unknown) as any}
|
||||
onDragSelect={(): void => {}}
|
||||
/>,
|
||||
);
|
||||
// checking the overall rendering of the table
|
||||
expect(container).toMatchSnapshot();
|
||||
|
||||
// the first row of the table should have the latency value with units
|
||||
expect(getByText('4.35 s')).toBeInTheDocument();
|
||||
|
||||
// the rows should have optimised value for human readability
|
||||
expect(getByText('31.3 ms')).toBeInTheDocument();
|
||||
|
||||
// the applied legend should appear as the column header
|
||||
expect(getByText('latency-per-service')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { render } from 'tests/test-utils';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
import ValuePanelWrapper from '../ValuePanelWrapper';
|
||||
import {
|
||||
thresholds,
|
||||
valuePanelQueryResponse,
|
||||
valuePanelWidget,
|
||||
} from './valuePanelWrapperHelper';
|
||||
|
||||
describe('Value panel wrappper tests', () => {
|
||||
it('should render value panel correctly with yaxis unit', () => {
|
||||
const { getByText } = render(
|
||||
<ValuePanelWrapper
|
||||
widget={(valuePanelWidget as unknown) as Widgets}
|
||||
queryResponse={(valuePanelQueryResponse as unknown) as any}
|
||||
onDragSelect={(): void => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
// selected y axis unit as miliseconds (ms)
|
||||
expect(getByText('295 ms')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render tooltip when there are conflicting thresholds', () => {
|
||||
const { getByTestId, container } = render(
|
||||
<ValuePanelWrapper
|
||||
widget={({ ...valuePanelWidget, thresholds } as unknown) as Widgets}
|
||||
queryResponse={(valuePanelQueryResponse as unknown) as any}
|
||||
onDragSelect={(): void => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('conflicting-thresholds')).toBeInTheDocument();
|
||||
// added snapshot test here for checking the thresholds color being applied properly
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,389 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Table panel wrappper tests table should render fine with the query response and column units 1`] = `
|
||||
.c1 {
|
||||
position: absolute;
|
||||
right: -0.313rem;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
width: 0.625rem;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
height: 95%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.c0 .ant-table-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.c0 .ant-spin-nested-loading {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.c0 .ant-spin-container {
|
||||
height: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.c0 .ant-table {
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.c0 .ant-table > .ant-table-container > .ant-table-content > table {
|
||||
min-width: 99% !important;
|
||||
}
|
||||
|
||||
<div>
|
||||
|
||||
<div
|
||||
class="c0"
|
||||
>
|
||||
<div
|
||||
class="query-table"
|
||||
>
|
||||
<div
|
||||
class="ant-table-wrapper css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
<div
|
||||
class="ant-spin-nested-loading css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
<div
|
||||
class="ant-spin-container"
|
||||
>
|
||||
<div
|
||||
class="ant-table ant-table-small ant-table-layout-fixed ant-table-scroll-horizontal"
|
||||
>
|
||||
<div
|
||||
class="ant-table-container"
|
||||
>
|
||||
<div
|
||||
class="ant-table-content"
|
||||
style="overflow-x: auto; overflow-y: hidden;"
|
||||
>
|
||||
<table
|
||||
style="width: auto; min-width: 100%; table-layout: fixed;"
|
||||
>
|
||||
<colgroup>
|
||||
<col
|
||||
style="width: 145px;"
|
||||
/>
|
||||
<col
|
||||
style="width: 145px;"
|
||||
/>
|
||||
</colgroup>
|
||||
<thead
|
||||
class="ant-table-thead"
|
||||
>
|
||||
<tr>
|
||||
<th
|
||||
aria-label="service_name"
|
||||
class="ant-table-cell ant-table-column-has-sorters react-resizable"
|
||||
scope="col"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="ant-table-column-sorters"
|
||||
>
|
||||
<span
|
||||
class="ant-table-column-title"
|
||||
>
|
||||
service_name
|
||||
</span>
|
||||
<span
|
||||
class="ant-table-column-sorter ant-table-column-sorter-full"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="ant-table-column-sorter-inner"
|
||||
>
|
||||
<span
|
||||
aria-label="caret-up"
|
||||
class="anticon anticon-caret-up ant-table-column-sorter-up"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="caret-up"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="0 0 1024 1024"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M858.9 689L530.5 308.2c-9.4-10.9-27.5-10.9-37 0L165.1 689c-12.2 14.2-1.2 35 18.5 35h656.8c19.7 0 30.7-20.8 18.5-35z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
aria-label="caret-down"
|
||||
class="anticon anticon-caret-down ant-table-column-sorter-down"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="caret-down"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="0 0 1024 1024"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M840.4 300H183.6c-19.7 0-30.7 20.8-18.5 35l328.4 380.8c9.4 10.9 27.5 10.9 37 0L858.9 335c12.2-14.2 1.2-35-18.5-35z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="c1 react-resizable-handle"
|
||||
/>
|
||||
</th>
|
||||
<th
|
||||
aria-label="latency-per-service"
|
||||
class="ant-table-cell ant-table-column-has-sorters react-resizable"
|
||||
scope="col"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="ant-table-column-sorters"
|
||||
>
|
||||
<span
|
||||
class="ant-table-column-title"
|
||||
>
|
||||
latency-per-service
|
||||
</span>
|
||||
<span
|
||||
class="ant-table-column-sorter ant-table-column-sorter-full"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="ant-table-column-sorter-inner"
|
||||
>
|
||||
<span
|
||||
aria-label="caret-up"
|
||||
class="anticon anticon-caret-up ant-table-column-sorter-up"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="caret-up"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="0 0 1024 1024"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M858.9 689L530.5 308.2c-9.4-10.9-27.5-10.9-37 0L165.1 689c-12.2 14.2-1.2 35 18.5 35h656.8c19.7 0 30.7-20.8 18.5-35z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
aria-label="caret-down"
|
||||
class="anticon anticon-caret-down ant-table-column-sorter-down"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="caret-down"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="0 0 1024 1024"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M840.4 300H183.6c-19.7 0-30.7 20.8-18.5 35l328.4 380.8c9.4 10.9 27.5 10.9 37 0L858.9 335c12.2-14.2 1.2-35-18.5-35z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="c1 react-resizable-handle"
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
class="ant-table-tbody"
|
||||
>
|
||||
<tr
|
||||
aria-hidden="true"
|
||||
class="ant-table-measure-row"
|
||||
style="height: 0px; font-size: 0px;"
|
||||
>
|
||||
<td
|
||||
style="padding: 0px; border: 0px; height: 0px;"
|
||||
>
|
||||
<div
|
||||
style="height: 0px; overflow: hidden;"
|
||||
>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
style="padding: 0px; border: 0px; height: 0px;"
|
||||
>
|
||||
<div
|
||||
style="height: 0px; overflow: hidden;"
|
||||
>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="ant-table-row ant-table-row-level-0"
|
||||
>
|
||||
<td
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
demo-app
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
4.35 s
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="ant-table-row ant-table-row-level-0"
|
||||
>
|
||||
<td
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
customer
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
431 ms
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="ant-table-row ant-table-row-level-0"
|
||||
>
|
||||
<td
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
mysql
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
431 ms
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="ant-table-row ant-table-row-level-0"
|
||||
>
|
||||
<td
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
frontend
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
287 ms
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="ant-table-row ant-table-row-level-0"
|
||||
>
|
||||
<td
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
driver
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
230 ms
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="ant-table-row ant-table-row-level-0"
|
||||
>
|
||||
<td
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
route
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
66.4 ms
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="ant-table-row ant-table-row-level-0"
|
||||
>
|
||||
<td
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
redis
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
31.3 ms
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,76 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Value panel wrappper tests should render tooltip when there are conflicting thresholds 1`] = `
|
||||
.c1 {
|
||||
height: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
text-align: center;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
<div>
|
||||
|
||||
<div
|
||||
class="c0"
|
||||
>
|
||||
<article
|
||||
class="ant-typography css-dev-only-do-not-override-2i2tap"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c1"
|
||||
>
|
||||
<div
|
||||
class="value-graph-container"
|
||||
>
|
||||
<span
|
||||
class="ant-typography value-graph-text css-dev-only-do-not-override-2i2tap"
|
||||
style="color: Blue;"
|
||||
>
|
||||
295 ms
|
||||
</span>
|
||||
<div
|
||||
class="value-graph-textconflict"
|
||||
>
|
||||
<span
|
||||
aria-label="exclamation-circle"
|
||||
class="anticon anticon-exclamation-circle value-graph-icon"
|
||||
data-testid="conflicting-thresholds"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="exclamation-circle"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,286 @@
|
||||
export const tablePanelWidgetQuery = {
|
||||
id: '727533b0-7718-4f99-a1db-a1875649325c',
|
||||
title: '',
|
||||
description: '',
|
||||
isStacked: false,
|
||||
nullZeroValues: 'zero',
|
||||
opacity: '1',
|
||||
panelTypes: 'table',
|
||||
query: {
|
||||
clickhouse_sql: [
|
||||
{
|
||||
name: 'A',
|
||||
legend: '',
|
||||
disabled: false,
|
||||
query: '',
|
||||
},
|
||||
],
|
||||
promql: [
|
||||
{
|
||||
name: 'A',
|
||||
query: '',
|
||||
legend: '',
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
dataSource: 'metrics',
|
||||
queryName: 'A',
|
||||
aggregateOperator: 'count',
|
||||
aggregateAttribute: {
|
||||
key: 'signoz_latency',
|
||||
dataType: 'float64',
|
||||
type: 'ExponentialHistogram',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'signoz_latency--float64--ExponentialHistogram--true',
|
||||
},
|
||||
timeAggregation: '',
|
||||
spaceAggregation: 'p90',
|
||||
functions: [],
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
stepInterval: 60,
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
groupBy: [
|
||||
{
|
||||
key: 'service_name',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
id: 'service_name--string--tag--false',
|
||||
},
|
||||
],
|
||||
legend: 'latency-per-service',
|
||||
reduceTo: 'avg',
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
},
|
||||
id: '7feafec2-a450-4b5a-8897-260c1a9fe1e4',
|
||||
queryType: 'builder',
|
||||
},
|
||||
timePreferance: 'GLOBAL_TIME',
|
||||
softMax: null,
|
||||
softMin: null,
|
||||
selectedLogFields: [
|
||||
{
|
||||
dataType: 'string',
|
||||
type: '',
|
||||
name: 'body',
|
||||
},
|
||||
{
|
||||
dataType: 'string',
|
||||
type: '',
|
||||
name: 'timestamp',
|
||||
},
|
||||
],
|
||||
selectedTracesFields: [
|
||||
{
|
||||
key: 'serviceName',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'serviceName--string--tag--true',
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'name--string--tag--true',
|
||||
},
|
||||
{
|
||||
key: 'durationNano',
|
||||
dataType: 'float64',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'durationNano--float64--tag--true',
|
||||
},
|
||||
{
|
||||
key: 'httpMethod',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'httpMethod--string--tag--true',
|
||||
},
|
||||
{
|
||||
key: 'responseStatusCode',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'responseStatusCode--string--tag--true',
|
||||
},
|
||||
],
|
||||
yAxisUnit: 'none',
|
||||
thresholds: [],
|
||||
fillSpans: false,
|
||||
columnUnits: {
|
||||
A: 'ms',
|
||||
},
|
||||
bucketCount: 30,
|
||||
stackedBarChart: false,
|
||||
bucketWidth: 0,
|
||||
mergeAllActiveQueries: false,
|
||||
};
|
||||
|
||||
export const tablePanelQueryResponse = {
|
||||
status: 'success',
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
isIdle: false,
|
||||
data: {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'success',
|
||||
payload: {
|
||||
status: 'success',
|
||||
data: {
|
||||
resultType: '',
|
||||
result: [
|
||||
{
|
||||
table: {
|
||||
columns: [
|
||||
{
|
||||
name: 'service_name',
|
||||
queryName: '',
|
||||
isValueColumn: false,
|
||||
},
|
||||
{
|
||||
name: 'A',
|
||||
queryName: 'A',
|
||||
isValueColumn: true,
|
||||
},
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
data: {
|
||||
A: 4353.81,
|
||||
service_name: 'demo-app',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 431.25,
|
||||
service_name: 'customer',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 431.25,
|
||||
service_name: 'mysql',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 287.11,
|
||||
service_name: 'frontend',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 230.02,
|
||||
service_name: 'driver',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 66.37,
|
||||
service_name: 'route',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 31.3,
|
||||
service_name: 'redis',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
params: {
|
||||
start: 1721207225000,
|
||||
end: 1721207525000,
|
||||
step: 60,
|
||||
variables: {},
|
||||
formatForWeb: true,
|
||||
compositeQuery: {
|
||||
queryType: 'builder',
|
||||
panelType: 'table',
|
||||
fillGaps: false,
|
||||
builderQueries: {
|
||||
A: {
|
||||
dataSource: 'metrics',
|
||||
queryName: 'A',
|
||||
aggregateOperator: 'count',
|
||||
aggregateAttribute: {
|
||||
key: 'signoz_latency',
|
||||
dataType: 'float64',
|
||||
type: 'ExponentialHistogram',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'signoz_latency--float64--ExponentialHistogram--true',
|
||||
},
|
||||
timeAggregation: '',
|
||||
spaceAggregation: 'p90',
|
||||
functions: [],
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
stepInterval: 60,
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
groupBy: [
|
||||
{
|
||||
key: 'service_name',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
id: 'service_name--string--tag--false',
|
||||
},
|
||||
],
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
dataUpdatedAt: 1721207526018,
|
||||
error: null,
|
||||
errorUpdatedAt: 0,
|
||||
failureCount: 0,
|
||||
errorUpdateCount: 0,
|
||||
isFetched: true,
|
||||
isFetchedAfterMount: true,
|
||||
isFetching: false,
|
||||
isRefetching: false,
|
||||
isLoadingError: false,
|
||||
isPlaceholderData: false,
|
||||
isPreviousData: false,
|
||||
isRefetchError: false,
|
||||
isStale: true,
|
||||
};
|
||||
@@ -0,0 +1,267 @@
|
||||
export const valuePanelWidget = {
|
||||
id: 'b8b93086-ef01-47bf-9044-1e7abd583be4',
|
||||
title: 'signoz latency in ms',
|
||||
description: '',
|
||||
isStacked: false,
|
||||
nullZeroValues: 'zero',
|
||||
opacity: '1',
|
||||
panelTypes: 'value',
|
||||
query: {
|
||||
clickhouse_sql: [
|
||||
{
|
||||
name: 'A',
|
||||
legend: '',
|
||||
disabled: false,
|
||||
query: '',
|
||||
},
|
||||
],
|
||||
promql: [
|
||||
{
|
||||
name: 'A',
|
||||
query: '',
|
||||
legend: '',
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
dataSource: 'metrics',
|
||||
queryName: 'A',
|
||||
aggregateOperator: 'count',
|
||||
aggregateAttribute: {
|
||||
key: 'signoz_latency',
|
||||
dataType: 'float64',
|
||||
type: 'ExponentialHistogram',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'signoz_latency--float64--ExponentialHistogram--true',
|
||||
},
|
||||
timeAggregation: '',
|
||||
spaceAggregation: 'p90',
|
||||
functions: [],
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
stepInterval: 60,
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
groupBy: [],
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
},
|
||||
id: '3bec289c-49c3-4d7e-98bb-84d47c79909c',
|
||||
queryType: 'builder',
|
||||
},
|
||||
timePreferance: 'GLOBAL_TIME',
|
||||
softMax: null,
|
||||
softMin: null,
|
||||
selectedLogFields: [
|
||||
{
|
||||
dataType: 'string',
|
||||
type: '',
|
||||
name: 'body',
|
||||
},
|
||||
{
|
||||
dataType: 'string',
|
||||
type: '',
|
||||
name: 'timestamp',
|
||||
},
|
||||
],
|
||||
selectedTracesFields: [
|
||||
{
|
||||
key: 'serviceName',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'serviceName--string--tag--true',
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'name--string--tag--true',
|
||||
},
|
||||
{
|
||||
key: 'durationNano',
|
||||
dataType: 'float64',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'durationNano--float64--tag--true',
|
||||
},
|
||||
{
|
||||
key: 'httpMethod',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'httpMethod--string--tag--true',
|
||||
},
|
||||
{
|
||||
key: 'responseStatusCode',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'responseStatusCode--string--tag--true',
|
||||
},
|
||||
],
|
||||
yAxisUnit: 'ms',
|
||||
thresholds: [],
|
||||
fillSpans: false,
|
||||
columnUnits: {},
|
||||
bucketCount: 30,
|
||||
stackedBarChart: false,
|
||||
bucketWidth: 0,
|
||||
mergeAllActiveQueries: false,
|
||||
};
|
||||
|
||||
export const thresholds = [
|
||||
{
|
||||
index: '8eb16a3a-b4f1-47c8-943a-4b1786884583',
|
||||
isEditEnabled: false,
|
||||
thresholdColor: 'Blue',
|
||||
thresholdFormat: 'Text',
|
||||
thresholdOperator: '>',
|
||||
thresholdUnit: 'none',
|
||||
thresholdValue: 100,
|
||||
keyIndex: 1,
|
||||
selectedGraph: 'value',
|
||||
thresholdTableOptions: '',
|
||||
thresholdLabel: '',
|
||||
},
|
||||
{
|
||||
index: 'eb9c1186-ad7d-42dd-8e7f-3913a321d7cf',
|
||||
isEditEnabled: false,
|
||||
thresholdColor: 'Red',
|
||||
thresholdFormat: 'Text',
|
||||
thresholdOperator: '>',
|
||||
thresholdUnit: 'none',
|
||||
thresholdValue: 0,
|
||||
keyIndex: 0,
|
||||
selectedGraph: 'value',
|
||||
thresholdTableOptions: '',
|
||||
thresholdLabel: '',
|
||||
},
|
||||
];
|
||||
|
||||
export const valuePanelQueryResponse = {
|
||||
status: 'success',
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
isIdle: false,
|
||||
data: {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'success',
|
||||
payload: {
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
metric: {
|
||||
A: 'A',
|
||||
},
|
||||
values: [[0, '295.4299833508185']],
|
||||
queryName: 'A',
|
||||
legend: 'A',
|
||||
},
|
||||
],
|
||||
resultType: '',
|
||||
newResult: {
|
||||
status: 'success',
|
||||
data: {
|
||||
resultType: '',
|
||||
result: [
|
||||
{
|
||||
queryName: 'A',
|
||||
series: [
|
||||
{
|
||||
labels: {
|
||||
A: 'A',
|
||||
},
|
||||
labelsArray: null,
|
||||
values: [
|
||||
{
|
||||
timestamp: 0,
|
||||
value: '295.4299833508185',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
params: {
|
||||
start: 1721203451000,
|
||||
end: 1721203751000,
|
||||
step: 60,
|
||||
variables: {},
|
||||
formatForWeb: false,
|
||||
compositeQuery: {
|
||||
queryType: 'builder',
|
||||
panelType: 'value',
|
||||
fillGaps: false,
|
||||
builderQueries: {
|
||||
A: {
|
||||
dataSource: 'metrics',
|
||||
queryName: 'A',
|
||||
aggregateOperator: 'count',
|
||||
aggregateAttribute: {
|
||||
key: 'signoz_latency',
|
||||
dataType: 'float64',
|
||||
type: 'ExponentialHistogram',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'signoz_latency--float64--ExponentialHistogram--true',
|
||||
},
|
||||
timeAggregation: '',
|
||||
spaceAggregation: 'p90',
|
||||
functions: [],
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
stepInterval: 60,
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
groupBy: [],
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
dataUpdatedAt: 1721203751775,
|
||||
error: null,
|
||||
errorUpdatedAt: 0,
|
||||
failureCount: 0,
|
||||
errorUpdateCount: 0,
|
||||
isFetched: true,
|
||||
isFetchedAfterMount: true,
|
||||
isFetching: false,
|
||||
isRefetching: false,
|
||||
isLoadingError: false,
|
||||
isPlaceholderData: false,
|
||||
isPreviousData: false,
|
||||
isRefetchError: false,
|
||||
isStale: true,
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EditFilled, PlusOutlined } from '@ant-design/icons';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ActionMode, ActionType, Pipeline } from 'types/api/pipeline/def';
|
||||
@@ -15,7 +15,6 @@ function CreatePipelineButton({
|
||||
pipelineData,
|
||||
}: CreatePipelineButtonProps): JSX.Element {
|
||||
const { t } = useTranslation(['pipeline']);
|
||||
const { trackEvent } = useAnalytics();
|
||||
|
||||
const isAddNewPipelineVisible = useMemo(
|
||||
() => checkDataLength(pipelineData?.pipelines),
|
||||
@@ -26,7 +25,7 @@ function CreatePipelineButton({
|
||||
const onEnterEditMode = (): void => {
|
||||
setActionMode(ActionMode.Editing);
|
||||
|
||||
trackEvent('Logs: Pipelines: Entered Edit Mode', {
|
||||
logEvent('Logs: Pipelines: Entered Edit Mode', {
|
||||
source: 'signoz-ui',
|
||||
});
|
||||
};
|
||||
@@ -34,7 +33,7 @@ function CreatePipelineButton({
|
||||
setActionMode(ActionMode.Editing);
|
||||
setActionType(ActionType.AddPipeline);
|
||||
|
||||
trackEvent('Logs: Pipelines: Clicked Add New Pipeline', {
|
||||
logEvent('Logs: Pipelines: Clicked Add New Pipeline', {
|
||||
source: 'signoz-ui',
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PlusCircleOutlined } from '@ant-design/icons';
|
||||
import { TableLocale } from 'antd/es/table/interface';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
@@ -39,7 +39,6 @@ function PipelineExpandView({
|
||||
}: PipelineExpandViewProps): JSX.Element {
|
||||
const { t } = useTranslation(['pipeline']);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const { trackEvent } = useAnalytics();
|
||||
const isEditingActionMode = isActionMode === ActionMode.Editing;
|
||||
|
||||
const deleteProcessorHandler = useCallback(
|
||||
@@ -192,7 +191,7 @@ function PipelineExpandView({
|
||||
const addNewProcessorHandler = useCallback((): void => {
|
||||
setActionType(ActionType.AddProcessor);
|
||||
|
||||
trackEvent('Logs: Pipelines: Clicked Add New Processor', {
|
||||
logEvent('Logs: Pipelines: Clicked Add New Processor', {
|
||||
source: 'signoz-ui',
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Card, Modal, Table, Typography } from 'antd';
|
||||
import { ExpandableConfig } from 'antd/es/table/interface';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import savePipeline from 'api/pipeline/post';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { isUndefined } from 'lodash-es';
|
||||
import cloneDeep from 'lodash-es/cloneDeep';
|
||||
@@ -100,7 +99,6 @@ function PipelineListsView({
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const { notifications } = useNotifications();
|
||||
const [pipelineSearchValue, setPipelineSearchValue] = useState<string>('');
|
||||
const { trackEvent } = useAnalytics();
|
||||
const [prevPipelineData, setPrevPipelineData] = useState<Array<PipelineData>>(
|
||||
cloneDeep(pipelineData?.pipelines || []),
|
||||
);
|
||||
@@ -376,7 +374,7 @@ function PipelineListsView({
|
||||
const addNewPipelineHandler = useCallback((): void => {
|
||||
setActionType(ActionType.AddPipeline);
|
||||
|
||||
trackEvent('Logs: Pipelines: Clicked Add New Pipeline', {
|
||||
logEvent('Logs: Pipelines: Clicked Add New Pipeline', {
|
||||
source: 'signoz-ui',
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -415,7 +413,7 @@ function PipelineListsView({
|
||||
setCurrPipelineData(pipelinesInDB);
|
||||
setPrevPipelineData(pipelinesInDB);
|
||||
|
||||
trackEvent('Logs: Pipelines: Saved Pipelines', {
|
||||
logEvent('Logs: Pipelines: Saved Pipelines', {
|
||||
count: pipelinesInDB.length,
|
||||
enabled: pipelinesInDB.filter((p) => p.enabled).length,
|
||||
source: 'signoz-ui',
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { EyeFilled } from '@ant-design/icons';
|
||||
import { Divider, Modal } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import PipelineProcessingPreview from 'container/PipelinePage/PipelineListsView/Preview/PipelineProcessingPreview';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import { useState } from 'react';
|
||||
import { PipelineData } from 'types/api/pipeline/def';
|
||||
|
||||
import { iconStyle } from '../../../config';
|
||||
|
||||
function PreviewAction({ pipeline }: PreviewActionProps): JSX.Element | null {
|
||||
const { trackEvent } = useAnalytics();
|
||||
|
||||
const [previewKey, setPreviewKey] = useState<string | null>(null);
|
||||
const isModalOpen = Boolean(previewKey);
|
||||
|
||||
@@ -23,7 +21,7 @@ function PreviewAction({ pipeline }: PreviewActionProps): JSX.Element | null {
|
||||
|
||||
const onOpenPreview = (): void => {
|
||||
openModal();
|
||||
trackEvent('Logs: Pipelines: Clicked Preview Pipeline', {
|
||||
logEvent('Logs: Pipelines: Clicked Preview Pipeline', {
|
||||
source: 'signoz-ui',
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
@@ -9,14 +10,7 @@ import store from 'store';
|
||||
import CreatePipelineButton from '../Layouts/Pipeline/CreatePipelineButton';
|
||||
import { pipelineApiResponseMockData } from '../mocks/pipeline';
|
||||
|
||||
const trackEventVar = jest.fn();
|
||||
jest.mock('hooks/analytics/useAnalytics', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(() => ({
|
||||
trackEvent: trackEventVar,
|
||||
trackPageView: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
jest.mock('api/common/logEvent');
|
||||
|
||||
describe('PipelinePage container test', () => {
|
||||
it('should render CreatePipelineButton section', async () => {
|
||||
@@ -58,7 +52,7 @@ describe('PipelinePage container test', () => {
|
||||
expect(editButton).toBeInTheDocument();
|
||||
await userEvent.click(editButton);
|
||||
|
||||
expect(trackEventVar).toBeCalledWith('Logs: Pipelines: Entered Edit Mode', {
|
||||
expect(logEvent).toBeCalledWith('Logs: Pipelines: Entered Edit Mode', {
|
||||
source: 'signoz-ui',
|
||||
});
|
||||
});
|
||||
@@ -83,11 +77,8 @@ describe('PipelinePage container test', () => {
|
||||
expect(editButton).toBeInTheDocument();
|
||||
await userEvent.click(editButton);
|
||||
|
||||
expect(trackEventVar).toBeCalledWith(
|
||||
'Logs: Pipelines: Clicked Add New Pipeline',
|
||||
{
|
||||
source: 'signoz-ui',
|
||||
},
|
||||
);
|
||||
expect(logEvent).toBeCalledWith('Logs: Pipelines: Clicked Add New Pipeline', {
|
||||
source: 'signoz-ui',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Typography } from 'antd';
|
||||
import Card from 'antd/es/card/Card';
|
||||
import { Card, Typography } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled(Card)`
|
||||
|
||||
@@ -23,7 +23,7 @@ import NewExplorerCTA from 'container/NewExplorerCTA';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import GetMinMax, { isValidTimeFormat } from 'lib/getMinMax';
|
||||
import getTimeString from 'lib/getTimeString';
|
||||
import history from 'lib/history';
|
||||
import { isObject } from 'lodash-es';
|
||||
@@ -73,6 +73,7 @@ function DateTimeSelection({
|
||||
const urlQuery = useUrlQuery();
|
||||
const searchStartTime = urlQuery.get('startTime');
|
||||
const searchEndTime = urlQuery.get('endTime');
|
||||
const relativeTimeFromUrl = urlQuery.get(QueryParams.relativeTime);
|
||||
const queryClient = useQueryClient();
|
||||
const [enableAbsoluteTime, setEnableAbsoluteTime] = useState(false);
|
||||
const [isValidteRelativeTime, setIsValidteRelativeTime] = useState(false);
|
||||
@@ -404,9 +405,18 @@ function DateTimeSelection({
|
||||
time: Time,
|
||||
currentRoute: string,
|
||||
): Time | CustomTimeType => {
|
||||
// if the relativeTime param is present in the url give top most preference to the same
|
||||
// if the relativeTime param is not valid then move to next preference
|
||||
if (relativeTimeFromUrl != null && isValidTimeFormat(relativeTimeFromUrl)) {
|
||||
return relativeTimeFromUrl as Time;
|
||||
}
|
||||
|
||||
// if the startTime and endTime params are present in the url give next preference to the them.
|
||||
if (searchEndTime !== null && searchStartTime !== null) {
|
||||
return 'custom';
|
||||
}
|
||||
|
||||
// if nothing is present in the url for time range then rely on the local storage values
|
||||
if (
|
||||
(localstorageEndTime === null || localstorageStartTime === null) &&
|
||||
time === 'custom'
|
||||
@@ -414,6 +424,7 @@ function DateTimeSelection({
|
||||
return getDefaultOption(currentRoute);
|
||||
}
|
||||
|
||||
// if not present in the local storage as well then rely on the defaults set for the page
|
||||
if (OLD_RELATIVE_TIME_VALUES.indexOf(time) > -1) {
|
||||
return convertOldTimeToNewValidCustomTimeFormat(time);
|
||||
}
|
||||
@@ -448,7 +459,11 @@ function DateTimeSelection({
|
||||
|
||||
setRefreshButtonHidden(updatedTime === 'custom');
|
||||
|
||||
updateTimeInterval(updatedTime, [preStartTime, preEndTime]);
|
||||
if (updatedTime !== 'custom') {
|
||||
updateTimeInterval(updatedTime);
|
||||
} else {
|
||||
updateTimeInterval(updatedTime, [preStartTime, preEndTime]);
|
||||
}
|
||||
|
||||
if (updatedTime !== 'custom') {
|
||||
urlQuery.delete('startTime');
|
||||
|
||||
@@ -31,7 +31,14 @@ function TopNav(): JSX.Element | null {
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
if (isSignUpPage || isDisabled || isRouteToSkip) {
|
||||
const isNewAlertsLandingPage = useMemo(
|
||||
() =>
|
||||
matchPath(location.pathname, { path: ROUTES.ALERTS_NEW, exact: true }) &&
|
||||
!location.search,
|
||||
[location.pathname, location.search],
|
||||
);
|
||||
|
||||
if (isSignUpPage || isDisabled || isRouteToSkip || isNewAlertsLandingPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import './TraceDetails.styles.scss';
|
||||
|
||||
import { FilterOutlined } from '@ant-design/icons';
|
||||
import { Button, Col, Typography } from 'antd';
|
||||
import Sider from 'antd/es/layout/Sider';
|
||||
import { Button, Col, Layout, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
StyledCol,
|
||||
@@ -42,6 +41,8 @@ import {
|
||||
INTERVAL_UNITS,
|
||||
} from './utils';
|
||||
|
||||
const { Sider } = Layout;
|
||||
|
||||
function TraceDetail({ response }: TraceDetailProps): JSX.Element {
|
||||
const spanServiceColors = useMemo(
|
||||
() => spanServiceNameToColorMapping(response[0].events),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Col } from 'antd';
|
||||
import Card from 'antd/es/card/Card';
|
||||
import { Card, Col } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled(Card)`
|
||||
|
||||
@@ -32,16 +32,6 @@ const useAnalytics = (): any => {
|
||||
}
|
||||
};
|
||||
|
||||
// useEffect(() => {
|
||||
// // Perform any setup or cleanup related to the analytics library
|
||||
// // For example, initialize analytics library here
|
||||
|
||||
// // Clean-up function (optional)
|
||||
// return () => {
|
||||
// // Perform cleanup if needed
|
||||
// };
|
||||
// }, []); // The empty dependency array ensures that this effect runs only once when the component mounts
|
||||
|
||||
return { trackPageView, trackEvent };
|
||||
};
|
||||
|
||||
|
||||
@@ -61,7 +61,10 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
|
||||
});
|
||||
queryRangeMutation.mutate(queryPayload, {
|
||||
onSuccess: (data) => {
|
||||
const updatedQuery = mapQueryDataFromApi(data.compositeQuery);
|
||||
const updatedQuery = mapQueryDataFromApi(
|
||||
data.compositeQuery,
|
||||
widget?.query,
|
||||
);
|
||||
|
||||
history.push(
|
||||
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { mapQueryDataFromApi } from '../mapQueryDataFromApi';
|
||||
import {
|
||||
compositeQueriesWithFunctions,
|
||||
compositeQueryWithoutVariables,
|
||||
compositeQueryWithVariables,
|
||||
defaultOutput,
|
||||
outputWithFunctions,
|
||||
replaceVariables,
|
||||
stepIntervalUnchanged,
|
||||
widgetQueriesWithFunctions,
|
||||
widgetQueryWithoutVariables,
|
||||
widgetQueryWithVariables,
|
||||
} from './mapQueryDataFromApiInputs';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: (): string => 'test-id',
|
||||
}));
|
||||
|
||||
describe('mapQueryDataFromApi function tests', () => {
|
||||
it('should not update the step interval when query is passed', () => {
|
||||
const output = mapQueryDataFromApi(
|
||||
compositeQueryWithoutVariables,
|
||||
widgetQueryWithoutVariables,
|
||||
);
|
||||
|
||||
// composite query is the response from the `v3/query_range/format` API call.
|
||||
// even if the composite query returns stepInterval updated do not modify it
|
||||
expect(output).toStrictEqual(stepIntervalUnchanged);
|
||||
});
|
||||
|
||||
it('should update filter from the composite query', () => {
|
||||
const output = mapQueryDataFromApi(
|
||||
compositeQueryWithVariables,
|
||||
widgetQueryWithVariables,
|
||||
);
|
||||
|
||||
// replace the variables in the widget query and leave the rest items untouched
|
||||
expect(output).toStrictEqual(replaceVariables);
|
||||
});
|
||||
|
||||
it('should not update the step intervals with multiple queries and functions', () => {
|
||||
const output = mapQueryDataFromApi(
|
||||
compositeQueriesWithFunctions,
|
||||
widgetQueriesWithFunctions,
|
||||
);
|
||||
|
||||
expect(output).toStrictEqual(outputWithFunctions);
|
||||
});
|
||||
|
||||
it('should use the default query values and the compositeQuery object when query is not passed', () => {
|
||||
const output = mapQueryDataFromApi(compositeQueryWithoutVariables);
|
||||
|
||||
// when the query object is not passed take the initial values and merge the composite query on top of it
|
||||
expect(output).toStrictEqual(defaultOutput);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,741 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export const compositeQueryWithoutVariables = ({
|
||||
builderQueries: {
|
||||
A: {
|
||||
queryName: 'A',
|
||||
stepInterval: 240,
|
||||
dataSource: DataSource.METRICS,
|
||||
aggregateOperator: 'rate',
|
||||
aggregateAttribute: {
|
||||
key: 'system_disk_operations',
|
||||
dataType: DataTypes.Float64,
|
||||
type: 'Sum',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
filters: {
|
||||
op: 'AND',
|
||||
items: [],
|
||||
},
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
limit: 0,
|
||||
offset: 0,
|
||||
pageSize: 0,
|
||||
reduceTo: 'avg',
|
||||
timeAggregation: 'rate',
|
||||
spaceAggregation: 'sum',
|
||||
ShiftBy: 0,
|
||||
},
|
||||
},
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
} as unknown) as ICompositeMetricQuery;
|
||||
|
||||
export const widgetQueryWithoutVariables = ({
|
||||
clickhouse_sql: [
|
||||
{
|
||||
name: 'A',
|
||||
legend: '',
|
||||
disabled: false,
|
||||
query: '',
|
||||
},
|
||||
],
|
||||
promql: [
|
||||
{
|
||||
name: 'A',
|
||||
query: '',
|
||||
legend: '',
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
dataSource: 'metrics',
|
||||
queryName: 'A',
|
||||
aggregateOperator: 'rate',
|
||||
aggregateAttribute: {
|
||||
key: 'system_disk_operations',
|
||||
dataType: 'float64',
|
||||
type: 'Sum',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'system_disk_operations--float64--Sum--true',
|
||||
},
|
||||
timeAggregation: 'rate',
|
||||
spaceAggregation: 'sum',
|
||||
functions: [],
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
stepInterval: 60,
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
groupBy: [],
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
},
|
||||
id: '2bbbd8d8-db99-40be-b9c6-9e197c5bc537',
|
||||
queryType: 'builder',
|
||||
} as unknown) as Query;
|
||||
|
||||
export const stepIntervalUnchanged = {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: 'float64',
|
||||
id: 'system_disk_operations--float64--Sum--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'system_disk_operations',
|
||||
type: 'Sum',
|
||||
},
|
||||
aggregateOperator: 'rate',
|
||||
dataSource: 'metrics',
|
||||
disabled: false,
|
||||
expression: 'A',
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
functions: [],
|
||||
groupBy: [],
|
||||
having: [],
|
||||
legend: '',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'rate',
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
disabled: false,
|
||||
legend: '',
|
||||
name: 'A',
|
||||
query: '',
|
||||
},
|
||||
],
|
||||
id: 'test-id',
|
||||
promql: [
|
||||
{
|
||||
disabled: false,
|
||||
legend: '',
|
||||
name: 'A',
|
||||
query: '',
|
||||
},
|
||||
],
|
||||
queryType: 'builder',
|
||||
unit: undefined,
|
||||
};
|
||||
|
||||
export const compositeQueryWithVariables = ({
|
||||
builderQueries: {
|
||||
A: {
|
||||
queryName: 'A',
|
||||
stepInterval: 240,
|
||||
dataSource: 'metrics',
|
||||
aggregateOperator: 'sum_rate',
|
||||
aggregateAttribute: {
|
||||
key: 'signoz_calls_total',
|
||||
dataType: 'float64',
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
filters: {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
key: {
|
||||
key: 'deployment_environment',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
value: 'default',
|
||||
op: 'in',
|
||||
},
|
||||
{
|
||||
key: {
|
||||
key: 'service_name',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
value: 'frontend',
|
||||
op: 'in',
|
||||
},
|
||||
{
|
||||
key: {
|
||||
key: 'operation',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
value: 'HTTP GET /dispatch',
|
||||
op: 'in',
|
||||
},
|
||||
],
|
||||
},
|
||||
groupBy: [
|
||||
{
|
||||
key: 'service_name',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
{
|
||||
key: 'operation',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
],
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
legend: '{{service_name}}-{{operation}}',
|
||||
limit: 0,
|
||||
offset: 0,
|
||||
pageSize: 0,
|
||||
reduceTo: 'sum',
|
||||
timeAggregation: 'rate',
|
||||
spaceAggregation: 'sum',
|
||||
ShiftBy: 0,
|
||||
},
|
||||
},
|
||||
panelType: 'graph',
|
||||
queryType: 'builder',
|
||||
} as unknown) as ICompositeMetricQuery;
|
||||
|
||||
export const widgetQueryWithVariables = ({
|
||||
clickhouse_sql: [
|
||||
{
|
||||
name: 'A',
|
||||
legend: '',
|
||||
disabled: false,
|
||||
query: '',
|
||||
},
|
||||
],
|
||||
promql: [
|
||||
{
|
||||
name: 'A',
|
||||
query: '',
|
||||
legend: '',
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
dataSource: 'metrics',
|
||||
queryName: 'A',
|
||||
aggregateOperator: 'sum_rate',
|
||||
aggregateAttribute: {
|
||||
dataType: 'float64',
|
||||
id: 'signoz_calls_total--float64----true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'signoz_calls_total',
|
||||
type: '',
|
||||
},
|
||||
timeAggregation: 'rate',
|
||||
spaceAggregation: 'sum',
|
||||
functions: [],
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: 'aa56621e',
|
||||
key: {
|
||||
dataType: 'string',
|
||||
id: 'deployment_environment--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'deployment_environment',
|
||||
type: 'tag',
|
||||
},
|
||||
op: 'in',
|
||||
value: ['{{.deployment_environment}}'],
|
||||
},
|
||||
{
|
||||
id: '97055a02',
|
||||
key: {
|
||||
dataType: 'string',
|
||||
id: 'service_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'service_name',
|
||||
type: 'tag',
|
||||
},
|
||||
op: 'in',
|
||||
value: ['{{.service_name}}'],
|
||||
},
|
||||
{
|
||||
id: '8c4599f2',
|
||||
key: {
|
||||
dataType: 'string',
|
||||
id: 'operation--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'operation',
|
||||
type: 'tag',
|
||||
},
|
||||
op: 'in',
|
||||
value: ['{{.endpoint}}'],
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
stepInterval: 60,
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
groupBy: [
|
||||
{
|
||||
dataType: 'string',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'service_name',
|
||||
type: 'tag',
|
||||
id: 'service_name--string--tag--false',
|
||||
},
|
||||
{
|
||||
dataType: 'string',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'operation',
|
||||
type: 'tag',
|
||||
id: 'operation--string--tag--false',
|
||||
},
|
||||
],
|
||||
legend: '{{service_name}}-{{operation}}',
|
||||
reduceTo: 'sum',
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
},
|
||||
id: '64fcd7be-61d0-4f92-bbb2-1449b089f766',
|
||||
queryType: 'builder',
|
||||
} as unknown) as Query;
|
||||
|
||||
export const replaceVariables = {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: 'float64',
|
||||
id: 'signoz_calls_total--float64----true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'signoz_calls_total',
|
||||
type: '',
|
||||
},
|
||||
aggregateOperator: 'sum_rate',
|
||||
dataSource: 'metrics',
|
||||
disabled: false,
|
||||
expression: 'A',
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
key: {
|
||||
dataType: 'string',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'deployment_environment',
|
||||
type: 'tag',
|
||||
},
|
||||
op: 'in',
|
||||
value: 'default',
|
||||
},
|
||||
{
|
||||
key: {
|
||||
dataType: 'string',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'service_name',
|
||||
type: 'tag',
|
||||
},
|
||||
op: 'in',
|
||||
value: 'frontend',
|
||||
},
|
||||
{
|
||||
key: {
|
||||
dataType: 'string',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'operation',
|
||||
type: 'tag',
|
||||
},
|
||||
op: 'in',
|
||||
value: 'HTTP GET /dispatch',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
functions: [],
|
||||
groupBy: [
|
||||
{
|
||||
dataType: 'string',
|
||||
id: 'service_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'service_name',
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
dataType: 'string',
|
||||
id: 'operation--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'operation',
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [],
|
||||
legend: '{{service_name}}-{{operation}}',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'sum',
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'rate',
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
disabled: false,
|
||||
legend: '',
|
||||
name: 'A',
|
||||
query: '',
|
||||
},
|
||||
],
|
||||
id: 'test-id',
|
||||
promql: [
|
||||
{
|
||||
disabled: false,
|
||||
legend: '',
|
||||
name: 'A',
|
||||
query: '',
|
||||
},
|
||||
],
|
||||
queryType: 'builder',
|
||||
unit: undefined,
|
||||
};
|
||||
|
||||
export const defaultOutput = {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
ShiftBy: 0,
|
||||
aggregateAttribute: {
|
||||
dataType: 'float64',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'system_disk_operations',
|
||||
type: 'Sum',
|
||||
},
|
||||
aggregateOperator: 'rate',
|
||||
dataSource: 'metrics',
|
||||
disabled: false,
|
||||
expression: 'A',
|
||||
filters: { items: [], op: 'AND' },
|
||||
functions: [],
|
||||
groupBy: [],
|
||||
having: [],
|
||||
legend: '',
|
||||
limit: 0,
|
||||
offset: 0,
|
||||
orderBy: [],
|
||||
pageSize: 0,
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 240,
|
||||
timeAggregation: 'rate',
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: 'test-id',
|
||||
promql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
queryType: 'builder',
|
||||
unit: undefined,
|
||||
};
|
||||
|
||||
export const compositeQueriesWithFunctions = ({
|
||||
builderQueries: {
|
||||
A: {
|
||||
queryName: 'A',
|
||||
stepInterval: 60,
|
||||
dataSource: 'metrics',
|
||||
aggregateOperator: 'count',
|
||||
aggregateAttribute: {
|
||||
key: 'signoz_latency_bucket',
|
||||
dataType: 'float64',
|
||||
type: 'Histogram',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
filters: {
|
||||
op: 'AND',
|
||||
items: [],
|
||||
},
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
limit: 0,
|
||||
offset: 0,
|
||||
pageSize: 0,
|
||||
reduceTo: 'avg',
|
||||
spaceAggregation: 'p90',
|
||||
ShiftBy: 0,
|
||||
},
|
||||
B: {
|
||||
queryName: 'B',
|
||||
stepInterval: 120,
|
||||
dataSource: 'metrics',
|
||||
aggregateOperator: 'rate',
|
||||
aggregateAttribute: {
|
||||
key: 'system_disk_io',
|
||||
dataType: 'float64',
|
||||
type: 'Sum',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
filters: {
|
||||
op: 'AND',
|
||||
items: [],
|
||||
},
|
||||
expression: 'B',
|
||||
disabled: false,
|
||||
limit: 0,
|
||||
offset: 0,
|
||||
pageSize: 0,
|
||||
reduceTo: 'avg',
|
||||
timeAggregation: 'rate',
|
||||
spaceAggregation: 'sum',
|
||||
ShiftBy: 0,
|
||||
},
|
||||
F1: {
|
||||
queryName: 'F1',
|
||||
stepInterval: 1,
|
||||
dataSource: '',
|
||||
aggregateOperator: '',
|
||||
aggregateAttribute: {
|
||||
key: '',
|
||||
dataType: '',
|
||||
type: '',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
expression: 'A / B ',
|
||||
disabled: false,
|
||||
limit: 0,
|
||||
offset: 0,
|
||||
pageSize: 0,
|
||||
ShiftBy: 0,
|
||||
},
|
||||
},
|
||||
panelType: 'graph',
|
||||
queryType: 'builder',
|
||||
} as unknown) as ICompositeMetricQuery;
|
||||
|
||||
export const widgetQueriesWithFunctions = ({
|
||||
clickhouse_sql: [
|
||||
{
|
||||
name: 'A',
|
||||
legend: '',
|
||||
disabled: false,
|
||||
query: '',
|
||||
},
|
||||
],
|
||||
promql: [
|
||||
{
|
||||
name: 'A',
|
||||
query: '',
|
||||
legend: '',
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
dataSource: 'metrics',
|
||||
queryName: 'A',
|
||||
aggregateOperator: 'count',
|
||||
aggregateAttribute: {
|
||||
dataType: 'float64',
|
||||
id: 'signoz_latency_bucket--float64--Histogram--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'signoz_latency_bucket',
|
||||
type: 'Histogram',
|
||||
},
|
||||
timeAggregation: '',
|
||||
spaceAggregation: 'p90',
|
||||
functions: [],
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
stepInterval: 120,
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
groupBy: [],
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
},
|
||||
{
|
||||
dataSource: 'metrics',
|
||||
queryName: 'B',
|
||||
aggregateOperator: 'rate',
|
||||
aggregateAttribute: {
|
||||
key: 'system_disk_io',
|
||||
dataType: 'float64',
|
||||
type: 'Sum',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'system_disk_io--float64--Sum--true',
|
||||
},
|
||||
timeAggregation: 'rate',
|
||||
spaceAggregation: 'sum',
|
||||
functions: [],
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
expression: 'B',
|
||||
disabled: false,
|
||||
stepInterval: 120,
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
groupBy: [],
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
},
|
||||
],
|
||||
queryFormulas: [
|
||||
{
|
||||
queryName: 'F1',
|
||||
expression: 'A / B ',
|
||||
disabled: false,
|
||||
legend: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
id: '5d1844fe-9b44-4f15-b6fe-f1b843550b77',
|
||||
queryType: 'builder',
|
||||
} as unknown) as Query;
|
||||
|
||||
export const outputWithFunctions = {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
dataSource: 'metrics',
|
||||
queryName: 'A',
|
||||
aggregateOperator: 'count',
|
||||
aggregateAttribute: {
|
||||
dataType: 'float64',
|
||||
id: 'signoz_latency_bucket--float64--Histogram--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'signoz_latency_bucket',
|
||||
type: 'Histogram',
|
||||
},
|
||||
timeAggregation: '',
|
||||
spaceAggregation: 'p90',
|
||||
functions: [],
|
||||
filters: {
|
||||
op: 'AND',
|
||||
items: [],
|
||||
},
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
stepInterval: 120,
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
groupBy: [],
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
},
|
||||
{
|
||||
dataSource: 'metrics',
|
||||
queryName: 'B',
|
||||
aggregateOperator: 'rate',
|
||||
aggregateAttribute: {
|
||||
key: 'system_disk_io',
|
||||
dataType: 'float64',
|
||||
type: 'Sum',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'system_disk_io--float64--Sum--true',
|
||||
},
|
||||
timeAggregation: 'rate',
|
||||
spaceAggregation: 'sum',
|
||||
functions: [],
|
||||
filters: {
|
||||
op: 'AND',
|
||||
items: [],
|
||||
},
|
||||
expression: 'B',
|
||||
disabled: false,
|
||||
stepInterval: 120,
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
groupBy: [],
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
},
|
||||
],
|
||||
queryFormulas: [
|
||||
{
|
||||
queryName: 'F1',
|
||||
expression: 'A / B ',
|
||||
disabled: false,
|
||||
legend: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: 'test-id',
|
||||
promql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
queryType: 'builder',
|
||||
unit: undefined,
|
||||
};
|
||||
@@ -7,9 +7,13 @@ import { transformQueryBuilderDataModel } from '../transformQueryBuilderDataMode
|
||||
|
||||
export const mapQueryDataFromApi = (
|
||||
compositeQuery: ICompositeMetricQuery,
|
||||
query?: Query,
|
||||
): Query => {
|
||||
const builder = compositeQuery.builderQueries
|
||||
? transformQueryBuilderDataModel(compositeQuery.builderQueries)
|
||||
? transformQueryBuilderDataModel(
|
||||
compositeQuery.builderQueries,
|
||||
query?.builder,
|
||||
)
|
||||
: initialQueryState.builder;
|
||||
|
||||
const promql = compositeQuery.promQueries
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
} from 'constants/queryBuilder';
|
||||
import { FORMULA_REGEXP } from 'constants/regExp';
|
||||
import { isUndefined } from 'lodash-es';
|
||||
import {
|
||||
BuilderQueryDataResourse,
|
||||
IBuilderFormula,
|
||||
@@ -12,6 +13,7 @@ import { QueryBuilderData } from 'types/common/queryBuilder';
|
||||
|
||||
export const transformQueryBuilderDataModel = (
|
||||
data: BuilderQueryDataResourse,
|
||||
query?: QueryBuilderData,
|
||||
): QueryBuilderData => {
|
||||
const queryData: QueryBuilderData['queryData'] = [];
|
||||
const queryFormulas: QueryBuilderData['queryFormulas'] = [];
|
||||
@@ -19,10 +21,37 @@ export const transformQueryBuilderDataModel = (
|
||||
Object.entries(data).forEach(([, value]) => {
|
||||
if (FORMULA_REGEXP.test(value.queryName)) {
|
||||
const formula = value as IBuilderFormula;
|
||||
queryFormulas.push({ ...initialFormulaBuilderFormValues, ...formula });
|
||||
const baseFormula = query?.queryFormulas?.find(
|
||||
(f) => f.queryName === value.queryName,
|
||||
);
|
||||
if (!isUndefined(baseFormula)) {
|
||||
// this is part of the flow where we create alerts from dashboard.
|
||||
// we pass the formula as is from the widget query as we do not want anything to update in formula from the format api call
|
||||
queryFormulas.push({ ...baseFormula });
|
||||
} else {
|
||||
queryFormulas.push({ ...initialFormulaBuilderFormValues, ...formula });
|
||||
}
|
||||
} else {
|
||||
const query = value as IBuilderQuery;
|
||||
queryData.push({ ...initialQueryBuilderFormValuesMap.metrics, ...query });
|
||||
const queryFromData = value as IBuilderQuery;
|
||||
const baseQuery = query?.queryData?.find(
|
||||
(q) => q.queryName === queryFromData.queryName,
|
||||
);
|
||||
|
||||
if (!isUndefined(baseQuery)) {
|
||||
// this is part of the flow where we create alerts from dashboard.
|
||||
// we pass the widget query as the base query and accept the filters from the format API response.
|
||||
// which fills the variable values inside the same and is used to create alerts
|
||||
// do not accept the full object as the stepInterval field is subject to changes
|
||||
queryData.push({
|
||||
...baseQuery,
|
||||
filters: queryFromData.filters,
|
||||
});
|
||||
} else {
|
||||
queryData.push({
|
||||
...initialQueryBuilderFormValuesMap.metrics,
|
||||
...queryFromData,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
export const dashboardSuccessResponse = {
|
||||
status: 'success',
|
||||
data: [
|
||||
@@ -48,3 +49,53 @@ export const dashboardEmptyState = {
|
||||
status: 'sucsess',
|
||||
data: [],
|
||||
};
|
||||
|
||||
export const getDashboardById = {
|
||||
status: 'success',
|
||||
data: {
|
||||
id: 1,
|
||||
uuid: '1',
|
||||
created_at: '2022-11-16T13:29:47.064874419Z',
|
||||
created_by: 'integration',
|
||||
updated_at: '2024-05-21T06:41:30.546630961Z',
|
||||
updated_by: 'thor@avengers.io',
|
||||
isLocked: true,
|
||||
data: {
|
||||
collapsableRowsMigrated: true,
|
||||
description: '',
|
||||
name: '',
|
||||
panelMap: {},
|
||||
tags: ['linux'],
|
||||
title: 'thor',
|
||||
uploadedGrafana: false,
|
||||
uuid: '',
|
||||
version: '',
|
||||
variables: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const getNonIntegrationDashboardById = {
|
||||
status: 'success',
|
||||
data: {
|
||||
id: 1,
|
||||
uuid: '1',
|
||||
created_at: '2022-11-16T13:29:47.064874419Z',
|
||||
created_by: 'thor',
|
||||
updated_at: '2024-05-21T06:41:30.546630961Z',
|
||||
updated_by: 'thor@avengers.io',
|
||||
isLocked: true,
|
||||
data: {
|
||||
collapsableRowsMigrated: true,
|
||||
description: '',
|
||||
name: '',
|
||||
panelMap: {},
|
||||
tags: ['linux'],
|
||||
title: 'thor',
|
||||
uploadedGrafana: false,
|
||||
uuid: '',
|
||||
version: '',
|
||||
variables: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
81
frontend/src/mocks-server/__mockdata__/explorer_views.ts
Normal file
81
frontend/src/mocks-server/__mockdata__/explorer_views.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
export const explorerView = {
|
||||
status: 'success',
|
||||
data: [
|
||||
{
|
||||
uuid: 'test-uuid-1',
|
||||
name: 'Table View',
|
||||
category: '',
|
||||
createdAt: '2023-08-29T18:04:10.906310033Z',
|
||||
createdBy: 'test-user-1',
|
||||
updatedAt: '2024-01-29T10:42:47.346331133Z',
|
||||
updatedBy: 'test-user-1',
|
||||
sourcePage: 'traces',
|
||||
tags: [''],
|
||||
compositeQuery: {
|
||||
builderQueries: {
|
||||
A: {
|
||||
queryName: 'A',
|
||||
stepInterval: 60,
|
||||
dataSource: 'traces',
|
||||
aggregateOperator: 'count',
|
||||
aggregateAttribute: {
|
||||
key: 'component',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
filters: {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
key: {
|
||||
key: 'component',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
value: 'test-component',
|
||||
op: '!=',
|
||||
},
|
||||
],
|
||||
},
|
||||
groupBy: [
|
||||
{
|
||||
key: 'component',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
{
|
||||
key: 'client-uuid',
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
],
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
limit: 0,
|
||||
offset: 0,
|
||||
pageSize: 0,
|
||||
orderBy: [
|
||||
{
|
||||
columnName: 'timestamp',
|
||||
order: 'desc',
|
||||
},
|
||||
],
|
||||
reduceTo: 'sum',
|
||||
ShiftBy: 0,
|
||||
},
|
||||
},
|
||||
panelType: 'table',
|
||||
queryType: 'builder',
|
||||
},
|
||||
extraData: '{"color":"#00ffd0"}',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,7 +1,11 @@
|
||||
import { rest } from 'msw';
|
||||
|
||||
import { billingSuccessResponse } from './__mockdata__/billing';
|
||||
import { dashboardSuccessResponse } from './__mockdata__/dashboards';
|
||||
import {
|
||||
dashboardSuccessResponse,
|
||||
getDashboardById,
|
||||
} from './__mockdata__/dashboards';
|
||||
import { explorerView } from './__mockdata__/explorer_views';
|
||||
import { inviteUser } from './__mockdata__/invite_user';
|
||||
import { licensesSuccessResponse } from './__mockdata__/licenses';
|
||||
import { membersResponse } from './__mockdata__/members';
|
||||
@@ -55,6 +59,51 @@ export const handlers = [
|
||||
const metricName = req.url.searchParams.get('metricName');
|
||||
const tagKey = req.url.searchParams.get('tagKey');
|
||||
|
||||
const attributeKey = req.url.searchParams.get('attributeKey');
|
||||
|
||||
if (attributeKey === 'serviceName') {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: {
|
||||
stringAttributeValues: [
|
||||
'customer',
|
||||
'demo-app',
|
||||
'driver',
|
||||
'frontend',
|
||||
'mysql',
|
||||
'redis',
|
||||
'route',
|
||||
'go-grpc-otel-server',
|
||||
'test',
|
||||
],
|
||||
numberAttributeValues: null,
|
||||
boolAttributeValues: null,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (attributeKey === 'name') {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: {
|
||||
stringAttributeValues: [
|
||||
'HTTP GET',
|
||||
'HTTP GET /customer',
|
||||
'HTTP GET /dispatch',
|
||||
'HTTP GET /route',
|
||||
],
|
||||
numberAttributeValues: null,
|
||||
boolAttributeValues: null,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
metricName === 'signoz_calls_total' &&
|
||||
tagKey === 'resource_signoz_collector_id'
|
||||
@@ -87,7 +136,6 @@ export const handlers = [
|
||||
res(ctx.status(200), ctx.json(licensesSuccessResponse)),
|
||||
),
|
||||
|
||||
// ?licenseKey=58707e3d-3bdb-44e7-8c89-a9be237939f4
|
||||
rest.get('http://localhost/api/v1/billing', (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(billingSuccessResponse)),
|
||||
),
|
||||
@@ -96,10 +144,41 @@ export const handlers = [
|
||||
res(ctx.status(200), ctx.json(dashboardSuccessResponse)),
|
||||
),
|
||||
|
||||
rest.get('http://localhost/api/v1/dashboards/4', (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(getDashboardById)),
|
||||
),
|
||||
|
||||
rest.get('http://localhost/api/v1/invite', (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(inviteUser)),
|
||||
),
|
||||
rest.post('http://localhost/api/v1/invite', (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(inviteUser)),
|
||||
),
|
||||
|
||||
rest.get(
|
||||
'http://localhost/api/v3/autocomplete/aggregate_attributes',
|
||||
(req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: { attributeKeys: null },
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
rest.get('http://localhost/api/v1/explorer/views', (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(explorerView)),
|
||||
),
|
||||
|
||||
rest.post('http://localhost/api/v1/event', (req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
payload: 'Event Processed Successfully',
|
||||
}),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Button, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import { INTEGRATION_TELEMETRY_EVENTS } from 'pages/Integrations/utils';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
@@ -18,8 +17,6 @@ function Configure(props: ConfigurationProps): JSX.Element {
|
||||
const { configuration, integrationId } = props;
|
||||
const [selectedConfigStep, setSelectedConfigStep] = useState(0);
|
||||
|
||||
const { trackEvent } = useAnalytics();
|
||||
|
||||
const handleMenuClick = (index: number, config: any): void => {
|
||||
setSelectedConfigStep(index);
|
||||
logEvent('Integrations Detail Page: Configure tab', {
|
||||
@@ -29,7 +26,7 @@ function Configure(props: ConfigurationProps): JSX.Element {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
trackEvent(
|
||||
logEvent(
|
||||
INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_CONFIGURE_INSTRUCTION,
|
||||
{
|
||||
integration: integrationId,
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
import './IntegrationDetailPage.styles.scss';
|
||||
|
||||
import { Button, Modal, Tooltip, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import installIntegration from 'api/Integrations/installIntegration';
|
||||
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
|
||||
import cx from 'classnames';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import dayjs from 'dayjs';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { ArrowLeftRight, Check } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
@@ -43,8 +43,6 @@ function IntegrationDetailHeader(
|
||||
} = props;
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const { trackEvent } = useAnalytics();
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const showModal = (): void => {
|
||||
@@ -137,11 +135,11 @@ function IntegrationDetailHeader(
|
||||
disabled={isInstallLoading}
|
||||
onClick={(): void => {
|
||||
if (connectionState === ConnectionStates.NotInstalled) {
|
||||
trackEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_CONNECT, {
|
||||
logEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_CONNECT, {
|
||||
integration: id,
|
||||
});
|
||||
} else {
|
||||
trackEvent(
|
||||
logEvent(
|
||||
INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_TEST_CONNECTION,
|
||||
{
|
||||
integration: id,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import './IntegrationDetailPage.styles.scss';
|
||||
|
||||
import { Button, Modal, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import unInstallIntegration from 'api/Integrations/uninstallIntegration';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { X } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
@@ -30,8 +30,6 @@ function IntergrationsUninstallBar(
|
||||
const { notifications } = useNotifications();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const { trackEvent } = useAnalytics();
|
||||
|
||||
const {
|
||||
mutate: uninstallIntegration,
|
||||
isLoading: isUninstallLoading,
|
||||
@@ -52,7 +50,7 @@ function IntergrationsUninstallBar(
|
||||
};
|
||||
|
||||
const handleOk = (): void => {
|
||||
trackEvent(
|
||||
logEvent(
|
||||
INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_REMOVE_INTEGRATION,
|
||||
{
|
||||
integration: integrationId,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import './Integrations.styles.scss';
|
||||
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
@@ -16,8 +16,6 @@ function Integrations(): JSX.Element {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
|
||||
const { trackEvent } = useAnalytics();
|
||||
|
||||
const selectedIntegration = useMemo(() => urlQuery.get('integration'), [
|
||||
urlQuery,
|
||||
]);
|
||||
@@ -25,7 +23,7 @@ function Integrations(): JSX.Element {
|
||||
const setSelectedIntegration = useCallback(
|
||||
(integration: string | null) => {
|
||||
if (integration) {
|
||||
trackEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_ITEM_LIST_CLICKED, {
|
||||
logEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_ITEM_LIST_CLICKED, {
|
||||
integration,
|
||||
});
|
||||
urlQuery.set('integration', integration);
|
||||
@@ -35,7 +33,7 @@ function Integrations(): JSX.Element {
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
history.push(generatedUrl);
|
||||
},
|
||||
[history, location.pathname, trackEvent, urlQuery],
|
||||
[history, location.pathname, urlQuery],
|
||||
);
|
||||
|
||||
const [activeDetailTab, setActiveDetailTab] = useState<string | null>(
|
||||
@@ -43,7 +41,7 @@ function Integrations(): JSX.Element {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
trackEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_LIST_VISITED);
|
||||
logEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_LIST_VISITED, {});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -26,7 +26,10 @@ export const getRoutes = (
|
||||
settings.push(...organizationSettings(t));
|
||||
}
|
||||
|
||||
if (isGatewayEnabled && userRole === USER_ROLES.ADMIN) {
|
||||
if (
|
||||
isGatewayEnabled &&
|
||||
(userRole === USER_ROLES.ADMIN || userRole === USER_ROLES.EDITOR)
|
||||
) {
|
||||
settings.push(...multiIngestionSettings(t));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Button, Form, Input, Space, Switch, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import editOrg from 'api/user/editOrg';
|
||||
import getInviteDetails from 'api/user/getInviteDetails';
|
||||
import loginApi from 'api/user/login';
|
||||
@@ -7,7 +8,6 @@ import afterLogin from 'AppRoutes/utils';
|
||||
import WelcomeLeftContainer from 'components/WelcomeLeftContainer';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import ROUTES from 'constants/routes';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import useFeatureFlag from 'hooks/useFeatureFlag';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
@@ -57,7 +57,6 @@ function SignUp({ version }: SignUpProps): JSX.Element {
|
||||
false,
|
||||
);
|
||||
const { search } = useLocation();
|
||||
const { trackEvent } = useAnalytics();
|
||||
const params = new URLSearchParams(search);
|
||||
const token = params.get('token');
|
||||
const [isDetailsDisable, setIsDetailsDisable] = useState<boolean>(false);
|
||||
@@ -88,7 +87,7 @@ function SignUp({ version }: SignUpProps): JSX.Element {
|
||||
form.setFieldValue('organizationName', responseDetails.organization);
|
||||
setIsDetailsDisable(true);
|
||||
|
||||
trackEvent('Account Creation Page Visited', {
|
||||
logEvent('Account Creation Page Visited', {
|
||||
email: responseDetails.email,
|
||||
name: responseDetails.name,
|
||||
company_name: responseDetails.organization,
|
||||
@@ -241,7 +240,7 @@ function SignUp({ version }: SignUpProps): JSX.Element {
|
||||
setLoading(true);
|
||||
|
||||
if (!isPasswordValid(values.password)) {
|
||||
trackEvent('Account Creation Page - Invalid Password', {
|
||||
logEvent('Account Creation Page - Invalid Password', {
|
||||
email: values.email,
|
||||
name: values.firstName,
|
||||
});
|
||||
@@ -253,7 +252,7 @@ function SignUp({ version }: SignUpProps): JSX.Element {
|
||||
if (isPreferenceVisible) {
|
||||
await commonHandler(values, onAdminAfterLogin);
|
||||
} else {
|
||||
trackEvent('Account Created Successfully', {
|
||||
logEvent('Account Created Successfully', {
|
||||
email: values.email,
|
||||
name: values.firstName,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import './Support.styles.scss';
|
||||
|
||||
import { Button, Card, Typography } from 'antd';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import {
|
||||
Book,
|
||||
Cable,
|
||||
@@ -85,7 +85,6 @@ const supportChannels = [
|
||||
];
|
||||
|
||||
export default function Support(): JSX.Element {
|
||||
const { trackEvent } = useAnalytics();
|
||||
const history = useHistory();
|
||||
|
||||
const handleChannelWithRedirects = (url: string): void => {
|
||||
@@ -97,7 +96,7 @@ export default function Support(): JSX.Element {
|
||||
const histroyState = history?.location?.state as any;
|
||||
|
||||
if (histroyState && histroyState?.from) {
|
||||
trackEvent(`Support : From URL : ${histroyState.from}`);
|
||||
logEvent(`Support : From URL : ${histroyState.from}`, {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +128,7 @@ export default function Support(): JSX.Element {
|
||||
};
|
||||
|
||||
const handleChannelClick = (channel: Channel): void => {
|
||||
trackEvent(`Support : ${channel.name}`);
|
||||
logEvent(`Support : ${channel.name}`, {});
|
||||
|
||||
switch (channel.key) {
|
||||
case channelsMap.documentation:
|
||||
|
||||
@@ -109,6 +109,7 @@ export function DurationSection(props: DurationProps): JSX.Element {
|
||||
className="min-max-input"
|
||||
onChange={onChangeMinHandler}
|
||||
value={preMin}
|
||||
data-testid="min-input"
|
||||
addonAfter="ms"
|
||||
/>
|
||||
<Input
|
||||
@@ -118,6 +119,7 @@ export function DurationSection(props: DurationProps): JSX.Element {
|
||||
className="min-max-input"
|
||||
onChange={onChangeMaxHandler}
|
||||
value={preMax}
|
||||
data-testid="max-input"
|
||||
addonAfter="ms"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -224,13 +224,18 @@ export function Filter(props: FilterProps): JSX.Element {
|
||||
<Button
|
||||
onClick={(): void => handleRun({ resetAll: true })}
|
||||
className="sync-icon"
|
||||
data-testid="reset-filters"
|
||||
>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<Tooltip title="Collapse" placement="right">
|
||||
<Button onClick={(): void => setOpen(false)} className="arrow-icon">
|
||||
<Button
|
||||
onClick={(): void => setOpen(false)}
|
||||
className="arrow-icon"
|
||||
data-testid="toggle-filter-panel"
|
||||
>
|
||||
<VerticalAlignTopOutlined rotate={270} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
@@ -64,7 +64,7 @@ export function Section(props: SectionProps): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<Divider plain className="divider" />
|
||||
<div className="section-body-header">
|
||||
<div className="section-body-header" data-testid={`collapse-${panelName}`}>
|
||||
<Collapse
|
||||
bordered={false}
|
||||
className="collapseContainer"
|
||||
@@ -96,7 +96,11 @@ export function Section(props: SectionProps): JSX.Element {
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Button type="link" onClick={onClearHandler}>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={onClearHandler}
|
||||
data-testid={`collapse-${panelName}-clearBtn`}
|
||||
>
|
||||
Clear All
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -145,6 +145,7 @@ export function SectionBody(props: SectionBodyProps): JSX.Element {
|
||||
key={`${type}-${item}`}
|
||||
onChange={(e): void => onCheckHandler(e, item)}
|
||||
checked={checkboxMatcher(item)}
|
||||
data-testid={`${type}-${item}`}
|
||||
>
|
||||
<div className="checkbox-label">
|
||||
<div className={labelClassname(item)} />
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {
|
||||
initialQueriesMap,
|
||||
initialQueryBuilderFormValues,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import * as compositeQueryHook from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { render } from 'tests/test-utils';
|
||||
import { QueryBuilderContext } from 'providers/QueryBuilder';
|
||||
import { fireEvent, render, screen, waitFor, within } from 'tests/test-utils';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import TracesExplorer from '..';
|
||||
import { Filter } from '../Filter/Filter';
|
||||
import { AllTraceFilterKeyValue } from '../Filter/filterUtils';
|
||||
|
||||
@@ -37,6 +40,48 @@ jest.mock('uplot', () => {
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock(
|
||||
'container/TopNav/DateTimeSelectionV2/index.tsx',
|
||||
() =>
|
||||
function MockDateTimeSelection(): JSX.Element {
|
||||
return <div>MockDateTimeSelection</div>;
|
||||
},
|
||||
);
|
||||
|
||||
function checkIfSectionIsOpen(
|
||||
getByTestId: (testId: string) => HTMLElement,
|
||||
panelName: string,
|
||||
): void {
|
||||
const section = getByTestId(`collapse-${panelName}`);
|
||||
expect(section.querySelector('.ant-collapse-item-active')).not.toBeNull();
|
||||
}
|
||||
|
||||
function checkIfSectionIsNotOpen(
|
||||
getByTestId: (testId: string) => HTMLElement,
|
||||
panelName: string,
|
||||
): void {
|
||||
const section = getByTestId(`collapse-${panelName}`);
|
||||
expect(section.querySelector('.ant-collapse-item-active')).toBeNull();
|
||||
}
|
||||
|
||||
const defaultOpenSections = ['hasError', 'durationNano', 'serviceName'];
|
||||
|
||||
const defaultClosedSections = Object.keys(AllTraceFilterKeyValue).filter(
|
||||
(section) =>
|
||||
![...defaultOpenSections, 'durationNanoMin', 'durationNanoMax'].includes(
|
||||
section,
|
||||
),
|
||||
);
|
||||
|
||||
async function checkForSectionContent(values: string[]): Promise<void> {
|
||||
for (const val of values) {
|
||||
const sectionContent = await screen.findByText(val);
|
||||
await waitFor(() => expect(sectionContent).toBeInTheDocument());
|
||||
}
|
||||
}
|
||||
|
||||
const redirectWithQueryBuilderData = jest.fn();
|
||||
|
||||
const compositeQuery: Query = {
|
||||
...initialQueriesMap.traces,
|
||||
builder: {
|
||||
@@ -81,6 +126,157 @@ const compositeQuery: Query = {
|
||||
};
|
||||
|
||||
describe('TracesExplorer - ', () => {
|
||||
// Initial filter panel rendering
|
||||
// Test the initial state like which filters section are opened, default state of duration slider, etc.
|
||||
it('should render the Trace filter', async () => {
|
||||
const { getByText, getByTestId } = render(<Filter setOpen={jest.fn()} />);
|
||||
|
||||
Object.values(AllTraceFilterKeyValue).forEach((filter) => {
|
||||
expect(getByText(filter)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Check default state of duration slider
|
||||
const minDuration = getByTestId('min-input') as HTMLInputElement;
|
||||
const maxDuration = getByTestId('max-input') as HTMLInputElement;
|
||||
expect(minDuration).toHaveValue(null);
|
||||
expect(minDuration).toHaveProperty('placeholder', '0');
|
||||
expect(maxDuration).toHaveValue(null);
|
||||
expect(maxDuration).toHaveProperty('placeholder', '100000000');
|
||||
|
||||
// Check which all filter section are opened by default
|
||||
defaultOpenSections.forEach((section) =>
|
||||
checkIfSectionIsOpen(getByTestId, section),
|
||||
);
|
||||
|
||||
// Check which all filter section are closed by default
|
||||
defaultClosedSections.forEach((section) =>
|
||||
checkIfSectionIsNotOpen(getByTestId, section),
|
||||
);
|
||||
|
||||
// check for the status section content
|
||||
await checkForSectionContent(['Ok', 'Error']);
|
||||
|
||||
// check for the service name section content from API response
|
||||
await checkForSectionContent([
|
||||
'customer',
|
||||
'demo-app',
|
||||
'driver',
|
||||
'frontend',
|
||||
'mysql',
|
||||
'redis',
|
||||
'route',
|
||||
'go-grpc-otel-server',
|
||||
'test',
|
||||
]);
|
||||
});
|
||||
|
||||
// test the filter panel actions like opening and closing the sections, etc.
|
||||
it('filter panel actions', async () => {
|
||||
const { getByTestId } = render(<Filter setOpen={jest.fn()} />);
|
||||
|
||||
// Check if the section is closed
|
||||
checkIfSectionIsNotOpen(getByTestId, 'name');
|
||||
// Open the section
|
||||
const name = getByTestId('collapse-name');
|
||||
expect(name).toBeInTheDocument();
|
||||
|
||||
userEvent.click(within(name).getByText(AllTraceFilterKeyValue.name));
|
||||
await waitFor(() => checkIfSectionIsOpen(getByTestId, 'name'));
|
||||
|
||||
await checkForSectionContent([
|
||||
'HTTP GET',
|
||||
'HTTP GET /customer',
|
||||
'HTTP GET /dispatch',
|
||||
'HTTP GET /route',
|
||||
]);
|
||||
|
||||
// Close the section
|
||||
userEvent.click(within(name).getByText(AllTraceFilterKeyValue.name));
|
||||
await waitFor(() => checkIfSectionIsNotOpen(getByTestId, 'name'));
|
||||
});
|
||||
|
||||
it('checking filters should update the query', async () => {
|
||||
const { getByText } = render(
|
||||
<QueryBuilderContext.Provider
|
||||
value={
|
||||
{
|
||||
currentQuery: {
|
||||
...initialQueriesMap.traces,
|
||||
builder: {
|
||||
...initialQueriesMap.traces.builder,
|
||||
queryData: [initialQueryBuilderFormValues],
|
||||
},
|
||||
},
|
||||
redirectWithQueryBuilderData,
|
||||
} as any
|
||||
}
|
||||
>
|
||||
<Filter setOpen={jest.fn()} />
|
||||
</QueryBuilderContext.Provider>,
|
||||
);
|
||||
|
||||
const okCheckbox = getByText('Ok');
|
||||
fireEvent.click(okCheckbox);
|
||||
expect(
|
||||
redirectWithQueryBuilderData.mock.calls[
|
||||
redirectWithQueryBuilderData.mock.calls.length - 1
|
||||
][0].builder.queryData[0].filters.items,
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: {
|
||||
id: expect.any(String),
|
||||
key: 'hasError',
|
||||
type: 'tag',
|
||||
dataType: 'bool',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: 'in',
|
||||
value: ['false'],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// Check if the query is updated when the error checkbox is clicked
|
||||
const errorCheckbox = getByText('Error');
|
||||
fireEvent.click(errorCheckbox);
|
||||
expect(
|
||||
redirectWithQueryBuilderData.mock.calls[
|
||||
redirectWithQueryBuilderData.mock.calls.length - 1
|
||||
][0].builder.queryData[0].filters.items,
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: {
|
||||
id: expect.any(String),
|
||||
key: 'hasError',
|
||||
type: 'tag',
|
||||
dataType: 'bool',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: 'in',
|
||||
value: ['false', 'true'],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the trace filter with the given query', async () => {
|
||||
jest
|
||||
.spyOn(compositeQueryHook, 'useGetCompositeQueryParam')
|
||||
.mockReturnValue(compositeQuery);
|
||||
|
||||
const { findByText, getByTestId } = render(<Filter setOpen={jest.fn()} />);
|
||||
|
||||
// check if the default query is applied - composite query has filters - serviceName : demo-app and name : HTTP GET /customer
|
||||
expect(await findByText('demo-app')).toBeInTheDocument();
|
||||
expect(getByTestId('serviceName-demo-app')).toBeChecked();
|
||||
expect(await findByText('HTTP GET /customer')).toBeInTheDocument();
|
||||
expect(getByTestId('name-HTTP GET /customer')).toBeChecked();
|
||||
});
|
||||
|
||||
it('test edge cases of undefined filters', async () => {
|
||||
jest.spyOn(compositeQueryHook, 'useGetCompositeQueryParam').mockReturnValue({
|
||||
...compositeQuery,
|
||||
@@ -98,7 +294,6 @@ describe('TracesExplorer - ', () => {
|
||||
|
||||
const { getByText } = render(<Filter setOpen={jest.fn()} />);
|
||||
|
||||
// we should have all the filters
|
||||
Object.values(AllTraceFilterKeyValue).forEach((filter) => {
|
||||
expect(getByText(filter)).toBeInTheDocument();
|
||||
});
|
||||
@@ -124,9 +319,141 @@ describe('TracesExplorer - ', () => {
|
||||
|
||||
const { getByText } = render(<Filter setOpen={jest.fn()} />);
|
||||
|
||||
// we should have all the filters
|
||||
Object.values(AllTraceFilterKeyValue).forEach((filter) => {
|
||||
expect(getByText(filter)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should clear filter on clear & reset button click', async () => {
|
||||
const { getByText, getByTestId } = render(
|
||||
<QueryBuilderContext.Provider
|
||||
value={
|
||||
{
|
||||
currentQuery: {
|
||||
...initialQueriesMap.traces,
|
||||
builder: {
|
||||
...initialQueriesMap.traces.builder,
|
||||
queryData: [initialQueryBuilderFormValues],
|
||||
},
|
||||
},
|
||||
redirectWithQueryBuilderData,
|
||||
} as any
|
||||
}
|
||||
>
|
||||
<Filter setOpen={jest.fn()} />
|
||||
</QueryBuilderContext.Provider>,
|
||||
);
|
||||
|
||||
// check for the status section content
|
||||
await checkForSectionContent(['Ok', 'Error']);
|
||||
|
||||
// check for the service name section content from API response
|
||||
await checkForSectionContent([
|
||||
'customer',
|
||||
'demo-app',
|
||||
'driver',
|
||||
'frontend',
|
||||
'mysql',
|
||||
'redis',
|
||||
'route',
|
||||
'go-grpc-otel-server',
|
||||
'test',
|
||||
]);
|
||||
|
||||
const okCheckbox = getByText('Ok');
|
||||
fireEvent.click(okCheckbox);
|
||||
|
||||
const frontendCheckbox = getByText('frontend');
|
||||
fireEvent.click(frontendCheckbox);
|
||||
|
||||
// check if checked and present in query
|
||||
expect(
|
||||
redirectWithQueryBuilderData.mock.calls[
|
||||
redirectWithQueryBuilderData.mock.calls.length - 1
|
||||
][0].builder.queryData[0].filters.items,
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: {
|
||||
id: expect.any(String),
|
||||
key: 'hasError',
|
||||
type: 'tag',
|
||||
dataType: 'bool',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: 'in',
|
||||
value: ['false'],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
key: {
|
||||
key: 'serviceName',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: expect.any(String),
|
||||
},
|
||||
op: 'in',
|
||||
value: ['frontend'],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
const clearButton = getByTestId('collapse-serviceName-clearBtn');
|
||||
expect(clearButton).toBeInTheDocument();
|
||||
fireEvent.click(clearButton);
|
||||
|
||||
// check if cleared and not present in query
|
||||
expect(
|
||||
redirectWithQueryBuilderData.mock.calls[
|
||||
redirectWithQueryBuilderData.mock.calls.length - 1
|
||||
][0].builder.queryData[0].filters.items,
|
||||
).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: {
|
||||
key: 'serviceName',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: expect.any(String),
|
||||
},
|
||||
op: 'in',
|
||||
value: ['frontend'],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// check if reset button is present
|
||||
const resetButton = getByTestId('reset-filters');
|
||||
expect(resetButton).toBeInTheDocument();
|
||||
fireEvent.click(resetButton);
|
||||
|
||||
// check if reset id done
|
||||
expect(
|
||||
redirectWithQueryBuilderData.mock.calls[
|
||||
redirectWithQueryBuilderData.mock.calls.length - 1
|
||||
][0].builder.queryData[0].filters.items,
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('filter panel should collapse & uncollapsed', async () => {
|
||||
const { getByText, getByTestId } = render(<TracesExplorer />);
|
||||
|
||||
Object.values(AllTraceFilterKeyValue).forEach((filter) => {
|
||||
expect(getByText(filter)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Filter panel should collapse
|
||||
const collapseButton = getByTestId('toggle-filter-panel');
|
||||
expect(collapseButton).toBeInTheDocument();
|
||||
fireEvent.click(collapseButton);
|
||||
|
||||
// uncollapse btn should be present
|
||||
expect(
|
||||
await screen.findByTestId('filter-uncollapse-btn'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -251,6 +251,7 @@ function TracesExplorer(): JSX.Element {
|
||||
<Button
|
||||
onClick={(): void => setOpen(!isOpen)}
|
||||
className="filter-outlined-btn"
|
||||
data-testid="filter-uncollapse-btn"
|
||||
>
|
||||
<FilterOutlined />
|
||||
</Button>
|
||||
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Card, Skeleton, Typography } from 'antd';
|
||||
import updateCreditCardApi from 'api/billing/checkout';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import ROUTES from 'constants/routes';
|
||||
import FullScreenHeader from 'container/FullScreenHeader/FullScreenHeader';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import useLicense from 'hooks/useLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
@@ -27,7 +27,6 @@ export default function WorkspaceBlocked(): JSX.Element {
|
||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const isAdmin = role === 'ADMIN';
|
||||
const [activeLicense, setActiveLicense] = useState<License | null>(null);
|
||||
const { trackEvent } = useAnalytics();
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
@@ -74,7 +73,7 @@ export default function WorkspaceBlocked(): JSX.Element {
|
||||
);
|
||||
|
||||
const handleUpdateCreditCard = useCallback(async () => {
|
||||
trackEvent('Workspace Blocked: User Clicked Update Credit Card');
|
||||
logEvent('Workspace Blocked: User Clicked Update Credit Card', {});
|
||||
|
||||
updateCreditCard({
|
||||
licenseKey: activeLicense?.key || '',
|
||||
@@ -85,7 +84,7 @@ export default function WorkspaceBlocked(): JSX.Element {
|
||||
}, [activeLicense?.key, updateCreditCard]);
|
||||
|
||||
const handleExtendTrial = (): void => {
|
||||
trackEvent('Workspace Blocked: User Clicked Extend Trial');
|
||||
logEvent('Workspace Blocked: User Clicked Extend Trial', {});
|
||||
|
||||
notifications.info({
|
||||
message: 'Extend Trial',
|
||||
|
||||
@@ -829,7 +829,7 @@ export function QueryBuilderProvider({
|
||||
unit,
|
||||
}));
|
||||
},
|
||||
[setCurrentQuery],
|
||||
[setCurrentQuery, setSupersetQuery],
|
||||
);
|
||||
|
||||
const query: Query = useMemo(
|
||||
|
||||
@@ -42,6 +42,7 @@ const mockStored = (role?: string): any =>
|
||||
accessJwt: '',
|
||||
refreshJwt: '',
|
||||
},
|
||||
isLoggedIn: true,
|
||||
org: [
|
||||
{
|
||||
createdAt: 0,
|
||||
|
||||
2
go.mod
2
go.mod
@@ -6,7 +6,7 @@ require (
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.20.0
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
|
||||
github.com/SigNoz/signoz-otel-collector v0.102.2
|
||||
github.com/SigNoz/signoz-otel-collector v0.102.3
|
||||
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974
|
||||
github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230822164844-1b861a431974
|
||||
github.com/antonmedv/expr v1.15.3
|
||||
|
||||
4
go.sum
4
go.sum
@@ -64,8 +64,8 @@ github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd h1:Bk43AsDYe0fhkb
|
||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc=
|
||||
github.com/SigNoz/prometheus v1.11.1 h1:roM8ugYf4UxaeKKujEeBvoX7ybq3IrS+TB26KiRtIJg=
|
||||
github.com/SigNoz/prometheus v1.11.1/go.mod h1:uv4mQwZQtx7y4GQ6EdHOi8Wsk07uHNn2XHd1zM85m6I=
|
||||
github.com/SigNoz/signoz-otel-collector v0.102.2 h1:SmjsBZjMjTVVpuOlfJXlsDJQbdefQP/9Wz3CyzSuZuU=
|
||||
github.com/SigNoz/signoz-otel-collector v0.102.2/go.mod h1:ISAXYhZenojCWg6CdDJtPMpfS6Zwc08+uoxH25tc6Y0=
|
||||
github.com/SigNoz/signoz-otel-collector v0.102.3 h1:q6iS5kqqwopwC2pS2UvYL3IiJMP75UdyK6d+rculXn4=
|
||||
github.com/SigNoz/signoz-otel-collector v0.102.3/go.mod h1:61WqwhnrtFjwj1FyfDYMXjxFx8gWgKok1Xy1C6LbjWo=
|
||||
github.com/SigNoz/zap_otlp v0.1.0 h1:T7rRcFN87GavY8lDGZj0Z3Xv6OhJA6Pj3I9dNPmqvRc=
|
||||
github.com/SigNoz/zap_otlp v0.1.0/go.mod h1:lcHvbDbRgvDnPxo9lDlaL1JK2PyOyouP/C3ynnYIvyo=
|
||||
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974 h1:PKVgdf83Yw+lZJbFtNGBgqXiXNf3+kOXW2qZ7Ms7OaY=
|
||||
|
||||
@@ -706,21 +706,25 @@ func (r *ClickHouseReader) GetServicesList(ctx context.Context) (*[]string, erro
|
||||
return &services, nil
|
||||
}
|
||||
|
||||
func (r *ClickHouseReader) GetTopLevelOperations(ctx context.Context, skipConfig *model.SkipConfig, start, end time.Time) (*map[string][]string, *map[string][]string, *model.ApiError) {
|
||||
func (r *ClickHouseReader) GetTopLevelOperations(ctx context.Context, skipConfig *model.SkipConfig, start, end time.Time, services []string) (*map[string][]string, *model.ApiError) {
|
||||
|
||||
start = start.In(time.UTC)
|
||||
|
||||
// The `top_level_operations` that have `time` >= start
|
||||
operations := map[string][]string{}
|
||||
// All top level operations for a service
|
||||
allOperations := map[string][]string{}
|
||||
query := fmt.Sprintf(`SELECT DISTINCT name, serviceName, time FROM %s.%s`, r.TraceDB, r.topLevelOperationsTable)
|
||||
// We can't use the `end` because the `top_level_operations` table has the most recent instances of the operations
|
||||
// We can only use the `start` time to filter the operations
|
||||
query := fmt.Sprintf(`SELECT name, serviceName, max(time) as ts FROM %s.%s WHERE time >= @start`, r.TraceDB, r.topLevelOperationsTable)
|
||||
if len(services) > 0 {
|
||||
query += ` AND serviceName IN @services`
|
||||
}
|
||||
query += ` GROUP BY name, serviceName ORDER BY ts DESC LIMIT 5000`
|
||||
|
||||
rows, err := r.db.Query(ctx, query)
|
||||
rows, err := r.db.Query(ctx, query, clickhouse.Named("start", start), clickhouse.Named("services", services))
|
||||
|
||||
if err != nil {
|
||||
zap.L().Error("Error in processing sql query", zap.Error(err))
|
||||
return nil, nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query")}
|
||||
return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query")}
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
@@ -728,25 +732,17 @@ func (r *ClickHouseReader) GetTopLevelOperations(ctx context.Context, skipConfig
|
||||
var name, serviceName string
|
||||
var t time.Time
|
||||
if err := rows.Scan(&name, &serviceName, &t); err != nil {
|
||||
return nil, nil, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error in reading data")}
|
||||
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error in reading data")}
|
||||
}
|
||||
if _, ok := operations[serviceName]; !ok {
|
||||
operations[serviceName] = []string{}
|
||||
}
|
||||
if _, ok := allOperations[serviceName]; !ok {
|
||||
allOperations[serviceName] = []string{}
|
||||
operations[serviceName] = []string{"overflow_operation"}
|
||||
}
|
||||
if skipConfig.ShouldSkip(serviceName, name) {
|
||||
continue
|
||||
}
|
||||
allOperations[serviceName] = append(allOperations[serviceName], name)
|
||||
// We can't use the `end` because the `top_level_operations` table has the most recent instances of the operations
|
||||
// We can only use the `start` time to filter the operations
|
||||
if t.After(start) {
|
||||
operations[serviceName] = append(operations[serviceName], name)
|
||||
}
|
||||
operations[serviceName] = append(operations[serviceName], name)
|
||||
}
|
||||
return &operations, &allOperations, nil
|
||||
return &operations, nil
|
||||
}
|
||||
|
||||
func (r *ClickHouseReader) GetServices(ctx context.Context, queryParams *model.GetServicesParams, skipConfig *model.SkipConfig) (*[]model.ServiceItem, *model.ApiError) {
|
||||
@@ -755,7 +751,7 @@ func (r *ClickHouseReader) GetServices(ctx context.Context, queryParams *model.G
|
||||
return nil, &model.ApiError{Typ: model.ErrorExec, Err: ErrNoIndexTable}
|
||||
}
|
||||
|
||||
topLevelOps, allTopLevelOps, apiErr := r.GetTopLevelOperations(ctx, skipConfig, *queryParams.Start, *queryParams.End)
|
||||
topLevelOps, apiErr := r.GetTopLevelOperations(ctx, skipConfig, *queryParams.Start, *queryParams.End, nil)
|
||||
if apiErr != nil {
|
||||
return nil, apiErr
|
||||
}
|
||||
@@ -779,7 +775,7 @@ func (r *ClickHouseReader) GetServices(ctx context.Context, queryParams *model.G
|
||||
// the top level operations are high, we want to warn to let user know the issue
|
||||
// with the instrumentation
|
||||
serviceItem.DataWarning = model.DataWarning{
|
||||
TopLevelOps: (*allTopLevelOps)[svc],
|
||||
TopLevelOps: (*topLevelOps)[svc],
|
||||
}
|
||||
|
||||
// default max_query_size = 262144
|
||||
@@ -868,7 +864,7 @@ func (r *ClickHouseReader) GetServices(ctx context.Context, queryParams *model.G
|
||||
|
||||
func (r *ClickHouseReader) GetServiceOverview(ctx context.Context, queryParams *model.GetServiceOverviewParams, skipConfig *model.SkipConfig) (*[]model.ServiceOverviewItem, *model.ApiError) {
|
||||
|
||||
topLevelOps, _, apiErr := r.GetTopLevelOperations(ctx, skipConfig, *queryParams.Start, *queryParams.End)
|
||||
topLevelOps, apiErr := r.GetTopLevelOperations(ctx, skipConfig, *queryParams.Start, *queryParams.End, nil)
|
||||
if apiErr != nil {
|
||||
return nil, apiErr
|
||||
}
|
||||
@@ -5005,3 +5001,27 @@ func (r *ClickHouseReader) LiveTailLogsV3(ctx context.Context, query string, tim
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ClickHouseReader) GetMinAndMaxTimestampForTraceID(ctx context.Context, traceID []string) (int64, int64, error) {
|
||||
var minTime, maxTime time.Time
|
||||
|
||||
query := fmt.Sprintf("SELECT min(timestamp), max(timestamp) FROM %s.%s WHERE traceID IN ('%s')",
|
||||
r.TraceDB, r.SpansTable, strings.Join(traceID, "','"))
|
||||
|
||||
zap.L().Debug("GetMinAndMaxTimestampForTraceID", zap.String("query", query))
|
||||
|
||||
err := r.db.QueryRow(ctx, query).Scan(&minTime, &maxTime)
|
||||
if err != nil {
|
||||
zap.L().Error("Error while executing query", zap.Error(err))
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
if minTime.IsZero() || maxTime.IsZero() {
|
||||
zap.L().Debug("minTime or maxTime is zero")
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
zap.L().Debug("GetMinAndMaxTimestampForTraceID", zap.Any("minTime", minTime), zap.Any("maxTime", maxTime))
|
||||
|
||||
return minTime.UnixNano(), maxTime.UnixNano(), nil
|
||||
}
|
||||
|
||||
@@ -29,12 +29,14 @@ import (
|
||||
logsv3 "go.signoz.io/signoz/pkg/query-service/app/logs/v3"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/metrics"
|
||||
metricsv3 "go.signoz.io/signoz/pkg/query-service/app/metrics/v3"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/preferences"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/querier"
|
||||
querierV2 "go.signoz.io/signoz/pkg/query-service/app/querier/v2"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/queryBuilder"
|
||||
tracesV3 "go.signoz.io/signoz/pkg/query-service/app/traces/v3"
|
||||
"go.signoz.io/signoz/pkg/query-service/auth"
|
||||
"go.signoz.io/signoz/pkg/query-service/cache"
|
||||
"go.signoz.io/signoz/pkg/query-service/common"
|
||||
"go.signoz.io/signoz/pkg/query-service/constants"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
"go.signoz.io/signoz/pkg/query-service/postprocess"
|
||||
@@ -398,6 +400,22 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *AuthMiddleware) {
|
||||
|
||||
router.HandleFunc("/api/v1/disks", am.ViewAccess(aH.getDisks)).Methods(http.MethodGet)
|
||||
|
||||
// === Preference APIs ===
|
||||
|
||||
// user actions
|
||||
router.HandleFunc("/api/v1/user/preferences", am.ViewAccess(aH.getAllUserPreferences)).Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v1/user/preferences/{preferenceId}", am.ViewAccess(aH.getUserPreference)).Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v1/user/preferences/{preferenceId}", am.ViewAccess(aH.updateUserPreference)).Methods(http.MethodPut)
|
||||
|
||||
// org actions
|
||||
router.HandleFunc("/api/v1/org/preferences", am.AdminAccess(aH.getAllOrgPreferences)).Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v1/org/preferences/{preferenceId}", am.AdminAccess(aH.getOrgPreference)).Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v1/org/preferences/{preferenceId}", am.AdminAccess(aH.updateOrgPreference)).Methods(http.MethodPut)
|
||||
|
||||
// === Authentication APIs ===
|
||||
router.HandleFunc("/api/v1/invite", am.AdminAccess(aH.inviteUser)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(aH.getInvite)).Methods(http.MethodGet)
|
||||
@@ -1329,8 +1347,44 @@ func (aH *APIHandler) getServiceOverview(w http.ResponseWriter, r *http.Request)
|
||||
func (aH *APIHandler) getServicesTopLevelOps(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var start, end time.Time
|
||||
var services []string
|
||||
|
||||
result, _, apiErr := aH.reader.GetTopLevelOperations(r.Context(), aH.skipConfig, start, end)
|
||||
type topLevelOpsParams struct {
|
||||
Service string `json:"service"`
|
||||
Start string `json:"start"`
|
||||
End string `json:"end"`
|
||||
}
|
||||
|
||||
var params topLevelOpsParams
|
||||
err := json.NewDecoder(r.Body).Decode(¶ms)
|
||||
if err != nil {
|
||||
zap.L().Error("Error in getting req body for get top operations API", zap.Error(err))
|
||||
}
|
||||
|
||||
if params.Service != "" {
|
||||
services = []string{params.Service}
|
||||
}
|
||||
|
||||
startEpoch := params.Start
|
||||
if startEpoch != "" {
|
||||
startEpochInt, err := strconv.ParseInt(startEpoch, 10, 64)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading start time")
|
||||
return
|
||||
}
|
||||
start = time.Unix(0, startEpochInt)
|
||||
}
|
||||
endEpoch := params.End
|
||||
if endEpoch != "" {
|
||||
endEpochInt, err := strconv.ParseInt(endEpoch, 10, 64)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading end time")
|
||||
return
|
||||
}
|
||||
end = time.Unix(0, endEpochInt)
|
||||
}
|
||||
|
||||
result, apiErr := aH.reader.GetTopLevelOperations(r.Context(), aH.skipConfig, start, end, services)
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
@@ -2192,6 +2246,115 @@ func (aH *APIHandler) WriteJSON(w http.ResponseWriter, r *http.Request, response
|
||||
w.Write(resp)
|
||||
}
|
||||
|
||||
// Preferences
|
||||
|
||||
func (ah *APIHandler) getUserPreference(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
) {
|
||||
preferenceId := mux.Vars(r)["preferenceId"]
|
||||
user := common.GetUserFromContext(r.Context())
|
||||
|
||||
preference, apiErr := preferences.GetUserPreference(
|
||||
r.Context(), preferenceId, user.User.OrgId, user.User.Id,
|
||||
)
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
ah.Respond(w, preference)
|
||||
}
|
||||
|
||||
func (ah *APIHandler) updateUserPreference(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
) {
|
||||
preferenceId := mux.Vars(r)["preferenceId"]
|
||||
user := common.GetUserFromContext(r.Context())
|
||||
req := preferences.UpdatePreference{}
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
if err != nil {
|
||||
RespondError(w, model.BadRequest(err), nil)
|
||||
return
|
||||
}
|
||||
preference, apiErr := preferences.UpdateUserPreference(r.Context(), preferenceId, req.PreferenceValue, user.User.Id)
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
ah.Respond(w, preference)
|
||||
}
|
||||
|
||||
func (ah *APIHandler) getAllUserPreferences(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
) {
|
||||
user := common.GetUserFromContext(r.Context())
|
||||
preference, apiErr := preferences.GetAllUserPreferences(
|
||||
r.Context(), user.User.OrgId, user.User.Id,
|
||||
)
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
ah.Respond(w, preference)
|
||||
}
|
||||
|
||||
func (ah *APIHandler) getOrgPreference(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
) {
|
||||
preferenceId := mux.Vars(r)["preferenceId"]
|
||||
user := common.GetUserFromContext(r.Context())
|
||||
preference, apiErr := preferences.GetOrgPreference(
|
||||
r.Context(), preferenceId, user.User.OrgId,
|
||||
)
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
ah.Respond(w, preference)
|
||||
}
|
||||
|
||||
func (ah *APIHandler) updateOrgPreference(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
) {
|
||||
preferenceId := mux.Vars(r)["preferenceId"]
|
||||
req := preferences.UpdatePreference{}
|
||||
user := common.GetUserFromContext(r.Context())
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
if err != nil {
|
||||
RespondError(w, model.BadRequest(err), nil)
|
||||
return
|
||||
}
|
||||
preference, apiErr := preferences.UpdateOrgPreference(r.Context(), preferenceId, req.PreferenceValue, user.User.OrgId)
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
ah.Respond(w, preference)
|
||||
}
|
||||
|
||||
func (ah *APIHandler) getAllOrgPreferences(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
) {
|
||||
user := common.GetUserFromContext(r.Context())
|
||||
preference, apiErr := preferences.GetAllOrgPreferences(
|
||||
r.Context(), user.User.OrgId,
|
||||
)
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
ah.Respond(w, preference)
|
||||
}
|
||||
|
||||
// Integrations
|
||||
func (ah *APIHandler) RegisterIntegrationRoutes(router *mux.Router, am *AuthMiddleware) {
|
||||
subRouter := router.PathPrefix("/api/v1/integrations").Subrouter()
|
||||
@@ -3050,6 +3213,22 @@ func (aH *APIHandler) queryRangeV3(ctx context.Context, queryRangeParams *v3.Que
|
||||
}
|
||||
}
|
||||
|
||||
// WARN: Only works for AND operator in traces query
|
||||
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder {
|
||||
// check if traceID is used as filter (with equal/similar operator) in traces query if yes add timestamp filter to queryRange params
|
||||
isUsed, traceIDs := tracesV3.TraceIdFilterUsedWithEqual(queryRangeParams)
|
||||
if isUsed == true && len(traceIDs) > 0 {
|
||||
zap.L().Debug("traceID used as filter in traces query")
|
||||
// query signoz_spans table with traceID to get min and max timestamp
|
||||
min, max, err := aH.reader.GetMinAndMaxTimestampForTraceID(ctx, traceIDs)
|
||||
if err == nil {
|
||||
// add timestamp filter to queryRange params
|
||||
tracesV3.AddTimestampFilters(min, max, queryRangeParams)
|
||||
zap.L().Debug("post adding timestamp filter in traces query", zap.Any("queryRangeParams", queryRangeParams))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result, errQuriesByName, err = aH.querier.QueryRange(ctx, queryRangeParams, spanKeys)
|
||||
|
||||
if err != nil {
|
||||
@@ -3319,6 +3498,22 @@ func (aH *APIHandler) queryRangeV4(ctx context.Context, queryRangeParams *v3.Que
|
||||
}
|
||||
}
|
||||
|
||||
// WARN: Only works for AND operator in traces query
|
||||
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder {
|
||||
// check if traceID is used as filter (with equal/similar operator) in traces query if yes add timestamp filter to queryRange params
|
||||
isUsed, traceIDs := tracesV3.TraceIdFilterUsedWithEqual(queryRangeParams)
|
||||
if isUsed == true && len(traceIDs) > 0 {
|
||||
zap.L().Debug("traceID used as filter in traces query")
|
||||
// query signoz_spans table with traceID to get min and max timestamp
|
||||
min, max, err := aH.reader.GetMinAndMaxTimestampForTraceID(ctx, traceIDs)
|
||||
if err == nil {
|
||||
// add timestamp filter to queryRange params
|
||||
tracesV3.AddTimestampFilters(min, max, queryRangeParams)
|
||||
zap.L().Debug("post adding timestamp filter in traces query", zap.Any("queryRangeParams", queryRangeParams))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result, errQuriesByName, err = aH.querierV2.QueryRange(ctx, queryRangeParams, spanKeys)
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -110,6 +110,13 @@ service:
|
||||
|
||||
```
|
||||
|
||||
### If using non-default nginx log format, adjust log parsing regex
|
||||
|
||||
If you are using a [custom nginx log format](https://docs.nginx.com/nginx/admin-guide/monitoring/logging/#setting-up-the-access-log),
|
||||
please adjust the regex used for parsing logs in the receivers named
|
||||
`filelog/nginx-access-logs` and `filelog/nginx-error-logs` in collector config.
|
||||
|
||||
|
||||
#### Set Environment Variables
|
||||
|
||||
Set the following environment variables in your otel-collector environment:
|
||||
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
ARRAY_INT64 = "Array(Int64)"
|
||||
ARRAY_FLOAT64 = "Array(Float64)"
|
||||
ARRAY_BOOL = "Array(Bool)"
|
||||
NGRAM_SIZE = 4
|
||||
)
|
||||
|
||||
var dataTypeMapping = map[string]string{
|
||||
@@ -72,6 +73,7 @@ func getPath(keyArr []string) string {
|
||||
|
||||
func getJSONFilterKey(key v3.AttributeKey, op v3.FilterOperator, isArray bool) (string, error) {
|
||||
keyArr := strings.Split(key.Key, ".")
|
||||
// i.e it should be at least body.name, and not something like body
|
||||
if len(keyArr) < 2 {
|
||||
return "", fmt.Errorf("incorrect key, should contain at least 2 parts")
|
||||
}
|
||||
@@ -106,6 +108,29 @@ func getJSONFilterKey(key v3.AttributeKey, op v3.FilterOperator, isArray bool) (
|
||||
return keyname, nil
|
||||
}
|
||||
|
||||
// takes the path and the values and generates where clauses for better usage of index
|
||||
func getPathIndexFilter(path string) string {
|
||||
filters := []string{}
|
||||
keyArr := strings.Split(path, ".")
|
||||
if len(keyArr) < 2 {
|
||||
return ""
|
||||
}
|
||||
|
||||
for i, key := range keyArr {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
key = strings.TrimSuffix(key, "[*]")
|
||||
if len(key) >= NGRAM_SIZE {
|
||||
filters = append(filters, strings.ToLower(key))
|
||||
}
|
||||
}
|
||||
if len(filters) > 0 {
|
||||
return fmt.Sprintf("lower(body) like lower('%%%s%%')", strings.Join(filters, "%"))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func GetJSONFilter(item v3.FilterItem) (string, error) {
|
||||
|
||||
dataType := item.Key.DataType
|
||||
@@ -154,11 +179,28 @@ func GetJSONFilter(item v3.FilterItem) (string, error) {
|
||||
return "", fmt.Errorf("unsupported operator: %s", op)
|
||||
}
|
||||
|
||||
filters := []string{}
|
||||
|
||||
pathFilter := getPathIndexFilter(item.Key.Key)
|
||||
if pathFilter != "" {
|
||||
filters = append(filters, pathFilter)
|
||||
}
|
||||
if op == v3.FilterOperatorContains ||
|
||||
op == v3.FilterOperatorEqual ||
|
||||
op == v3.FilterOperatorHas {
|
||||
val, ok := item.Value.(string)
|
||||
if ok && len(val) >= NGRAM_SIZE {
|
||||
filters = append(filters, fmt.Sprintf("lower(body) like lower('%%%s%%')", utils.QuoteEscapedString(strings.ToLower(val))))
|
||||
}
|
||||
}
|
||||
|
||||
// add exists check for non array items as default values of int/float/bool will corrupt the results
|
||||
if !isArray && !(item.Operator == v3.FilterOperatorExists || item.Operator == v3.FilterOperatorNotExists) {
|
||||
existsFilter := fmt.Sprintf("JSON_EXISTS(body, '$.%s')", getPath(strings.Split(item.Key.Key, ".")[1:]))
|
||||
filter = fmt.Sprintf("%s AND %s", existsFilter, filter)
|
||||
}
|
||||
|
||||
return filter, nil
|
||||
filters = append(filters, filter)
|
||||
|
||||
return strings.Join(filters, " AND "), nil
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ var testGetJSONFilterData = []struct {
|
||||
Operator: "has",
|
||||
Value: "index_service",
|
||||
},
|
||||
Filter: "has(JSONExtract(JSON_QUERY(body, '$.\"requestor_list\"[*]'), 'Array(String)'), 'index_service')",
|
||||
Filter: "lower(body) like lower('%requestor_list%') AND lower(body) like lower('%index_service%') AND has(JSONExtract(JSON_QUERY(body, '$.\"requestor_list\"[*]'), 'Array(String)'), 'index_service')",
|
||||
},
|
||||
{
|
||||
Name: "Array membership int64",
|
||||
@@ -181,7 +181,7 @@ var testGetJSONFilterData = []struct {
|
||||
Operator: "has",
|
||||
Value: 2,
|
||||
},
|
||||
Filter: "has(JSONExtract(JSON_QUERY(body, '$.\"int_numbers\"[*]'), '" + ARRAY_INT64 + "'), 2)",
|
||||
Filter: "lower(body) like lower('%int_numbers%') AND has(JSONExtract(JSON_QUERY(body, '$.\"int_numbers\"[*]'), '" + ARRAY_INT64 + "'), 2)",
|
||||
},
|
||||
{
|
||||
Name: "Array membership float64",
|
||||
@@ -194,7 +194,7 @@ var testGetJSONFilterData = []struct {
|
||||
Operator: "nhas",
|
||||
Value: 2.2,
|
||||
},
|
||||
Filter: "NOT has(JSONExtract(JSON_QUERY(body, '$.\"nested_num\"[*].\"float_nums\"[*]'), '" + ARRAY_FLOAT64 + "'), 2.200000)",
|
||||
Filter: "lower(body) like lower('%nested_num%float_nums%') AND NOT has(JSONExtract(JSON_QUERY(body, '$.\"nested_num\"[*].\"float_nums\"[*]'), '" + ARRAY_FLOAT64 + "'), 2.200000)",
|
||||
},
|
||||
{
|
||||
Name: "Array membership bool",
|
||||
@@ -207,7 +207,7 @@ var testGetJSONFilterData = []struct {
|
||||
Operator: "has",
|
||||
Value: true,
|
||||
},
|
||||
Filter: "has(JSONExtract(JSON_QUERY(body, '$.\"bool\"[*]'), '" + ARRAY_BOOL + "'), true)",
|
||||
Filter: "lower(body) like lower('%bool%') AND has(JSONExtract(JSON_QUERY(body, '$.\"bool\"[*]'), '" + ARRAY_BOOL + "'), true)",
|
||||
},
|
||||
{
|
||||
Name: "eq operator",
|
||||
@@ -220,7 +220,7 @@ var testGetJSONFilterData = []struct {
|
||||
Operator: "=",
|
||||
Value: "hello",
|
||||
},
|
||||
Filter: "JSON_EXISTS(body, '$.\"message\"') AND JSON_VALUE(body, '$.\"message\"') = 'hello'",
|
||||
Filter: "lower(body) like lower('%message%') AND lower(body) like lower('%hello%') AND JSON_EXISTS(body, '$.\"message\"') AND JSON_VALUE(body, '$.\"message\"') = 'hello'",
|
||||
},
|
||||
{
|
||||
Name: "eq operator number",
|
||||
@@ -233,7 +233,7 @@ var testGetJSONFilterData = []struct {
|
||||
Operator: "=",
|
||||
Value: 1,
|
||||
},
|
||||
Filter: "JSON_EXISTS(body, '$.\"status\"') AND JSONExtract(JSON_VALUE(body, '$.\"status\"'), '" + INT64 + "') = 1",
|
||||
Filter: "lower(body) like lower('%status%') AND JSON_EXISTS(body, '$.\"status\"') AND JSONExtract(JSON_VALUE(body, '$.\"status\"'), '" + INT64 + "') = 1",
|
||||
},
|
||||
{
|
||||
Name: "neq operator number",
|
||||
@@ -246,7 +246,7 @@ var testGetJSONFilterData = []struct {
|
||||
Operator: "=",
|
||||
Value: 1.1,
|
||||
},
|
||||
Filter: "JSON_EXISTS(body, '$.\"status\"') AND JSONExtract(JSON_VALUE(body, '$.\"status\"'), '" + FLOAT64 + "') = 1.100000",
|
||||
Filter: "lower(body) like lower('%status%') AND JSON_EXISTS(body, '$.\"status\"') AND JSONExtract(JSON_VALUE(body, '$.\"status\"'), '" + FLOAT64 + "') = 1.100000",
|
||||
},
|
||||
{
|
||||
Name: "eq operator bool",
|
||||
@@ -259,7 +259,7 @@ var testGetJSONFilterData = []struct {
|
||||
Operator: "=",
|
||||
Value: true,
|
||||
},
|
||||
Filter: "JSON_EXISTS(body, '$.\"boolkey\"') AND JSONExtract(JSON_VALUE(body, '$.\"boolkey\"'), '" + BOOL + "') = true",
|
||||
Filter: "lower(body) like lower('%boolkey%') AND JSON_EXISTS(body, '$.\"boolkey\"') AND JSONExtract(JSON_VALUE(body, '$.\"boolkey\"'), '" + BOOL + "') = true",
|
||||
},
|
||||
{
|
||||
Name: "greater than operator",
|
||||
@@ -272,7 +272,7 @@ var testGetJSONFilterData = []struct {
|
||||
Operator: ">",
|
||||
Value: 1,
|
||||
},
|
||||
Filter: "JSON_EXISTS(body, '$.\"status\"') AND JSONExtract(JSON_VALUE(body, '$.\"status\"'), '" + INT64 + "') > 1",
|
||||
Filter: "lower(body) like lower('%status%') AND JSON_EXISTS(body, '$.\"status\"') AND JSONExtract(JSON_VALUE(body, '$.\"status\"'), '" + INT64 + "') > 1",
|
||||
},
|
||||
{
|
||||
Name: "regex operator",
|
||||
@@ -285,7 +285,7 @@ var testGetJSONFilterData = []struct {
|
||||
Operator: "regex",
|
||||
Value: "a*",
|
||||
},
|
||||
Filter: "JSON_EXISTS(body, '$.\"message\"') AND match(JSON_VALUE(body, '$.\"message\"'), 'a*')",
|
||||
Filter: "lower(body) like lower('%message%') AND JSON_EXISTS(body, '$.\"message\"') AND match(JSON_VALUE(body, '$.\"message\"'), 'a*')",
|
||||
},
|
||||
{
|
||||
Name: "contains operator",
|
||||
@@ -298,7 +298,7 @@ var testGetJSONFilterData = []struct {
|
||||
Operator: "contains",
|
||||
Value: "a",
|
||||
},
|
||||
Filter: "JSON_EXISTS(body, '$.\"message\"') AND JSON_VALUE(body, '$.\"message\"') ILIKE '%a%'",
|
||||
Filter: "lower(body) like lower('%message%') AND JSON_EXISTS(body, '$.\"message\"') AND JSON_VALUE(body, '$.\"message\"') ILIKE '%a%'",
|
||||
},
|
||||
{
|
||||
Name: "contains operator with quotes",
|
||||
@@ -311,7 +311,7 @@ var testGetJSONFilterData = []struct {
|
||||
Operator: "contains",
|
||||
Value: "hello 'world'",
|
||||
},
|
||||
Filter: "JSON_EXISTS(body, '$.\"message\"') AND JSON_VALUE(body, '$.\"message\"') ILIKE '%hello \\'world\\'%'",
|
||||
Filter: "lower(body) like lower('%message%') AND lower(body) like lower('%hello \\'world\\'%') AND JSON_EXISTS(body, '$.\"message\"') AND JSON_VALUE(body, '$.\"message\"') ILIKE '%hello \\'world\\'%'",
|
||||
},
|
||||
{
|
||||
Name: "exists",
|
||||
@@ -324,7 +324,7 @@ var testGetJSONFilterData = []struct {
|
||||
Operator: "exists",
|
||||
Value: "",
|
||||
},
|
||||
Filter: "JSON_EXISTS(body, '$.\"message\"')",
|
||||
Filter: "lower(body) like lower('%message%') AND JSON_EXISTS(body, '$.\"message\"')",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,8 @@ var logOperators = map[v3.FilterOperator]string{
|
||||
v3.FilterOperatorNotExists: "not has(%s_%s_key, '%s')",
|
||||
}
|
||||
|
||||
const BODY = "body"
|
||||
|
||||
func getClickhouseLogsColumnType(columnType v3.AttributeKeyType) string {
|
||||
if columnType == v3.AttributeKeyTypeTag {
|
||||
return "attributes"
|
||||
@@ -193,10 +195,24 @@ func buildLogsTimeSeriesFilterQuery(fs *v3.FilterSet, groupBy []v3.AttributeKey,
|
||||
case v3.FilterOperatorContains, v3.FilterOperatorNotContains:
|
||||
columnName := getClickhouseColumnName(item.Key)
|
||||
val := utils.QuoteEscapedString(fmt.Sprintf("%v", item.Value))
|
||||
conditions = append(conditions, fmt.Sprintf("%s %s '%%%s%%'", columnName, logsOp, val))
|
||||
if columnName == BODY {
|
||||
logsOp = strings.Replace(logsOp, "ILIKE", "LIKE", 1) // removing i from ilike and not ilike
|
||||
conditions = append(conditions, fmt.Sprintf("lower(%s) %s lower('%%%s%%')", columnName, logsOp, val))
|
||||
} else {
|
||||
conditions = append(conditions, fmt.Sprintf("%s %s '%%%s%%'", columnName, logsOp, val))
|
||||
}
|
||||
default:
|
||||
columnName := getClickhouseColumnName(item.Key)
|
||||
fmtVal := utils.ClickHouseFormattedValue(value)
|
||||
|
||||
// for use lower for like and ilike
|
||||
if op == v3.FilterOperatorLike || op == v3.FilterOperatorNotLike {
|
||||
if columnName == BODY {
|
||||
logsOp = strings.Replace(logsOp, "ILIKE", "LIKE", 1) // removing i from ilike and not ilike
|
||||
columnName = fmt.Sprintf("lower(%s)", columnName)
|
||||
fmtVal = fmt.Sprintf("lower(%s)", fmtVal)
|
||||
}
|
||||
}
|
||||
conditions = append(conditions, fmt.Sprintf("%s %s %s", columnName, logsOp, fmtVal))
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -130,6 +130,14 @@ var timeSeriesFilterQueryData = []struct {
|
||||
}},
|
||||
ExpectedFilter: "attributes_string_value[indexOf(attributes_string_key, 'user_name')] = 'john' AND resources_string_value[indexOf(resources_string_key, 'k8s_namespace')] != 'my_service'",
|
||||
},
|
||||
{
|
||||
Name: "Test attribute and resource attribute with different case",
|
||||
FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "user_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "%JoHn%", Operator: "like"},
|
||||
{Key: v3.AttributeKey{Key: "k8s_namespace", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, Value: "%MyService%", Operator: "nlike"},
|
||||
}},
|
||||
ExpectedFilter: "attributes_string_value[indexOf(attributes_string_key, 'user_name')] ILIKE '%JoHn%' AND resources_string_value[indexOf(resources_string_key, 'k8s_namespace')] NOT ILIKE '%MyService%'",
|
||||
},
|
||||
{
|
||||
Name: "Test materialized column",
|
||||
FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
@@ -287,6 +295,22 @@ var timeSeriesFilterQueryData = []struct {
|
||||
}},
|
||||
ExpectedFilter: "`attribute_int64_status_exists`=false",
|
||||
},
|
||||
{
|
||||
Name: "Test for body contains and ncontains",
|
||||
FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, IsColumn: true}, Operator: "contains", Value: "test"},
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, IsColumn: true}, Operator: "ncontains", Value: "test1"},
|
||||
}},
|
||||
ExpectedFilter: "lower(body) LIKE lower('%test%') AND lower(body) NOT LIKE lower('%test1%')",
|
||||
},
|
||||
{
|
||||
Name: "Test for body like and nlike",
|
||||
FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, IsColumn: true}, Operator: "like", Value: "test"},
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, IsColumn: true}, Operator: "nlike", Value: "test1"},
|
||||
}},
|
||||
ExpectedFilter: "lower(body) LIKE lower('test') AND lower(body) NOT LIKE lower('test1')",
|
||||
},
|
||||
}
|
||||
|
||||
func TestBuildLogsTimeSeriesFilterQuery(t *testing.T) {
|
||||
@@ -851,7 +875,7 @@ var testBuildLogsQueryData = []struct {
|
||||
},
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND body ILIKE '%test%' AND has(attributes_string_key, 'name') group by ts having value > 10 order by value DESC",
|
||||
ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND lower(body) LIKE lower('%test%') AND has(attributes_string_key, 'name') group by ts having value > 10 order by value DESC",
|
||||
},
|
||||
{
|
||||
Name: "Test attribute with same name as top level key",
|
||||
@@ -981,7 +1005,7 @@ var testBuildLogsQueryData = []struct {
|
||||
},
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT now() as ts, attributes_string_value[indexOf(attributes_string_key, 'name')] as `name`, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND JSON_EXISTS(body, '$.\"message\"') AND JSON_VALUE(body, '$.\"message\"') ILIKE '%a%' AND has(attributes_string_key, 'name') group by `name` order by `name` DESC",
|
||||
ExpectedQuery: "SELECT now() as ts, attributes_string_value[indexOf(attributes_string_key, 'name')] as `name`, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND lower(body) like lower('%message%') AND JSON_EXISTS(body, '$.\"message\"') AND JSON_VALUE(body, '$.\"message\"') ILIKE '%a%' AND has(attributes_string_key, 'name') group by `name` order by `name` DESC",
|
||||
},
|
||||
{
|
||||
Name: "TABLE: Test count with JSON Filter Array, groupBy, orderBy",
|
||||
@@ -1015,7 +1039,7 @@ var testBuildLogsQueryData = []struct {
|
||||
},
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT now() as ts, attributes_string_value[indexOf(attributes_string_key, 'name')] as `name`, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND has(JSONExtract(JSON_QUERY(body, '$.\"requestor_list\"[*]'), 'Array(String)'), 'index_service') AND has(attributes_string_key, 'name') group by `name` order by `name` DESC",
|
||||
ExpectedQuery: "SELECT now() as ts, attributes_string_value[indexOf(attributes_string_key, 'name')] as `name`, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND lower(body) like lower('%requestor_list%') AND lower(body) like lower('%index_service%') AND has(JSONExtract(JSON_QUERY(body, '$.\"requestor_list\"[*]'), 'Array(String)'), 'index_service') AND has(attributes_string_key, 'name') group by `name` order by `name` DESC",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
37
pkg/query-service/app/preferences/map.go
Normal file
37
pkg/query-service/app/preferences/map.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package preferences
|
||||
|
||||
var preferenceMap = map[string]Preference{
|
||||
"DASHBOARDS_LIST_VIEW": {
|
||||
Key: "DASHBOARDS_LIST_VIEW",
|
||||
Name: "Dashboards List View",
|
||||
Description: "",
|
||||
ValueType: "string",
|
||||
DefaultValue: "grid",
|
||||
AllowedValues: []interface{}{"grid", "list"},
|
||||
IsDiscreteValues: true,
|
||||
AllowedScopes: []string{"user", "org"},
|
||||
},
|
||||
"LOGS_TOOLBAR_COLLAPSED": {
|
||||
Key: "LOGS_TOOLBAR_COLLAPSED",
|
||||
Name: "Logs toolbar",
|
||||
Description: "",
|
||||
ValueType: "boolean",
|
||||
DefaultValue: false,
|
||||
AllowedValues: []interface{}{true, false},
|
||||
IsDiscreteValues: true,
|
||||
AllowedScopes: []string{"user", "org"},
|
||||
},
|
||||
"MAX_DEPTH_ALLOWED": {
|
||||
Key: "MAX_DEPTH_ALLOWED",
|
||||
Name: "Max Depth Allowed",
|
||||
Description: "",
|
||||
ValueType: "integer",
|
||||
DefaultValue: 10,
|
||||
IsDiscreteValues: false,
|
||||
Range: Range{
|
||||
Min: 0,
|
||||
Max: 100,
|
||||
},
|
||||
AllowedScopes: []string{"user", "org"},
|
||||
},
|
||||
}
|
||||
544
pkg/query-service/app/preferences/model.go
Normal file
544
pkg/query-service/app/preferences/model.go
Normal file
@@ -0,0 +1,544 @@
|
||||
package preferences
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.signoz.io/signoz/ee/query-service/model"
|
||||
)
|
||||
|
||||
type Range struct {
|
||||
Min int64 `json:"min"`
|
||||
Max int64 `json:"max"`
|
||||
}
|
||||
|
||||
type Preference struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
ValueType string `json:"valueType"`
|
||||
DefaultValue interface{} `json:"defaultValue"`
|
||||
AllowedValues []interface{} `json:"allowedValues"`
|
||||
IsDiscreteValues bool `json:"isDiscreteValues"`
|
||||
Range Range `json:"range"`
|
||||
AllowedScopes []string `json:"allowedScopes"`
|
||||
}
|
||||
|
||||
func (p *Preference) ErrorValueTypeMismatch() *model.ApiError {
|
||||
return &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("the preference value is not of expected type: %s", p.ValueType)}
|
||||
}
|
||||
|
||||
const (
|
||||
PreferenceValueTypeInteger string = "integer"
|
||||
PreferenceValueTypeFloat string = "float"
|
||||
PreferenceValueTypeString string = "string"
|
||||
PreferenceValueTypeBoolean string = "boolean"
|
||||
)
|
||||
|
||||
const (
|
||||
OrgAllowedScope string = "org"
|
||||
UserAllowedScope string = "user"
|
||||
)
|
||||
|
||||
func (p *Preference) checkIfInAllowedValues(preferenceValue interface{}) (bool, *model.ApiError) {
|
||||
|
||||
switch p.ValueType {
|
||||
case PreferenceValueTypeInteger:
|
||||
_, ok := preferenceValue.(int64)
|
||||
if !ok {
|
||||
return false, p.ErrorValueTypeMismatch()
|
||||
}
|
||||
case PreferenceValueTypeFloat:
|
||||
_, ok := preferenceValue.(float64)
|
||||
if !ok {
|
||||
return false, p.ErrorValueTypeMismatch()
|
||||
}
|
||||
case PreferenceValueTypeString:
|
||||
_, ok := preferenceValue.(string)
|
||||
if !ok {
|
||||
return false, p.ErrorValueTypeMismatch()
|
||||
}
|
||||
case PreferenceValueTypeBoolean:
|
||||
_, ok := preferenceValue.(bool)
|
||||
if !ok {
|
||||
return false, p.ErrorValueTypeMismatch()
|
||||
}
|
||||
}
|
||||
isInAllowedValues := false
|
||||
for _, value := range p.AllowedValues {
|
||||
switch p.ValueType {
|
||||
case PreferenceValueTypeInteger:
|
||||
allowedValue, ok := value.(int64)
|
||||
if !ok {
|
||||
return false, p.ErrorValueTypeMismatch()
|
||||
}
|
||||
|
||||
if allowedValue == preferenceValue {
|
||||
isInAllowedValues = true
|
||||
}
|
||||
case PreferenceValueTypeFloat:
|
||||
allowedValue, ok := value.(float64)
|
||||
if !ok {
|
||||
return false, p.ErrorValueTypeMismatch()
|
||||
}
|
||||
|
||||
if allowedValue == preferenceValue {
|
||||
isInAllowedValues = true
|
||||
}
|
||||
case PreferenceValueTypeString:
|
||||
allowedValue, ok := value.(string)
|
||||
if !ok {
|
||||
return false, p.ErrorValueTypeMismatch()
|
||||
}
|
||||
|
||||
if allowedValue == preferenceValue {
|
||||
isInAllowedValues = true
|
||||
}
|
||||
case PreferenceValueTypeBoolean:
|
||||
allowedValue, ok := value.(bool)
|
||||
if !ok {
|
||||
return false, p.ErrorValueTypeMismatch()
|
||||
}
|
||||
|
||||
if allowedValue == preferenceValue {
|
||||
isInAllowedValues = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return isInAllowedValues, nil
|
||||
}
|
||||
|
||||
func (p *Preference) IsValidValue(preferenceValue interface{}) *model.ApiError {
|
||||
typeSafeValue := preferenceValue
|
||||
switch p.ValueType {
|
||||
case PreferenceValueTypeInteger:
|
||||
val, ok := preferenceValue.(int64)
|
||||
if !ok {
|
||||
floatVal, ok := preferenceValue.(float64)
|
||||
if !ok || floatVal != float64(int64(floatVal)) {
|
||||
return p.ErrorValueTypeMismatch()
|
||||
}
|
||||
val = int64(floatVal)
|
||||
typeSafeValue = val
|
||||
}
|
||||
if !p.IsDiscreteValues {
|
||||
if val < p.Range.Min || val > p.Range.Max {
|
||||
return &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("the preference value is not in the range specified, min: %v , max:%v", p.Range.Min, p.Range.Max)}
|
||||
}
|
||||
}
|
||||
case PreferenceValueTypeString:
|
||||
_, ok := preferenceValue.(string)
|
||||
if !ok {
|
||||
return p.ErrorValueTypeMismatch()
|
||||
}
|
||||
case PreferenceValueTypeFloat:
|
||||
_, ok := preferenceValue.(float64)
|
||||
if !ok {
|
||||
return p.ErrorValueTypeMismatch()
|
||||
}
|
||||
case PreferenceValueTypeBoolean:
|
||||
_, ok := preferenceValue.(bool)
|
||||
if !ok {
|
||||
return p.ErrorValueTypeMismatch()
|
||||
}
|
||||
}
|
||||
|
||||
// check the validity of the value being part of allowed values or the range specified if any
|
||||
if p.IsDiscreteValues {
|
||||
if p.AllowedValues != nil {
|
||||
isInAllowedValues, valueMisMatchErr := p.checkIfInAllowedValues(typeSafeValue)
|
||||
|
||||
if valueMisMatchErr != nil {
|
||||
return valueMisMatchErr
|
||||
}
|
||||
if !isInAllowedValues {
|
||||
return &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("the preference value is not in the list of allowedValues: %v", p.AllowedValues)}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Preference) IsEnabledForScope(scope string) bool {
|
||||
isPreferenceEnabledForGivenScope := false
|
||||
if p.AllowedScopes != nil {
|
||||
for _, allowedScope := range p.AllowedScopes {
|
||||
if allowedScope == strings.ToLower(scope) {
|
||||
isPreferenceEnabledForGivenScope = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return isPreferenceEnabledForGivenScope
|
||||
}
|
||||
|
||||
func (p *Preference) SanitizeValue(preferenceValue interface{}) interface{} {
|
||||
switch p.ValueType {
|
||||
case PreferenceValueTypeBoolean:
|
||||
if preferenceValue == "1" || preferenceValue == true {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return preferenceValue
|
||||
}
|
||||
}
|
||||
|
||||
type AllPreferences struct {
|
||||
Preference
|
||||
Value interface{} `json:"value"`
|
||||
}
|
||||
|
||||
type PreferenceKV struct {
|
||||
PreferenceId string `json:"preference_id" db:"preference_id"`
|
||||
PreferenceValue interface{} `json:"preference_value" db:"preference_value"`
|
||||
}
|
||||
|
||||
type UpdatePreference struct {
|
||||
PreferenceValue interface{} `json:"preference_value"`
|
||||
}
|
||||
|
||||
var db *sqlx.DB
|
||||
|
||||
func InitDB(datasourceName string) error {
|
||||
var err error
|
||||
db, err = sqlx.Open("sqlite3", datasourceName)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create the user preference table
|
||||
tableSchema := `
|
||||
PRAGMA foreign_keys = ON;
|
||||
CREATE TABLE IF NOT EXISTS user_preference(
|
||||
preference_id TEXT NOT NULL,
|
||||
preference_value TEXT,
|
||||
user_id TEXT NOT NULL,
|
||||
PRIMARY KEY (preference_id,user_id),
|
||||
FOREIGN KEY (user_id)
|
||||
REFERENCES users(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE
|
||||
);`
|
||||
|
||||
_, err = db.Exec(tableSchema)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error in creating user_preference table: %s", err.Error())
|
||||
}
|
||||
|
||||
// create the org preference table
|
||||
tableSchema = `
|
||||
PRAGMA foreign_keys = ON;
|
||||
CREATE TABLE IF NOT EXISTS org_preference(
|
||||
preference_id TEXT NOT NULL,
|
||||
preference_value TEXT,
|
||||
org_id TEXT NOT NULL,
|
||||
PRIMARY KEY (preference_id,org_id),
|
||||
FOREIGN KEY (org_id)
|
||||
REFERENCES organizations(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE
|
||||
);`
|
||||
|
||||
_, err = db.Exec(tableSchema)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error in creating org_preference table: %s", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// org preference functions
|
||||
func GetOrgPreference(ctx context.Context, preferenceId string, orgId string) (*PreferenceKV, *model.ApiError) {
|
||||
// check if the preference key exists or not
|
||||
preference, seen := preferenceMap[preferenceId]
|
||||
if !seen {
|
||||
return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no such preferenceId exists: %s", preferenceId)}
|
||||
}
|
||||
|
||||
// check if the preference is enabled for org scope or not
|
||||
isPreferenceEnabled := preference.IsEnabledForScope(OrgAllowedScope)
|
||||
if !isPreferenceEnabled {
|
||||
return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("preference is not enabled at org scope: %s", preferenceId)}
|
||||
}
|
||||
|
||||
// fetch the value from the database
|
||||
var orgPreference PreferenceKV
|
||||
query := `SELECT preference_id , preference_value FROM org_preference WHERE preference_id=$1 AND org_id=$2;`
|
||||
err := db.Get(&orgPreference, query, preferenceId, orgId)
|
||||
|
||||
// if the value doesn't exist in db then return the default value
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return &PreferenceKV{
|
||||
PreferenceId: preferenceId,
|
||||
PreferenceValue: preference.DefaultValue,
|
||||
}, nil
|
||||
}
|
||||
return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in fetching the org preference: %s", err.Error())}
|
||||
|
||||
}
|
||||
|
||||
// else return the value fetched from the org_preference table
|
||||
return &PreferenceKV{
|
||||
PreferenceId: preferenceId,
|
||||
PreferenceValue: preference.SanitizeValue(orgPreference.PreferenceValue),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func UpdateOrgPreference(ctx context.Context, preferenceId string, preferenceValue interface{}, orgId string) (*PreferenceKV, *model.ApiError) {
|
||||
// check if the preference key exists or not
|
||||
preference, seen := preferenceMap[preferenceId]
|
||||
if !seen {
|
||||
return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no such preferenceId exists: %s", preferenceId)}
|
||||
}
|
||||
|
||||
// check if the preference is enabled at org scope or not
|
||||
isPreferenceEnabled := preference.IsEnabledForScope(OrgAllowedScope)
|
||||
if !isPreferenceEnabled {
|
||||
return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("preference is not enabled at org scope: %s", preferenceId)}
|
||||
}
|
||||
|
||||
err := preference.IsValidValue(preferenceValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update the values in the org_preference table and return the key and the value
|
||||
query := `INSERT INTO org_preference(preference_id,preference_value,org_id) VALUES($1,$2,$3)
|
||||
ON CONFLICT(preference_id,org_id) DO
|
||||
UPDATE SET preference_value= $2 WHERE preference_id=$1 AND org_id=$3;`
|
||||
|
||||
_, dberr := db.Exec(query, preferenceId, preferenceValue, orgId)
|
||||
|
||||
if dberr != nil {
|
||||
return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in setting the preference value: %s", dberr.Error())}
|
||||
}
|
||||
|
||||
return &PreferenceKV{
|
||||
PreferenceId: preferenceId,
|
||||
PreferenceValue: preferenceValue,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetAllOrgPreferences(ctx context.Context, orgId string) (*[]AllPreferences, *model.ApiError) {
|
||||
// filter out all the org enabled preferences from the preference variable
|
||||
allOrgPreferences := []AllPreferences{}
|
||||
|
||||
// fetch all the org preference values stored in org_preference table
|
||||
orgPreferenceValues := []PreferenceKV{}
|
||||
|
||||
query := `SELECT preference_id,preference_value FROM org_preference WHERE org_id=$1;`
|
||||
err := db.Select(&orgPreferenceValues, query, orgId)
|
||||
|
||||
if err != nil {
|
||||
return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in getting all org preference values: %s", err)}
|
||||
}
|
||||
|
||||
// create a map of key vs values from the above response
|
||||
preferenceValueMap := map[string]interface{}{}
|
||||
|
||||
for _, preferenceValue := range orgPreferenceValues {
|
||||
preferenceValueMap[preferenceValue.PreferenceId] = preferenceValue.PreferenceValue
|
||||
}
|
||||
|
||||
// update in the above filtered list wherver value present in the map
|
||||
for _, preference := range preferenceMap {
|
||||
isEnabledForOrgScope := preference.IsEnabledForScope(OrgAllowedScope)
|
||||
if isEnabledForOrgScope {
|
||||
preferenceWithValue := AllPreferences{}
|
||||
preferenceWithValue.Key = preference.Key
|
||||
preferenceWithValue.Name = preference.Name
|
||||
preferenceWithValue.Description = preference.Description
|
||||
preferenceWithValue.AllowedScopes = preference.AllowedScopes
|
||||
preferenceWithValue.AllowedValues = preference.AllowedValues
|
||||
preferenceWithValue.DefaultValue = preference.DefaultValue
|
||||
preferenceWithValue.Range = preference.Range
|
||||
preferenceWithValue.ValueType = preference.ValueType
|
||||
preferenceWithValue.IsDiscreteValues = preference.IsDiscreteValues
|
||||
value, seen := preferenceValueMap[preference.Key]
|
||||
|
||||
if seen {
|
||||
preferenceWithValue.Value = value
|
||||
} else {
|
||||
preferenceWithValue.Value = preference.DefaultValue
|
||||
}
|
||||
|
||||
preferenceWithValue.Value = preference.SanitizeValue(preferenceWithValue.Value)
|
||||
allOrgPreferences = append(allOrgPreferences, preferenceWithValue)
|
||||
}
|
||||
}
|
||||
return &allOrgPreferences, nil
|
||||
}
|
||||
|
||||
// user preference functions
|
||||
func GetUserPreference(ctx context.Context, preferenceId string, orgId string, userId string) (*PreferenceKV, *model.ApiError) {
|
||||
// check if the preference key exists
|
||||
preference, seen := preferenceMap[preferenceId]
|
||||
if !seen {
|
||||
return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no such preferenceId exists: %s", preferenceId)}
|
||||
}
|
||||
|
||||
preferenceValue := PreferenceKV{
|
||||
PreferenceId: preferenceId,
|
||||
PreferenceValue: preference.DefaultValue,
|
||||
}
|
||||
|
||||
// check if the preference is enabled at user scope
|
||||
isPreferenceEnabledAtUserScope := preference.IsEnabledForScope(UserAllowedScope)
|
||||
if !isPreferenceEnabledAtUserScope {
|
||||
return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("preference is not enabled at user scope: %s", preferenceId)}
|
||||
}
|
||||
|
||||
isPreferenceEnabledAtOrgScope := preference.IsEnabledForScope(OrgAllowedScope)
|
||||
// get the value from the org scope if enabled at org scope
|
||||
if isPreferenceEnabledAtOrgScope {
|
||||
orgPreference := PreferenceKV{}
|
||||
|
||||
query := `SELECT preference_id , preference_value FROM org_preference WHERE preference_id=$1 AND org_id=$2;`
|
||||
|
||||
err := db.Get(&orgPreference, query, preferenceId, orgId)
|
||||
|
||||
// if there is error in getting values and its not an empty rows error return from here
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in getting org preference values: %s", err.Error())}
|
||||
}
|
||||
|
||||
// if there is no error update the preference value with value from org preference
|
||||
if err == nil {
|
||||
preferenceValue.PreferenceValue = orgPreference.PreferenceValue
|
||||
}
|
||||
}
|
||||
|
||||
// get the value from the user_preference table, if exists return this value else the one calculated in the above step
|
||||
userPreference := PreferenceKV{}
|
||||
|
||||
query := `SELECT preference_id, preference_value FROM user_preference WHERE preference_id=$1 AND user_id=$2;`
|
||||
err := db.Get(&userPreference, query, preferenceId, userId)
|
||||
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in getting user preference values: %s", err.Error())}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
preferenceValue.PreferenceValue = userPreference.PreferenceValue
|
||||
}
|
||||
|
||||
return &PreferenceKV{
|
||||
PreferenceId: preferenceValue.PreferenceId,
|
||||
PreferenceValue: preference.SanitizeValue(preferenceValue.PreferenceValue),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func UpdateUserPreference(ctx context.Context, preferenceId string, preferenceValue interface{}, userId string) (*PreferenceKV, *model.ApiError) {
|
||||
// check if the preference id is valid
|
||||
preference, seen := preferenceMap[preferenceId]
|
||||
if !seen {
|
||||
return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no such preferenceId exists: %s", preferenceId)}
|
||||
}
|
||||
|
||||
// check if the preference is enabled at user scope
|
||||
isPreferenceEnabledAtUserScope := preference.IsEnabledForScope(UserAllowedScope)
|
||||
if !isPreferenceEnabledAtUserScope {
|
||||
return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("preference is not enabled at user scope: %s", preferenceId)}
|
||||
}
|
||||
|
||||
err := preference.IsValidValue(preferenceValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// update the user preference values
|
||||
query := `INSERT INTO user_preference(preference_id,preference_value,user_id) VALUES($1,$2,$3)
|
||||
ON CONFLICT(preference_id,user_id) DO
|
||||
UPDATE SET preference_value= $2 WHERE preference_id=$1 AND user_id=$3;`
|
||||
|
||||
_, dberrr := db.Exec(query, preferenceId, preferenceValue, userId)
|
||||
|
||||
if dberrr != nil {
|
||||
return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in setting the preference value: %s", dberrr.Error())}
|
||||
}
|
||||
|
||||
return &PreferenceKV{
|
||||
PreferenceId: preferenceId,
|
||||
PreferenceValue: preferenceValue,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetAllUserPreferences(ctx context.Context, orgId string, userId string) (*[]AllPreferences, *model.ApiError) {
|
||||
allUserPreferences := []AllPreferences{}
|
||||
|
||||
// fetch all the org preference values stored in org_preference table
|
||||
orgPreferenceValues := []PreferenceKV{}
|
||||
|
||||
query := `SELECT preference_id,preference_value FROM org_preference WHERE org_id=$1;`
|
||||
err := db.Select(&orgPreferenceValues, query, orgId)
|
||||
|
||||
if err != nil {
|
||||
return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in getting all org preference values: %s", err)}
|
||||
}
|
||||
|
||||
// create a map of key vs values from the above response
|
||||
preferenceOrgValueMap := map[string]interface{}{}
|
||||
|
||||
for _, preferenceValue := range orgPreferenceValues {
|
||||
preferenceOrgValueMap[preferenceValue.PreferenceId] = preferenceValue.PreferenceValue
|
||||
}
|
||||
|
||||
// fetch all the user preference values stored in user_preference table
|
||||
userPreferenceValues := []PreferenceKV{}
|
||||
|
||||
query = `SELECT preference_id,preference_value FROM user_preference WHERE user_id=$1;`
|
||||
err = db.Select(&userPreferenceValues, query, userId)
|
||||
|
||||
if err != nil {
|
||||
return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in getting all user preference values: %s", err)}
|
||||
}
|
||||
|
||||
// create a map of key vs values from the above response
|
||||
preferenceUserValueMap := map[string]interface{}{}
|
||||
|
||||
for _, preferenceValue := range userPreferenceValues {
|
||||
preferenceUserValueMap[preferenceValue.PreferenceId] = preferenceValue.PreferenceValue
|
||||
}
|
||||
|
||||
// update in the above filtered list wherver value present in the map
|
||||
for _, preference := range preferenceMap {
|
||||
isEnabledForUserScope := preference.IsEnabledForScope(UserAllowedScope)
|
||||
|
||||
if isEnabledForUserScope {
|
||||
preferenceWithValue := AllPreferences{}
|
||||
preferenceWithValue.Key = preference.Key
|
||||
preferenceWithValue.Name = preference.Name
|
||||
preferenceWithValue.Description = preference.Description
|
||||
preferenceWithValue.AllowedScopes = preference.AllowedScopes
|
||||
preferenceWithValue.AllowedValues = preference.AllowedValues
|
||||
preferenceWithValue.DefaultValue = preference.DefaultValue
|
||||
preferenceWithValue.Range = preference.Range
|
||||
preferenceWithValue.ValueType = preference.ValueType
|
||||
preferenceWithValue.IsDiscreteValues = preference.IsDiscreteValues
|
||||
preferenceWithValue.Value = preference.DefaultValue
|
||||
|
||||
isEnabledForOrgScope := preference.IsEnabledForScope(OrgAllowedScope)
|
||||
if isEnabledForOrgScope {
|
||||
value, seen := preferenceOrgValueMap[preference.Key]
|
||||
if seen {
|
||||
preferenceWithValue.Value = value
|
||||
}
|
||||
}
|
||||
|
||||
value, seen := preferenceUserValueMap[preference.Key]
|
||||
|
||||
if seen {
|
||||
preferenceWithValue.Value = value
|
||||
}
|
||||
|
||||
preferenceWithValue.Value = preference.SanitizeValue(preferenceWithValue.Value)
|
||||
allUserPreferences = append(allUserPreferences, preferenceWithValue)
|
||||
}
|
||||
}
|
||||
return &allUserPreferences, nil
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -27,6 +28,7 @@ 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/common"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
|
||||
@@ -94,6 +96,10 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := preferences.InitDB(constants.RELATIONAL_DATASOURCE_PATH); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
localDB, err := dashboards.InitDB(constants.RELATIONAL_DATASOURCE_PATH)
|
||||
explorer.InitWithDSN(constants.RELATIONAL_DATASOURCE_PATH)
|
||||
|
||||
@@ -268,7 +274,21 @@ func (s *Server) createPublicServer(api *APIHandler) (*http.Server, error) {
|
||||
r.Use(s.analyticsMiddleware)
|
||||
r.Use(loggingMiddleware)
|
||||
|
||||
am := NewAuthMiddleware(auth.GetUserFromRequest)
|
||||
// add auth middleware
|
||||
getUserFromRequest := func(r *http.Request) (*model.UserPayload, error) {
|
||||
user, err := auth.GetUserFromRequest(r)
|
||||
|
||||
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
|
||||
}
|
||||
am := NewAuthMiddleware(getUserFromRequest)
|
||||
|
||||
api.RegisterRoutes(r, am)
|
||||
api.RegisterLogsRoutes(r, am)
|
||||
|
||||
183
pkg/query-service/app/traces/v3/utils.go
Normal file
183
pkg/query-service/app/traces/v3/utils.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package v3
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
"go.signoz.io/signoz/pkg/query-service/utils"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// check if traceId filter is used in traces query and return the list of traceIds
|
||||
func TraceIdFilterUsedWithEqual(params *v3.QueryRangeParamsV3) (bool, []string) {
|
||||
compositeQuery := params.CompositeQuery
|
||||
if compositeQuery == nil {
|
||||
return false, []string{}
|
||||
}
|
||||
var traceIds []string
|
||||
var traceIdFilterUsed bool
|
||||
|
||||
// Build queries for each builder query
|
||||
for queryName, query := range compositeQuery.BuilderQueries {
|
||||
if query.Expression != queryName && query.DataSource != v3.DataSourceTraces {
|
||||
continue
|
||||
}
|
||||
|
||||
// check filter attribute
|
||||
if query.Filters != nil && len(query.Filters.Items) != 0 {
|
||||
for _, item := range query.Filters.Items {
|
||||
|
||||
if item.Key.Key == "traceID" && (item.Operator == v3.FilterOperatorIn ||
|
||||
item.Operator == v3.FilterOperatorEqual) {
|
||||
traceIdFilterUsed = true
|
||||
// validate value
|
||||
var err error
|
||||
val := item.Value
|
||||
val, err = utils.ValidateAndCastValue(val, item.Key.DataType)
|
||||
if err != nil {
|
||||
zap.L().Error("invalid value for key", zap.String("key", item.Key.Key), zap.Error(err))
|
||||
return false, []string{}
|
||||
}
|
||||
if val != nil {
|
||||
fmtVal := extractFormattedStringValues(val)
|
||||
traceIds = append(traceIds, fmtVal...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
zap.L().Debug("traceIds", zap.Any("traceIds", traceIds))
|
||||
return traceIdFilterUsed, traceIds
|
||||
}
|
||||
|
||||
func extractFormattedStringValues(v interface{}) []string {
|
||||
// if it's pointer convert it to a value
|
||||
v = getPointerValue(v)
|
||||
|
||||
switch x := v.(type) {
|
||||
case string:
|
||||
return []string{x}
|
||||
|
||||
case []interface{}:
|
||||
if len(x) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
switch x[0].(type) {
|
||||
case string:
|
||||
values := []string{}
|
||||
for _, val := range x {
|
||||
values = append(values, val.(string))
|
||||
}
|
||||
return values
|
||||
default:
|
||||
return []string{}
|
||||
}
|
||||
default:
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
|
||||
func getPointerValue(v interface{}) interface{} {
|
||||
switch x := v.(type) {
|
||||
case *uint8:
|
||||
return *x
|
||||
case *uint16:
|
||||
return *x
|
||||
case *uint32:
|
||||
return *x
|
||||
case *uint64:
|
||||
return *x
|
||||
case *int:
|
||||
return *x
|
||||
case *int8:
|
||||
return *x
|
||||
case *int16:
|
||||
return *x
|
||||
case *int32:
|
||||
return *x
|
||||
case *int64:
|
||||
return *x
|
||||
case *float32:
|
||||
return *x
|
||||
case *float64:
|
||||
return *x
|
||||
case *string:
|
||||
return *x
|
||||
case *bool:
|
||||
return *x
|
||||
case []interface{}:
|
||||
values := []interface{}{}
|
||||
for _, val := range x {
|
||||
values = append(values, getPointerValue(val))
|
||||
}
|
||||
return values
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func AddTimestampFilters(minTime int64, maxTime int64, params *v3.QueryRangeParamsV3) {
|
||||
if minTime == 0 && maxTime == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
compositeQuery := params.CompositeQuery
|
||||
if compositeQuery == nil {
|
||||
return
|
||||
}
|
||||
// Build queries for each builder query
|
||||
for queryName, query := range compositeQuery.BuilderQueries {
|
||||
if query.Expression != queryName && query.DataSource != v3.DataSourceTraces {
|
||||
continue
|
||||
}
|
||||
|
||||
addTimeStampFilter := false
|
||||
|
||||
// check filter attribute
|
||||
if query.Filters != nil && len(query.Filters.Items) != 0 {
|
||||
for _, item := range query.Filters.Items {
|
||||
if item.Key.Key == "traceID" && (item.Operator == v3.FilterOperatorIn ||
|
||||
item.Operator == v3.FilterOperatorEqual) {
|
||||
addTimeStampFilter = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add timestamp filter to query only if traceID filter along with equal/similar operator is used
|
||||
if addTimeStampFilter {
|
||||
timeFilters := []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "timestamp",
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: true,
|
||||
},
|
||||
Value: strconv.FormatUint(uint64(minTime), 10),
|
||||
Operator: v3.FilterOperatorGreaterThanOrEq,
|
||||
},
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "timestamp",
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: true,
|
||||
},
|
||||
Value: strconv.FormatUint(uint64(maxTime), 10),
|
||||
Operator: v3.FilterOperatorLessThanOrEq,
|
||||
},
|
||||
}
|
||||
|
||||
// add new timestamp filter to query
|
||||
if query.Filters == nil {
|
||||
query.Filters = &v3.FilterSet{
|
||||
Items: timeFilters,
|
||||
}
|
||||
} else {
|
||||
query.Filters.Items = append(query.Filters.Items, timeFilters...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -467,6 +467,10 @@ func authenticateLogin(ctx context.Context, req *model.LoginRequest) (*model.Use
|
||||
return nil, errors.Wrap(err, "failed to validate refresh token")
|
||||
}
|
||||
|
||||
if user.OrgId == "" {
|
||||
return nil, model.UnauthorizedError(errors.New("orgId is missing in the claims"))
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@@ -505,6 +509,7 @@ func GenerateJWTForUser(user *model.User) (model.UserJwtObject, error) {
|
||||
"gid": user.GroupId,
|
||||
"email": user.Email,
|
||||
"exp": j.AccessJwtExpiry,
|
||||
"orgId": user.OrgId,
|
||||
})
|
||||
|
||||
j.AccessJwt, err = token.SignedString([]byte(JwtSecret))
|
||||
@@ -518,6 +523,7 @@ func GenerateJWTForUser(user *model.User) (model.UserJwtObject, error) {
|
||||
"gid": user.GroupId,
|
||||
"email": user.Email,
|
||||
"exp": j.RefreshJwtExpiry,
|
||||
"orgId": user.OrgId,
|
||||
})
|
||||
|
||||
j.RefreshJwt, err = token.SignedString([]byte(JwtSecret))
|
||||
|
||||
@@ -20,6 +20,8 @@ var (
|
||||
)
|
||||
|
||||
func ParseJWT(jwtStr string) (jwt.MapClaims, error) {
|
||||
// TODO[@vikrantgupta25] : to update this to the claims check function for better integrity of JWT
|
||||
// reference - https://pkg.go.dev/github.com/golang-jwt/jwt/v5#Parser.ParseWithClaims
|
||||
token, err := jwt.Parse(jwtStr, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.Errorf("unknown signing algo: %v", token.Header["alg"])
|
||||
@@ -35,6 +37,7 @@ func ParseJWT(jwtStr string) (jwt.MapClaims, error) {
|
||||
if !ok || !token.Valid {
|
||||
return nil, errors.Errorf("Not a valid jwt claim")
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
@@ -47,11 +50,18 @@ func validateUser(tok string) (*model.UserPayload, error) {
|
||||
if !claims.VerifyExpiresAt(now, true) {
|
||||
return nil, model.ErrorTokenExpired
|
||||
}
|
||||
|
||||
var orgId string
|
||||
if claims["orgId"] != nil {
|
||||
orgId = claims["orgId"].(string)
|
||||
}
|
||||
|
||||
return &model.UserPayload{
|
||||
User: model.User{
|
||||
Id: claims["id"].(string),
|
||||
GroupId: claims["gid"].(string),
|
||||
Email: claims["email"].(string),
|
||||
OrgId: orgId,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ type Reader interface {
|
||||
GetInstantQueryMetricsResult(ctx context.Context, query *model.InstantQueryMetricsParams) (*promql.Result, *stats.QueryStats, *model.ApiError)
|
||||
GetQueryRangeResult(ctx context.Context, query *model.QueryRangeParams) (*promql.Result, *stats.QueryStats, *model.ApiError)
|
||||
GetServiceOverview(ctx context.Context, query *model.GetServiceOverviewParams, skipConfig *model.SkipConfig) (*[]model.ServiceOverviewItem, *model.ApiError)
|
||||
GetTopLevelOperations(ctx context.Context, skipConfig *model.SkipConfig, start, end time.Time) (*map[string][]string, *map[string][]string, *model.ApiError)
|
||||
GetTopLevelOperations(ctx context.Context, skipConfig *model.SkipConfig, start, end time.Time, services []string) (*map[string][]string, *model.ApiError)
|
||||
GetServices(ctx context.Context, query *model.GetServicesParams, skipConfig *model.SkipConfig) (*[]model.ServiceItem, *model.ApiError)
|
||||
GetTopOperations(ctx context.Context, query *model.GetTopOperationsParams) (*[]model.TopOperationsItem, *model.ApiError)
|
||||
GetUsage(ctx context.Context, query *model.GetUsageParams) (*[]model.UsageItem, error)
|
||||
@@ -103,6 +103,8 @@ type Reader interface {
|
||||
CheckClickHouse(ctx context.Context) error
|
||||
|
||||
GetMetricMetadata(context.Context, string, string) (*v3.MetricMetadataResponse, error)
|
||||
|
||||
GetMinAndMaxTimestampForTraceID(ctx context.Context, traceID []string) (int64, int64, error)
|
||||
}
|
||||
|
||||
type Querier interface {
|
||||
|
||||
@@ -638,6 +638,12 @@ type AlertsInfo struct {
|
||||
LogsBasedAlerts int `json:"logsBasedAlerts"`
|
||||
MetricBasedAlerts int `json:"metricBasedAlerts"`
|
||||
TracesBasedAlerts int `json:"tracesBasedAlerts"`
|
||||
SlackChannels int `json:"slackChannels"`
|
||||
WebHookChannels int `json:"webHookChannels"`
|
||||
PagerDutyChannels int `json:"pagerDutyChannels"`
|
||||
OpsGenieChannels int `json:"opsGenieChannels"`
|
||||
EmailChannels int `json:"emailChannels"`
|
||||
MSTeamsChannels int `json:"microsoftTeamsChannels"`
|
||||
}
|
||||
|
||||
type SavedViewsInfo struct {
|
||||
|
||||
@@ -907,7 +907,8 @@ const (
|
||||
FilterOperatorNotContains FilterOperator = "ncontains"
|
||||
FilterOperatorRegex FilterOperator = "regex"
|
||||
FilterOperatorNotRegex FilterOperator = "nregex"
|
||||
// (I)LIKE is faster than REGEX and supports index
|
||||
// (I)LIKE is faster than REGEX
|
||||
// ilike doesn't support index so internally we use lower(body) like for query
|
||||
FilterOperatorLike FilterOperator = "like"
|
||||
FilterOperatorNotLike FilterOperator = "nlike"
|
||||
|
||||
|
||||
@@ -293,6 +293,22 @@ func createTelemetry() {
|
||||
if err == nil {
|
||||
channels, err := telemetry.reader.GetChannels()
|
||||
if err == nil {
|
||||
for _, channel := range *channels {
|
||||
switch channel.Type {
|
||||
case "slack":
|
||||
alertsInfo.SlackChannels++
|
||||
case "webhook":
|
||||
alertsInfo.WebHookChannels++
|
||||
case "pagerduty":
|
||||
alertsInfo.PagerDutyChannels++
|
||||
case "opsgenie":
|
||||
alertsInfo.OpsGenieChannels++
|
||||
case "email":
|
||||
alertsInfo.EmailChannels++
|
||||
case "msteams":
|
||||
alertsInfo.MSTeamsChannels++
|
||||
}
|
||||
}
|
||||
savedViewsInfo, err := telemetry.reader.GetSavedViewsInfo(ctx)
|
||||
if err == nil {
|
||||
dashboardsAlertsData := map[string]interface{}{
|
||||
@@ -309,6 +325,12 @@ func createTelemetry() {
|
||||
"totalSavedViews": savedViewsInfo.TotalSavedViews,
|
||||
"logsSavedViews": savedViewsInfo.LogsSavedViews,
|
||||
"tracesSavedViews": savedViewsInfo.TracesSavedViews,
|
||||
"slackChannels": alertsInfo.SlackChannels,
|
||||
"webHookChannels": alertsInfo.WebHookChannels,
|
||||
"pagerDutyChannels": alertsInfo.PagerDutyChannels,
|
||||
"opsGenieChannels": alertsInfo.OpsGenieChannels,
|
||||
"emailChannels": alertsInfo.EmailChannels,
|
||||
"msteamsChannels": alertsInfo.MSTeamsChannels,
|
||||
}
|
||||
// send event only if there are dashboards or alerts or channels
|
||||
if (dashboardsInfo.TotalDashboards > 0 || alertsInfo.TotalAlerts > 0 || len(*channels) > 0 || savedViewsInfo.TotalSavedViews > 0) && apiErr == nil {
|
||||
|
||||
@@ -192,7 +192,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.3}
|
||||
container_name: otel-migrator
|
||||
command:
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
@@ -205,7 +205,7 @@ services:
|
||||
# condition: service_healthy
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:0.102.2
|
||||
image: signoz/signoz-otel-collector:0.102.3
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user