Merge pull request #4690 from SigNoz/release/v0.41.0

Release/v0.41.0
This commit is contained in:
Prashant Shahi 2024-03-14 00:50:36 +05:30 committed by GitHub
commit d0feff00a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
341 changed files with 16113 additions and 2015 deletions

View File

@ -133,7 +133,7 @@ services:
# - ./data/clickhouse-3/:/var/lib/clickhouse/
alertmanager:
image: signoz/alertmanager:0.23.4
image: signoz/alertmanager:0.23.5
volumes:
- ./data/alertmanager:/data
command:
@ -146,7 +146,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.40.0
image: signoz/query-service:0.41.0
command:
[
"-config=/root/config/prometheus.yml",
@ -186,7 +186,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:0.40.0
image: signoz/frontend:0.41.0
deploy:
restart_policy:
condition: on-failure
@ -199,7 +199,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:0.88.14
image: signoz/signoz-otel-collector:0.88.15
command:
[
"--config=/etc/otel-collector-config.yaml",
@ -237,7 +237,7 @@ services:
- query-service
otel-collector-migrator:
image: signoz/signoz-schema-migrator:0.88.14
image: signoz/signoz-schema-migrator:0.88.15
deploy:
restart_policy:
condition: on-failure

View File

@ -54,7 +54,7 @@ services:
alertmanager:
container_name: signoz-alertmanager
image: signoz/alertmanager:0.23.4
image: signoz/alertmanager:0.23.5
volumes:
- ./data/alertmanager:/data
depends_on:
@ -66,7 +66,7 @@ services:
- --storage.path=/data
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.14}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.15}
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.88.14
image: signoz/signoz-otel-collector:0.88.15
command:
[
"--config=/etc/otel-collector-config.yaml",

View File

@ -149,7 +149,7 @@ services:
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
alertmanager:
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.4}
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.5}
container_name: signoz-alertmanager
volumes:
- ./data/alertmanager:/data
@ -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.40.0}
image: signoz/query-service:${DOCKER_TAG:-0.41.0}
container_name: signoz-query-service
command:
[
@ -203,7 +203,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.40.0}
image: signoz/frontend:${DOCKER_TAG:-0.41.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.88.14}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.15}
container_name: otel-migrator
command:
- "--dsn=tcp://clickhouse:9000"
@ -229,7 +229,7 @@ services:
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.14}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.15}
container_name: signoz-otel-collector
command:
[

View File

@ -10,6 +10,7 @@ import (
"go.signoz.io/signoz/ee/query-service/license"
"go.signoz.io/signoz/ee/query-service/usage"
baseapp "go.signoz.io/signoz/pkg/query-service/app"
"go.signoz.io/signoz/pkg/query-service/app/integrations"
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
"go.signoz.io/signoz/pkg/query-service/cache"
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
@ -31,6 +32,7 @@ type APIHandlerOptions struct {
UsageManager *usage.Manager
FeatureFlags baseint.FeatureLookup
LicenseManager *license.Manager
IntegrationsController *integrations.Controller
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
Cache cache.Cache
// Querier Influx Interval
@ -56,6 +58,7 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
AppDao: opts.AppDao,
RuleManager: opts.RulesManager,
FeatureFlags: opts.FeatureFlags,
IntegrationsController: opts.IntegrationsController,
LogsParsingPipelineController: opts.LogsParsingPipelineController,
Cache: opts.Cache,
FluxInterval: opts.FluxInterval,

View File

@ -12,6 +12,20 @@ import (
"go.uber.org/zap"
)
type DayWiseBreakdown struct {
Type string `json:"type"`
Breakdown []DayWiseData `json:"breakdown"`
}
type DayWiseData struct {
Timestamp int64 `json:"timestamp"`
Count float64 `json:"count"`
Size float64 `json:"size"`
UnitPrice float64 `json:"unitPrice"`
Quantity float64 `json:"quantity"`
Total float64 `json:"total"`
}
type tierBreakdown struct {
UnitPrice float64 `json:"unitPrice"`
Quantity float64 `json:"quantity"`
@ -21,9 +35,10 @@ type tierBreakdown struct {
}
type usageResponse struct {
Type string `json:"type"`
Unit string `json:"unit"`
Tiers []tierBreakdown `json:"tiers"`
Type string `json:"type"`
Unit string `json:"unit"`
Tiers []tierBreakdown `json:"tiers"`
DayWiseBreakdown DayWiseBreakdown `json:"dayWiseBreakdown"`
}
type details struct {

View File

@ -35,6 +35,7 @@ import (
baseapp "go.signoz.io/signoz/pkg/query-service/app"
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
baseexplorer "go.signoz.io/signoz/pkg/query-service/app/explorer"
"go.signoz.io/signoz/pkg/query-service/app/integrations"
"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"
@ -171,13 +172,22 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
// initiate opamp
_, err = opAmpModel.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH)
_, err = opAmpModel.InitDB(localDB)
if err != nil {
return nil, err
}
integrationsController, err := integrations.NewController(localDB)
if err != nil {
return nil, fmt.Errorf(
"couldn't create integrations controller: %w", err,
)
}
// ingestion pipelines manager
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(localDB, "sqlite")
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(
localDB, "sqlite", integrationsController.GetPipelinesForInstalledIntegrations,
)
if err != nil {
return nil, err
}
@ -233,6 +243,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
UsageManager: usageManager,
FeatureFlags: lm,
LicenseManager: lm,
IntegrationsController: integrationsController,
LogsParsingPipelineController: logParsingPipelineController,
Cache: c,
FluxInterval: fluxInterval,
@ -278,6 +289,7 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
r := mux.NewRouter()
r.Use(baseapp.LogCommentEnricher)
r.Use(setTimeoutMiddleware)
r.Use(s.analyticsMiddleware)
r.Use(loggingMiddlewarePrivate)
@ -310,6 +322,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
}
am := baseapp.NewAuthMiddleware(getUserFromRequest)
r.Use(baseapp.LogCommentEnricher)
r.Use(setTimeoutMiddleware)
r.Use(s.analyticsMiddleware)
r.Use(loggingMiddleware)
@ -317,6 +330,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
apiHandler.RegisterRoutes(r, am)
apiHandler.RegisterMetricsRoutes(r, am)
apiHandler.RegisterLogsRoutes(r, am)
apiHandler.RegisterIntegrationRoutes(r, am)
apiHandler.RegisterQueryRangeV3Routes(r, am)
apiHandler.RegisterQueryRangeV4Routes(r, am)
@ -412,7 +426,7 @@ func extractQueryRangeV3Data(path string, r *http.Request) (map[string]interface
data["queryType"] = postData.CompositeQuery.QueryType
data["panelType"] = postData.CompositeQuery.PanelType
signozLogsUsed, signozMetricsUsed = telemetry.GetInstance().CheckSigNozSignals(postData)
signozLogsUsed, signozMetricsUsed, _ = telemetry.GetInstance().CheckSigNozSignals(postData)
}
}

View File

@ -90,6 +90,13 @@ var BasicPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelEmail,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelMsTeams,
Active: false,
@ -177,6 +184,13 @@ var ProPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelEmail,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelMsTeams,
Active: true,
@ -264,6 +278,13 @@ var EnterprisePlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelEmail,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelMsTeams,
Active: true,
@ -279,17 +300,17 @@ var EnterprisePlan = basemodel.FeatureSet{
Route: "",
},
basemodel.Feature{
Name: Onboarding,
Active: true,
Usage: 0,
Name: Onboarding,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
Route: "",
},
basemodel.Feature{
Name: ChatSupport,
Active: true,
Usage: 0,
Name: ChatSupport,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
Route: "",
},
}

View File

@ -107,6 +107,7 @@
"react-virtuoso": "4.0.3",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"rehype-raw": "7.0.0",
"stream": "^0.0.2",
"style-loader": "1.3.0",
"styled-components": "^5.3.11",
@ -203,6 +204,7 @@
"jest-styled-components": "^7.0.8",
"lint-staged": "^12.5.0",
"msw": "1.3.2",
"npm-run-all": "latest",
"portfinder-sync": "^0.0.2",
"prettier": "2.2.1",
"raw-loader": "4.0.2",
@ -216,8 +218,7 @@
"ts-node": "^10.2.1",
"typescript-plugin-css-modules": "5.0.1",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.9.2",
"npm-run-all": "latest"
"webpack-cli": "^4.9.2"
},
"lint-staged": {
"*.(js|jsx|ts|tsx)": [

View File

@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M23.06 17.526c-1.281.668-7.916 3.396-9.328 4.132-1.413.736-2.198.73-3.314.196C9.303 21.32 2.242 18.468.97 17.86c-.636-.303-.97-.56-.97-.802v-2.426s9.192-2.001 10.676-2.534c1.484-.532 1.999-.551 3.262-.089 1.263.463 8.814 1.826 10.062 2.283v2.391c0 .24-.288.503-.94.843z" fill="#912626"/><path d="M23.06 15.114c-1.281.668-7.916 3.396-9.329 4.132-1.412.737-2.197.73-3.313.196C9.302 18.91 2.242 16.056.97 15.45c-1.272-.608-1.298-1.027-.049-1.516 1.25-.49 8.271-3.244 9.755-3.776 1.484-.533 1.999-.552 3.262-.09 1.263.463 7.858 3.088 9.106 3.546 1.248.457 1.296.834.015 1.501z" fill="#C6302B"/><path d="M23.06 13.6c-1.281.668-7.916 3.396-9.328 4.133-1.413.736-2.198.73-3.314.196S2.242 14.543.97 13.935c-.636-.304-.97-.56-.97-.802v-2.426s9.192-2.001 10.676-2.534c1.484-.532 1.999-.551 3.262-.089C15.2 8.547 22.752 9.91 24 10.366v2.392c0 .24-.288.503-.94.843z" fill="#912626"/><path d="M23.06 11.19c-1.281.667-7.916 3.395-9.329 4.131-1.412.737-2.197.73-3.313.196-1.116-.533-8.176-3.386-9.448-3.993-1.272-.608-1.298-1.027-.049-1.516 1.25-.49 8.271-3.244 9.755-3.776 1.484-.533 1.999-.552 3.262-.09 1.263.463 7.858 3.088 9.106 3.545 1.248.458 1.296.835.015 1.502z" fill="#C6302B"/><path d="M23.06 9.53c-1.281.668-7.916 3.396-9.328 4.132-1.413.737-2.198.73-3.314.196-1.116-.533-8.176-3.386-9.448-3.993C.334 9.56 0 9.305 0 9.062V6.636s9.192-2 10.676-2.533c1.484-.533 1.999-.552 3.262-.09C15.2 4.477 22.752 5.84 24 6.297v2.392c0 .24-.288.502-.94.842z" fill="#912626"/><path d="M23.06 7.118c-1.281.668-7.916 3.396-9.329 4.132-1.412.737-2.197.73-3.313.196C9.303 10.913 2.242 8.061.97 7.453-.302 6.845-.328 6.427.921 5.937c1.25-.489 8.271-3.244 9.755-3.776 1.484-.532 1.999-.552 3.262-.089 1.263.463 7.858 3.088 9.106 3.545 1.248.457 1.296.834.015 1.501z" fill="#C6302B"/><path d="M14.933 4.758l-2.064.215-.462 1.111-.746-1.24L9.28 4.63l1.778-.641-.534-.985 1.665.651 1.569-.513-.424 1.017 1.6.6zm-2.649 5.393l-3.85-1.597 5.517-.847-1.667 2.444zM6.945 5.376c1.63 0 2.95.512 2.95 1.143 0 .632-1.32 1.144-2.95 1.144-1.629 0-2.95-.512-2.95-1.144 0-.63 1.321-1.143 2.95-1.143z" fill="#fff"/><path d="M17.371 5.062l3.266 1.29-3.263 1.29-.003-2.58z" fill="#621B1C"/><path d="M13.758 6.492l3.613-1.43.003 2.58-.354.139-3.262-1.29z" fill="#9A2928"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -111,5 +111,7 @@
"exceptions_based_alert": "Exceptions-based Alert",
"exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.",
"field_unit": "Threshold unit",
"text_alert_on_absent": "Send a notification if data is missing for",
"text_for": "minutes",
"selected_query_placeholder": "Select query"
}

View File

@ -25,5 +25,5 @@
"dashboard_unsave_changes": "There are unsaved changes in the Query builder, please stage and run the query or the changes will be lost. Press OK to discard.",
"dashboard_save_changes": "Your graph built with {{queryTag}} query will be saved. Press OK to confirm.",
"your_graph_build_with": "Your graph built with",
"dashboar_ok_confirm": "query will be saved. Press OK to confirm."
"dashboard_ok_confirm": "query will be saved. Press OK to confirm."
}

View File

@ -111,5 +111,7 @@
"exceptions_based_alert": "Exceptions-based Alert",
"exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.",
"field_unit": "Threshold unit",
"text_alert_on_absent": "Send a notification if data is missing for",
"text_for": "minutes",
"selected_query_placeholder": "Select query"
}

View File

@ -23,6 +23,12 @@
"field_opsgenie_api_key": "API Key",
"field_opsgenie_description": "Description",
"placeholder_opsgenie_description": "Description",
"help_email_to": "Email address(es) to send alerts to (comma separated)",
"field_email_to": "To",
"placeholder_email_to": "To",
"help_email_html": "Send email in html format",
"field_email_html": "Email body template",
"placeholder_email_html": "Email body template",
"field_webhook_username": "User Name (optional)",
"field_webhook_password": "Password (optional)",
"field_pager_routing_key": "Routing Key",

View File

@ -28,5 +28,5 @@
"dashboard_unsave_changes": "There are unsaved changes in the Query builder, please stage and run the query or the changes will be lost. Press OK to discard.",
"dashboard_save_changes": "Your graph built with {{queryTag}} query will be saved. Press OK to confirm.",
"your_graph_build_with": "Your graph built with",
"dashboar_ok_confirm": "query will be saved. Press OK to confirm."
"dashboard_ok_confirm": "query will be saved. Press OK to confirm."
}

View File

@ -4,6 +4,10 @@
"SERVICE_METRICS": "SigNoz | Service Metrics",
"SERVICE_MAP": "SigNoz | Service Map",
"GET_STARTED": "SigNoz | Get Started",
"GET_STARTED_APPLICATION_MONITORING": "SigNoz | Get Started | APM",
"GET_STARTED_LOGS_MANAGEMENT": "SigNoz | Get Started | Logs",
"GET_STARTED_INFRASTRUCTURE_MONITORING": "SigNoz | Get Started | Infrastructure",
"GET_STARTED_AWS_MONITORING": "SigNoz | Get Started | AWS",
"TRACE": "SigNoz | Trace",
"TRACE_DETAIL": "SigNoz | Trace Detail",
"TRACES_EXPLORER": "SigNoz | Traces Explorer",
@ -40,8 +44,9 @@
"LIST_LICENSES": "SigNoz | List of Licenses",
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
"SUPPORT": "SigNoz | Support",
"LOGS_SAVE_VIEWS": "SigNoz | Logs Save Views",
"TRACES_SAVE_VIEWS": "SigNoz | Traces Save Views",
"LOGS_SAVE_VIEWS": "SigNoz | Logs Saved Views",
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
"DEFAULT": "Open source Observability Platform | SigNoz",
"SHORTCUTS": "SigNoz | Shortcuts"
"SHORTCUTS": "SigNoz | Shortcuts",
"INTEGRATIONS_INSTALLED": "SigNoz | Integrations"
}

View File

@ -9,7 +9,7 @@ done
# create temporary tsconfig which includes only passed files
str="{
\"extends\": \"./tsconfig.json\",
\"include\": [\"src/types/global.d.ts\",\"src/typings/window.ts\", \"src/typings/chartjs-adapter-date-fns.d.ts\", \"src/typings/environment.ts\" ,\"src/container/OnboardingContainer/typings.d.ts\",$files]
\"include\": [ \"src/typings/**/*.ts\",\"src/**/*.d.ts\", \"./babel.config.js\", \"./jest.config.ts\", \"./.eslintrc.js\",\"./__mocks__\",\"./conf/default.conf\",\"./public\",\"./tests\",\"./playwright.config.ts\",\"./commitlint.config.ts\",\"./webpack.config.js\",\"./webpack.config.prod.js\",\"./jest.setup.ts\",\"./**/*.d.ts\",$files]
}"
echo $str > tsconfig.tmp

View File

@ -190,3 +190,18 @@ export const WorkspaceBlocked = Loadable(
export const ShortcutsPage = Loadable(
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Shortcuts'),
);
export const InstalledIntegrations = Loadable(
() =>
import(
/* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage'
),
);
export const IntegrationsMarketPlace = Loadable(
// eslint-disable-next-line sonarjs/no-identical-functions
() =>
import(
/* webpackChunkName: "IntegrationsMarketPlace" */ 'pages/IntegrationsModulePage'
),
);

View File

@ -1,6 +1,4 @@
import ROUTES from 'constants/routes';
import Shortcuts from 'pages/Shortcuts/Shortcuts';
import WorkspaceBlocked from 'pages/WorkspaceLocked';
import { RouteProps } from 'react-router-dom';
import {
@ -16,6 +14,8 @@ import {
EditRulesPage,
ErrorDetails,
IngestionSettings,
InstalledIntegrations,
IntegrationsMarketPlace,
LicensePage,
ListAllALertsPage,
LiveLogs,
@ -35,6 +35,7 @@ import {
ServiceMetricsPage,
ServicesTablePage,
SettingsPage,
ShortcutsPage,
SignupPage,
SomethingWentWrong,
StatusPage,
@ -45,6 +46,7 @@ import {
TracesSaveViews,
UnAuthorized,
UsageExplorerPage,
WorkspaceBlocked,
} from './pageComponents';
const routes: AppRoutes[] = [
@ -57,7 +59,7 @@ const routes: AppRoutes[] = [
},
{
path: ROUTES.GET_STARTED,
exact: true,
exact: false,
component: Onboarding,
isPrivate: true,
key: 'GET_STARTED',
@ -331,10 +333,24 @@ const routes: AppRoutes[] = [
{
path: ROUTES.SHORTCUTS,
exact: true,
component: Shortcuts,
component: ShortcutsPage,
isPrivate: true,
key: 'SHORTCUTS',
},
{
path: ROUTES.INTEGRATIONS_INSTALLED,
exact: true,
component: InstalledIntegrations,
isPrivate: true,
key: 'INTEGRATIONS_INSTALLED',
},
{
path: ROUTES.INTEGRATIONS_MARKETPLACE,
exact: true,
component: IntegrationsMarketPlace,
isPrivate: true,
key: 'INTEGRATIONS_MARKETPLACE',
},
];
export const SUPPORT_ROUTE: AppRoutes = {
@ -358,6 +374,8 @@ export const oldRoutes = [
'/logs/old-logs-explorer',
'/logs-explorer',
'/logs-explorer/live',
'/logs-save-views',
'/traces-save-views',
'/settings/api-keys',
];
@ -366,6 +384,8 @@ export const oldNewRoutesMapping: Record<string, string> = {
'/logs/old-logs-explorer': '/logs/old-logs-explorer',
'/logs-explorer': '/logs/logs-explorer',
'/logs-explorer/live': '/logs/logs-explorer/live',
'/logs-save-views': '/logs/saved-views',
'/traces-save-views': '/traces/saved-views',
'/settings/api-keys': '/settings/access-tokens',
};

View File

@ -0,0 +1,7 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import { AllIntegrationsProps } from 'types/api/integrations/types';
export const getAllIntegrations = (): Promise<
AxiosResponse<AllIntegrationsProps>
> => axios.get(`/integrations`);

View File

@ -0,0 +1,11 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import {
GetIntegrationPayloadProps,
GetIntegrationProps,
} from 'types/api/integrations/types';
export const getIntegration = (
props: GetIntegrationPayloadProps,
): Promise<AxiosResponse<GetIntegrationProps>> =>
axios.get(`/integrations/${props.integrationId}`);

View File

@ -0,0 +1,11 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import {
GetIntegrationPayloadProps,
GetIntegrationStatusProps,
} from 'types/api/integrations/types';
export const getIntegrationStatus = (
props: GetIntegrationPayloadProps,
): Promise<AxiosResponse<GetIntegrationStatusProps>> =>
axios.get(`/integrations/${props.integrationId}/connection_status`);

View File

@ -0,0 +1,31 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
InstalledIntegrationsSuccessResponse,
InstallIntegrationKeyProps,
} from 'types/api/integrations/types';
const installIntegration = async (
props: InstallIntegrationKeyProps,
): Promise<
SuccessResponse<InstalledIntegrationsSuccessResponse> | ErrorResponse
> => {
try {
const response = await axios.post('/integrations/install', {
...props,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default installIntegration;

View File

@ -0,0 +1,31 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
UninstallIntegrationProps,
UninstallIntegrationSuccessResponse,
} from 'types/api/integrations/types';
const unInstallIntegration = async (
props: UninstallIntegrationProps,
): Promise<
SuccessResponse<UninstallIntegrationSuccessResponse> | ErrorResponse
> => {
try {
const response = await axios.post('/integrations/uninstall', {
...props,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default unInstallIntegration;

View File

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

View File

@ -0,0 +1,34 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/createEmail';
const create = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post('/channels', {
name: props.name,
email_configs: [
{
send_resolved: true,
to: props.to,
html: props.html,
headers: props.headers,
},
],
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default create;

View File

@ -0,0 +1,34 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/editEmail';
const editEmail = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.put(`/channels/${props.id}`, {
name: props.name,
email_configs: [
{
send_resolved: true,
to: props.to,
html: props.html,
headers: props.headers,
},
],
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default editEmail;

View File

@ -0,0 +1,34 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/createEmail';
const testEmail = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post('/testChannel', {
name: props.name,
email_configs: [
{
send_resolved: true,
to: props.to,
html: props.html,
headers: props.headers,
},
],
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default testEmail;

View File

@ -9,7 +9,7 @@ import { ENVIRONMENT } from 'constants/env';
import { LOCALSTORAGE } from 'constants/localStorage';
import store from 'store';
import apiV1, { apiAlertManager, apiV2, apiV3 } from './apiV1';
import apiV1, { apiAlertManager, apiV2, apiV3, apiV4 } from './apiV1';
import { Logout } from './utils';
const interceptorsResponse = (
@ -114,6 +114,7 @@ ApiV2Instance.interceptors.request.use(interceptorsRequestResponse);
export const ApiV3Instance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${apiV3}`,
});
ApiV3Instance.interceptors.response.use(
interceptorsResponse,
interceptorRejected,
@ -121,6 +122,18 @@ ApiV3Instance.interceptors.response.use(
ApiV3Instance.interceptors.request.use(interceptorsRequestResponse);
//
// axios V4
export const ApiV4Instance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${apiV4}`,
});
ApiV4Instance.interceptors.response.use(
interceptorsResponse,
interceptorRejected,
);
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
//
AxiosAlertManagerInstance.interceptors.response.use(
interceptorsResponse,
interceptorRejected,

View File

@ -1,6 +1,7 @@
import { ApiV3Instance as axios } from 'api';
import { ApiV3Instance, ApiV4Instance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MetricRangePayloadV3,
@ -9,10 +10,23 @@ import {
export const getMetricsQueryRange = async (
props: QueryRangePayload,
version: string,
signal: AbortSignal,
): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => {
try {
const response = await axios.post('/query_range', props, { signal });
if (version && version === ENTITY_VERSION_V4) {
const response = await ApiV4Instance.post('/query_range', props, { signal });
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
params: props,
};
}
const response = await ApiV3Instance.post('/query_range', props, { signal });
return {
statusCode: 200,

View File

@ -115,6 +115,9 @@ function CustomTimePicker({
const handleOpenChange = (newOpen: boolean): void => {
setOpen(newOpen);
if (!newOpen) {
setCustomDTPickerVisible?.(false);
}
};
const debouncedHandleInputChange = debounce((inputValue): void => {

View File

@ -1,6 +1,6 @@
import './CustomTimePicker.styles.scss';
import { Button, DatePicker } from 'antd';
import { Button } from 'antd';
import cx from 'classnames';
import ROUTES from 'constants/routes';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
@ -9,12 +9,10 @@ import {
Option,
RelativeDurationSuggestionOptions,
} from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs, { Dayjs } from 'dayjs';
import { Dispatch, SetStateAction, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import RangePickerModal from './RangePickerModal';
interface CustomTimePickerPopoverContentProps {
options: any[];
@ -40,35 +38,12 @@ function CustomTimePickerPopoverContent({
handleGoLive,
selectedTime,
}: CustomTimePickerPopoverContentProps): JSX.Element {
const { RangePicker } = DatePicker;
const { pathname } = useLocation();
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
pathname,
]);
const disabledDate = (current: Dayjs): boolean => {
const currentDay = dayjs(current);
return currentDay.isAfter(dayjs());
};
const onPopoverClose = (visible: boolean): void => {
if (!visible) {
setCustomDTPickerVisible(false);
}
setIsOpen(visible);
};
const onModalOkHandler = (date_time: any): void => {
if (date_time?.[1]) {
onPopoverClose(false);
}
onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER);
};
function getTimeChips(options: Option[]): JSX.Element {
return (
<div className="relative-date-time-section">
@ -105,26 +80,32 @@ function CustomTimePickerPopoverContent({
}}
className={cx(
'date-time-options-btn',
selectedTime === option.value && 'active',
customDateTimeVisible
? option.value === 'custom' && 'active'
: selectedTime === option.value && 'active',
)}
>
{option.label}
</Button>
))}
</div>
<div className="relative-date-time">
<div
className={cx(
'relative-date-time',
selectedTime === 'custom' || customDateTimeVisible
? 'date-picker'
: 'relative-times',
)}
>
{selectedTime === 'custom' || customDateTimeVisible ? (
<RangePicker
disabledDate={disabledDate}
allowClear
onCalendarChange={onModalOkHandler}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(selectedTime === 'custom' && {
defaultValue: [dayjs(minTime / 1000000), dayjs(maxTime / 1000000)],
})}
<RangePickerModal
setCustomDTPickerVisible={setCustomDTPickerVisible}
setIsOpen={setIsOpen}
onCustomDateHandler={onCustomDateHandler}
selectedTime={selectedTime}
/>
) : (
<div>
<div className="relative-times-container">
<div className="time-heading">RELATIVE TIMES</div>
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
</div>

View File

@ -0,0 +1,4 @@
.custom-date-picker {
display: flex;
flex-direction: column;
}

View File

@ -0,0 +1,68 @@
import './RangePickerModal.styles.scss';
import { DatePicker } from 'antd';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import { LexicalContext } from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs, { Dayjs } from 'dayjs';
import { Dispatch, SetStateAction } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
interface RangePickerModalProps {
setCustomDTPickerVisible: Dispatch<SetStateAction<boolean>>;
setIsOpen: Dispatch<SetStateAction<boolean>>;
onCustomDateHandler: (
dateTimeRange: DateTimeRangeType,
lexicalContext?: LexicalContext | undefined,
) => void;
selectedTime: string;
}
function RangePickerModal(props: RangePickerModalProps): JSX.Element {
const {
setCustomDTPickerVisible,
setIsOpen,
onCustomDateHandler,
selectedTime,
} = props;
const { RangePicker } = DatePicker;
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const disabledDate = (current: Dayjs): boolean => {
const currentDay = dayjs(current);
return currentDay.isAfter(dayjs());
};
const onPopoverClose = (visible: boolean): void => {
if (!visible) {
setCustomDTPickerVisible(false);
}
setIsOpen(visible);
};
const onModalOkHandler = (date_time: any): void => {
if (date_time?.[1]) {
onPopoverClose(false);
}
onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER);
};
return (
<div className="custom-date-picker">
<RangePicker
disabledDate={disabledDate}
allowClear
showTime
onOk={onModalOkHandler}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(selectedTime === 'custom' && {
defaultValue: [dayjs(minTime / 1000000), dayjs(maxTime / 1000000)],
})}
/>
</div>
);
}
export default RangePickerModal;

View File

@ -18,6 +18,8 @@
}
.ant-drawer-body {
display: flex;
flex-direction: column;
padding: 16px;
}

View File

@ -9,6 +9,7 @@ import dayjs from 'dayjs';
import dompurify from 'dompurify';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useIsDarkMode } from 'hooks/useDarkMode';
// utils
import { FlatLogData } from 'lib/logs/flatLogData';
import { useCallback, useMemo, useState } from 'react';
@ -19,9 +20,8 @@ import { ILog } from 'types/api/logs/log';
// components
import AddToQueryHOC, { AddToQueryHOCProps } from '../AddToQueryHOC';
import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons';
import LogStateIndicator, {
LogType,
} from '../LogStateIndicator/LogStateIndicator';
import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
import { getLogIndicatorType } from '../LogStateIndicator/utils';
// styles
import {
Container,
@ -114,6 +114,8 @@ function ListLogView({
onClearActiveLog: handleClearActiveContextLog,
} = useActiveLog();
const isDarkMode = useIsDarkMode();
const handlerClearActiveContextLog = useCallback(
(event: React.MouseEvent | React.KeyboardEvent) => {
event.preventDefault();
@ -149,7 +151,7 @@ function ListLogView({
[flattenLogData.timestamp],
);
const logType = logData?.attributes_string?.log_level || LogType.INFO;
const logType = getLogIndicatorType(logData);
const handleMouseEnter = (): void => {
setHasActionButtons(true);
@ -163,6 +165,7 @@ function ListLogView({
<>
<Container
$isActiveLog={isHighlighted}
$isDarkMode={isDarkMode}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleDetailedView}

View File

@ -1,18 +1,24 @@
import { Color } from '@signozhq/design-tokens';
import { Card, Typography } from 'antd';
import styled from 'styled-components';
import { getActiveLogBackground } from 'utils/logs';
export const Container = styled(Card)<{
$isActiveLog: boolean;
$isDarkMode: boolean;
}>`
width: 100% !important;
margin-bottom: 0.3rem;
cursor: pointer;
.ant-card-body {
padding: 0.3rem 0.6rem;
}
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
${({ $isActiveLog, $isDarkMode }): string =>
$isActiveLog
? `background-color: ${
$isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300
} !important`
: ''}
}
`;
export const Text = styled(Typography.Text)`

View File

@ -10,15 +10,27 @@
background-color: transparent;
&.INFO {
background-color: #1d212d;
background-color: var(--bg-slate-400);
}
&.WARNING {
background-color: #ffcd56;
&.WARNING, &.WARN {
background-color: var(--bg-amber-500);
}
&.ERROR {
background-color: #e5484d;
background-color: var(--bg-cherry-500);
}
&.TRACE {
background-color: var(--bg-robin-300);
}
&.DEBUG {
background-color: var(--bg-forest-500);
}
&.FATAL {
background-color: var(--bg-sakura-500);
}
}

View File

@ -0,0 +1,45 @@
import { render } from '@testing-library/react';
import LogStateIndicator from './LogStateIndicator';
describe('LogStateIndicator', () => {
it('renders correctly with default props', () => {
const { container } = render(<LogStateIndicator type="INFO" />);
const indicator = container.firstChild as HTMLElement;
expect(indicator.classList.contains('log-state-indicator')).toBe(true);
expect(indicator.classList.contains('isActive')).toBe(false);
expect(container.querySelector('.line')).toBeTruthy();
expect(container.querySelector('.line')?.classList.contains('INFO')).toBe(
true,
);
});
it('renders correctly when isActive is true', () => {
const { container } = render(<LogStateIndicator type="INFO" isActive />);
const indicator = container.firstChild as HTMLElement;
expect(indicator.classList.contains('isActive')).toBe(true);
});
it('renders correctly with different types', () => {
const { container: containerInfo } = render(
<LogStateIndicator type="INFO" />,
);
expect(containerInfo.querySelector('.line')?.classList.contains('INFO')).toBe(
true,
);
const { container: containerWarning } = render(
<LogStateIndicator type="WARNING" />,
);
expect(
containerWarning.querySelector('.line')?.classList.contains('WARNING'),
).toBe(true);
const { container: containerError } = render(
<LogStateIndicator type="ERROR" />,
);
expect(
containerError.querySelector('.line')?.classList.contains('ERROR'),
).toBe(true);
});
});

View File

@ -2,11 +2,40 @@ import './LogStateIndicator.styles.scss';
import cx from 'classnames';
export const SEVERITY_TEXT_TYPE = {
TRACE: 'TRACE',
TRACE2: 'TRACE2',
TRACE3: 'TRACE3',
TRACE4: 'TRACE4',
DEBUG: 'DEBUG',
DEBUG2: 'DEBUG2',
DEBUG3: 'DEBUG3',
DEBUG4: 'DEBUG4',
INFO: 'INFO',
INFO2: 'INFO2',
INFO3: 'INFO3',
INFO4: 'INFO4',
WARN: 'WARN',
WARN2: 'WARN2',
WARN3: 'WARN3',
WARN4: 'WARN4',
WARNING: 'WARNING',
ERROR: 'ERROR',
ERROR2: 'ERROR2',
ERROR3: 'ERROR3',
ERROR4: 'ERROR4',
FATAL: 'FATAL',
FATAL2: 'FATAL2',
FATAL3: 'FATAL3',
FATAL4: 'FATAL4',
} as const;
export const LogType = {
INFO: 'INFO',
WARNING: 'WARNING',
ERROR: 'ERROR',
};
} as const;
function LogStateIndicator({
type,
isActive,

View File

@ -0,0 +1,89 @@
import { ILog } from 'types/api/logs/log';
import { getLogIndicatorType, getLogIndicatorTypeForTable } from './utils';
describe('getLogIndicatorType', () => {
it('should return severity type for valid log with severityText', () => {
const log = {
date: '2024-02-29T12:34:46Z',
timestamp: 1646115296,
id: '123456',
traceId: '987654',
spanId: '54321',
traceFlags: 0,
severityText: 'INFO',
severityNumber: 2,
body: 'Sample log Message',
resources_string: {},
attributesString: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
severity_text: 'INFO',
};
expect(getLogIndicatorType(log)).toBe('INFO');
});
it('should return log level if severityText is missing', () => {
const log: ILog = {
date: '2024-02-29T12:34:58Z',
timestamp: 1646115296,
id: '123456',
traceId: '987654',
spanId: '54321',
traceFlags: 0,
severityNumber: 2,
body: 'Sample log',
resources_string: {},
attributesString: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
severity_text: 'FATAL',
severityText: '',
};
expect(getLogIndicatorType(log)).toBe('FATAL');
});
});
describe('getLogIndicatorTypeForTable', () => {
it('should return severity type for valid log with severityText', () => {
const log = {
date: '2024-02-29T12:34:56Z',
timestamp: 1646115296,
id: '123456',
traceId: '987654',
spanId: '54321',
traceFlags: 0,
severity_number: 2,
body: 'Sample log message',
resources_string: {},
attributesString: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
severity_text: 'WARN',
};
expect(getLogIndicatorTypeForTable(log)).toBe('WARN');
});
it('should return log level if severityText is missing', () => {
const log = {
date: '2024-02-29T12:34:56Z',
timestamp: 1646115296,
id: '123456',
traceId: '987654',
spanId: '54321',
traceFlags: 0,
severityNumber: 2,
body: 'Sample log message',
resources_string: {},
attributesString: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
log_level: 'INFO',
};
expect(getLogIndicatorTypeForTable(log)).toBe('INFO');
});
});

View File

@ -0,0 +1,57 @@
import { ILog } from 'types/api/logs/log';
import { LogType, SEVERITY_TEXT_TYPE } from './LogStateIndicator';
const getSeverityType = (severityText: string): string => {
switch (severityText) {
case SEVERITY_TEXT_TYPE.TRACE:
case SEVERITY_TEXT_TYPE.TRACE2:
case SEVERITY_TEXT_TYPE.TRACE3:
case SEVERITY_TEXT_TYPE.TRACE4:
return SEVERITY_TEXT_TYPE.TRACE;
case SEVERITY_TEXT_TYPE.DEBUG:
case SEVERITY_TEXT_TYPE.DEBUG2:
case SEVERITY_TEXT_TYPE.DEBUG3:
case SEVERITY_TEXT_TYPE.DEBUG4:
return SEVERITY_TEXT_TYPE.DEBUG;
case SEVERITY_TEXT_TYPE.INFO:
case SEVERITY_TEXT_TYPE.INFO2:
case SEVERITY_TEXT_TYPE.INFO3:
case SEVERITY_TEXT_TYPE.INFO4:
return SEVERITY_TEXT_TYPE.INFO;
case SEVERITY_TEXT_TYPE.WARN:
case SEVERITY_TEXT_TYPE.WARN2:
case SEVERITY_TEXT_TYPE.WARN3:
case SEVERITY_TEXT_TYPE.WARN4:
case SEVERITY_TEXT_TYPE.WARNING:
return SEVERITY_TEXT_TYPE.WARN;
case SEVERITY_TEXT_TYPE.ERROR:
case SEVERITY_TEXT_TYPE.ERROR2:
case SEVERITY_TEXT_TYPE.ERROR3:
case SEVERITY_TEXT_TYPE.ERROR4:
return SEVERITY_TEXT_TYPE.ERROR;
case SEVERITY_TEXT_TYPE.FATAL:
case SEVERITY_TEXT_TYPE.FATAL2:
case SEVERITY_TEXT_TYPE.FATAL3:
case SEVERITY_TEXT_TYPE.FATAL4:
return SEVERITY_TEXT_TYPE.FATAL;
default:
return SEVERITY_TEXT_TYPE.INFO;
}
};
export const getLogIndicatorType = (logData: ILog): string => {
if (logData.severity_text) {
return getSeverityType(logData.severity_text);
}
return logData.attributes_string?.log_level || LogType.INFO;
};
export const getLogIndicatorTypeForTable = (
log: Record<string, unknown>,
): string => {
if (log.severity_text) {
return getSeverityType(log.severity_text as string);
}
return (log.log_level as string) || LogType.INFO;
};

View File

@ -23,9 +23,8 @@ import {
} from 'react';
import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons';
import LogStateIndicator, {
LogType,
} from '../LogStateIndicator/LogStateIndicator';
import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
import { getLogIndicatorType } from '../LogStateIndicator/utils';
// styles
import { RawLogContent, RawLogViewContainer } from './styles';
import { RawLogViewProps } from './types';
@ -64,7 +63,7 @@ function RawLogView({
const severityText = data.severity_text ? `${data.severity_text} |` : '';
const logType = data?.attributes_string?.log_level || LogType.INFO;
const logType = getLogIndicatorType(data);
const updatedSelecedFields = useMemo(
() => selectedFields.filter((e) => e.name !== 'id'),
@ -164,7 +163,11 @@ function RawLogView({
>
<LogStateIndicator
type={logType}
isActive={activeLog?.id === data.id || activeContextLog?.id === data.id}
isActive={
activeLog?.id === data.id ||
activeContextLog?.id === data.id ||
isActiveLog
}
/>
<RawLogContent

View File

@ -30,6 +30,14 @@ export const RawLogViewContainer = styled(Row)<{
$isActiveLog
? getActiveLogBackground($isActiveLog, $isDarkMode)
: getDefaultLogBackground($isReadOnly, $isDarkMode)}
${({ $isHightlightedLog, $isDarkMode }): string =>
$isHightlightedLog
? `background-color: ${
$isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300
};
transition: background-color 2s ease-in;`
: ''}
`;
export const ExpandIconWrapper = styled(Col)`

View File

@ -14,12 +14,12 @@ export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
lineHeight: '18px',
letterSpacing: '-0.07px',
marginBottom: '0px',
minWidth: '10rem',
};
}
export const defaultTableStyle: CSSProperties = {
minWidth: '40rem',
maxWidth: '40rem',
};
export const defaultListViewPanelStyle: CSSProperties = {

View File

@ -23,6 +23,7 @@ export type UseTableViewProps = {
onOpenLogsContext?: (log: ILog) => void;
onClickExpand?: (log: ILog) => void;
activeLog?: ILog | null;
activeLogIndex?: number;
activeContextLog?: ILog | null;
isListViewPanel?: boolean;
} & LogsTableViewProps;

View File

@ -7,12 +7,10 @@ import dayjs from 'dayjs';
import dompurify from 'dompurify';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { FlatLogData } from 'lib/logs/flatLogData';
import { defaultTo } from 'lodash-es';
import { useMemo } from 'react';
import LogStateIndicator, {
LogType,
} from '../LogStateIndicator/LogStateIndicator';
import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
import { getLogIndicatorTypeForTable } from '../LogStateIndicator/utils';
import {
defaultListViewPanelStyle,
defaultTableStyle,
@ -84,7 +82,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
children: (
<div className="table-timestamp">
<LogStateIndicator
type={defaultTo(item.log_level, LogType.INFO) as string}
type={getLogIndicatorTypeForTable(item)}
isActive={
activeLog?.id === item.id || activeContextLog?.id === item.id
}

View File

@ -72,8 +72,6 @@ export default function LogsFormatOptionsMenu({
setAddNewColumn(!addNewColumn);
};
// console.log('optionsMenuConfig', config);
const handleLinesPerRowChange = (maxLinesPerRow: number | null): void => {
if (
maxLinesPerRow &&
@ -221,8 +219,6 @@ export default function LogsFormatOptionsMenu({
className="column-name"
key={value}
onClick={(eve): void => {
console.log('coluimn name', label, value);
eve.stopPropagation();
if (addColumn && addColumn?.onSelect) {

View File

@ -1,10 +1,12 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import ReactMarkdown from 'react-markdown';
import { CodeProps } from 'react-markdown/lib/ast-to-react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { a11yDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import rehypeRaw from 'rehype-raw';
import CodeCopyBtn from './CodeCopyBtn/CodeCopyBtn';
@ -74,6 +76,10 @@ const interpolateMarkdown = (
return interpolatedContent;
};
function CustomTag({ color }: { color: string }): JSX.Element {
return <h1 style={{ color }}>This is custom element</h1>;
}
function MarkdownRenderer({
markdownContent,
variables,
@ -85,12 +91,14 @@ function MarkdownRenderer({
return (
<ReactMarkdown
rehypePlugins={[rehypeRaw as any]}
components={{
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
a: Link,
pre: Pre,
code: Code,
customtag: CustomTag,
}}
>
{interpolatedMarkdown}

View File

@ -13,3 +13,6 @@ export const SIGNOZ_UPGRADE_PLAN_URL =
'https://upgrade.signoz.io/upgrade-from-app';
export const DASHBOARD_TIME_IN_DURATION = 'refreshInterval';
export const DEFAULT_ENTITY_VERSION = 'v3';
export const ENTITY_VERSION_V4 = 'v4';

View File

@ -16,4 +16,5 @@ export enum LOCALSTORAGE {
CHAT_SUPPORT = 'CHAT_SUPPORT',
IS_IDENTIFIED_USER = 'IS_IDENTIFIED_USER',
DASHBOARD_VARIABLES = 'DASHBOARD_VARIABLES',
SHOW_EXPLORER_TOOLBAR = 'SHOW_EXPLORER_TOOLBAR',
}

View File

@ -36,6 +36,11 @@ import { v4 as uuid } from 'uuid';
import {
logsAggregateOperatorOptions,
metricAggregateOperatorOptions,
metricsGaugeAggregateOperatorOptions,
metricsGaugeSpaceAggregateOperatorOptions,
metricsHistogramSpaceAggregateOperatorOptions,
metricsSumAggregateOperatorOptions,
metricsSumSpaceAggregateOperatorOptions,
tracesAggregateOperatorOptions,
} from './queryBuilderOperators';
@ -74,6 +79,18 @@ export const mapOfOperators = {
traces: tracesAggregateOperatorOptions,
};
export const metricsOperatorsByType = {
Sum: metricsSumAggregateOperatorOptions,
Gauge: metricsGaugeAggregateOperatorOptions,
};
export const metricsSpaceAggregationOperatorsByType = {
Sum: metricsSumSpaceAggregateOperatorOptions,
Gauge: metricsGaugeSpaceAggregateOperatorOptions,
Histogram: metricsHistogramSpaceAggregateOperatorOptions,
ExponentialHistogram: metricsHistogramSpaceAggregateOperatorOptions,
};
export const mapOfQueryFilters: Record<DataSource, QueryAdditionalFilter[]> = {
metrics: [
// eslint-disable-next-line sonarjs/no-duplicate-string
@ -148,6 +165,9 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
aggregateOperator: MetricAggregateOperator.COUNT,
aggregateAttribute: initialAutocompleteData,
timeAggregation: MetricAggregateOperator.RATE,
spaceAggregation: MetricAggregateOperator.SUM,
functions: [],
filters: { items: [], op: 'AND' },
expression: createNewBuilderItemName({
existNames: [],
@ -160,7 +180,7 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
orderBy: [],
groupBy: [],
legend: '',
reduceTo: 'sum',
reduceTo: 'avg',
};
const initialQueryBuilderFormLogsValues: IBuilderQuery = {
@ -268,6 +288,14 @@ export enum PANEL_TYPES {
EMPTY_WIDGET = 'EMPTY_WIDGET',
}
// eslint-disable-next-line @typescript-eslint/naming-convention
export enum ATTRIBUTE_TYPES {
SUM = 'Sum',
GAUGE = 'Gauge',
HISTOGRAM = 'Histogram',
EXPONENTIAL_HISTOGRAM = 'ExponentialHistogram',
}
export type IQueryBuilderState = 'search';
export const QUERY_BUILDER_SEARCH_VALUES = {

View File

@ -302,3 +302,126 @@ export const logsAggregateOperatorOptions: SelectOption<string, string>[] = [
label: 'Rate_max',
},
];
export const metricsSumAggregateOperatorOptions: SelectOption<
string,
string
>[] = [
{
value: MetricAggregateOperator.RATE,
label: 'Rate',
},
{
value: MetricAggregateOperator.INCREASE,
label: 'Increase',
},
];
export const metricsGaugeAggregateOperatorOptions: SelectOption<
string,
string
>[] = [
{
value: MetricAggregateOperator.LATEST,
label: 'Latest',
},
{
value: MetricAggregateOperator.SUM,
label: 'Sum',
},
{
value: MetricAggregateOperator.AVG,
label: 'Avg',
},
{
value: MetricAggregateOperator.MIN,
label: 'Min',
},
{
value: MetricAggregateOperator.MAX,
label: 'Max',
},
{
value: MetricAggregateOperator.COUNT,
label: 'Count',
},
{
value: MetricAggregateOperator.COUNT_DISTINCT,
label: 'Count Distinct',
},
];
export const metricsSumSpaceAggregateOperatorOptions: SelectOption<
string,
string
>[] = [
{
value: MetricAggregateOperator.SUM,
label: 'Sum',
},
{
value: MetricAggregateOperator.AVG,
label: 'Avg',
},
{
value: MetricAggregateOperator.MIN,
label: 'Min',
},
{
value: MetricAggregateOperator.MAX,
label: 'Max',
},
];
export const metricsGaugeSpaceAggregateOperatorOptions: SelectOption<
string,
string
>[] = [
{
value: MetricAggregateOperator.SUM,
label: 'Sum',
},
{
value: MetricAggregateOperator.AVG,
label: 'Avg',
},
{
value: MetricAggregateOperator.MIN,
label: 'Min',
},
{
value: MetricAggregateOperator.MAX,
label: 'Max',
},
];
export const metricsHistogramSpaceAggregateOperatorOptions: SelectOption<
string,
string
>[] = [
{
value: MetricAggregateOperator.P50,
label: 'P50',
},
{
value: MetricAggregateOperator.P75,
label: 'P75',
},
{
value: MetricAggregateOperator.P90,
label: 'P90',
},
{
value: MetricAggregateOperator.P95,
label: 'P95',
},
{
value: MetricAggregateOperator.P99,
label: 'P99',
},
];
export const metricsEmptyTimeAggregateOperatorOptions: SelectOption<
string,
string
>[] = [];

View File

@ -0,0 +1,137 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { QueryFunctionsTypes } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select';
export const queryFunctionOptions: SelectOption<string, string>[] = [
{
value: QueryFunctionsTypes.CUTOFF_MIN,
label: 'Cut Off Min',
},
{
value: QueryFunctionsTypes.CUTOFF_MAX,
label: 'Cut Off Max',
},
{
value: QueryFunctionsTypes.CLAMP_MIN,
label: 'Clamp Min',
},
{
value: QueryFunctionsTypes.CLAMP_MAX,
label: 'Clamp Max',
},
{
value: QueryFunctionsTypes.ABSOLUTE,
label: 'Absolute',
},
{
value: QueryFunctionsTypes.LOG_2,
label: 'Log2',
},
{
value: QueryFunctionsTypes.LOG_10,
label: 'Log10',
},
{
value: QueryFunctionsTypes.CUMULATIVE_SUM,
label: 'Cumulative Sum',
},
{
value: QueryFunctionsTypes.EWMA_3,
label: 'EWMA 3',
},
{
value: QueryFunctionsTypes.EWMA_5,
label: 'EWMA 5',
},
{
value: QueryFunctionsTypes.EWMA_7,
label: 'EWMA 7',
},
{
value: QueryFunctionsTypes.MEDIAN_3,
label: 'Median 3',
},
{
value: QueryFunctionsTypes.MEDIAN_5,
label: 'Median 5',
},
{
value: QueryFunctionsTypes.MEDIAN_7,
label: 'Median 7',
},
{
value: QueryFunctionsTypes.TIME_SHIFT,
label: 'Time Shift',
},
];
interface QueryFunctionConfigType {
[key: string]: {
showInput: boolean;
inputType?: string;
placeholder?: string;
};
}
export const queryFunctionsTypesConfig: QueryFunctionConfigType = {
cutOffMin: {
showInput: true,
inputType: 'text',
placeholder: 'Threshold',
},
cutOffMax: {
showInput: true,
inputType: 'text',
placeholder: 'Threshold',
},
clampMin: {
showInput: true,
inputType: 'text',
placeholder: 'Threshold',
},
clampMax: {
showInput: true,
inputType: 'text',
placeholder: 'Threshold',
},
absolute: {
showInput: false,
},
log2: {
showInput: false,
},
log10: {
showInput: false,
},
cumSum: {
showInput: false,
},
ewma3: {
showInput: true,
inputType: 'text',
placeholder: 'Alpha',
},
ewma5: {
showInput: true,
inputType: 'text',
placeholder: 'Alpha',
},
ewma7: {
showInput: true,
inputType: 'text',
placeholder: 'Alpha',
},
median3: {
showInput: false,
},
median5: {
showInput: false,
},
median7: {
showInput: false,
},
timeShift: {
showInput: true,
inputType: 'text',
},
};

View File

@ -7,6 +7,11 @@ const ROUTES = {
TRACE_DETAIL: '/trace/:id',
TRACES_EXPLORER: '/traces-explorer',
GET_STARTED: '/get-started',
GET_STARTED_APPLICATION_MONITORING: '/get-started/application-monitoring',
GET_STARTED_LOGS_MANAGEMENT: '/get-started/logs-management',
GET_STARTED_INFRASTRUCTURE_MONITORING:
'/get-started/infrastructure-monitoring',
GET_STARTED_AWS_MONITORING: '/get-started/aws-monitoring',
USAGE_EXPLORER: '/usage-explorer',
APPLICATION: '/services',
ALL_DASHBOARD: '/dashboard',
@ -42,10 +47,13 @@ const ROUTES = {
TRACE_EXPLORER: '/trace-explorer',
BILLING: '/billing',
SUPPORT: '/support',
LOGS_SAVE_VIEWS: '/logs-save-views',
TRACES_SAVE_VIEWS: '/traces-save-views',
LOGS_SAVE_VIEWS: '/logs/saved-views',
TRACES_SAVE_VIEWS: '/traces/saved-views',
WORKSPACE_LOCKED: '/workspace-locked',
SHORTCUTS: '/shortcuts',
INTEGRATIONS_BASE: '/integrations',
INTEGRATIONS_INSTALLED: '/integrations/installed',
INTEGRATIONS_MARKETPLACE: '/integrations/marketplace',
} as const;
export default ROUTES;

View File

@ -0,0 +1,17 @@
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
const userOS = getUserOperatingSystem();
export const DashboardShortcuts = {
SaveChanges: 's+meta',
DiscardChanges: 'd+meta',
};
export const DashboardShortcutsName = {
SaveChanges: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'}+s`,
};
export const DashboardShortcutsDescription = {
SaveChanges: 'Save Changes',
DiscardChanges: 'Discard Changes',
};

View File

@ -0,0 +1,17 @@
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
const userOS = getUserOperatingSystem();
export const QBShortcuts = {
StageAndRunQuery: 'enter+meta',
};
export const QBShortcutsName = {
StageAndRunQuery: `${
userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'
}+enter`,
};
export const QBShortcutsDescription = {
StageAndRunQuery: 'Stage and Run the query',
};

View File

@ -231,7 +231,12 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const routeKey = useMemo(() => getRouteKey(pathname), [pathname]);
const pageTitle = t(routeKey);
const renderFullScreen =
pathname === ROUTES.GET_STARTED || pathname === ROUTES.WORKSPACE_LOCKED;
pathname === ROUTES.GET_STARTED ||
pathname === ROUTES.WORKSPACE_LOCKED ||
pathname === ROUTES.GET_STARTED_APPLICATION_MONITORING ||
pathname === ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING ||
pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT ||
pathname === ROUTES.GET_STARTED_AWS_MONITORING;
const [showTrialExpiryBanner, setShowTrialExpiryBanner] = useState(false);

View File

@ -1,13 +1,29 @@
.billing-container {
padding: 16px 0;
width: 100%;
padding-top: 36px;
width: 65%;
.billing-summary {
margin: 24px 8px;
}
.billing-details {
margin: 36px 8px;
margin: 24px 0px;
.ant-table-title {
color: var(--bg-vanilla-400);
background-color: rgb(27, 28, 32);
}
.ant-table-cell {
background-color: var(--bg-ink-400);
border-color: var(--bg-slate-500);
}
.ant-table-tbody {
td {
border-color: var(--bg-slate-500);
}
}
}
.upgrade-plan-benefits {
@ -24,6 +40,15 @@
}
}
}
.empty-graph-card {
.ant-card-body {
height: 40vh;
display: flex;
justify-content: center;
align-items: center;
}
}
}
.ant-skeleton.ant-skeleton-element.ant-skeleton-active {
@ -34,3 +59,20 @@
.ant-skeleton.ant-skeleton-element .ant-skeleton-input {
min-width: 100% !important;
}
.lightMode {
.billing-container {
.billing-details {
.ant-table-cell {
background: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-200);
}
.ant-table-tbody {
td {
border-color: var(--bg-vanilla-200);
}
}
}
}
}

View File

@ -12,13 +12,36 @@ import BillingContainer from './BillingContainer';
const lisenceUrl = 'http://localhost/api/v2/licenses';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
window.ResizeObserver =
window.ResizeObserver ||
jest.fn().mockImplementation(() => ({
disconnect: jest.fn(),
observe: jest.fn(),
unobserve: jest.fn(),
}));
describe('BillingContainer', () => {
test('Component should render', async () => {
act(() => {
render(<BillingContainer />);
});
const unit = screen.getAllByText(/unit/i);
expect(unit[1]).toBeInTheDocument();
const dataInjection = screen.getByRole('columnheader', {
name: /data ingested/i,
});
@ -32,24 +55,15 @@ describe('BillingContainer', () => {
});
expect(cost).toBeInTheDocument();
const total = screen.getByRole('cell', {
name: /total/i,
});
expect(total).toBeInTheDocument();
const manageBilling = screen.getByRole('button', {
name: /manage billing/i,
});
expect(manageBilling).toBeInTheDocument();
const dollar = screen.getByRole('cell', {
name: /\$0/i,
});
const dollar = screen.getByText(/\$0/i);
expect(dollar).toBeInTheDocument();
const currentBill = screen.getByRole('heading', {
name: /current bill total/i,
});
const currentBill = screen.getByText('Billing');
expect(currentBill).toBeInTheDocument();
});
@ -61,9 +75,7 @@ describe('BillingContainer', () => {
const freeTrailText = await screen.findByText('Free Trial');
expect(freeTrailText).toBeInTheDocument();
const currentBill = await screen.findByRole('heading', {
name: /current bill total/i,
});
const currentBill = screen.getByText('Billing');
expect(currentBill).toBeInTheDocument();
const dollar0 = await screen.findByText(/\$0/i);
@ -102,9 +114,7 @@ describe('BillingContainer', () => {
render(<BillingContainer />);
});
const currentBill = await screen.findByRole('heading', {
name: /current bill total/i,
});
const currentBill = screen.getByText('Billing');
expect(currentBill).toBeInTheDocument();
const dollar0 = await screen.findByText(/\$0/i);
@ -137,45 +147,30 @@ describe('BillingContainer', () => {
res(ctx.status(200), ctx.json(notOfTrailResponse)),
),
);
render(<BillingContainer />);
const { findByText } = render(<BillingContainer />);
const billingPeriodText = `Your current billing period is from ${getFormattedDate(
billingSuccessResponse.data.billingPeriodStart,
)} to ${getFormattedDate(billingSuccessResponse.data.billingPeriodEnd)}`;
const billingPeriod = await screen.findByRole('heading', {
name: new RegExp(billingPeriodText, 'i'),
});
const billingPeriod = await findByText(billingPeriodText);
expect(billingPeriod).toBeInTheDocument();
const currentBill = await screen.findByRole('heading', {
name: /current bill total/i,
});
const currentBill = screen.getByText('Billing');
expect(currentBill).toBeInTheDocument();
const dollar0 = await screen.findAllByText(/\$1278.3/i);
expect(dollar0[0]).toBeInTheDocument();
expect(dollar0.length).toBe(2);
const dollar0 = await screen.findByText(/\$1,278.3/i);
expect(dollar0).toBeInTheDocument();
const metricsRow = await screen.findByRole('row', {
name: /metrics Million 4012 0.1 \$ 401.2/i,
name: /metrics 4012 Million 0.1 \$ 401.2/i,
});
expect(metricsRow).toBeInTheDocument();
const logRow = await screen.findByRole('row', {
name: /Logs GB 497 0.4 \$ 198.8/i,
name: /Logs 497 GB 0.4 \$ 198.8/i,
});
expect(logRow).toBeInTheDocument();
const totalBill = await screen.findByRole('cell', {
name: /\$1278/i,
});
expect(totalBill).toBeInTheDocument();
const totalBillRow = await screen.findByRole('row', {
name: /total \$1278/i,
});
expect(totalBillRow).toBeInTheDocument();
});
test('Should render corrent day remaining in billing period', async () => {

View File

@ -2,11 +2,24 @@
import './BillingContainer.styles.scss';
import { CheckCircleOutlined } from '@ant-design/icons';
import { Button, Col, Row, Skeleton, Table, Tag, Typography } from 'antd';
import { Color } from '@signozhq/design-tokens';
import {
Alert,
Button,
Card,
Col,
Flex,
Row,
Skeleton,
Table,
Tag,
Typography,
} from 'antd';
import { ColumnsType } from 'antd/es/table';
import updateCreditCardApi from 'api/billing/checkout';
import getUsage from 'api/billing/getUsage';
import manageCreditCardApi from 'api/billing/manage';
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';
@ -22,8 +35,11 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { License } from 'types/api/licenses/def';
import AppReducer from 'types/reducer/app';
import { isCloudUser } from 'utils/app';
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph';
interface DataType {
key: string;
name: string;
@ -104,12 +120,11 @@ export default function BillingContainer(): JSX.Element {
const daysRemainingStr = 'days remaining in your billing period.';
const [headerText, setHeaderText] = useState('');
const [billAmount, setBillAmount] = useState(0);
const [totalBillAmount, setTotalBillAmount] = useState(0);
const [activeLicense, setActiveLicense] = useState<License | null>(null);
const [daysRemaining, setDaysRemaining] = useState(0);
const [isFreeTrial, setIsFreeTrial] = useState(false);
const [data, setData] = useState<any[]>([]);
const billCurrency = '$';
const [apiResponse, setApiResponse] = useState<any>({});
const { trackEvent } = useAnalytics();
@ -120,10 +135,12 @@ export default function BillingContainer(): JSX.Element {
const handleError = useAxiosError();
const isCloudUserVal = isCloudUser();
const processUsageData = useCallback(
(data: any): void => {
const {
details: { breakdown = [], total, billTotal },
details: { breakdown = [], billTotal },
billingPeriodStart,
billingPeriodEnd,
} = data?.payload || {};
@ -141,8 +158,7 @@ export default function BillingContainer(): JSX.Element {
formattedUsageData.push({
key: `${index}${i}`,
name: i === 0 ? element?.type : '',
unit: element?.unit,
dataIngested: tier.quantity,
dataIngested: `${tier.quantity} ${element?.unit}`,
pricePerUnit: tier.unitPrice,
cost: `$ ${tier.tierCost}`,
});
@ -152,7 +168,6 @@ export default function BillingContainer(): JSX.Element {
}
setData(formattedUsageData);
setTotalBillAmount(total);
if (!licensesData?.payload?.onTrial) {
const remainingDays = getRemainingDays(billingPeriodEnd) - 1;
@ -165,11 +180,13 @@ export default function BillingContainer(): JSX.Element {
setDaysRemaining(remainingDays > 0 ? remainingDays : 0);
setBillAmount(billTotal);
}
setApiResponse(data?.payload || {});
},
[licensesData?.payload?.onTrial],
);
const { isLoading } = useQuery(
const { isLoading, isFetching: isFetchingBillingData } = useQuery(
[REACT_QUERY_KEY.GET_BILLING_USAGE, user?.userId],
{
queryFn: () => getUsage(activeLicense?.key || ''),
@ -208,11 +225,6 @@ export default function BillingContainer(): JSX.Element {
key: 'name',
render: (text): JSX.Element => <div>{text}</div>,
},
{
title: 'Unit',
dataIndex: 'unit',
key: 'unit',
},
{
title: 'Data Ingested',
dataIndex: 'dataIngested',
@ -230,24 +242,6 @@ export default function BillingContainer(): JSX.Element {
},
];
const renderSummary = (): JSX.Element => (
<Table.Summary.Row>
<Table.Summary.Cell index={0}>
<Typography.Title level={3} style={{ fontWeight: 500, margin: ' 0px' }}>
Total
</Typography.Title>
</Table.Summary.Cell>
<Table.Summary.Cell index={1}> &nbsp; </Table.Summary.Cell>
<Table.Summary.Cell index={2}> &nbsp;</Table.Summary.Cell>
<Table.Summary.Cell index={3}> &nbsp; </Table.Summary.Cell>
<Table.Summary.Cell index={4}>
<Typography.Title level={3} style={{ fontWeight: 500, margin: ' 0px' }}>
${totalBillAmount}
</Typography.Title>
</Table.Summary.Cell>
</Table.Summary.Row>
);
const renderTableSkeleton = (): JSX.Element => (
<Table
dataSource={dummyData}
@ -336,78 +330,95 @@ export default function BillingContainer(): JSX.Element {
updateCreditCard,
]);
const BillingUsageGraphCallback = useCallback(
() =>
!isLoading && !isFetchingBillingData ? (
<BillingUsageGraph data={apiResponse} billAmount={billAmount} />
) : (
<Card className="empty-graph-card" bordered={false}>
<Spinner size="large" tip="Loading..." height="35vh" />
</Card>
),
[apiResponse, billAmount, isLoading, isFetchingBillingData],
);
return (
<div className="billing-container">
<Row
justify="space-between"
align="middle"
gutter={[16, 16]}
style={{
margin: 0,
}}
<Flex vertical style={{ marginBottom: 16 }}>
<Typography.Text style={{ fontWeight: 500, fontSize: 18 }}>
Billing
</Typography.Text>
<Typography.Text color={Color.BG_VANILLA_400}>
Manage your billing information, invoices, and monitor costs.
</Typography.Text>
</Flex>
<Card
bordered={false}
style={{ minHeight: 150, marginBottom: 16 }}
className="page-info"
>
<Col span={20}>
<Typography.Title level={4} ellipsis style={{ fontWeight: '300' }}>
{headerText}
</Typography.Title>
{licensesData?.payload?.onTrial &&
licensesData?.payload?.trialConvertedToSubscription && (
<Typography.Title
level={5}
ellipsis
style={{ fontWeight: '300', color: '#49aa19' }}
>
We have received your card details, your billing will only start after
the end of your free trial period.
</Typography.Title>
)}
</Col>
<Col span={4} style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Flex justify="space-between" align="center">
<Flex vertical>
<Typography.Title level={5} style={{ marginTop: 2, fontWeight: 500 }}>
{isCloudUserVal ? 'Enterprise Cloud' : 'Enterprise'}{' '}
{isFreeTrial ? <Tag color="success"> Free Trial </Tag> : ''}
</Typography.Title>
{!isLoading && !isFetchingBillingData ? (
<Typography.Text style={{ fontSize: 12, color: Color.BG_VANILLA_400 }}>
{daysRemaining} {daysRemainingStr}
</Typography.Text>
) : null}
</Flex>
<Button
type="primary"
size="middle"
loading={isLoadingBilling || isLoadingManageBilling}
disabled={isLoading}
onClick={handleBilling}
>
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription
? 'Upgrade Plan'
: 'Manage Billing'}
</Button>
</Col>
</Row>
</Flex>
<div className="billing-summary">
<Typography.Title level={4} style={{ margin: '16px 0' }}>
Current bill total
</Typography.Title>
{licensesData?.payload?.onTrial &&
licensesData?.payload?.trialConvertedToSubscription && (
<Typography.Text
ellipsis
style={{ fontWeight: '300', color: '#49aa19', fontSize: 12 }}
>
We have received your card details, your billing will only start after
the end of your free trial period.
</Typography.Text>
)}
<Typography.Title
level={3}
style={{ margin: '16px 0', display: 'flex', alignItems: 'center' }}
>
{billCurrency}
{billAmount} &nbsp;
{isFreeTrial ? <Tag color="success"> Free Trial </Tag> : ''}
</Typography.Title>
{!isLoading && !isFetchingBillingData ? (
<Alert
message={headerText}
type="info"
showIcon
style={{ marginTop: 12 }}
/>
) : (
<Skeleton.Input active style={{ height: 20, marginTop: 20 }} />
)}
</Card>
<Typography.Paragraph style={{ margin: '16px 0' }}>
{daysRemaining} {daysRemainingStr}
</Typography.Paragraph>
</div>
<BillingUsageGraphCallback />
<div className="billing-details">
{!isLoading && (
{!isLoading && !isFetchingBillingData && (
<Table
columns={columns}
dataSource={data}
pagination={false}
summary={renderSummary}
bordered={false}
/>
)}
{isLoading && renderTableSkeleton()}
{(isLoading || isFetchingBillingData) && renderTableSkeleton()}
</div>
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription && (

View File

@ -0,0 +1,29 @@
.billing-graph-card {
.ant-card-body {
height: 40vh;
.uplot-graph-container {
padding: 8px;
}
}
.total-spent {
font-family: 'SF Mono' monospace;
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 24px;
}
.total-spent-title {
font-size: 12px;
font-weight: 500;
line-height: 22px;
letter-spacing: 0.48px;
color: rgba(255, 255, 255, 0.5);
}
}
.lightMode {
.total-spent-title {
color: var(--bg-ink-100);
}
}

View File

@ -0,0 +1,190 @@
import './BillingUsageGraph.styles.scss';
import '../../../lib/uPlotLib/uPlotLib.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Card, Flex, Typography } from 'antd';
import { getComponentForPanelType } from 'constants/panelTypes';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PropsTypePropsMap } from 'container/GridPanelSwitch/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import tooltipPlugin from 'lib/uPlotLib/plugins/tooltipPlugin';
import getAxes from 'lib/uPlotLib/utils/getAxes';
import getRenderer from 'lib/uPlotLib/utils/getRenderer';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { getXAxisScale } from 'lib/uPlotLib/utils/getXAxisScale';
import { getYAxisScale } from 'lib/uPlotLib/utils/getYAxisScale';
import { FC, useMemo, useRef } from 'react';
import uPlot from 'uplot';
import {
convertDataToMetricRangePayload,
fillMissingValuesForQuantities,
} from './utils';
interface BillingUsageGraphProps {
data: any;
billAmount: number;
}
const paths = (
u: any,
seriesIdx: number,
idx0: number,
idx1: number,
extendGap: boolean,
buildClip: boolean,
): uPlot.Series.PathBuilder => {
const s = u.series[seriesIdx];
const style = s.drawStyle;
const interp = s.lineInterpolation;
const renderer = getRenderer(style, interp);
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
};
export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element {
const { data, billAmount } = props;
const graphCompatibleData = useMemo(
() => convertDataToMetricRangePayload(data),
[data],
);
const chartData = getUPlotChartData(graphCompatibleData);
const graphRef = useRef<HTMLDivElement>(null);
const isDarkMode = useIsDarkMode();
const containerDimensions = useResizeObserver(graphRef);
const { billingPeriodStart: startTime, billingPeriodEnd: endTime } = data;
const Component = getComponentForPanelType(PANEL_TYPES.BAR) as FC<
PropsTypePropsMap[PANEL_TYPES]
>;
const getGraphSeries = (color: string, label: string): any => ({
drawStyle: 'bars',
paths,
lineInterpolation: 'spline',
show: true,
label,
fill: color,
stroke: color,
width: 2,
spanGaps: true,
points: {
size: 5,
show: false,
stroke: color,
},
});
const uPlotSeries: any = useMemo(
() => [
{ label: 'Timestamp', stroke: 'purple' },
getGraphSeries(
'#7CEDBE',
graphCompatibleData.data.result[0]?.legend as string,
),
getGraphSeries(
'#4E74F8',
graphCompatibleData.data.result[1]?.legend as string,
),
getGraphSeries(
'#F24769',
graphCompatibleData.data.result[2]?.legend as string,
),
],
[graphCompatibleData.data.result],
);
const axesOptions = getAxes(isDarkMode, '');
const optionsForChart: uPlot.Options = useMemo(
() => ({
id: 'billing-usage-breakdown',
series: uPlotSeries,
width: containerDimensions.width,
height: containerDimensions.height - 30,
axes: [
{
...axesOptions[0],
grid: {
...axesOptions.grid,
show: false,
stroke: isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400,
},
},
{
...axesOptions[1],
stroke: isDarkMode ? Color.BG_SLATE_200 : Color.BG_INK_400,
},
],
scales: {
x: {
...getXAxisScale(startTime - 86400, endTime), // Minus 86400 from startTime to decrease a day to have a buffer start
},
y: {
...getYAxisScale({
series: graphCompatibleData?.data.newResult.data.result,
yAxisUnit: '',
softMax: null,
softMin: null,
}),
},
},
legend: {
show: true,
live: false,
isolate: true,
},
cursor: {
lock: false,
focus: {
prox: 1e6,
bias: 1,
},
},
focus: {
alpha: 0.3,
},
padding: [32, 32, 16, 16],
plugins: [
tooltipPlugin(
fillMissingValuesForQuantities(graphCompatibleData, chartData[0]),
'',
true,
),
],
}),
[
axesOptions,
chartData,
containerDimensions.height,
containerDimensions.width,
endTime,
graphCompatibleData,
isDarkMode,
startTime,
uPlotSeries,
],
);
const numberFormatter = new Intl.NumberFormat('en-US');
return (
<Card bordered={false} className="billing-graph-card">
<Flex justify="space-between">
<Flex vertical gap={6}>
<Typography.Text className="total-spent-title">
TOTAL SPENT
</Typography.Text>
<Typography.Text color={Color.BG_VANILLA_100} className="total-spent">
${numberFormatter.format(billAmount)}
</Typography.Text>
</Flex>
</Flex>
<div ref={graphRef} style={{ height: '100%', paddingBottom: 48 }}>
<Component data={chartData} options={optionsForChart} />
</div>
</Card>
);
}

View File

@ -0,0 +1,87 @@
import { isEmpty, isNull } from 'lodash-es';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
export const convertDataToMetricRangePayload = (
data: any,
): MetricRangePayloadProps => {
const emptyStateData = {
data: {
newResult: { data: { result: [], resultType: '' } },
result: [],
resultType: '',
},
};
if (isEmpty(data)) {
return emptyStateData;
}
const {
details: { breakdown = [] },
} = data || {};
if (isNull(breakdown) || breakdown.length === 0) {
return emptyStateData;
}
const payload = breakdown.map((info: any) => {
const metric = info.type;
const sortedBreakdownData = (info?.dayWiseBreakdown?.breakdown || []).sort(
(a: any, b: any) => a.timestamp - b.timestamp,
);
const values = (sortedBreakdownData || []).map((categoryInfo: any) => [
categoryInfo.timestamp,
categoryInfo.total,
]);
const queryName = info.type;
const legend = info.type;
const { unit } = info;
const quantity = sortedBreakdownData.map(
(categoryInfo: any) => categoryInfo.quantity,
);
return { metric, values, queryName, legend, quantity, unit };
});
const sortedData = payload.sort((a: any, b: any) => {
const sumA = a.values.reduce((acc: any, val: any) => acc + val[1], 0);
const avgA = a.values.length ? sumA / a.values.length : 0;
const sumB = b.values.reduce((acc: any, val: any) => acc + val[1], 0);
const avgB = b.values.length ? sumB / b.values.length : 0;
return sumA === sumB ? avgB - avgA : sumB - sumA;
});
return {
data: {
newResult: { data: { result: sortedData, resultType: '' } },
result: sortedData,
resultType: '',
},
};
};
export function fillMissingValuesForQuantities(
data: any,
timestampArray: number[],
): MetricRangePayloadProps {
const { result } = data.data;
const transformedResultArr: any[] = [];
result.forEach((item: any) => {
const timestampToQuantityMap: { [timestamp: number]: number } = {};
item.values.forEach((val: number[], index: number) => {
timestampToQuantityMap[val[0]] = item.quantity[index];
});
const quantityArray = timestampArray.map(
(timestamp: number) => timestampToQuantityMap[timestamp] ?? null,
);
transformedResultArr.push({ ...item, quantity: quantityArray });
});
return {
data: {
newResult: { data: { result: transformedResultArr, resultType: '' } },
result: transformedResultArr,
resultType: '',
},
};
}

View File

@ -64,6 +64,16 @@ export interface OpsgenieChannel extends Channel {
priority?: string;
}
export interface EmailChannel extends Channel {
// comma separated list of email addresses to send alerts to
to: string;
// HTML body of the email notification.
html: string;
// Further headers email header key/value pairs.
// [ headers: { <string>: <tmpl_string>, ... } ]
headers: Record<string, string>;
}
export const ValidatePagerChannel = (p: PagerChannel): string => {
if (!p) {
return 'Received unexpected input for this channel, please contact your administrator ';

View File

@ -1,4 +1,4 @@
import { OpsgenieChannel, PagerChannel } from './config';
import { EmailChannel, OpsgenieChannel, PagerChannel } from './config';
export const PagerInitialConfig: Partial<PagerChannel> = {
description: `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }}
@ -50,3 +50,399 @@ export const OpsgenieInitialConfig: Partial<OpsgenieChannel> = {
priority:
'{{ if eq (index .Alerts 0).Labels.severity "critical" }}P1{{ else if eq (index .Alerts 0).Labels.severity "warning" }}P2{{ else if eq (index .Alerts 0).Labels.severity "info" }}P3{{ else }}P4{{ end }}',
};
export const EmailInitialConfig: Partial<EmailChannel> = {
send_resolved: true,
html: `<!--
Credits: https://github.com/mailgun/transactional-email-templates
-->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>{{ template "__subject" . }}</title>
<style>
/* -------------------------------------
GLOBAL
A very basic CSS reset
------------------------------------- */
* {
margin: 0;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
}
img {
max-width: 100%;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
/* 1.6em * 14px = 22.4px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
/*line-height: 22px;*/
}
/* Let's make sure all tables have defaults */
table td {
vertical-align: top;
}
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
body {
background-color: #f6f6f6;
}
.body-wrap {
background-color: #f6f6f6;
width: 100%;
}
.container {
display: block !important;
max-width: 600px !important;
margin: 0 auto !important;
/* makes it centered */
clear: both !important;
}
.content {
max-width: 600px;
margin: 0 auto;
display: block;
padding: 20px;
}
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background-color: #fff;
border: 1px solid #e9e9e9;
border-radius: 3px;
}
.content-wrap {
padding: 30px;
}
.content-block {
padding: 0 0 20px;
}
.header {
width: 100%;
margin-bottom: 20px;
}
.footer {
width: 100%;
clear: both;
color: #999;
padding: 20px;
}
.footer p,
.footer a,
.footer td {
color: #999;
font-size: 12px;
}
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3 {
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
color: #000;
margin: 40px 0 0;
line-height: 1.2em;
font-weight: 400;
}
h1 {
font-size: 32px;
font-weight: 500;
/* 1.2em * 32px = 38.4px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
/*line-height: 38px;*/
}
h2 {
font-size: 24px;
/* 1.2em * 24px = 28.8px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
/*line-height: 29px;*/
}
h3 {
font-size: 18px;
/* 1.2em * 18px = 21.6px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
/*line-height: 22px;*/
}
h4 {
font-size: 14px;
font-weight: 600;
}
p,
ul,
ol {
margin-bottom: 10px;
font-weight: normal;
}
p li,
ul li,
ol li {
margin-left: 5px;
list-style-position: inside;
}
/* -------------------------------------
LINKS & BUTTONS
------------------------------------- */
a {
color: #348eda;
text-decoration: underline;
}
.btn-primary {
text-decoration: none;
color: #FFF;
background-color: #348eda;
border: solid #348eda;
border-width: 10px 20px;
line-height: 2em;
/* 2em * 14px = 28px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
/*line-height: 28px;*/
font-weight: bold;
text-align: center;
cursor: pointer;
display: inline-block;
border-radius: 5px;
text-transform: capitalize;
}
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0;
}
.first {
margin-top: 0;
}
.aligncenter {
text-align: center;
}
.alignright {
text-align: right;
}
.alignleft {
text-align: left;
}
.clear {
clear: both;
}
/* -------------------------------------
ALERTS
Change the class depending on warning email, good email or bad email
------------------------------------- */
.alert {
font-size: 16px;
color: #fff;
font-weight: 500;
padding: 20px;
text-align: center;
border-radius: 3px 3px 0 0;
}
.alert a {
color: #fff;
text-decoration: none;
font-weight: 500;
font-size: 16px;
}
.alert.alert-warning {
background-color: #E6522C;
}
.alert.alert-bad {
background-color: #D0021B;
}
.alert.alert-good {
background-color: #68B90F;
}
/* -------------------------------------
INVOICE
Styles for the billing table
------------------------------------- */
.invoice {
margin: 40px auto;
text-align: left;
width: 80%;
}
.invoice td {
padding: 5px 0;
}
.invoice .invoice-items {
width: 100%;
}
.invoice .invoice-items td {
border-top: #eee 1px solid;
}
.invoice .invoice-items .total td {
border-top: 2px solid #333;
border-bottom: 2px solid #333;
font-weight: 700;
}
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 640px) {
body {
padding: 0 !important;
}
h1,
h2,
h3,
h4 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
padding: 0 !important;
width: 100% !important;
}
.content {
padding: 0 !important;
}
.content-wrap {
padding: 10px !important;
}
.invoice {
width: 100% !important;
}
}
</style>
</head>
<body itemscope itemtype="http://schema.org/EmailMessage">
<table class="body-wrap">
<tr>
<td></td>
<td class="container" width="600">
<div class="content">
<table class="main" width="100%" cellpadding="0" cellspacing="0">
<tr>
{{ if gt (len .Alerts.Firing) 0 }}
<td class="alert alert-warning">
{{ else }}
<td class="alert alert-good">
{{ end }}
{{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }}
{{ .Name }}={{ .Value }}
{{ end }}
</td>
</tr>
<tr>
<td class="content-wrap">
<table width="100%" cellpadding="0" cellspacing="0">
{{ if gt (len .Alerts.Firing) 0 }}
<tr>
<td class="content-block">
<strong>[{{ .Alerts.Firing | len }}] Firing</strong>
</td>
</tr>
{{ end }}
{{ range .Alerts.Firing }}
<tr>
<td class="content-block">
<strong>Labels</strong><br />
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br />{{ end }}
{{ if gt (len .Annotations) 0 }}<strong>Annotations</strong><br />{{ end }}
{{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br />{{ end }}
<a href="{{ .GeneratorURL }}">Source</a><br />
</td>
</tr>
{{ end }}
{{ if gt (len .Alerts.Resolved) 0 }}
{{ if gt (len .Alerts.Firing) 0 }}
<tr>
<td class="content-block">
<br />
<hr />
<br />
</td>
</tr>
{{ end }}
<tr>
<td class="content-block">
<strong>[{{ .Alerts.Resolved | len }}] Resolved</strong>
</td>
</tr>
{{ end }}
{{ range .Alerts.Resolved }}
<tr>
<td class="content-block">
<strong>Labels</strong><br />
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br />{{ end }}
{{ if gt (len .Annotations) 0 }}<strong>Annotations</strong><br />{{ end }}
{{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br />{{ end }}
<a href="{{ .GeneratorURL }}">Source</a><br />
</td>
</tr>
{{ end }}
</table>
</td>
</tr>
</table>
</div>
</td>
<td></td>
</tr>
</table>
</body>
</html>`,
};

View File

@ -1,9 +1,11 @@
import { Form } from 'antd';
import createEmail from 'api/channels/createEmail';
import createMsTeamsApi from 'api/channels/createMsTeams';
import createOpsgenie from 'api/channels/createOpsgenie';
import createPagerApi from 'api/channels/createPager';
import createSlackApi from 'api/channels/createSlack';
import createWebhookApi from 'api/channels/createWebhook';
import testEmail from 'api/channels/testEmail';
import testMsTeamsApi from 'api/channels/testMsTeams';
import testOpsGenie from 'api/channels/testOpsgenie';
import testPagerApi from 'api/channels/testPager';
@ -18,6 +20,7 @@ import { useTranslation } from 'react-i18next';
import {
ChannelType,
EmailChannel,
MsTeamsChannel,
OpsgenieChannel,
PagerChannel,
@ -25,7 +28,11 @@ import {
ValidatePagerChannel,
WebhookChannel,
} from './config';
import { OpsgenieInitialConfig, PagerInitialConfig } from './defaults';
import {
EmailInitialConfig,
OpsgenieInitialConfig,
PagerInitialConfig,
} from './defaults';
import { isChannelType } from './utils';
function CreateAlertChannels({
@ -42,7 +49,8 @@ function CreateAlertChannels({
WebhookChannel &
PagerChannel &
MsTeamsChannel &
OpsgenieChannel
OpsgenieChannel &
EmailChannel
>
>({
text: `{{ range .Alerts -}}
@ -94,6 +102,14 @@ function CreateAlertChannels({
...OpsgenieInitialConfig,
}));
}
// reset config to email defaults
if (value === ChannelType.Email && currentType !== value) {
setSelectedConfig((selectedConfig) => ({
...selectedConfig,
...EmailInitialConfig,
}));
}
},
[type, selectedConfig],
);
@ -293,6 +309,43 @@ function CreateAlertChannels({
setSavingState(false);
}, [prepareOpsgenieRequest, t, notifications]);
const prepareEmailRequest = useCallback(
() => ({
name: selectedConfig?.name || '',
send_resolved: true,
to: selectedConfig?.to || '',
html: selectedConfig?.html || '',
headers: selectedConfig?.headers || {},
}),
[selectedConfig],
);
const onEmailHandler = useCallback(async () => {
setSavingState(true);
try {
const request = prepareEmailRequest();
const response = await createEmail(request);
if (response.statusCode === 200) {
notifications.success({
message: 'Success',
description: t('channel_creation_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
} else {
notifications.error({
message: 'Error',
description: response.error || t('channel_creation_failed'),
});
}
} catch (error) {
notifications.error({
message: 'Error',
description: t('channel_creation_failed'),
});
}
setSavingState(false);
}, [prepareEmailRequest, t, notifications]);
const prepareMsTeamsRequest = useCallback(
() => ({
webhook_url: selectedConfig?.webhook_url || '',
@ -339,6 +392,7 @@ function CreateAlertChannels({
[ChannelType.Pagerduty]: onPagerHandler,
[ChannelType.Opsgenie]: onOpsgenieHandler,
[ChannelType.MsTeams]: onMsTeamsHandler,
[ChannelType.Email]: onEmailHandler,
};
if (isChannelType(value)) {
@ -360,6 +414,7 @@ function CreateAlertChannels({
onPagerHandler,
onOpsgenieHandler,
onMsTeamsHandler,
onEmailHandler,
notifications,
t,
],
@ -392,6 +447,10 @@ function CreateAlertChannels({
request = prepareOpsgenieRequest();
response = await testOpsGenie(request);
break;
case ChannelType.Email:
request = prepareEmailRequest();
response = await testEmail(request);
break;
default:
notifications.error({
message: 'Error',
@ -427,6 +486,7 @@ function CreateAlertChannels({
prepareOpsgenieRequest,
prepareSlackRequest,
prepareMsTeamsRequest,
prepareEmailRequest,
notifications,
],
);
@ -455,6 +515,7 @@ function CreateAlertChannels({
...selectedConfig,
...PagerInitialConfig,
...OpsgenieInitialConfig,
...EmailInitialConfig,
},
}}
/>

View File

@ -1,9 +1,9 @@
import { ENTITY_VERSION_V4 } from 'constants/app';
import {
initialQueryBuilderFormValuesMap,
initialQueryPromQLData,
PANEL_TYPES,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import {
AlertDef,
@ -25,6 +25,7 @@ const defaultAnnotations = {
export const alertDefaults: AlertDef = {
alertType: AlertTypes.METRICS_BASED_ALERT,
version: ENTITY_VERSION_V4,
condition: {
compositeQuery: {
builderQueries: {
@ -78,7 +79,6 @@ export const logAlertDefaults: AlertDef = {
},
labels: {
severity: 'warning',
details: `${window.location.protocol}//${window.location.host}${ROUTES.LOGS_EXPLORER}`,
},
annotations: defaultAnnotations,
evalWindow: defaultEvalWindow,
@ -109,7 +109,6 @@ export const traceAlertDefaults: AlertDef = {
},
labels: {
severity: 'warning',
details: `${window.location.protocol}//${window.location.host}/traces`,
},
annotations: defaultAnnotations,
evalWindow: defaultEvalWindow,
@ -140,7 +139,6 @@ export const exceptionAlertDefaults: AlertDef = {
},
labels: {
severity: 'warning',
details: `${window.location.protocol}//${window.location.host}/exceptions`,
},
annotations: defaultAnnotations,
evalWindow: defaultEvalWindow,

View File

@ -1,7 +1,9 @@
import { Form, Row } from 'antd';
import { ENTITY_VERSION_V4 } from 'constants/app';
import FormAlertRules from 'container/FormAlertRules';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { AlertDef } from 'types/api/alerts/def';
@ -20,6 +22,10 @@ function CreateRules(): JSX.Element {
AlertTypes.METRICS_BASED_ALERT,
);
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const version = queryParams.get('version');
const compositeQuery = useGetCompositeQueryParam();
const [formInstance] = Form.useForm();
@ -37,7 +43,10 @@ function CreateRules(): JSX.Element {
setInitValues(exceptionAlertDefaults);
break;
default:
setInitValues(alertDefaults);
setInitValues({
...alertDefaults,
version: version || ENTITY_VERSION_V4,
});
}
};
@ -52,6 +61,7 @@ function CreateRules(): JSX.Element {
if (alertType) {
onSelectType(alertType);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [compositeQuery]);
if (!initValues) {

View File

@ -1,9 +1,11 @@
import { Form } from 'antd';
import editEmail from 'api/channels/editEmail';
import editMsTeamsApi from 'api/channels/editMsTeams';
import editOpsgenie from 'api/channels/editOpsgenie';
import editPagerApi from 'api/channels/editPager';
import editSlackApi from 'api/channels/editSlack';
import editWebhookApi from 'api/channels/editWebhook';
import testEmail from 'api/channels/testEmail';
import testMsTeamsApi from 'api/channels/testMsTeams';
import testOpsgenie from 'api/channels/testOpsgenie';
import testPagerApi from 'api/channels/testPager';
@ -12,6 +14,7 @@ import testWebhookApi from 'api/channels/testWebhook';
import ROUTES from 'constants/routes';
import {
ChannelType,
EmailChannel,
MsTeamsChannel,
OpsgenieChannel,
PagerChannel,
@ -39,7 +42,8 @@ function EditAlertChannels({
WebhookChannel &
PagerChannel &
MsTeamsChannel &
OpsgenieChannel
OpsgenieChannel &
EmailChannel
>
>({
...initialValue,
@ -156,6 +160,36 @@ function EditAlertChannels({
setSavingState(false);
}, [prepareWebhookRequest, t, notifications, selectedConfig]);
const prepareEmailRequest = useCallback(
() => ({
name: selectedConfig?.name || '',
to: selectedConfig.to || '',
html: selectedConfig.html || '',
headers: selectedConfig.headers || {},
id,
}),
[id, selectedConfig],
);
const onEmailEditHandler = useCallback(async () => {
setSavingState(true);
const request = prepareEmailRequest();
const response = await editEmail(request);
if (response.statusCode === 200) {
notifications.success({
message: 'Success',
description: t('channel_edit_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
} else {
notifications.error({
message: 'Error',
description: response.error || t('channel_edit_failed'),
});
}
setSavingState(false);
}, [prepareEmailRequest, t, notifications]);
const preparePagerRequest = useCallback(
() => ({
name: selectedConfig.name || '',
@ -300,6 +334,8 @@ function EditAlertChannels({
onMsTeamsEditHandler();
} else if (value === ChannelType.Opsgenie) {
onOpsgenieEditHandler();
} else if (value === ChannelType.Email) {
onEmailEditHandler();
}
},
[
@ -308,6 +344,7 @@ function EditAlertChannels({
onPagerEditHandler,
onMsTeamsEditHandler,
onOpsgenieEditHandler,
onEmailEditHandler,
],
);
@ -338,6 +375,10 @@ function EditAlertChannels({
request = prepareOpsgenieRequest();
if (request) response = await testOpsgenie(request);
break;
case ChannelType.Email:
request = prepareEmailRequest();
if (request) response = await testEmail(request);
break;
default:
notifications.error({
message: 'Error',
@ -373,6 +414,7 @@ function EditAlertChannels({
prepareSlackRequest,
prepareMsTeamsRequest,
prepareOpsgenieRequest,
prepareEmailRequest,
notifications,
],
);

View File

@ -0,0 +1,56 @@
import { DndContext, DragEndEvent } from '@dnd-kit/core';
import { useEffect, useState } from 'react';
import ExplorerOptions, { ExplorerOptionsProps } from './ExplorerOptions';
import {
getExplorerToolBarVisibility,
setExplorerToolBarVisibility,
} from './utils';
type ExplorerOptionsWrapperProps = Omit<
ExplorerOptionsProps,
'isExplorerOptionDrop'
>;
function ExplorerOptionWrapper({
disabled,
query,
isLoading,
onExport,
sourcepage,
}: ExplorerOptionsWrapperProps): JSX.Element {
const [isExplorerOptionHidden, setIsExplorerOptionHidden] = useState(false);
useEffect(() => {
const toolbarVisibility = getExplorerToolBarVisibility(sourcepage);
setIsExplorerOptionHidden(!toolbarVisibility);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleDragEnd = (event: DragEndEvent): void => {
const { active, over } = event;
if (
over !== null &&
active.id === 'explorer-options-draggable' &&
over.id === 'explorer-options-droppable'
) {
setIsExplorerOptionHidden(true);
setExplorerToolBarVisibility(false, sourcepage);
}
};
return (
<DndContext onDragEnd={handleDragEnd}>
<ExplorerOptions
disabled={disabled}
query={query}
isLoading={isLoading}
onExport={onExport}
sourcepage={sourcepage}
isExplorerOptionHidden={isExplorerOptionHidden}
setIsExplorerOptionHidden={setIsExplorerOptionHidden}
/>
</DndContext>
);
}
export default ExplorerOptionWrapper;

View File

@ -1,6 +1,9 @@
.hide-update {
left: calc(50% - 41px) !important;
}
.explorer-update {
position: fixed;
bottom: 16px;
bottom: 24px;
left: calc(50% - 225px);
display: flex;
align-items: center;
@ -23,6 +26,10 @@
cursor: pointer;
}
.hidden {
display: none;
}
.ant-divider {
margin: 0;
height: 28px;
@ -40,7 +47,7 @@
box-shadow: 4px 4px 16px 4px rgba(0, 0, 0, 0.25);
backdrop-filter: blur(20px);
position: fixed;
bottom: 16px;
bottom: 24px;
left: calc(50% + 240px);
transform: translate(calc(-50% - 120px), 0);
transition: left 0.2s linear;
@ -55,6 +62,10 @@
.view-options,
.actions {
.hidden {
display: none;
}
display: flex;
justify-content: center;
align-items: center;
@ -102,6 +113,9 @@
}
}
}
.hidden {
display: none;
}
}
.app-content {

View File

@ -1,5 +1,7 @@
/* eslint-disable react/jsx-props-no-spreading */
import './ExplorerOptions.styles.scss';
import { useDraggable } from '@dnd-kit/core';
import { Color } from '@signozhq/design-tokens';
import {
Button,
@ -13,6 +15,7 @@ import {
Typography,
} from 'antd';
import axios from 'axios';
import cx from 'classnames';
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
@ -30,12 +33,25 @@ import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications';
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
import { Check, ConciergeBell, Disc3, Plus, X, XCircle } from 'lucide-react';
import { CSSProperties, useCallback, useMemo, useRef, useState } from 'react';
import {
CSSProperties,
Dispatch,
SetStateAction,
useCallback,
useMemo,
useRef,
useState,
} from 'react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import ExplorerOptionsDroppableArea from './ExplorerOptionsDroppableArea';
import {
DATASOURCE_VS_ROUTES,
generateRGBAFromHex,
@ -43,12 +59,17 @@ import {
saveNewViewHandler,
} from './utils';
const allowedRoles = [USER_ROLES.ADMIN, USER_ROLES.AUTHOR, USER_ROLES.EDITOR];
// eslint-disable-next-line sonarjs/cognitive-complexity
function ExplorerOptions({
disabled,
isLoading,
onExport,
query,
sourcepage,
isExplorerOptionHidden = false,
setIsExplorerOptionHidden,
}: ExplorerOptionsProps): JSX.Element {
const [isExport, setIsExport] = useState<boolean>(false);
const [isSaveModalOpen, setIsSaveModalOpen] = useState(false);
@ -58,6 +79,7 @@ function ExplorerOptions({
const history = useHistory();
const ref = useRef<RefSelectProps>(null);
const isDarkMode = useIsDarkMode();
const [isDragEnabled, setIsDragEnabled] = useState(false);
const onModalToggle = useCallback((value: boolean) => {
setIsExport(value);
@ -71,6 +93,8 @@ function ExplorerOptions({
setIsSaveModalOpen(false);
};
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const onCreateAlertsHandler = useCallback(() => {
history.push(
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
@ -247,10 +271,37 @@ function ExplorerOptions({
[isDarkMode],
);
const {
attributes,
listeners,
setNodeRef,
transform,
isDragging,
} = useDraggable({
id: 'explorer-options-draggable',
disabled: isDragEnabled,
});
const isEditDeleteSupported = allowedRoles.includes(role as string);
const style: React.CSSProperties | undefined = transform
? {
transform: `translate3d(${transform.x - 338}px, ${transform.y}px, 0)`,
width: `${400 - transform.y * 6}px`,
maxWidth: '440px', // initial width of the explorer options
overflow: 'hidden',
}
: undefined;
return (
<>
{isQueryUpdated && (
<div className="explorer-update">
{isQueryUpdated && !isExplorerOptionHidden && !isDragging && (
<div
className={cx(
isEditDeleteSupported ? '' : 'hide-update',
'explorer-update',
)}
>
<Tooltip title="Clear this view" placement="top">
<Button
className="action-icon"
@ -258,10 +309,13 @@ function ExplorerOptions({
icon={<X size={14} />}
/>
</Tooltip>
<Divider type="vertical" />
<Divider
type="vertical"
className={isEditDeleteSupported ? '' : 'hidden'}
/>
<Tooltip title="Update this view" placement="top">
<Button
className="action-icon"
className={cx('action-icon', isEditDeleteSupported ? ' ' : 'hidden')}
disabled={isViewUpdating}
onClick={onUpdateQueryHandler}
icon={<Disc3 size={14} />}
@ -269,86 +323,105 @@ function ExplorerOptions({
</Tooltip>
</div>
)}
<div
className="explorer-options"
style={{
background: extraData
? `linear-gradient(90deg, rgba(0,0,0,0) -5%, ${rgbaColor} 9%, rgba(0,0,0,0) 30%)`
: 'transparent',
backdropFilter: 'blur(20px)',
}}
>
<div className="view-options">
<Select<string, { key: string; value: string }>
showSearch
placeholder="Select a view"
loading={viewsIsLoading || isRefetching}
value={viewName || undefined}
onSelect={handleSelect}
style={{
minWidth: 170,
}}
dropdownStyle={dropdownStyle}
className="views-dropdown"
allowClear={{
clearIcon: <XCircle size={16} style={{ marginTop: '-3px' }} />,
}}
onClear={handleClearSelect}
ref={ref}
>
{viewsData?.data?.data?.map((view) => {
const extraData =
view.extraData !== '' ? JSON.parse(view.extraData) : '';
let bgColor = getRandomColor();
if (extraData !== '') {
bgColor = extraData.color;
}
return (
<Select.Option key={view.uuid} value={view.name}>
<div className="render-options">
<span
className="dot"
style={{
background: bgColor,
boxShadow: `0px 0px 6px 0px ${bgColor}`,
}}
/>{' '}
{view.name}
</div>
</Select.Option>
);
})}
</Select>
<Button
shape="round"
onClick={handleSaveViewModalToggle}
disabled={viewsIsLoading || isRefetching}
>
<Disc3 size={16} /> Save this view
</Button>
</div>
<hr />
<div className="actions">
<Tooltip title="Create Alerts">
<Button
disabled={disabled}
shape="circle"
onClick={onCreateAlertsHandler}
{!isExplorerOptionHidden && (
<div
className="explorer-options"
style={{
background: extraData
? `linear-gradient(90deg, rgba(0,0,0,0) -5%, ${rgbaColor} 9%, rgba(0,0,0,0) 30%)`
: 'transparent',
backdropFilter: 'blur(20px)',
...style,
}}
ref={setNodeRef}
{...listeners}
{...attributes}
>
<div className="view-options">
<Select<string, { key: string; value: string }>
showSearch
placeholder="Select a view"
loading={viewsIsLoading || isRefetching}
value={viewName || undefined}
onSelect={handleSelect}
style={{
minWidth: 170,
}}
dropdownStyle={dropdownStyle}
className="views-dropdown"
allowClear={{
clearIcon: <XCircle size={16} style={{ marginTop: '-3px' }} />,
}}
onDropdownVisibleChange={(open): void => {
setIsDragEnabled(open);
}}
onClear={handleClearSelect}
ref={ref}
>
<ConciergeBell size={16} />
</Button>
</Tooltip>
{viewsData?.data?.data?.map((view) => {
const extraData =
view.extraData !== '' ? JSON.parse(view.extraData) : '';
let bgColor = getRandomColor();
if (extraData !== '') {
bgColor = extraData.color;
}
return (
<Select.Option key={view.uuid} value={view.name}>
<div className="render-options">
<span
className="dot"
style={{
background: bgColor,
boxShadow: `0px 0px 6px 0px ${bgColor}`,
}}
/>{' '}
{view.name}
</div>
</Select.Option>
);
})}
</Select>
<Tooltip title="Add to Dashboard">
<Button disabled={disabled} shape="circle" onClick={onAddToDashboard}>
<Plus size={16} />
<Button
shape="round"
onClick={handleSaveViewModalToggle}
className={isEditDeleteSupported ? '' : 'hidden'}
disabled={viewsIsLoading || isRefetching}
>
<Disc3 size={16} /> Save this view
</Button>
</Tooltip>
</div>
<hr className={isEditDeleteSupported ? '' : 'hidden'} />
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
<Tooltip title="Create Alerts">
<Button
disabled={disabled}
shape="circle"
onClick={onCreateAlertsHandler}
>
<ConciergeBell size={16} />
</Button>
</Tooltip>
<Tooltip title="Add to Dashboard">
<Button disabled={disabled} shape="circle" onClick={onAddToDashboard}>
<Plus size={16} />
</Button>
</Tooltip>
</div>
</div>
</div>
)}
<ExplorerOptionsDroppableArea
isExplorerOptionHidden={isExplorerOptionHidden}
setIsExplorerOptionHidden={setIsExplorerOptionHidden}
sourcepage={sourcepage}
isQueryUpdated={isQueryUpdated}
handleClearSelect={handleClearSelect}
onUpdateQueryHandler={onUpdateQueryHandler}
/>
<Modal
className="save-view-modal"
@ -406,8 +479,14 @@ export interface ExplorerOptionsProps {
query: Query | null;
disabled: boolean;
sourcepage: DataSource;
isExplorerOptionHidden?: boolean;
setIsExplorerOptionHidden?: Dispatch<SetStateAction<boolean>>;
}
ExplorerOptions.defaultProps = { isLoading: false };
ExplorerOptions.defaultProps = {
isLoading: false,
isExplorerOptionHidden: false,
setIsExplorerOptionHidden: undefined,
};
export default ExplorerOptions;

View File

@ -0,0 +1,55 @@
.explorer-option-droppable-container {
position: fixed;
bottom: 0;
width: -webkit-fill-available;
height: 24px;
display: flex;
justify-content: center;
border-radius: 10px 10px 0px 0px;
// box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.25);
// backdrop-filter: blur(20px);
.explorer-actions-btn {
display: flex;
gap: 8px;
margin-right: 8px;
.action-btn {
display: flex;
justify-content: center;
align-items: center;
border-radius: 10px 10px 0px 0px;
box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.25);
backdrop-filter: blur(20px);
height: 24px !important;
border: none;
}
}
.explorer-show-btn {
border-radius: 10px 10px 0px 0px;
border: 1px solid var(--bg-slate-400);
background: rgba(22, 24, 29, 0.40);
box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.25);
backdrop-filter: blur(20px);
align-self: center;
padding: 8px 12px;
height: 24px !important;
.menu-bar {
border-radius: 50px;
background: var(--bg-slate-200);
height: 4px;
width: 50px;
}
}
}
.lightMode {
.explorer-option-droppable-container {
.explorer-show-btn {
background: var(--bg-vanilla-400);
}
}
}

View File

@ -0,0 +1,83 @@
/* eslint-disable no-nested-ternary */
import './ExplorerOptionsDroppableArea.styles.scss';
import { useDroppable } from '@dnd-kit/core';
import { Color } from '@signozhq/design-tokens';
import { Button, Tooltip } from 'antd';
import { Disc3, X } from 'lucide-react';
import { Dispatch, SetStateAction } from 'react';
import { DataSource } from 'types/common/queryBuilder';
import { setExplorerToolBarVisibility } from './utils';
interface DroppableAreaProps {
isQueryUpdated: boolean;
isExplorerOptionHidden?: boolean;
sourcepage: DataSource;
setIsExplorerOptionHidden?: Dispatch<SetStateAction<boolean>>;
handleClearSelect: () => void;
onUpdateQueryHandler: () => void;
}
function ExplorerOptionsDroppableArea({
isQueryUpdated,
isExplorerOptionHidden,
sourcepage,
setIsExplorerOptionHidden,
handleClearSelect,
onUpdateQueryHandler,
}: DroppableAreaProps): JSX.Element {
const { setNodeRef } = useDroppable({
id: 'explorer-options-droppable',
});
const handleShowExplorerOption = (): void => {
if (setIsExplorerOptionHidden) {
setIsExplorerOptionHidden(false);
setExplorerToolBarVisibility(true, sourcepage);
}
};
return (
<div ref={setNodeRef} className="explorer-option-droppable-container">
{isExplorerOptionHidden && (
<>
{isQueryUpdated && (
<div className="explorer-actions-btn">
<Tooltip title="Clear this view">
<Button
onClick={handleClearSelect}
className="action-btn"
style={{ background: Color.BG_CHERRY_500 }}
icon={<X size={14} color={Color.BG_INK_500} />}
/>
</Tooltip>
<Tooltip title="Update this View">
<Button
onClick={onUpdateQueryHandler}
className="action-btn"
style={{ background: Color.BG_ROBIN_500 }}
icon={<Disc3 size={14} color={Color.BG_INK_500} />}
/>
</Tooltip>
</div>
)}
<Button
// style={{ alignSelf: 'center', marginRight: 'calc(10% - 20px)' }}
className="explorer-show-btn"
onClick={handleShowExplorerOption}
>
<div className="menu-bar" />
</Button>
</>
)}
</div>
);
}
ExplorerOptionsDroppableArea.defaultProps = {
isExplorerOptionHidden: undefined,
setIsExplorerOptionHidden: undefined,
};
export default ExplorerOptionsDroppableArea;

View File

@ -1,5 +1,6 @@
import { Color } from '@signozhq/design-tokens';
import { showErrorNotification } from 'components/ExplorerCard/utils';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
@ -67,3 +68,54 @@ export const generateRGBAFromHex = (hex: string, opacity: number): string =>
hex.slice(3, 5),
16,
)}, ${parseInt(hex.slice(5, 7), 16)}, ${opacity})`;
export const getExplorerToolBarVisibility = (dataSource: string): boolean => {
try {
const showExplorerToolbar = localStorage.getItem(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
);
if (showExplorerToolbar === null) {
const parsedShowExplorerToolbar: {
[DataSource.LOGS]: boolean;
[DataSource.TRACES]: boolean;
[DataSource.METRICS]: boolean;
} = {
[DataSource.METRICS]: true,
[DataSource.TRACES]: true,
[DataSource.LOGS]: true,
};
localStorage.setItem(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
JSON.stringify(parsedShowExplorerToolbar),
);
return true;
}
const parsedShowExplorerToolbar = JSON.parse(showExplorerToolbar || '{}');
return parsedShowExplorerToolbar[dataSource];
} catch (error) {
console.error(error);
return false;
}
};
export const setExplorerToolBarVisibility = (
value: boolean,
dataSource: string,
): void => {
try {
const showExplorerToolbar = localStorage.getItem(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
);
if (showExplorerToolbar) {
const parsedShowExplorerToolbar = JSON.parse(showExplorerToolbar);
parsedShowExplorerToolbar[dataSource] = value;
localStorage.setItem(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
JSON.stringify(parsedShowExplorerToolbar),
);
return;
}
} catch (error) {
console.error(error);
}
};

View File

@ -0,0 +1,48 @@
import { Form, Input } from 'antd';
import { Dispatch, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';
import { EmailChannel } from '../../CreateAlertChannels/config';
function EmailForm({ setSelectedConfig }: EmailFormProps): JSX.Element {
const { t } = useTranslation('channels');
const handleInputChange = (field: string) => (
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
): void => {
setSelectedConfig((value) => ({
...value,
[field]: event.target.value,
}));
};
return (
<>
<Form.Item
name="to"
help={t('help_email_to')}
label={t('field_email_to')}
required
>
<Input
onChange={handleInputChange('to')}
placeholder={t('placeholder_email_to')}
/>
</Form.Item>
{/* <Form.Item name="html" label={t('field_email_html')} required>
<TextArea
rows={4}
onChange={handleInputChange('html')}
placeholder={t('placeholder_email_html')}
/>
</Form.Item> */}
</>
);
}
interface EmailFormProps {
setSelectedConfig: Dispatch<SetStateAction<Partial<EmailChannel>>>;
}
export default EmailForm;

View File

@ -5,6 +5,7 @@ import { FeatureKeys } from 'constants/features';
import ROUTES from 'constants/routes';
import {
ChannelType,
EmailChannel,
OpsgenieChannel,
PagerChannel,
SlackChannel,
@ -16,6 +17,7 @@ import history from 'lib/history';
import { Dispatch, ReactElement, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';
import EmailSettings from './Settings/Email';
import MsTeamsSettings from './Settings/MsTeams';
import OpsgenieSettings from './Settings/Opsgenie';
import PagerSettings from './Settings/Pager';
@ -69,6 +71,8 @@ function FormAlertChannels({
return <MsTeamsSettings setSelectedConfig={setSelectedConfig} />;
case ChannelType.Opsgenie:
return <OpsgenieSettings setSelectedConfig={setSelectedConfig} />;
case ChannelType.Email:
return <EmailSettings setSelectedConfig={setSelectedConfig} />;
default:
return null;
}
@ -105,6 +109,9 @@ function FormAlertChannels({
<Select.Option value="opsgenie" key="opsgenie">
Opsgenie
</Select.Option>
<Select.Option value="email" key="email">
Email
</Select.Option>
{!isOssFeature?.active && (
<Select.Option value="msteams" key="msteams">
<div>
@ -151,7 +158,13 @@ interface FormAlertChannelsProps {
type: ChannelType;
setSelectedConfig: Dispatch<
SetStateAction<
Partial<SlackChannel & WebhookChannel & PagerChannel & OpsgenieChannel>
Partial<
SlackChannel &
WebhookChannel &
PagerChannel &
OpsgenieChannel &
EmailChannel
>
>
>;
onTypeChangeHandler: (value: ChannelType) => void;

View File

@ -1,5 +1,6 @@
import { InfoCircleOutlined } from '@ant-design/icons';
import Spinner from 'components/Spinner';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import GridPanelSwitch from 'container/GridPanelSwitch';
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
@ -39,6 +40,7 @@ export interface ChartPreviewProps {
yAxisUnit: string;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
function ChartPreview({
name,
query,
@ -94,6 +96,7 @@ function ChartPreview({
allowSelectedIntervalForStepGen,
},
},
alertDef?.version || DEFAULT_ENTITY_VERSION,
{
queryKey: [
'chartPreview',

View File

@ -0,0 +1,45 @@
.create-alert-modal {
.ant-modal-content {
background-color: var(--bg-ink-300);
.ant-modal-confirm-title {
color: var(--bg-vanilla-100);
}
.ant-modal-confirm-content {
.ant-typography {
color: var(--bg-vanilla-100);
}
}
.ant-modal-confirm-btns {
button:nth-of-type(1) {
background-color: var(--bg-slate-400);
border: none;
color: var(--bg-vanilla-100);
}
}
}
}
.lightMode {
.ant-modal-content {
background-color: var(--bg-vanilla-100);
.ant-modal-confirm-title {
color: var(--bg-ink-500);
}
.ant-modal-confirm-content {
.ant-typography {
color: var(--bg-ink-500);
}
}
.ant-modal-confirm-btns {
button:nth-of-type(1) {
background-color: var(--bg-vanilla-300);
border: none;
color: var(--bg-ink-500);
}
}
}
}

View File

@ -2,14 +2,18 @@ import './QuerySection.styles.scss';
import { Button, Tabs, Tooltip } from 'antd';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
import { QueryBuilder } from 'container/QueryBuilder';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { Atom, Play, Terminal } from 'lucide-react';
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { AlertDef } from 'types/api/alerts/def';
import { EQueryType } from 'types/common/dashboard';
import AppReducer from 'types/reducer/app';
@ -22,6 +26,7 @@ function QuerySection({
setQueryCategory,
alertType,
runQuery,
alertDef,
panelType,
}: QuerySectionProps): JSX.Element {
// init namespace for translations
@ -50,6 +55,11 @@ function QuerySection({
queryVariant: 'static',
initialDataSource: ALERTS_DATA_SOURCE_MAP[alertType],
}}
showFunctions={
alertType === AlertTypes.METRICS_BASED_ALERT &&
alertDef.version === ENTITY_VERSION_V4
}
version={alertDef.version || 'v3'}
/>
);
@ -112,6 +122,17 @@ function QuerySection({
[],
);
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
useEffect(() => {
registerShortcut(QBShortcuts.StageAndRunQuery, runQuery);
return (): void => {
deregisterShortcut(QBShortcuts.StageAndRunQuery);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [runQuery]);
const renderTabs = (typ: AlertTypes): JSX.Element | null => {
switch (typ) {
case AlertTypes.TRACES_BASED_ALERT:
@ -197,6 +218,7 @@ interface QuerySectionProps {
setQueryCategory: (n: EQueryType) => void;
alertType: AlertTypes;
runQuery: VoidFunction;
alertDef: AlertDef;
panelType: PANEL_TYPES;
}

View File

@ -1,4 +1,5 @@
import {
Checkbox,
Form,
InputNumber,
InputNumberProps,
@ -213,28 +214,66 @@ function RuleOptions({
? renderPromRuleOptions()
: renderThresholdRuleOpts()}
<Space direction="horizontal" align="center">
<Form.Item noStyle name={['condition', 'target']}>
<InputNumber
addonBefore={t('field_threshold')}
value={alertDef?.condition?.target}
onChange={onChange}
type="number"
onWheel={(e): void => e.currentTarget.blur()}
/>
</Form.Item>
<Space direction="vertical" size="large">
<Space direction="horizontal" align="center">
<Form.Item noStyle name={['condition', 'target']}>
<InputNumber
addonBefore={t('field_threshold')}
value={alertDef?.condition?.target}
onChange={onChange}
type="number"
onWheel={(e): void => e.currentTarget.blur()}
/>
</Form.Item>
<Form.Item noStyle>
<Select
getPopupContainer={popupContainer}
allowClear
showSearch
options={categorySelectOptions}
placeholder={t('field_unit')}
value={alertDef.condition.targetUnit}
onChange={onChangeAlertUnit}
/>
</Form.Item>
<Form.Item noStyle>
<Select
getPopupContainer={popupContainer}
allowClear
showSearch
options={categorySelectOptions}
placeholder={t('field_unit')}
value={alertDef.condition.targetUnit}
onChange={onChangeAlertUnit}
/>
</Form.Item>
</Space>
<Space direction="horizontal" align="center">
<Form.Item noStyle name={['condition', 'alertOnAbsent']}>
<Checkbox
checked={alertDef?.condition?.alertOnAbsent}
onChange={(e): void => {
setAlertDef({
...alertDef,
condition: {
...alertDef.condition,
alertOnAbsent: e.target.checked,
},
});
}}
/>
</Form.Item>
<Typography.Text>{t('text_alert_on_absent')}</Typography.Text>
<Form.Item noStyle name={['condition', 'absentFor']}>
<InputNumber
min={1}
value={alertDef?.condition?.absentFor}
onChange={(value): void => {
setAlertDef({
...alertDef,
condition: {
...alertDef.condition,
absentFor: Number(value) || 0,
},
});
}}
type="number"
onWheel={(e): void => e.currentTarget.blur()}
/>
</Form.Item>
<Typography.Text>{t('text_for')}</Typography.Text>
</Space>
</Space>
</FormContainer>
</>

View File

@ -1,3 +1,5 @@
import './FormAlertRules.styles.scss';
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
import {
Col,
@ -304,7 +306,7 @@ function FormAlertRules({
panelType,
]);
const isAlertAvialable = useIsFeatureDisabled(
const isAlertAvailable = useIsFeatureDisabled(
FeatureKeys.QUERY_BUILDER_ALERTS,
);
@ -373,6 +375,7 @@ function FormAlertRules({
centered: true,
content,
onOk: saveRule,
className: 'create-alert-modal',
});
}, [t, saveRule, currentQuery]);
@ -458,8 +461,8 @@ function FormAlertRules({
const isAlertNameMissing = !formInstance.getFieldValue('alert');
const isAlertAvialableToSave =
isAlertAvialable &&
const isAlertAvailableToSave =
isAlertAvailable &&
currentQuery.queryType === EQueryType.QUERY_BUILDER &&
alertType !== AlertTypes.METRICS_BASED_ALERT;
@ -509,6 +512,7 @@ function FormAlertRules({
setQueryCategory={onQueryCategoryChange}
alertType={alertType || AlertTypes.METRICS_BASED_ALERT}
runQuery={handleRunQuery}
alertDef={alertDef}
panelType={panelType || PANEL_TYPES.TIME_SERIES}
/>
@ -521,7 +525,7 @@ function FormAlertRules({
{renderBasicInfo()}
<ButtonContainer>
<Tooltip title={isAlertAvialableToSave ? MESSAGE.ALERT : ''}>
<Tooltip title={isAlertAvailableToSave ? MESSAGE.ALERT : ''}>
<ActionButton
loading={loading || false}
type="primary"
@ -529,7 +533,7 @@ function FormAlertRules({
icon={<SaveOutlined />}
disabled={
isAlertNameMissing ||
isAlertAvialableToSave ||
isAlertAvailableToSave ||
!isChannelConfigurationValid
}
>

View File

@ -5,7 +5,7 @@ import { CheckboxChangeEvent } from 'antd/es/checkbox';
import { ResizeTable } from 'components/ResizeTable';
import { useNotifications } from 'hooks/useNotifications';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { memo, useCallback, useState } from 'react';
import { memo, useCallback, useEffect, useState } from 'react';
import { getGraphManagerTableColumns } from './TableRender/GraphManagerColumns';
import { ExtendedChartDataset, GraphManagerProps } from './types';
@ -29,6 +29,10 @@ function GraphManager({
getDefaultTableDataSet(options, data),
);
useEffect(() => {
setTableDataSet(getDefaultTableDataSet(options, data));
}, [data, options]);
const { notifications } = useNotifications();
const { isDashboardLocked } = useDashboard();

View File

@ -6,6 +6,7 @@ import cx from 'classnames';
import { ToggleGraphProps } from 'components/Graph/types';
import Spinner from 'components/Spinner';
import TimePreference from 'components/TimePreferenceDropDown';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import GridPanelSwitch from 'container/GridPanelSwitch';
import {
@ -20,7 +21,7 @@ import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariab
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
@ -28,7 +29,7 @@ import uPlot from 'uplot';
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { getTimeRange } from 'utils/getTimeRange';
import { getGraphVisibilityStateOnDataChange } from '../utils';
import { getLocalStorageGraphVisibilityState } from '../utils';
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
import GraphManager from './GraphManager';
import { GraphContainer, TimeContainer } from './styles';
@ -39,14 +40,13 @@ function FullView({
fullViewOptions = true,
onClickHandler,
name,
version,
originalName,
yAxisUnit,
options,
onDragSelect,
isDependedDataLoaded = false,
onToggleModelHandler,
parentChartRef,
parentGraphVisibilityState,
}: FullViewProps): JSX.Element {
const { selectedTime: globalSelectedTime } = useSelector<
AppState,
@ -59,20 +59,6 @@ function FullView({
const { selectedDashboard, isDashboardLocked } = useDashboard();
const { graphVisibilityStates: localStoredVisibilityStates } = useMemo(
() =>
getGraphVisibilityStateOnDataChange({
options,
isExpandedName: false,
name: originalName,
}),
[options, originalName],
);
const [graphsVisibilityStates, setGraphsVisibilityStates] = useState(
localStoredVisibilityStates,
);
const getSelectedTime = useCallback(
() =>
timeItems.find((e) => e.enum === (widget?.timePreferance || 'GLOBAL_TIME')),
@ -91,17 +77,35 @@ function FullView({
const response = useGetQueryRange(
{
selectedTime: selectedTime.enum,
graphType: widget.panelTypes,
graphType:
widget.panelTypes === PANEL_TYPES.BAR
? PANEL_TYPES.TIME_SERIES
: widget.panelTypes,
query: updatedQuery,
globalSelectedInterval: globalSelectedTime,
variables: getDashboardVariables(selectedDashboard?.data.variables),
},
selectedDashboard?.data?.version || version || DEFAULT_ENTITY_VERSION,
{
queryKey: `FullViewGetMetricsQueryRange-${selectedTime.enum}-${globalSelectedTime}-${widget.id}`,
enabled: !isDependedDataLoaded && widget.panelTypes !== PANEL_TYPES.LIST, // Internally both the list view panel has it's own query range api call, so we don't need to call it again
},
);
const [graphsVisibilityStates, setGraphsVisibilityStates] = useState<
boolean[]
>(Array(response.data?.payload.data.result.length).fill(true));
useEffect(() => {
const {
graphVisibilityStates: localStoredVisibilityState,
} = getLocalStorageGraphVisibilityState({
apiResponse: response.data?.payload.data.result || [],
name: originalName,
});
setGraphsVisibilityStates(localStoredVisibilityState);
}, [originalName, response.data?.payload.data.result]);
const canModifyChart = useChartMutable({
panelType: widget.panelTypes,
panelTypeAndGraphManagerVisibility: PANEL_TYPES_VS_FULL_VIEW_TABLE,
@ -144,6 +148,7 @@ function FullView({
: 300;
const newChartOptions = getUPlotChartOptions({
id: originalName,
yAxisUnit: yAxisUnit || '',
apiResponse: response.data?.payload,
dimensions: {
@ -171,8 +176,7 @@ function FullView({
graphsVisibilityStates?.forEach((e, i) => {
fullViewChartRef?.current?.toggleGraph(i, e);
});
parentGraphVisibilityState(graphsVisibilityStates);
}, [graphsVisibilityStates, parentGraphVisibilityState]);
}, [graphsVisibilityStates]);
const isListView = widget.panelTypes === PANEL_TYPES.LIST;

View File

@ -50,14 +50,13 @@ export interface FullViewProps {
fullViewOptions?: boolean;
onClickHandler?: OnClickPluginOpts['onClick'];
name: string;
version?: string;
originalName: string;
options: uPlot.Options;
yAxisUnit?: string;
onDragSelect: (start: number, end: number) => void;
isDependedDataLoaded?: boolean;
onToggleModelHandler?: GraphManagerProps['onToggleModelHandler'];
parentChartRef: GraphManagerProps['lineChartRef'];
parentGraphVisibilityState: Dispatch<SetStateAction<boolean[]>>;
}
export interface GraphManagerProps extends UplotProps {

View File

@ -1,4 +1,6 @@
import { LOCALSTORAGE } from 'constants/localStorage';
import getLabelName from 'lib/getLabelName';
import { QueryData } from 'types/api/widgets/getQuery';
import uPlot from 'uplot';
import {
@ -55,6 +57,20 @@ export const getAbbreviatedLabel = (label: string): string => {
return newLabel;
};
export const showAllDataSetFromApiResponse = (
apiResponse: QueryData[],
): LegendEntryProps[] =>
apiResponse.map(
(item): LegendEntryProps => ({
label: getLabelName(
item.metric || {},
item.queryName || '',
item.legend || '',
),
show: true,
}),
);
export const showAllDataSet = (options: uPlot.Options): LegendEntryProps[] =>
options.series
.map(

View File

@ -32,13 +32,14 @@ import WidgetHeader from '../WidgetHeader';
import FullView from './FullView';
import { Modal } from './styles';
import { WidgetGraphComponentProps } from './types';
import { getGraphVisibilityStateOnDataChange } from './utils';
import { getLocalStorageGraphVisibilityState } from './utils';
function WidgetGraphComponent({
widget,
queryResponse,
errorMessage,
name,
version,
threshold,
headerMenuList,
isWarning,
@ -62,20 +63,6 @@ function WidgetGraphComponent({
const lineChartRef = useRef<ToggleGraphProps>();
const graphRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (queryResponse.isSuccess) {
const {
graphVisibilityStates: localStoredVisibilityState,
} = getGraphVisibilityStateOnDataChange({
options,
isExpandedName: false,
name,
});
setGraphVisibility(localStoredVisibilityState);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [queryResponse.isSuccess]);
useEffect(() => {
if (!lineChartRef.current) return;
@ -218,6 +205,15 @@ function WidgetGraphComponent({
const existingSearchParams = new URLSearchParams(search);
existingSearchParams.delete(QueryParams.expandedWidgetId);
const updatedQueryParams = Object.fromEntries(existingSearchParams.entries());
if (queryResponse.data?.payload) {
const {
graphVisibilityStates: localStoredVisibilityState,
} = getLocalStorageGraphVisibilityState({
apiResponse: queryResponse.data.payload.data.result,
name,
});
setGraphVisibility(localStoredVisibilityState);
}
history.push({
pathname,
search: createQueryParams(updatedQueryParams),
@ -283,14 +279,13 @@ function WidgetGraphComponent({
>
<FullView
name={`${name}expanded`}
version={version}
originalName={name}
widget={widget}
yAxisUnit={widget.yAxisUnit}
onToggleModelHandler={onToggleModelHandler}
parentChartRef={lineChartRef}
parentGraphVisibilityState={setGraphVisibility}
onDragSelect={onDragSelect}
options={options}
/>
</Modal>

View File

@ -1,3 +1,4 @@
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
@ -28,6 +29,7 @@ import { getTimeRange } from 'utils/getTimeRange';
import EmptyWidget from '../EmptyWidget';
import { MenuItemKeys } from '../WidgetHeader/contants';
import { GridCardGraphProps } from './types';
import { getLocalStorageGraphVisibilityState } from './utils';
import WidgetGraphComponent from './WidgetGraphComponent';
function GridCardGraph({
@ -39,6 +41,7 @@ function GridCardGraph({
threshold,
variables,
fillSpans = false,
version,
}: GridCardGraphProps): JSX.Element {
const dispatch = useDispatch();
const [errorMessage, setErrorMessage] = useState<string>();
@ -132,6 +135,7 @@ function GridCardGraph({
globalSelectedInterval,
variables: getDashboardVariables(variables),
},
version || DEFAULT_ENTITY_VERSION,
{
queryKey: [
maxTime,
@ -183,6 +187,16 @@ function GridCardGraph({
Array(queryResponse.data?.payload?.data.result.length || 0).fill(true),
);
useEffect(() => {
const {
graphVisibilityStates: localStoredVisibilityState,
} = getLocalStorageGraphVisibilityState({
apiResponse: queryResponse.data?.payload.data.result || [],
name,
});
setGraphVisibility(localStoredVisibilityState);
}, [name, queryResponse.data?.payload.data.result]);
const options = useMemo(
() =>
getUPlotChartOptions({
@ -234,6 +248,7 @@ function GridCardGraph({
errorMessage={errorMessage}
isWarning={false}
name={name}
version={version}
onDragSelect={onDragSelect}
threshold={threshold}
headerMenuList={menuList}
@ -253,6 +268,7 @@ GridCardGraph.defaultProps = {
isQueryEnabled: true,
threshold: undefined,
headerMenuList: [MenuItemKeys.View],
version: 'v3',
};
export default memo(GridCardGraph);

View File

@ -23,6 +23,7 @@ export interface WidgetGraphComponentProps extends UplotProps {
>;
errorMessage: string | undefined;
name: string;
version?: string;
onDragSelect: (start: number, end: number) => void;
onClickHandler?: OnClickPluginOpts['onClick'];
threshold?: ReactNode;
@ -43,6 +44,7 @@ export interface GridCardGraphProps {
isQueryEnabled: boolean;
variables?: Dashboard['data']['variables'];
fillSpans?: boolean;
version?: string;
}
export interface GetGraphVisibilityStateOnLegendClickProps {

View File

@ -1,14 +1,78 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { LOCALSTORAGE } from 'constants/localStorage';
import getLabelName from 'lib/getLabelName';
import { QueryData } from 'types/api/widgets/getQuery';
import { LegendEntryProps } from './FullView/types';
import { showAllDataSet } from './FullView/utils';
import {
showAllDataSet,
showAllDataSetFromApiResponse,
} from './FullView/utils';
import {
GetGraphVisibilityStateOnLegendClickProps,
GraphVisibilityLegendEntryProps,
ToggleGraphsVisibilityInChartProps,
} from './types';
export const getLocalStorageGraphVisibilityState = ({
apiResponse,
name,
}: {
apiResponse: QueryData[];
name: string;
}): GraphVisibilityLegendEntryProps => {
const visibilityStateAndLegendEntry: GraphVisibilityLegendEntryProps = {
graphVisibilityStates: Array(apiResponse.length + 1).fill(true),
legendEntry: [
{
label: 'Timestamp',
show: true,
},
...showAllDataSetFromApiResponse(apiResponse),
],
};
if (localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) !== null) {
const legendGraphFromLocalStore = localStorage.getItem(
LOCALSTORAGE.GRAPH_VISIBILITY_STATES,
);
let legendFromLocalStore: {
name: string;
dataIndex: LegendEntryProps[];
}[] = [];
try {
legendFromLocalStore = JSON.parse(legendGraphFromLocalStore || '[]');
} catch (error) {
console.error(
'Error parsing GRAPH_VISIBILITY_STATES from local storage',
error,
);
}
const newGraphVisibilityStates = Array(apiResponse.length + 1).fill(true);
legendFromLocalStore.forEach((item) => {
const newName = name;
if (item.name === newName) {
visibilityStateAndLegendEntry.legendEntry = item.dataIndex;
apiResponse.forEach((datasets, i) => {
const index = item.dataIndex.findIndex(
(dataKey) =>
dataKey.label ===
getLabelName(datasets.metric, datasets.queryName, datasets.legend || ''),
);
if (index !== -1) {
newGraphVisibilityStates[i + 1] = item.dataIndex[index].show;
}
});
visibilityStateAndLegendEntry.graphVisibilityStates = newGraphVisibilityStates;
}
});
}
return visibilityStateAndLegendEntry;
};
export const getGraphVisibilityStateOnDataChange = ({
options,
isExpandedName,

View File

@ -1,6 +1,6 @@
.fullscreen-grid-container {
overflow: auto;
margin-top: 1rem;
margin: 8px -8px;
.react-grid-layout {
border: none !important;

View File

@ -1,6 +1,7 @@
import './GridCardLayout.styles.scss';
import { PlusOutlined } from '@ant-design/icons';
import { Tooltip } from 'antd';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { themeColors } from 'constants/theme';
@ -144,17 +145,19 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
return (
<>
<ButtonContainer>
<Button
loading={updateDashboardMutation.isLoading}
onClick={handle.enter}
icon={<FullscreenIcon size={16} />}
disabled={updateDashboardMutation.isLoading}
>
{t('dashboard:full_view')}
</Button>
<Tooltip title="Open in Full Screen">
<Button
className="periscope-btn"
loading={updateDashboardMutation.isLoading}
onClick={handle.enter}
icon={<FullscreenIcon size={16} />}
disabled={updateDashboardMutation.isLoading}
/>
</Tooltip>
{!isDashboardLocked && addPanelPermission && (
<Button
className="periscope-btn"
onClick={onAddPanelHandler}
icon={<PlusOutlined />}
data-testid="add-panel"
@ -201,6 +204,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
headerMenuList={widgetActions}
variables={variables}
fillSpans={currentWidget?.fillSpans}
version={selectedDashboard?.data?.version}
/>
</Card>
</CardContainer>

View File

@ -80,7 +80,6 @@ export const ReactGridLayout = styled(ReactGridLayoutComponent)`
export const ButtonContainer = styled(Space)`
display: flex;
justify-content: end;
margin-top: 1rem;
`;
export const Button = styled(ButtonComponent)`

View File

@ -1,4 +1,5 @@
import { ToggleGraphProps } from 'components/Graph/types';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { getComponentForPanelType } from 'constants/panelTypes';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GRID_TABLE_CONFIG } from 'container/GridTableComponent/config';
@ -50,11 +51,13 @@ const GridPanelSwitch = forwardRef<
? {
selectedLogsFields: selectedLogFields || [],
query,
version: DEFAULT_ENTITY_VERSION, // As we don't support for Metrics, defaulting to v3
selectedTime,
}
: {
selectedTracesFields: selectedTracesFields || [],
query,
version: DEFAULT_ENTITY_VERSION, // As we don't support for Metrics, defaulting to v3
selectedTime,
},
[PANEL_TYPES.TRACE]: null,

View File

@ -10,6 +10,7 @@ import {
import DynamicColumnTable from 'components/ResizeTable/DynamicColumnTable';
import LabelColumn from 'components/TableRenderer/LabelColumn';
import TextToolTip from 'components/TextToolTip';
import { ENTITY_VERSION_V4 } from 'constants/app';
import ROUTES from 'constants/routes';
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
import useComponentPermission from 'hooks/useComponentPermission';
@ -109,7 +110,6 @@ function DashboardsList(): JSX.Element {
width: 30,
key: DynamicColumnsKey.CreatedAt,
sorter: (a: Data, b: Data): number => {
console.log({ a });
const prev = new Date(a.createdAt).getTime();
const next = new Date(b.createdAt).getTime();
@ -211,6 +211,7 @@ function DashboardsList(): JSX.Element {
ns: 'dashboard',
}),
uploadedGrafana: false,
version: ENTITY_VERSION_V4,
});
if (response.statusCode === 200) {
@ -304,52 +305,56 @@ function DashboardsList(): JSX.Element {
loading={isFilteringDashboards}
style={{ marginBottom: 16, marginTop: 16 }}
defaultValue={searchString}
autoFocus
/>
</Col>
<Col
span={6}
style={{
display: 'flex',
justifyContent: 'flex-end',
}}
>
<ButtonContainer>
<TextToolTip
{...{
text: `More details on how to create dashboards`,
url: 'https://signoz.io/docs/userguide/dashboards',
}}
/>
</ButtonContainer>
<Dropdown
menu={{ items: getMenuItems }}
disabled={isDashboardListLoading}
placement="bottomRight"
{createNewDashboard && (
<Col
span={6}
style={{
display: 'flex',
justifyContent: 'flex-end',
}}
>
<NewDashboardButton
icon={<PlusOutlined />}
type="primary"
data-testid="create-new-dashboard"
loading={newDashboardState.loading}
danger={newDashboardState.error}
<ButtonContainer>
<TextToolTip
{...{
text: `More details on how to create dashboards`,
url: 'https://signoz.io/docs/userguide/dashboards',
}}
/>
</ButtonContainer>
<Dropdown
menu={{ items: getMenuItems }}
disabled={isDashboardListLoading}
placement="bottomRight"
>
{getText()}
</NewDashboardButton>
</Dropdown>
</Col>
<NewDashboardButton
icon={<PlusOutlined />}
type="primary"
data-testid="create-new-dashboard"
loading={newDashboardState.loading}
danger={newDashboardState.error}
>
{getText()}
</NewDashboardButton>
</Dropdown>
</Col>
)}
</Row>
),
[
isDashboardListLoading,
handleSearch,
isFilteringDashboards,
searchString,
createNewDashboard,
getMenuItems,
newDashboardState.loading,
newDashboardState.error,
getText,
searchString,
],
);

View File

@ -122,12 +122,14 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
fields: selectedFields,
linesPerRow: options.maxLines,
appendTo: 'end',
activeLogIndex,
}}
/>
) : (
<Card style={{ width: '100%' }} bodyStyle={CARD_BODY_STYLE}>
<Virtuoso
ref={ref}
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
data={logs}
totalCount={logs.length}
itemContent={getItemContent}

View File

@ -1,3 +1,4 @@
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { LIVE_TAIL_GRAPH_INTERVAL } from 'constants/liveTail';
import { PANEL_TYPES } from 'constants/queryBuilder';
import LogsExplorerChart from 'container/LogsExplorerChart';
@ -41,6 +42,7 @@ function LiveLogsListChart({
const { data, isFetching } = useGetExplorerQueryRange(
listChartQuery,
PANEL_TYPES.TIME_SERIES,
DEFAULT_ENTITY_VERSION,
{
enabled: isConnectionOpen,
refetchInterval: LIVE_TAIL_GRAPH_INTERVAL,

View File

@ -0,0 +1,25 @@
.context-log-renderer {
.virtuoso-list {
overflow-y: hidden !important;
&::-webkit-scrollbar {
width: 0.3rem;
height: 0.3rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-slate-200);
}
.ant-row {
width: fit-content;
}
}
}

View File

@ -0,0 +1,129 @@
import './ContextLogRenderer.styles.scss';
import { Skeleton } from 'antd';
import RawLogView from 'components/Logs/RawLogView';
import ShowButton from 'container/LogsContextList/ShowButton';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { useCallback, useEffect, useState } from 'react';
import { Virtuoso } from 'react-virtuoso';
import { ILog } from 'types/api/logs/log';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { useContextLogData } from './useContextLogData';
function ContextLogRenderer({
isEdit,
query,
log,
filters,
}: ContextLogRendererProps): JSX.Element {
const [prevLogPage, setPrevLogPage] = useState<number>(1);
const [afterLogPage, setAfterLogPage] = useState<number>(1);
const [logs, setLogs] = useState<ILog[]>([log]);
const {
logs: previousLogs,
isFetching: isPreviousLogsFetching,
handleShowNextLines: handlePreviousLogsShowNextLine,
} = useContextLogData({
log,
filters,
isEdit,
query,
order: ORDERBY_FILTERS.ASC,
page: prevLogPage,
setPage: setPrevLogPage,
});
const {
logs: afterLogs,
isFetching: isAfterLogsFetching,
handleShowNextLines: handleAfterLogsShowNextLine,
} = useContextLogData({
log,
filters,
isEdit,
query,
order: ORDERBY_FILTERS.DESC,
page: afterLogPage,
setPage: setAfterLogPage,
});
useEffect(() => {
setLogs((prev) => [...previousLogs, ...prev]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [previousLogs]);
useEffect(() => {
setLogs((prev) => [...prev, ...afterLogs]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [afterLogs]);
useEffect(() => {
setLogs([log]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filters]);
const getItemContent = useCallback(
(_: number, logTorender: ILog): JSX.Element => (
<RawLogView
isActiveLog={logTorender.id === log.id}
isReadOnly
isTextOverflowEllipsisDisabled
key={logTorender.id}
data={logTorender}
linesPerRow={1}
/>
),
[log.id],
);
return (
<div className="context-log-renderer">
<ShowButton
isLoading={isPreviousLogsFetching}
isDisabled={false}
order={ORDERBY_FILTERS.ASC}
onClick={handlePreviousLogsShowNextLine}
/>
{isPreviousLogsFetching && (
<Skeleton
style={{
height: '100%',
padding: '16px',
}}
/>
)}
<Virtuoso
className="virtuoso-list"
initialTopMostItemIndex={0}
data={logs}
itemContent={getItemContent}
style={{ height: `calc(${logs.length} * 32px)` }}
/>
{isAfterLogsFetching && (
<Skeleton
style={{
height: '100%',
padding: '16px',
}}
/>
)}
<ShowButton
isLoading={isAfterLogsFetching}
isDisabled={false}
order={ORDERBY_FILTERS.DESC}
onClick={handleAfterLogsShowNextLine}
/>
</div>
);
}
interface ContextLogRendererProps {
isEdit: boolean;
query: Query;
log: ILog;
filters: TagFilter | null;
}
export default ContextLogRenderer;

View File

@ -1,3 +1,23 @@
.log-context-container {
border: 1px solid var(--bg-slate-400);
}
flex: 1;
position: relative;
overflow: scroll;
overflow-x: hidden;
&::-webkit-scrollbar {
width: 0.3rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-slate-200);
}
}

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