Compare commits
55 Commits
v0.37.1
...
feat/logs-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a959328411 | ||
|
|
8452b69d1c | ||
|
|
85beee876c | ||
|
|
19c42bf5ee | ||
|
|
a40fae85b8 | ||
|
|
3d83872196 | ||
|
|
6e41f900dc | ||
|
|
c5dbac793b | ||
|
|
8b2f8c5b1e | ||
|
|
b7dc7d519b | ||
|
|
58d63695d8 | ||
|
|
b1133bf812 | ||
|
|
065d064ac1 | ||
|
|
4ec7dc550b | ||
|
|
554c4332c4 | ||
|
|
9d689693b4 | ||
|
|
26bc94fc46 | ||
|
|
6837c41090 | ||
|
|
8fe0e60208 | ||
|
|
00b111fbe3 | ||
|
|
f5b5a9a657 | ||
|
|
ac835c80e9 | ||
|
|
2cf0bb4fa5 | ||
|
|
0f44246795 | ||
|
|
64307f323f | ||
|
|
2f361de693 | ||
|
|
457380c065 | ||
|
|
96e3d00e74 | ||
|
|
d224e08145 | ||
|
|
13ced00a35 | ||
|
|
5c60a862e5 | ||
|
|
78c9330666 | ||
|
|
01fc7a7fd4 | ||
|
|
0200fb3a21 | ||
|
|
e977963763 | ||
|
|
824d9aaf85 | ||
|
|
4db3e5e542 | ||
|
|
a8b293a510 | ||
|
|
4a4f48cec8 | ||
|
|
7e5cf65ea3 | ||
|
|
bb7417ffbd | ||
|
|
085cf34a49 | ||
|
|
be27a92fc9 | ||
|
|
253137a6b8 | ||
|
|
fce7ab7d24 | ||
|
|
71f6b355c4 | ||
|
|
110b545454 | ||
|
|
5b0e3d375a | ||
|
|
9e05cb48fe | ||
|
|
6d67ca72a0 | ||
|
|
0626081eee | ||
|
|
199d52b39f | ||
|
|
204cad8448 | ||
|
|
8c6096d60e | ||
|
|
9de9fb5863 |
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
@@ -19,4 +19,4 @@ jobs:
|
||||
- name: 'Dependency Review'
|
||||
with:
|
||||
fail-on-severity: high
|
||||
uses: actions/dependency-review-action@v2
|
||||
uses: actions/dependency-review-action@v3
|
||||
|
||||
11
.github/workflows/e2e-k3s.yaml
vendored
11
.github/workflows/e2e-k3s.yaml
vendored
@@ -15,6 +15,11 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup golang
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.21"
|
||||
|
||||
- name: Build query-service image
|
||||
env:
|
||||
DEV_BUILD: 1
|
||||
@@ -65,9 +70,9 @@ jobs:
|
||||
- name: Kick off a sample-app workload
|
||||
run: |
|
||||
# start the locust swarm
|
||||
kubectl -n sample-application run strzal --image=djbingham/curl \
|
||||
--restart='OnFailure' -i --rm --command -- curl -X POST -F \
|
||||
'locust_count=6' -F 'hatch_rate=2' http://locust-master:8089/swarm
|
||||
kubectl --namespace sample-application run strzal --image=djbingham/curl \
|
||||
--restart='OnFailure' -i --tty --rm --command -- curl -X POST -F \
|
||||
'user_count=6' -F 'spawn_rate=2' http://locust-master:8089/swarm
|
||||
|
||||
- name: Get short commit SHA, display tunnel URL and IP Address of the worker node
|
||||
id: get-subdomain
|
||||
|
||||
20
.github/workflows/push.yaml
vendored
20
.github/workflows/push.yaml
vendored
@@ -20,13 +20,13 @@ jobs:
|
||||
with:
|
||||
go-version: "1.21"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
version: latest
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -64,13 +64,13 @@ jobs:
|
||||
with:
|
||||
go-version: "1.21"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
version: latest
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -115,11 +115,11 @@ jobs:
|
||||
run: npm run lint
|
||||
continue-on-error: true
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
version: latest
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -164,11 +164,11 @@ jobs:
|
||||
run: npm run lint
|
||||
continue-on-error: true
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
version: latest
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
2
.github/workflows/staging-deployment.yaml
vendored
2
.github/workflows/staging-deployment.yaml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
environment: staging
|
||||
steps:
|
||||
- name: Executing remote ssh commands using ssh key
|
||||
uses: appleboy/ssh-action@v0.1.8
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
env:
|
||||
GITHUB_BRANCH: develop
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
|
||||
2
.github/workflows/testing-deployment.yaml
vendored
2
.github/workflows/testing-deployment.yaml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
if: ${{ github.event.label.name == 'testing-deploy' }}
|
||||
steps:
|
||||
- name: Executing remote ssh commands using ssh key
|
||||
uses: appleboy/ssh-action@v0.1.8
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
env:
|
||||
GITHUB_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
|
||||
@@ -146,7 +146,7 @@ services:
|
||||
condition: on-failure
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.37.1
|
||||
image: signoz/query-service:0.38.0
|
||||
command:
|
||||
[
|
||||
"-config=/root/config/prometheus.yml",
|
||||
@@ -186,7 +186,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.37.1
|
||||
image: signoz/frontend:0.38.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.8
|
||||
image: signoz/signoz-otel-collector:0.88.9
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-config.yaml",
|
||||
@@ -237,7 +237,7 @@ services:
|
||||
- query-service
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:0.88.8
|
||||
image: signoz/signoz-schema-migrator:0.88.9
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
||||
@@ -123,15 +123,7 @@ exporters:
|
||||
clickhouselogsexporter:
|
||||
dsn: tcp://clickhouse:9000/
|
||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||
timeout: 5s
|
||||
sending_queue:
|
||||
queue_size: 100
|
||||
retry_on_failure:
|
||||
enabled: true
|
||||
initial_interval: 5s
|
||||
max_interval: 30s
|
||||
max_elapsed_time: 300s
|
||||
|
||||
timeout: 10s
|
||||
extensions:
|
||||
health_check:
|
||||
endpoint: 0.0.0.0:13133
|
||||
|
||||
@@ -66,7 +66,7 @@ services:
|
||||
- --storage.path=/data
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.8}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.9}
|
||||
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.8
|
||||
image: signoz/signoz-otel-collector:0.88.9
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-config.yaml",
|
||||
|
||||
@@ -164,7 +164,7 @@ services:
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.37.1}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.38.0}
|
||||
container_name: signoz-query-service
|
||||
command:
|
||||
[
|
||||
@@ -203,7 +203,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.37.1}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.38.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.8}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.9}
|
||||
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.8}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.9}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
[
|
||||
|
||||
@@ -135,14 +135,7 @@ exporters:
|
||||
clickhouselogsexporter:
|
||||
dsn: tcp://clickhouse:9000/
|
||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||
timeout: 5s
|
||||
sending_queue:
|
||||
queue_size: 100
|
||||
retry_on_failure:
|
||||
enabled: true
|
||||
initial_interval: 5s
|
||||
max_interval: 30s
|
||||
max_elapsed_time: 300s
|
||||
timeout: 10s
|
||||
|
||||
service:
|
||||
telemetry:
|
||||
|
||||
@@ -152,9 +152,9 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
|
||||
router.HandleFunc("/api/v2/metrics/query_range", am.ViewAccess(ah.queryRangeMetricsV2)).Methods(http.MethodPost)
|
||||
|
||||
// PAT APIs
|
||||
router.HandleFunc("/api/v1/pat", am.OpenAccess(ah.createPAT)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/pat", am.OpenAccess(ah.getPATs)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/pat/{id}", am.OpenAccess(ah.deletePAT)).Methods(http.MethodDelete)
|
||||
router.HandleFunc("/api/v1/pat", am.AdminAccess(ah.createPAT)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/pat", am.AdminAccess(ah.getPATs)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/pat/{id}", am.AdminAccess(ah.deletePAT)).Methods(http.MethodDelete)
|
||||
|
||||
router.HandleFunc("/api/v1/checkout", am.AdminAccess(ah.checkout)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
|
||||
|
||||
@@ -40,6 +40,7 @@ type billingDetails struct {
|
||||
BillingPeriodEnd int64 `json:"billingPeriodEnd"`
|
||||
Details details `json:"details"`
|
||||
Discount float64 `json:"discount"`
|
||||
SubscriptionStatus string `json:"subscriptionStatus"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
|
||||
@@ -139,17 +139,13 @@ func (lm *Manager) UploadUsage() {
|
||||
|
||||
zap.S().Info("uploading usage data")
|
||||
|
||||
// Try to get the org name
|
||||
orgName := ""
|
||||
orgNames, err := lm.modelDao.GetOrgs(ctx)
|
||||
if err != nil {
|
||||
zap.S().Errorf("failed to get org data: %v", zap.Error(err))
|
||||
} else {
|
||||
if len(orgNames) != 1 {
|
||||
zap.S().Errorf("expected one org but got %d orgs", len(orgNames))
|
||||
} else {
|
||||
orgName = orgNames[0].Name
|
||||
}
|
||||
orgNames, orgError := lm.modelDao.GetOrgs(ctx)
|
||||
if orgError != nil {
|
||||
zap.S().Errorf("failed to get org data: %v", zap.Error(orgError))
|
||||
}
|
||||
if len(orgNames) == 1 {
|
||||
orgName = orgNames[0].Name
|
||||
}
|
||||
|
||||
usagesPayload := []model.Usage{}
|
||||
|
||||
@@ -210,7 +210,8 @@
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.(js|jsx|ts|tsx)": [
|
||||
"eslint --fix"
|
||||
"eslint --fix",
|
||||
"sh scripts/typecheck-staged.sh"
|
||||
]
|
||||
},
|
||||
"resolutions": {
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"button_cancel": "No",
|
||||
"field_promql_expr": "PromQL Expression",
|
||||
"field_alert_name": "Alert Name",
|
||||
"field_notification_channel": "Notification Channel",
|
||||
"field_alert_desc": "Alert Description",
|
||||
"field_labels": "Labels",
|
||||
"field_severity": "Severity",
|
||||
@@ -100,7 +101,7 @@
|
||||
"user_guide_ch_step3a": "Set alert severity, name and descriptions",
|
||||
"user_guide_ch_step3b": "Add tags to the alert in the Label field if needed",
|
||||
"user_tooltip_more_help": "More details on how to create alerts",
|
||||
"choose_alert_type": "Choose a type for the alert:",
|
||||
"choose_alert_type": "Choose a type for the alert",
|
||||
"metric_based_alert": "Metric based Alert",
|
||||
"metric_based_alert_desc": "Send a notification when a condition occurs in the metric data",
|
||||
"log_based_alert": "Log-based Alert",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"field_promql_expr": "PromQL Expression",
|
||||
"field_alert_name": "Alert Name",
|
||||
"field_alert_desc": "Alert Description",
|
||||
"field_notification_channel": "Notification Channel",
|
||||
"field_labels": "Labels",
|
||||
"field_severity": "Severity",
|
||||
"option_critical": "Critical",
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"field_promql_expr": "PromQL Expression",
|
||||
"field_alert_name": "Alert Name",
|
||||
"field_alert_desc": "Alert Description",
|
||||
"field_notification_channel": "Notification Channel",
|
||||
"field_labels": "Labels",
|
||||
"field_severity": "Severity",
|
||||
"option_critical": "Critical",
|
||||
@@ -100,7 +101,7 @@
|
||||
"user_guide_ch_step3a": "Set alert severity, name and descriptions",
|
||||
"user_guide_ch_step3b": "Add tags to the alert in the Label field if needed",
|
||||
"user_tooltip_more_help": "More details on how to create alerts",
|
||||
"choose_alert_type": "Choose a type for the alert:",
|
||||
"choose_alert_type": "Choose a type for the alert",
|
||||
"metric_based_alert": "Metric based Alert",
|
||||
"metric_based_alert_desc": "Send a notification when a condition occurs in the metric data",
|
||||
"log_based_alert": "Log-based Alert",
|
||||
|
||||
@@ -42,5 +42,7 @@
|
||||
"processor_span_id_placeholder": "Parse Span ID from",
|
||||
"processor_trace_flags_placeholder": "Parse Trace flags from",
|
||||
"processor_from_placeholder": "From",
|
||||
"processor_to_placeholder": "To"
|
||||
"processor_to_placeholder": "To",
|
||||
"share_pipelines": "Share Pipelines",
|
||||
"import_pipelines": "Import Pipelines"
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"field_promql_expr": "PromQL Expression",
|
||||
"field_alert_name": "Alert Name",
|
||||
"field_alert_desc": "Alert Description",
|
||||
"field_notification_channel": "Notification Channel",
|
||||
"field_labels": "Labels",
|
||||
"field_severity": "Severity",
|
||||
"option_critical": "Critical",
|
||||
|
||||
25
frontend/scripts/typecheck-staged.sh
Normal file
25
frontend/scripts/typecheck-staged.sh
Normal file
@@ -0,0 +1,25 @@
|
||||
files="";
|
||||
|
||||
# lint-staged will pass all files in $1 $2 $3 etc. iterate and concat.
|
||||
for var in "$@"
|
||||
do
|
||||
files="$files \"$var\","
|
||||
done
|
||||
|
||||
# create temporary tsconfig which includes only passed files
|
||||
str="{
|
||||
\"extends\": \"./tsconfig.json\",
|
||||
\"include\": [\"src/types/global.d.ts\",\"src/typings/window.ts\", $files]
|
||||
}"
|
||||
echo $str > tsconfig.tmp
|
||||
|
||||
# run typecheck using temp config
|
||||
tsc -p ./tsconfig.tmp
|
||||
|
||||
# capture exit code of tsc
|
||||
code=$?
|
||||
|
||||
# delete temp config
|
||||
rm ./tsconfig.tmp
|
||||
|
||||
exit $code
|
||||
@@ -28,7 +28,11 @@ import AppReducer, { User } from 'types/reducer/app';
|
||||
import { extractDomain, isCloudUser, isEECloudUser } from 'utils/app';
|
||||
|
||||
import PrivateRoute from './Private';
|
||||
import defaultRoutes, { AppRoutes, SUPPORT_ROUTE } from './routes';
|
||||
import defaultRoutes, {
|
||||
AppRoutes,
|
||||
LIST_LICENSES,
|
||||
SUPPORT_ROUTE,
|
||||
} from './routes';
|
||||
|
||||
function App(): JSX.Element {
|
||||
const themeConfig = useThemeConfig();
|
||||
@@ -150,6 +154,10 @@ function App(): JSX.Element {
|
||||
if (isCloudUserVal || isEECloudUser()) {
|
||||
const newRoutes = [...routes, SUPPORT_ROUTE];
|
||||
|
||||
setRoutes(newRoutes);
|
||||
} else {
|
||||
const newRoutes = [...routes, LIST_LICENSES];
|
||||
|
||||
setRoutes(newRoutes);
|
||||
}
|
||||
|
||||
|
||||
@@ -191,13 +191,6 @@ const routes: AppRoutes[] = [
|
||||
component: AllErrors,
|
||||
key: 'ALL_ERROR',
|
||||
},
|
||||
{
|
||||
path: ROUTES.LIST_LICENSES,
|
||||
exact: true,
|
||||
component: LicensePage,
|
||||
isPrivate: true,
|
||||
key: 'LIST_LICENSES',
|
||||
},
|
||||
{
|
||||
path: ROUTES.ERROR_DETAIL,
|
||||
exact: true,
|
||||
@@ -320,6 +313,14 @@ export const SUPPORT_ROUTE: AppRoutes = {
|
||||
isPrivate: true,
|
||||
};
|
||||
|
||||
export const LIST_LICENSES: AppRoutes = {
|
||||
path: ROUTES.LIST_LICENSES,
|
||||
exact: true,
|
||||
component: LicensePage,
|
||||
isPrivate: true,
|
||||
key: 'LIST_LICENSES',
|
||||
};
|
||||
|
||||
export interface AppRoutes {
|
||||
component: RouteProps['component'];
|
||||
path: RouteProps['path'];
|
||||
|
||||
@@ -37,6 +37,12 @@
|
||||
padding: 4px 8px;
|
||||
padding-left: 0px !important;
|
||||
|
||||
&.custom-time {
|
||||
input:not(:focus) {
|
||||
min-width: 240px;
|
||||
}
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -178,7 +178,10 @@ function CustomTimePicker({
|
||||
return (
|
||||
<div className="custom-time-picker">
|
||||
<Popover
|
||||
className="timeSelection-input-container"
|
||||
className={cx(
|
||||
'timeSelection-input-container',
|
||||
selectedTime === 'custom' && inputValue === '' ? 'custom-time' : '',
|
||||
)}
|
||||
placement="bottomRight"
|
||||
getPopupContainer={popupContainer}
|
||||
content={content}
|
||||
@@ -193,12 +196,7 @@ function CustomTimePicker({
|
||||
<Input
|
||||
className="timeSelection-input"
|
||||
type="text"
|
||||
style={{
|
||||
minWidth: '120px',
|
||||
width: '100%',
|
||||
}}
|
||||
status={inputValue && inputStatus === 'error' ? 'error' : ''}
|
||||
allowClear={!isInputFocused && selectedTime === 'custom'}
|
||||
placeholder={
|
||||
isInputFocused
|
||||
? 'Time Format (1m or 2h or 3d or 4w)'
|
||||
|
||||
@@ -9,6 +9,8 @@ function Editor({
|
||||
readOnly,
|
||||
height,
|
||||
options,
|
||||
beforeMount,
|
||||
onValidate,
|
||||
}: MEditorProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
@@ -31,6 +33,8 @@ function Editor({
|
||||
options={editorOptions}
|
||||
height={height}
|
||||
onChange={onChangeHandler}
|
||||
beforeMount={beforeMount}
|
||||
onValidate={onValidate}
|
||||
data-testid="monaco-editor"
|
||||
/>
|
||||
);
|
||||
@@ -43,6 +47,8 @@ interface MEditorProps {
|
||||
readOnly?: boolean;
|
||||
height?: string;
|
||||
options?: EditorProps['options'];
|
||||
beforeMount?: EditorProps['beforeMount'];
|
||||
onValidate?: EditorProps['onValidate'];
|
||||
}
|
||||
|
||||
Editor.defaultProps = {
|
||||
@@ -51,6 +57,8 @@ Editor.defaultProps = {
|
||||
height: '40vh',
|
||||
options: {},
|
||||
onChange: (): void => {},
|
||||
beforeMount: (): void => {},
|
||||
onValidate: (): void => {},
|
||||
};
|
||||
|
||||
export default Editor;
|
||||
|
||||
@@ -13,6 +13,8 @@ import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
// hooks
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { isEmpty, isUndefined } from 'lodash-es';
|
||||
import {
|
||||
KeyboardEvent,
|
||||
MouseEvent,
|
||||
@@ -39,10 +41,13 @@ function RawLogView({
|
||||
data,
|
||||
linesPerRow,
|
||||
isTextOverflowEllipsisDisabled,
|
||||
selectedFields = [],
|
||||
}: RawLogViewProps): JSX.Element {
|
||||
const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink(
|
||||
data.id,
|
||||
);
|
||||
const flattenLogData = useMemo(() => FlatLogData(data), [data]);
|
||||
|
||||
const {
|
||||
activeLog: activeContextLog,
|
||||
onSetActiveLog: handleSetActiveContextLog,
|
||||
@@ -62,12 +67,31 @@ function RawLogView({
|
||||
|
||||
const severityText = data.severity_text ? `${data.severity_text} |` : '';
|
||||
|
||||
const updatedSelecedFields = useMemo(
|
||||
() => selectedFields.filter((e) => e.name !== 'id'),
|
||||
[selectedFields],
|
||||
);
|
||||
|
||||
const attributesValues = updatedSelecedFields
|
||||
.map((field) => flattenLogData[field.name])
|
||||
.filter((attribute) => !isUndefined(attribute) && !isEmpty(attribute));
|
||||
|
||||
let attributesText = attributesValues.join(' | ');
|
||||
|
||||
if (attributesText.length > 0) {
|
||||
attributesText += ' | ';
|
||||
}
|
||||
|
||||
const text = useMemo(
|
||||
() =>
|
||||
typeof data.timestamp === 'string'
|
||||
? `${dayjs(data.timestamp).format()} | ${severityText} ${data.body}`
|
||||
: `${dayjs(data.timestamp / 1e6).format()} | ${severityText} ${data.body}`,
|
||||
[data.timestamp, data.body, severityText],
|
||||
? `${dayjs(data.timestamp).format()} | ${attributesText} ${severityText} ${
|
||||
data.body
|
||||
}`
|
||||
: `${dayjs(
|
||||
data.timestamp / 1e6,
|
||||
).format()} | ${attributesText} ${severityText} ${data.body}`,
|
||||
[data.timestamp, data.body, severityText, attributesText],
|
||||
);
|
||||
|
||||
const handleClickExpand = useCallback(() => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
export interface RawLogViewProps {
|
||||
@@ -6,6 +7,7 @@ export interface RawLogViewProps {
|
||||
isTextOverflowEllipsisDisabled?: boolean;
|
||||
data: ILog;
|
||||
linesPerRow: number;
|
||||
selectedFields?: IField[];
|
||||
}
|
||||
|
||||
export interface RawLogContentProps {
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
.DynamicColumnTable {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.dynamicColumnTable-button {
|
||||
align-self: flex-end;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.dynamicColumnTable-button {
|
||||
align-self: flex-end;
|
||||
margin: 10px 0;
|
||||
|
||||
&.filter-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dynamicColumnsTable-items {
|
||||
display: flex;
|
||||
width: 10.625rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
width: 10.625rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dynamicColumnsTable-items {
|
||||
flex-direction: column;
|
||||
width: auto;
|
||||
text-align: center;
|
||||
}
|
||||
.dynamicColumnsTable-items {
|
||||
flex-direction: column;
|
||||
width: auto;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import './DynamicColumnTable.syles.scss';
|
||||
|
||||
import { SettingOutlined } from '@ant-design/icons';
|
||||
import { Button, Dropdown, MenuProps, Switch } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { SlidersHorizontal } from 'lucide-react';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
@@ -90,9 +90,9 @@ function DynamicColumnTable({
|
||||
trigger={['click']}
|
||||
>
|
||||
<Button
|
||||
className="dynamicColumnTable-button"
|
||||
className="dynamicColumnTable-button filter-btn"
|
||||
size="middle"
|
||||
icon={<SettingOutlined />}
|
||||
icon={<SlidersHorizontal size={14} />}
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
|
||||
@@ -15,4 +15,5 @@ export enum LOCALSTORAGE {
|
||||
LOGGED_IN_USER_EMAIL = 'LOGGED_IN_USER_EMAIL',
|
||||
CHAT_SUPPORT = 'CHAT_SUPPORT',
|
||||
IS_IDENTIFIED_USER = 'IS_IDENTIFIED_USER',
|
||||
DASHBOARD_VARIABLES = 'DASHBOARD_VARIABLES',
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Row } from 'antd';
|
||||
import { Row, Typography } from 'antd';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
@@ -33,7 +33,14 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
||||
|
||||
return (
|
||||
<SelectTypeContainer>
|
||||
<h3> {t('choose_alert_type')} </h3>
|
||||
<Typography.Title
|
||||
level={4}
|
||||
style={{
|
||||
padding: '0 8px',
|
||||
}}
|
||||
>
|
||||
{t('choose_alert_type')}
|
||||
</Typography.Title>
|
||||
<Row>{renderOptions}</Row>
|
||||
</SelectTypeContainer>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Form, Select } from 'antd';
|
||||
import { Form, Select, Switch } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AlertDef, Labels } from 'types/api/alerts/def';
|
||||
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
|
||||
@@ -7,7 +8,6 @@ import { popupContainer } from 'utils/selectPopupContainer';
|
||||
import ChannelSelect from './ChannelSelect';
|
||||
import LabelSelect from './labels';
|
||||
import {
|
||||
ChannelSelectTip,
|
||||
FormContainer,
|
||||
FormItemMedium,
|
||||
InputSmall,
|
||||
@@ -19,14 +19,41 @@ import {
|
||||
const { Option } = Select;
|
||||
|
||||
interface BasicInfoProps {
|
||||
isNewRule: boolean;
|
||||
alertDef: AlertDef;
|
||||
setAlertDef: (a: AlertDef) => void;
|
||||
}
|
||||
|
||||
function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
|
||||
// init namespace for translations
|
||||
function BasicInfo({
|
||||
isNewRule,
|
||||
alertDef,
|
||||
setAlertDef,
|
||||
}: BasicInfoProps): JSX.Element {
|
||||
const { t } = useTranslation('alerts');
|
||||
|
||||
const [
|
||||
shouldBroadCastToAllChannels,
|
||||
setShouldBroadCastToAllChannels,
|
||||
] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const hasPreferredChannels =
|
||||
(alertDef.preferredChannels && alertDef.preferredChannels.length > 0) ||
|
||||
isNewRule;
|
||||
|
||||
setShouldBroadCastToAllChannels(!hasPreferredChannels);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleBroadcastToAllChannels = (shouldBroadcast: boolean): void => {
|
||||
setShouldBroadCastToAllChannels(shouldBroadcast);
|
||||
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
broadcastToAll: shouldBroadcast,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StepHeading> {t('alert_form_step3')} </StepHeading>
|
||||
@@ -105,18 +132,38 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
|
||||
initialValues={alertDef.labels}
|
||||
/>
|
||||
</FormItemMedium>
|
||||
<FormItemMedium label="Notification Channels">
|
||||
<ChannelSelect
|
||||
currentValue={alertDef.preferredChannels}
|
||||
onSelectChannels={(preferredChannels): void => {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
preferredChannels,
|
||||
});
|
||||
}}
|
||||
|
||||
<FormItemMedium
|
||||
name="alert_all_configured_channels"
|
||||
label="Alert all the configured channels"
|
||||
>
|
||||
<Switch
|
||||
checked={shouldBroadCastToAllChannels}
|
||||
onChange={handleBroadcastToAllChannels}
|
||||
/>
|
||||
<ChannelSelectTip> {t('channel_select_tooltip')}</ChannelSelectTip>
|
||||
</FormItemMedium>
|
||||
|
||||
{!shouldBroadCastToAllChannels && (
|
||||
<FormItemMedium
|
||||
label="Notification Channels"
|
||||
name="notification_channels"
|
||||
required
|
||||
rules={[
|
||||
{ required: true, message: requireErrorMessage(t('field_alert_name')) },
|
||||
]}
|
||||
>
|
||||
<ChannelSelect
|
||||
disabled={shouldBroadCastToAllChannels}
|
||||
currentValue={alertDef.preferredChannels}
|
||||
onSelectChannels={(preferredChannels): void => {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
preferredChannels,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormItemMedium>
|
||||
)}
|
||||
</FormContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -8,11 +8,13 @@ import { useTranslation } from 'react-i18next';
|
||||
import { StyledSelect } from './styles';
|
||||
|
||||
export interface ChannelSelectProps {
|
||||
disabled?: boolean;
|
||||
currentValue?: string[];
|
||||
onSelectChannels: (s: string[]) => void;
|
||||
}
|
||||
|
||||
function ChannelSelect({
|
||||
disabled,
|
||||
currentValue,
|
||||
onSelectChannels,
|
||||
}: ChannelSelectProps): JSX.Element | null {
|
||||
@@ -52,6 +54,7 @@ function ChannelSelect({
|
||||
};
|
||||
return (
|
||||
<StyledSelect
|
||||
disabled={disabled}
|
||||
status={error ? 'error' : ''}
|
||||
mode="multiple"
|
||||
style={{ width: '100%' }}
|
||||
@@ -68,6 +71,7 @@ function ChannelSelect({
|
||||
}
|
||||
|
||||
ChannelSelect.defaultProps = {
|
||||
disabled: false,
|
||||
currentValue: [],
|
||||
};
|
||||
export default ChannelSelect;
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
import UserGuide from './UserGuide';
|
||||
import { getSelectedQueryOptions } from './utils';
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function FormAlertRules({
|
||||
alertType,
|
||||
formInstance,
|
||||
@@ -78,6 +79,8 @@ function FormAlertRules({
|
||||
// use query client
|
||||
const ruleCache = useQueryClient();
|
||||
|
||||
const isNewRule = ruleId === 0;
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// alertDef holds the form values to be posted
|
||||
@@ -108,8 +111,17 @@ function FormAlertRules({
|
||||
useShareBuilderUrl(sq);
|
||||
|
||||
useEffect(() => {
|
||||
setAlertDef(initialValue);
|
||||
}, [initialValue]);
|
||||
const broadcastToSpecificChannels =
|
||||
(initialValue &&
|
||||
initialValue.preferredChannels &&
|
||||
initialValue.preferredChannels.length > 0) ||
|
||||
isNewRule;
|
||||
|
||||
setAlertDef({
|
||||
...initialValue,
|
||||
broadcastToAll: !broadcastToSpecificChannels,
|
||||
});
|
||||
}, [initialValue, isNewRule]);
|
||||
|
||||
useEffect(() => {
|
||||
// Set selectedQueryName based on the length of queryOptions
|
||||
@@ -243,6 +255,7 @@ function FormAlertRules({
|
||||
const preparePostData = (): AlertDef => {
|
||||
const postableAlert: AlertDef = {
|
||||
...alertDef,
|
||||
preferredChannels: alertDef.broadcastToAll ? [] : alertDef.preferredChannels,
|
||||
alertType,
|
||||
source: window?.location.toString(),
|
||||
ruleType:
|
||||
@@ -386,7 +399,11 @@ function FormAlertRules({
|
||||
}, [t, isFormValid, memoizedPreparePostData, notifications]);
|
||||
|
||||
const renderBasicInfo = (): JSX.Element => (
|
||||
<BasicInfo alertDef={alertDef} setAlertDef={setAlertDef} />
|
||||
<BasicInfo
|
||||
alertDef={alertDef}
|
||||
setAlertDef={setAlertDef}
|
||||
isNewRule={isNewRule}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderQBChartPreview = (): JSX.Element => (
|
||||
@@ -421,8 +438,6 @@ function FormAlertRules({
|
||||
/>
|
||||
);
|
||||
|
||||
const isNewRule = ruleId === 0;
|
||||
|
||||
const isAlertNameMissing = !formInstance.getFieldValue('alert');
|
||||
|
||||
const isAlertAvialableToSave =
|
||||
@@ -442,6 +457,10 @@ function FormAlertRules({
|
||||
}));
|
||||
};
|
||||
|
||||
const isChannelConfigurationValid =
|
||||
alertDef?.broadcastToAll ||
|
||||
(alertDef.preferredChannels && alertDef.preferredChannels.length > 0);
|
||||
|
||||
return (
|
||||
<>
|
||||
{Element}
|
||||
@@ -489,7 +508,11 @@ function FormAlertRules({
|
||||
type="primary"
|
||||
onClick={onSaveHandler}
|
||||
icon={<SaveOutlined />}
|
||||
disabled={isAlertNameMissing || isAlertAvialableToSave}
|
||||
disabled={
|
||||
isAlertNameMissing ||
|
||||
isAlertAvialableToSave ||
|
||||
!isChannelConfigurationValid
|
||||
}
|
||||
>
|
||||
{isNewRule ? t('button_createrule') : t('button_savechanges')}
|
||||
</ActionButton>
|
||||
@@ -497,6 +520,7 @@ function FormAlertRules({
|
||||
|
||||
<ActionButton
|
||||
loading={loading || false}
|
||||
disabled={isAlertNameMissing || !isChannelConfigurationValid}
|
||||
type="default"
|
||||
onClick={onTestRuleHandler}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.full-view-header-container {
|
||||
.full-screen-header-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 24px 0;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import './FullViewHeader.styles.scss';
|
||||
import './FullScreenHeader.styles.scss';
|
||||
|
||||
import history from 'lib/history';
|
||||
|
||||
export default function FullViewHeader({
|
||||
export default function FullScreenHeader({
|
||||
overrideRoute,
|
||||
}: {
|
||||
overrideRoute?: string;
|
||||
@@ -13,7 +13,7 @@ export default function FullViewHeader({
|
||||
history.push(overrideRoute || '/');
|
||||
};
|
||||
return (
|
||||
<div className="full-view-header-container">
|
||||
<div className="full-screen-header-container">
|
||||
<div className="brand-logo" onClick={handleLogoClick}>
|
||||
<img src="/Logos/signoz-brand-logo.svg" alt="SigNoz" />
|
||||
|
||||
@@ -23,6 +23,6 @@ export default function FullViewHeader({
|
||||
);
|
||||
}
|
||||
|
||||
FullViewHeader.defaultProps = {
|
||||
FullScreenHeader.defaultProps = {
|
||||
overrideRoute: '/',
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
.general-settings-container {
|
||||
.ant-card-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import './GeneralSettingsCloud.styles.scss';
|
||||
|
||||
import { Card, Typography } from 'antd';
|
||||
import { Info } from 'lucide-react';
|
||||
|
||||
export default function GeneralSettingsCloud(): JSX.Element {
|
||||
return (
|
||||
<Card className="general-settings-container">
|
||||
<Info size={16} />
|
||||
<Typography.Text>
|
||||
Please <a href="mailto:cloud-support@signoz.io"> email us </a> or connect
|
||||
with us via intercom support to change the retention period.
|
||||
</Typography.Text>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
3
frontend/src/container/GeneralSettingsCloud/index.tsx
Normal file
3
frontend/src/container/GeneralSettingsCloud/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import GeneralSettingsCloud from './GeneralSettingsCloud';
|
||||
|
||||
export default GeneralSettingsCloud;
|
||||
@@ -105,7 +105,7 @@ function FullView({
|
||||
panelTypeAndGraphManagerVisibility: PANEL_TYPES_VS_FULL_VIEW_TABLE,
|
||||
});
|
||||
|
||||
const chartData = getUPlotChartData(response?.data?.payload);
|
||||
const chartData = getUPlotChartData(response?.data?.payload, widget.fillSpans);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
|
||||
@@ -124,6 +124,7 @@ function WidgetGraphComponent({
|
||||
if (setSelectedDashboard && updatedDashboard.payload) {
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
}
|
||||
setDeleteModal(false);
|
||||
featureResponse.refetch();
|
||||
},
|
||||
onError: () => {
|
||||
@@ -255,6 +256,7 @@ function WidgetGraphComponent({
|
||||
destroyOnClose
|
||||
onCancel={onDeleteModelHandler}
|
||||
open={deleteModal}
|
||||
confirmLoading={updateDashboardMutation.isLoading}
|
||||
title="Delete"
|
||||
height="10vh"
|
||||
onOk={onDeleteHandler}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
.fullscreen-grid-container {
|
||||
overflow: auto;
|
||||
margin-top: 1rem;
|
||||
|
||||
.react-grid-layout {
|
||||
border: none !important;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,3 +15,9 @@
|
||||
height: calc(100% - 30px);
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.fullscreen-grid-container {
|
||||
background-color: rgb(250, 250, 250);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const [dashboardLayout, setDashboardLayout] = useState(layouts);
|
||||
const [dashboardLayout, setDashboardLayout] = useState<Layout[]>([]);
|
||||
|
||||
const updateDashboardMutation = useUpdateDashboard();
|
||||
|
||||
@@ -77,6 +77,10 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
userRole,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setDashboardLayout(layouts);
|
||||
}, [layouts]);
|
||||
|
||||
const onSaveHandler = (): void => {
|
||||
if (!selectedDashboard) return;
|
||||
|
||||
@@ -196,6 +200,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
name={currentWidget?.id || ''}
|
||||
headerMenuList={widgetActions}
|
||||
variables={variables}
|
||||
fillSpans={currentWidget?.fillSpans}
|
||||
/>
|
||||
</Card>
|
||||
</CardContainer>
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useCallback } from 'react';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
|
||||
import { EMPTY_WIDGET_LAYOUT } from './config';
|
||||
import GraphLayoutContainer from './GridCardLayout';
|
||||
|
||||
function GridGraph(): JSX.Element {
|
||||
const { handleToggleDashboardSlider, setLayouts } = useDashboard();
|
||||
const { handleToggleDashboardSlider } = useDashboard();
|
||||
|
||||
const onEmptyWidgetHandler = useCallback(() => {
|
||||
handleToggleDashboardSlider(true);
|
||||
|
||||
setLayouts((preLayout: Layout[]) => [
|
||||
EMPTY_WIDGET_LAYOUT,
|
||||
...(preLayout || []),
|
||||
]);
|
||||
}, [handleToggleDashboardSlider, setLayouts]);
|
||||
}, [handleToggleDashboardSlider]);
|
||||
|
||||
return <GraphLayoutContainer onAddPanelHandler={onEmptyWidgetHandler} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.delete-modal {
|
||||
.ant-modal-confirm-body {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import './DeleteButton.styles.scss';
|
||||
|
||||
import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
|
||||
import { Modal, Tooltip, Typography } from 'antd';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
@@ -64,6 +66,7 @@ function DeleteButton({
|
||||
okText: 'Delete',
|
||||
okButtonProps: { danger: true },
|
||||
centered: true,
|
||||
className: 'delete-modal',
|
||||
});
|
||||
}, [modal, name, deleteDashboardMutation, notifications, t, queryClient]);
|
||||
|
||||
|
||||
@@ -3,18 +3,25 @@ import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
export const filterDashboard = (
|
||||
searchValue: string,
|
||||
dashboardList: Dashboard[],
|
||||
): any[] => {
|
||||
// Convert the searchValue to lowercase for case-insensitive search
|
||||
const searchValueLowerCase = searchValue.toLowerCase();
|
||||
): Dashboard[] => {
|
||||
const searchValueLowerCase = searchValue?.toLowerCase();
|
||||
|
||||
// Use the filter method to find matching objects
|
||||
// Filter by title, description, tags
|
||||
return dashboardList.filter((item: Dashboard) => {
|
||||
// Convert each property value to lowercase for case-insensitive search
|
||||
const itemValues = Object.values(item?.data).map((value) =>
|
||||
value.toString().toLowerCase(),
|
||||
);
|
||||
const { title, description, tags } = item.data;
|
||||
const itemValuesNew = [title, description];
|
||||
|
||||
if (tags && tags.length > 0) {
|
||||
itemValuesNew.push(...tags);
|
||||
}
|
||||
|
||||
// Check if any property value contains the searchValue
|
||||
return itemValues.some((value) => value.includes(searchValueLowerCase));
|
||||
return itemValuesNew.some((value) => {
|
||||
if (value) {
|
||||
return value.toLowerCase().includes(searchValueLowerCase);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -70,7 +70,12 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
(_: number, log: ILog): JSX.Element => {
|
||||
if (options.format === 'raw') {
|
||||
return (
|
||||
<RawLogView key={log.id} data={log} linesPerRow={options.maxLines} />
|
||||
<RawLogView
|
||||
key={log.id}
|
||||
data={log}
|
||||
linesPerRow={options.maxLines}
|
||||
selectedFields={selectedFields}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,12 @@ function LogsExplorerList({
|
||||
(_: number, log: ILog): JSX.Element => {
|
||||
if (options.format === 'raw') {
|
||||
return (
|
||||
<RawLogView key={log.id} data={log} linesPerRow={options.maxLines} />
|
||||
<RawLogView
|
||||
key={log.id}
|
||||
data={log}
|
||||
linesPerRow={options.maxLines}
|
||||
selectedFields={selectedFields}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,14 @@ function LogsTable(props: LogsTableProps): JSX.Element {
|
||||
const log = logs[index];
|
||||
|
||||
if (viewMode === 'raw') {
|
||||
return <RawLogView key={log.id} data={log} linesPerRow={linesPerRow} />;
|
||||
return (
|
||||
<RawLogView
|
||||
key={log.id}
|
||||
data={log}
|
||||
linesPerRow={linesPerRow}
|
||||
selectedFields={selected}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,3 +6,11 @@
|
||||
text-overflow: ellipsis;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.variable-item {
|
||||
.variable-select {
|
||||
.ant-select-dropdown {
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import { Row } from 'antd';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
import { convertVariablesToDbFormat } from './util';
|
||||
import VariableItem from './VariableItem';
|
||||
|
||||
function DashboardVariableSelection(): JSX.Element | null {
|
||||
const { selectedDashboard, setSelectedDashboard } = useDashboard();
|
||||
const {
|
||||
selectedDashboard,
|
||||
setSelectedDashboard,
|
||||
dashboardId,
|
||||
} = useDashboard();
|
||||
|
||||
const {
|
||||
updateLocalStorageDashboardVariables,
|
||||
} = useDashboardVariablesFromLocalStorage(dashboardId);
|
||||
|
||||
const { data } = selectedDashboard || {};
|
||||
|
||||
@@ -23,8 +27,6 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
|
||||
const [variablesTableData, setVariablesTableData] = useState<any>([]);
|
||||
|
||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
useEffect(() => {
|
||||
if (variables) {
|
||||
const tableRowData = [];
|
||||
@@ -52,40 +54,6 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
setUpdate(!update);
|
||||
};
|
||||
|
||||
const updateMutation = useUpdateDashboard();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const updateVariables = (
|
||||
name: string,
|
||||
updatedVariablesData: Dashboard['data']['variables'],
|
||||
): void => {
|
||||
if (!selectedDashboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateMutation.mutateAsync(
|
||||
{
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
variables: updatedVariablesData,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: (updatedDashboard) => {
|
||||
if (updatedDashboard.payload) {
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
message: `Error updating ${name} variable`,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const onValueUpdate = (
|
||||
name: string,
|
||||
id: string,
|
||||
@@ -105,12 +73,22 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
return variableCopy;
|
||||
},
|
||||
);
|
||||
updateLocalStorageDashboardVariables(id, value, allSelected);
|
||||
|
||||
const variables = convertVariablesToDbFormat(newVariablesArr);
|
||||
|
||||
if (role !== 'VIEWER' && selectedDashboard) {
|
||||
updateVariables(name, variables);
|
||||
if (selectedDashboard) {
|
||||
setSelectedDashboard({
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
...selectedDashboard?.data,
|
||||
variables: {
|
||||
...variables,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onVarChanged(name);
|
||||
|
||||
setUpdate(!update);
|
||||
|
||||
@@ -5,15 +5,16 @@ import { WarningOutlined } from '@ant-design/icons';
|
||||
import { Input, Popover, Select, Tooltip, Typography } from 'antd';
|
||||
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
|
||||
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
|
||||
import { debounce } from 'lodash-es';
|
||||
import map from 'lodash-es/map';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { VariableResponseProps } from 'types/api/dashboard/variables/query';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { variablePropsToPayloadVariables } from '../utils';
|
||||
import { SelectItemStyle, VariableContainer, VariableValue } from './styles';
|
||||
@@ -44,6 +45,7 @@ const getSelectValue = (
|
||||
return selectedValue?.toString() || '';
|
||||
};
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function VariableItem({
|
||||
variableData,
|
||||
existingVariables,
|
||||
@@ -55,24 +57,8 @@ function VariableItem({
|
||||
[],
|
||||
);
|
||||
|
||||
const [variableValue, setVaribleValue] = useState(
|
||||
variableData?.selectedValue?.toString() || '',
|
||||
);
|
||||
|
||||
const debouncedVariableValue = useDebounce(variableValue, 500);
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<null | string>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const { selectedValue } = variableData;
|
||||
|
||||
if (selectedValue) {
|
||||
setVaribleValue(selectedValue?.toString());
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [variableData]);
|
||||
|
||||
const getDependentVariables = (queryValue: string): string[] => {
|
||||
const matches = queryValue.match(variableRegexPattern);
|
||||
|
||||
@@ -92,7 +78,12 @@ function VariableItem({
|
||||
const variableName = variableData.name || '';
|
||||
|
||||
dependentVariables?.forEach((element) => {
|
||||
dependentVariablesStr += `${element}${existingVariables[element]?.selectedValue}`;
|
||||
const [, variable] =
|
||||
Object.entries(existingVariables).find(
|
||||
([, value]) => value.name === element,
|
||||
) || [];
|
||||
|
||||
dependentVariablesStr += `${element}${variable?.selectedValue}`;
|
||||
});
|
||||
|
||||
const variableKey = dependentVariablesStr.replace(/\s/g, '');
|
||||
@@ -204,6 +195,9 @@ function VariableItem({
|
||||
}
|
||||
};
|
||||
|
||||
// do not debounce the above function as we do not need debounce in select variables
|
||||
const debouncedHandleChange = debounce(handleChange, 500);
|
||||
|
||||
const { selectedValue } = variableData;
|
||||
const selectedValueStringified = useMemo(() => getSelectValue(selectedValue), [
|
||||
selectedValue,
|
||||
@@ -219,14 +213,6 @@ function VariableItem({
|
||||
: undefined;
|
||||
const enableSelectAll = variableData.multiSelect && variableData.showALLOption;
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedVariableValue !== variableData?.selectedValue?.toString()) {
|
||||
handleChange(debouncedVariableValue);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedVariableValue]);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch options for CUSTOM Type
|
||||
if (variableData.type === 'CUSTOM') {
|
||||
@@ -240,7 +226,7 @@ function VariableItem({
|
||||
placement="top"
|
||||
title={isDashboardLocked ? 'Dashboard is locked' : ''}
|
||||
>
|
||||
<VariableContainer>
|
||||
<VariableContainer className="variable-item">
|
||||
<Typography.Text className="variable-name" ellipsis>
|
||||
${variableData.name}
|
||||
</Typography.Text>
|
||||
@@ -250,9 +236,10 @@ function VariableItem({
|
||||
placeholder="Enter value"
|
||||
disabled={isDashboardLocked}
|
||||
bordered={false}
|
||||
value={variableValue}
|
||||
key={variableData.selectedValue?.toString()}
|
||||
defaultValue={variableData.selectedValue?.toString()}
|
||||
onChange={(e): void => {
|
||||
setVaribleValue(e.target.value || '');
|
||||
debouncedHandleChange(e.target.value || '');
|
||||
}}
|
||||
style={{
|
||||
width:
|
||||
@@ -263,18 +250,25 @@ function VariableItem({
|
||||
!errorMessage &&
|
||||
optionsData && (
|
||||
<Select
|
||||
value={selectValue}
|
||||
key={
|
||||
selectValue && Array.isArray(selectValue)
|
||||
? selectValue.join(' ')
|
||||
: selectValue || variableData.id
|
||||
}
|
||||
defaultValue={selectValue}
|
||||
onChange={handleChange}
|
||||
bordered={false}
|
||||
placeholder="Select value"
|
||||
placement="bottomRight"
|
||||
mode={mode}
|
||||
dropdownMatchSelectWidth={false}
|
||||
style={SelectItemStyle}
|
||||
loading={isLoading}
|
||||
showArrow
|
||||
showSearch
|
||||
data-testid="variable-select"
|
||||
className="variable-select"
|
||||
disabled={isDashboardLocked}
|
||||
getPopupContainer={popupContainer}
|
||||
>
|
||||
{enableSelectAll && (
|
||||
<Select.Option data-testid="option-ALL" value={ALL_SELECT_VALUE}>
|
||||
|
||||
@@ -307,7 +307,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
disabled={isSaveDisabled}
|
||||
onClick={onSaveDashboard}
|
||||
>
|
||||
Save
|
||||
Save Changes
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -316,13 +316,14 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
<Button
|
||||
type="primary"
|
||||
data-testid="new-widget-save"
|
||||
loading={updateDashboardMutation.isLoading}
|
||||
disabled={isSaveDisabled}
|
||||
onClick={onSaveDashboard}
|
||||
>
|
||||
Save
|
||||
Save Changes
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={onClickDiscardHandler}>Discard</Button>
|
||||
<Button onClick={onClickDiscardHandler}>Discard Changes</Button>
|
||||
</ButtonContainer>
|
||||
|
||||
<PanelContainer>
|
||||
@@ -385,6 +386,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
closable
|
||||
onCancel={closeModal}
|
||||
onOk={onClickSaveHandler}
|
||||
confirmLoading={updateDashboardMutation.isLoading}
|
||||
centered
|
||||
open={saveModal}
|
||||
width={600}
|
||||
|
||||
@@ -25,7 +25,7 @@ export const LeftContainerWrapper = styled(Col)`
|
||||
|
||||
export const ButtonContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
gap: 8px;
|
||||
margin-bottom: 1rem;
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ArrowRightOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Typography } from 'antd';
|
||||
import getIngestionData from 'api/settings/getIngestionData';
|
||||
import cx from 'classnames';
|
||||
import FullViewHeader from 'container/FullViewHeader/FullViewHeader';
|
||||
import FullScreenHeader from 'container/FullScreenHeader/FullScreenHeader';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -211,7 +211,7 @@ export default function Onboarding(): JSX.Element {
|
||||
<div className={cx('container', isDarkMode ? 'darkMode' : 'lightMode')}>
|
||||
{activeStep === 1 && (
|
||||
<>
|
||||
<FullViewHeader />
|
||||
<FullScreenHeader />
|
||||
<div className="onboardingHeader">
|
||||
<h1> Select a use-case to get started</h1>
|
||||
</div>
|
||||
|
||||
@@ -30,6 +30,7 @@ export default function DataSource(): JSX.Element {
|
||||
selectedDataSource,
|
||||
selectedFramework,
|
||||
updateSelectedDataSource,
|
||||
updateSelectedEnvironment,
|
||||
updateServiceName,
|
||||
updateSelectedFramework,
|
||||
} = useOnboardingContext();
|
||||
@@ -89,6 +90,7 @@ export default function DataSource(): JSX.Element {
|
||||
key={dataSource.name}
|
||||
onClick={(): void => {
|
||||
updateSelectedFramework(null);
|
||||
updateSelectedEnvironment(null);
|
||||
updateSelectedDataSource(dataSource);
|
||||
form.setFieldsValue({ selectFramework: null });
|
||||
}}
|
||||
|
||||
@@ -5,6 +5,7 @@ export const MaxLinesFieldWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.125rem;
|
||||
`;
|
||||
|
||||
export const MaxLinesInput = styled(InputNumber)`
|
||||
|
||||
@@ -31,9 +31,7 @@ function OptionsMenu({
|
||||
{selectedOptionFormat === OptionFormatTypes.RAW && config?.maxLines && (
|
||||
<MaxLinesField config={config.maxLines} />
|
||||
)}
|
||||
{(selectedOptionFormat === OptionFormatTypes.LIST ||
|
||||
selectedOptionFormat === OptionFormatTypes.TABLE) &&
|
||||
config?.addColumn && <AddColumnField config={config.addColumn} />}
|
||||
{config?.addColumn && <AddColumnField config={config.addColumn} />}
|
||||
</OptionsContentWrapper>
|
||||
),
|
||||
[config, selectedOptionFormat],
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import { EditFilled, PlusOutlined } from '@ant-design/icons';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ActionMode, ActionType, Pipeline } from 'types/api/pipeline/def';
|
||||
|
||||
import { ButtonContainer, CustomButton } from '../../styles';
|
||||
import { checkDataLength } from '../utils';
|
||||
|
||||
function CreatePipelineButton({
|
||||
setActionType,
|
||||
isActionMode,
|
||||
setActionMode,
|
||||
pipelineData,
|
||||
}: CreatePipelineButtonProps): JSX.Element {
|
||||
const { t } = useTranslation(['pipeline']);
|
||||
const { trackEvent } = useAnalytics();
|
||||
|
||||
const isAddNewPipelineVisible = useMemo(
|
||||
() => checkDataLength(pipelineData?.pipelines),
|
||||
[pipelineData?.pipelines],
|
||||
);
|
||||
const isDisabled = isActionMode === ActionMode.Editing;
|
||||
|
||||
const onEnterEditMode = (): void => {
|
||||
setActionMode(ActionMode.Editing);
|
||||
|
||||
trackEvent('Logs: Pipelines: Entered Edit Mode', {
|
||||
source: 'signoz-ui',
|
||||
});
|
||||
};
|
||||
const onAddNewPipeline = (): void => {
|
||||
setActionMode(ActionMode.Editing);
|
||||
setActionType(ActionType.AddPipeline);
|
||||
|
||||
trackEvent('Logs: Pipelines: Clicked Add New Pipeline', {
|
||||
source: 'signoz-ui',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ButtonContainer>
|
||||
<TextToolTip
|
||||
text={t('learn_more')}
|
||||
url="https://signoz.io/docs/logs-pipelines/introduction/"
|
||||
/>
|
||||
{isAddNewPipelineVisible && (
|
||||
<CustomButton
|
||||
icon={<EditFilled />}
|
||||
onClick={onEnterEditMode}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{t('enter_edit_mode')}
|
||||
</CustomButton>
|
||||
)}
|
||||
{!isAddNewPipelineVisible && (
|
||||
<CustomButton
|
||||
icon={<PlusOutlined />}
|
||||
onClick={onAddNewPipeline}
|
||||
type="primary"
|
||||
>
|
||||
{t('new_pipeline')}
|
||||
</CustomButton>
|
||||
)}
|
||||
</ButtonContainer>
|
||||
);
|
||||
}
|
||||
|
||||
interface CreatePipelineButtonProps {
|
||||
setActionType: (actionType: string) => void;
|
||||
isActionMode: string;
|
||||
setActionMode: (actionMode: string) => void;
|
||||
pipelineData: Pipeline;
|
||||
}
|
||||
|
||||
export default CreatePipelineButton;
|
||||
@@ -0,0 +1,125 @@
|
||||
import {
|
||||
EditFilled,
|
||||
ImportOutlined,
|
||||
PlusOutlined,
|
||||
ShareAltOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActionMode,
|
||||
ActionType,
|
||||
Pipeline,
|
||||
PipelineData,
|
||||
} from 'types/api/pipeline/def';
|
||||
|
||||
import { ButtonContainer, CustomButton } from '../../styles';
|
||||
import PipelinesExportModal from './PipelinesExportModal';
|
||||
import PipelinesImportModal from './PipelinesImportModal/PipelinesImportModal';
|
||||
|
||||
function PipelinesActions({
|
||||
setActionType,
|
||||
isActionMode,
|
||||
setActionMode,
|
||||
pipelineData,
|
||||
setCurrentPipelines,
|
||||
}: PipelinesActionsProps): JSX.Element {
|
||||
const { t } = useTranslation(['pipeline']);
|
||||
const { trackEvent } = useAnalytics();
|
||||
|
||||
const [isExportModalVisible, setIsExportModalVisible] = useState(false);
|
||||
const [isImportModalVisible, setIsImportModalVisible] = useState(false);
|
||||
|
||||
const pipelinesExist = useMemo(() => pipelineData?.pipelines?.length > 0, [
|
||||
pipelineData?.pipelines,
|
||||
]);
|
||||
const inEditMode = isActionMode === ActionMode.Editing;
|
||||
|
||||
const onEnterEditMode = (): void => {
|
||||
setActionMode(ActionMode.Editing);
|
||||
|
||||
trackEvent('Logs: Pipelines: Entered Edit Mode', {
|
||||
source: 'signoz-ui',
|
||||
});
|
||||
};
|
||||
const onAddNewPipeline = (): void => {
|
||||
setActionMode(ActionMode.Editing);
|
||||
setActionType(ActionType.AddPipeline);
|
||||
|
||||
trackEvent('Logs: Pipelines: Clicked Add New Pipeline', {
|
||||
source: 'signoz-ui',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonContainer>
|
||||
<TextToolTip
|
||||
text={t('learn_more')}
|
||||
url="https://signoz.io/docs/logs-pipelines/introduction/"
|
||||
/>
|
||||
{pipelinesExist && !inEditMode && (
|
||||
<CustomButton
|
||||
onClick={(): void => setIsExportModalVisible(true)}
|
||||
icon={<ShareAltOutlined />}
|
||||
>
|
||||
{t('share_pipelines')}
|
||||
</CustomButton>
|
||||
)}
|
||||
{(inEditMode || !pipelinesExist) && (
|
||||
<CustomButton
|
||||
onClick={(): void => {
|
||||
onEnterEditMode();
|
||||
setIsImportModalVisible(true);
|
||||
}}
|
||||
icon={<ImportOutlined />}
|
||||
>
|
||||
{t('import_pipelines')}
|
||||
</CustomButton>
|
||||
)}
|
||||
{pipelinesExist && (
|
||||
<CustomButton
|
||||
icon={<EditFilled />}
|
||||
onClick={onEnterEditMode}
|
||||
disabled={inEditMode}
|
||||
>
|
||||
{t('enter_edit_mode')}
|
||||
</CustomButton>
|
||||
)}
|
||||
{!pipelinesExist && (
|
||||
<CustomButton
|
||||
icon={<PlusOutlined />}
|
||||
onClick={onAddNewPipeline}
|
||||
type="primary"
|
||||
>
|
||||
{t('new_pipeline')}
|
||||
</CustomButton>
|
||||
)}
|
||||
</ButtonContainer>
|
||||
<PipelinesExportModal
|
||||
open={isExportModalVisible}
|
||||
onClose={(): void => setIsExportModalVisible(false)}
|
||||
pipelines={pipelineData.pipelines}
|
||||
/>
|
||||
<PipelinesImportModal
|
||||
open={isImportModalVisible}
|
||||
onClose={(): void => setIsImportModalVisible(false)}
|
||||
setCurrentPipelines={setCurrentPipelines}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface PipelinesActionsProps {
|
||||
setActionType: (actionType: string) => void;
|
||||
isActionMode: string;
|
||||
setActionMode: (actionMode: string) => void;
|
||||
pipelineData: Pipeline;
|
||||
setCurrentPipelines: (
|
||||
value: React.SetStateAction<Array<PipelineData>>,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export default PipelinesActions;
|
||||
@@ -0,0 +1,99 @@
|
||||
import { CopyFilled, DownloadOutlined } from '@ant-design/icons';
|
||||
import { Button, Modal } from 'antd';
|
||||
import Editor from 'components/Editor';
|
||||
import { downloadObjectAsJson } from 'container/NewDashboard/DashboardDescription/utils';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { PipelineData } from 'types/api/pipeline/def';
|
||||
|
||||
export default function PipelinesExportModal({
|
||||
open,
|
||||
onClose,
|
||||
pipelines,
|
||||
}: PipelinesExportModalProps): JSX.Element {
|
||||
const { t } = useTranslation(['pipeline']);
|
||||
const postablePipelines = pipelines.map((p) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(p).filter((e) => !['createdBy', 'createdAt'].includes(e[0])),
|
||||
),
|
||||
);
|
||||
const pipelinesPropJson = JSON.stringify(postablePipelines, null, 2);
|
||||
const [pipelinesJson, setPipelinesJson] = useState(pipelinesPropJson);
|
||||
useEffect(() => {
|
||||
setPipelinesJson(pipelinesPropJson);
|
||||
}, [open, pipelinesPropJson]);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
const [clipboardContent, setClipboardContent] = useCopyToClipboard();
|
||||
useEffect(() => {
|
||||
if (clipboardContent.error) {
|
||||
notifications.error({
|
||||
message: t('something_went_wrong', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (clipboardContent.value) {
|
||||
notifications.success({
|
||||
message: t('success', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
}
|
||||
}, [clipboardContent.error, clipboardContent.value, t, notifications]);
|
||||
|
||||
const footer = useMemo(
|
||||
() => (
|
||||
<>
|
||||
<Button
|
||||
style={{
|
||||
marginTop: '16px',
|
||||
}}
|
||||
onClick={(): void => setClipboardContent(pipelinesJson)}
|
||||
type="primary"
|
||||
size="small"
|
||||
>
|
||||
<CopyFilled /> {t('copy_to_clipboard')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={(): void => {
|
||||
downloadObjectAsJson(JSON.parse(pipelinesJson), 'pipelines');
|
||||
}}
|
||||
>
|
||||
<DownloadOutlined /> {t('download_json')}
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
[pipelinesJson, t, setClipboardContent],
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
width="80vw"
|
||||
centered
|
||||
title={t('share')}
|
||||
destroyOnClose
|
||||
footer={footer}
|
||||
>
|
||||
<Editor
|
||||
height="70vh"
|
||||
onChange={(value): void => setPipelinesJson(value)}
|
||||
value={pipelinesJson}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
interface PipelinesExportModalProps {
|
||||
open: boolean;
|
||||
onClose: VoidFunction;
|
||||
pipelines: Array<PipelineData>;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import './styles.scss';
|
||||
|
||||
import { ImportOutlined } from '@ant-design/icons';
|
||||
import { Monaco } from '@monaco-editor/react';
|
||||
import { Button, Modal } from 'antd';
|
||||
import Editor from 'components/Editor';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PipelineData } from 'types/api/pipeline/def';
|
||||
|
||||
import { PipelinesJSONSchema } from '../schema';
|
||||
|
||||
export default function PipelinesImportModal({
|
||||
open,
|
||||
onClose,
|
||||
setCurrentPipelines,
|
||||
}: PipelinesImportModalProps): JSX.Element {
|
||||
const { t } = useTranslation(['pipeline']);
|
||||
const [pipelinesJson, setPipelinesJson] = useState('');
|
||||
const [editorErrors, setEditorErrors] = useState<string[]>([]);
|
||||
const isEmpty = pipelinesJson.trim().length < 1;
|
||||
const isInvalid = (editorErrors || []).length > 0;
|
||||
|
||||
const firstError = editorErrors?.[0];
|
||||
const onImport = useCallback((): void => {
|
||||
try {
|
||||
const pipelines = JSON.parse(pipelinesJson);
|
||||
setCurrentPipelines(pipelines);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
setEditorErrors([String(error)]);
|
||||
}
|
||||
}, [pipelinesJson, setCurrentPipelines, onClose]);
|
||||
const footer = useMemo(
|
||||
() => (
|
||||
<div className="pipelines-import-modal-footer">
|
||||
<div className="pipelines-import-modal-error">{firstError || ''}</div>
|
||||
<Button
|
||||
disabled={isEmpty || isInvalid}
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={onImport}
|
||||
>
|
||||
<ImportOutlined /> {t('import')}
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
[t, isEmpty, isInvalid, firstError, onImport],
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
width="80vw"
|
||||
centered
|
||||
title={t('import')}
|
||||
destroyOnClose
|
||||
footer={footer}
|
||||
>
|
||||
<Editor
|
||||
height="70vh"
|
||||
onChange={(value): void => setPipelinesJson(value)}
|
||||
value={pipelinesJson}
|
||||
language="json"
|
||||
beforeMount={(monaco: Monaco): void => {
|
||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||
validate: true,
|
||||
schemas: [
|
||||
{
|
||||
fileMatch: ['*'],
|
||||
schema: PipelinesJSONSchema,
|
||||
},
|
||||
],
|
||||
});
|
||||
}}
|
||||
onValidate={(markers): void =>
|
||||
setEditorErrors(
|
||||
markers.map(
|
||||
(m) => `Ln ${m.startLineNumber}, Col ${m.startColumn}: ${m.message}`,
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
interface PipelinesImportModalProps {
|
||||
open: boolean;
|
||||
onClose: VoidFunction;
|
||||
setCurrentPipelines: (
|
||||
value: React.SetStateAction<Array<PipelineData>>,
|
||||
) => void;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import PipelinesImportModal from './PipelinesImportModal';
|
||||
|
||||
export default PipelinesImportModal;
|
||||
@@ -0,0 +1,10 @@
|
||||
.pipelines-import-modal-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pipelines-import-modal-error {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import cloneDeep from 'lodash-es/cloneDeep';
|
||||
import { useState } from 'react';
|
||||
import { Pipeline } from 'types/api/pipeline/def';
|
||||
import { Pipeline, PipelineData } from 'types/api/pipeline/def';
|
||||
|
||||
import PipelineListsView from '../../PipelineListsView';
|
||||
import CreatePipelineButton from './CreatePipelineButton';
|
||||
import PipelinesActions from './PipelinesActions';
|
||||
|
||||
function PipelinePageLayout({
|
||||
refetchPipelineLists,
|
||||
@@ -10,21 +11,32 @@ function PipelinePageLayout({
|
||||
}: PipelinePageLayoutProps): JSX.Element {
|
||||
const [isActionType, setActionType] = useState<string>();
|
||||
const [isActionMode, setActionMode] = useState<string>('viewing-mode');
|
||||
const [savedPipelines, setSavedPipelines] = useState<Array<PipelineData>>(
|
||||
cloneDeep(pipelineData?.pipelines || []),
|
||||
);
|
||||
const [currentPipelines, setCurrentPipelines] = useState<Array<PipelineData>>(
|
||||
cloneDeep(pipelineData?.pipelines || []),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreatePipelineButton
|
||||
<PipelinesActions
|
||||
setActionType={setActionType}
|
||||
setActionMode={setActionMode}
|
||||
isActionMode={isActionMode}
|
||||
pipelineData={pipelineData}
|
||||
setCurrentPipelines={setCurrentPipelines}
|
||||
/>
|
||||
<PipelineListsView
|
||||
isActionType={String(isActionType)}
|
||||
setActionType={setActionType}
|
||||
setActionMode={setActionMode}
|
||||
isActionMode={isActionMode}
|
||||
pipelineData={pipelineData}
|
||||
savedPipelinesVersion={pipelineData?.version}
|
||||
savedPipelines={savedPipelines}
|
||||
setSavedPipelines={setSavedPipelines}
|
||||
currentPipelines={currentPipelines}
|
||||
setCurrentPipelines={setCurrentPipelines}
|
||||
refetchPipelineLists={refetchPipelineLists}
|
||||
/>
|
||||
</>
|
||||
|
||||
247
frontend/src/container/PipelinePage/Layouts/Pipeline/schema.ts
Normal file
247
frontend/src/container/PipelinePage/Layouts/Pipeline/schema.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
// JSON schema for pipelines payload.
|
||||
|
||||
export const PipelinesJSONSchema = JSON.parse(`
|
||||
{
|
||||
"items": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"orderId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"alias": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"filter": {
|
||||
"properties": {
|
||||
"op": {
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"key": {
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"dataType": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"isColumn": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isJSON": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"key",
|
||||
"dataType",
|
||||
"type",
|
||||
"isColumn",
|
||||
"isJSON"
|
||||
]
|
||||
},
|
||||
"value": true,
|
||||
"op": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"key",
|
||||
"value",
|
||||
"op"
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"op",
|
||||
"items"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"output": {
|
||||
"type": "string"
|
||||
},
|
||||
"on_error": {
|
||||
"type": "string"
|
||||
},
|
||||
"if": {
|
||||
"type": "string"
|
||||
},
|
||||
"orderId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"parse_to": {
|
||||
"type": "string"
|
||||
},
|
||||
"pattern": {
|
||||
"type": "string"
|
||||
},
|
||||
"regex": {
|
||||
"type": "string"
|
||||
},
|
||||
"parse_from": {
|
||||
"type": "string"
|
||||
},
|
||||
"trace_id": {
|
||||
"properties": {
|
||||
"parse_from": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"parse_from"
|
||||
]
|
||||
},
|
||||
"span_id": {
|
||||
"properties": {
|
||||
"parse_from": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"parse_from"
|
||||
]
|
||||
},
|
||||
"trace_flags": {
|
||||
"properties": {
|
||||
"parse_from": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"parse_from"
|
||||
]
|
||||
},
|
||||
"field": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
},
|
||||
"from": {
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"type": "string"
|
||||
},
|
||||
"expr": {
|
||||
"type": "string"
|
||||
},
|
||||
"routes": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"output": {
|
||||
"type": "string"
|
||||
},
|
||||
"expr": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"output",
|
||||
"expr"
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"fields": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"default": {
|
||||
"type": "string"
|
||||
},
|
||||
"layout": {
|
||||
"type": "string"
|
||||
},
|
||||
"layout_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"mapping": {
|
||||
"additionalProperties": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"overwrite_text": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"orderId",
|
||||
"enabled"
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"orderId",
|
||||
"name",
|
||||
"alias",
|
||||
"description",
|
||||
"enabled",
|
||||
"filter",
|
||||
"config"
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
`);
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Button, Divider, Form, Modal } from 'antd';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ActionMode, ActionType, PipelineData } from 'types/api/pipeline/def';
|
||||
import { ActionType, PipelineData } from 'types/api/pipeline/def';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
@@ -15,7 +15,6 @@ function AddNewPipeline({
|
||||
isActionType,
|
||||
setActionType,
|
||||
selectedPipelineData,
|
||||
setShowSaveButton,
|
||||
setCurrPipelineData,
|
||||
currPipelineData,
|
||||
}: AddNewPipelineProps): JSX.Element {
|
||||
@@ -90,11 +89,6 @@ function AddNewPipeline({
|
||||
[isEdit, selectedPipelineData?.name, t],
|
||||
);
|
||||
|
||||
const onOkModalHandler = useCallback(
|
||||
() => setShowSaveButton(ActionMode.Editing),
|
||||
[setShowSaveButton],
|
||||
);
|
||||
|
||||
const isOpen = useMemo(() => isEdit || isAdd, [isAdd, isEdit]);
|
||||
|
||||
return (
|
||||
@@ -122,7 +116,7 @@ function AddNewPipeline({
|
||||
key="submit"
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
onClick={onOkModalHandler}
|
||||
onClick={(): void => {}}
|
||||
>
|
||||
{isEdit ? t('update') : t('create')}
|
||||
</Button>
|
||||
@@ -140,7 +134,6 @@ interface AddNewPipelineProps {
|
||||
isActionType: string;
|
||||
setActionType: (actionType?: ActionType) => void;
|
||||
selectedPipelineData: PipelineData | undefined;
|
||||
setShowSaveButton: (actionMode: ActionMode) => void;
|
||||
setCurrPipelineData: (
|
||||
value: React.SetStateAction<Array<PipelineData>>,
|
||||
) => void;
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Button, Divider, Form, Modal } from 'antd';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActionMode,
|
||||
ActionType,
|
||||
PipelineData,
|
||||
ProcessorData,
|
||||
@@ -19,7 +18,6 @@ function AddNewProcessor({
|
||||
isActionType,
|
||||
setActionType,
|
||||
selectedProcessorData,
|
||||
setShowSaveButton,
|
||||
expandedPipelineData,
|
||||
setExpandedPipelineData,
|
||||
}: AddNewProcessorProps): JSX.Element {
|
||||
@@ -134,11 +132,6 @@ function AddNewProcessor({
|
||||
[isEdit, selectedProcessorData?.name, t],
|
||||
);
|
||||
|
||||
const onOkModalHandler = useCallback(
|
||||
() => setShowSaveButton(ActionMode.Editing),
|
||||
[setShowSaveButton],
|
||||
);
|
||||
|
||||
const isOpen = useMemo(() => isEdit || isAdd, [isAdd, isEdit]);
|
||||
|
||||
const onFormValuesChanged = useCallback(
|
||||
@@ -179,7 +172,7 @@ function AddNewProcessor({
|
||||
key="submit"
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
onClick={onOkModalHandler}
|
||||
onClick={(): void => {}}
|
||||
>
|
||||
{isEdit ? t('update') : t('create')}
|
||||
</Button>
|
||||
@@ -202,7 +195,6 @@ interface AddNewProcessorProps {
|
||||
isActionType: string;
|
||||
setActionType: (actionType?: ActionType) => void;
|
||||
selectedProcessorData?: ProcessorData;
|
||||
setShowSaveButton: (actionMode: ActionMode) => void;
|
||||
expandedPipelineData?: PipelineData;
|
||||
setExpandedPipelineData: (data: PipelineData) => void;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ function PipelineExpandView({
|
||||
setActionType,
|
||||
processorEditAction,
|
||||
isActionMode,
|
||||
setShowSaveButton,
|
||||
expandedPipelineData,
|
||||
setExpandedPipelineData,
|
||||
prevPipelineData,
|
||||
@@ -44,7 +43,6 @@ function PipelineExpandView({
|
||||
|
||||
const deleteProcessorHandler = useCallback(
|
||||
(record: ProcessorData) => (): void => {
|
||||
setShowSaveButton(ActionMode.Editing);
|
||||
if (expandedPipelineData && expandedPipelineData?.config) {
|
||||
const filteredData = expandedPipelineData?.config.filter(
|
||||
(item: ProcessorData) => item.id !== record.id,
|
||||
@@ -62,7 +60,7 @@ function PipelineExpandView({
|
||||
setExpandedPipelineData(pipelineData);
|
||||
}
|
||||
},
|
||||
[expandedPipelineData, setShowSaveButton, setExpandedPipelineData],
|
||||
[expandedPipelineData, setExpandedPipelineData],
|
||||
);
|
||||
|
||||
const processorDeleteAction = useCallback(
|
||||
@@ -80,7 +78,6 @@ function PipelineExpandView({
|
||||
const onSwitchProcessorChange = useCallback(
|
||||
(checked: boolean, record: ProcessorData): void => {
|
||||
if (expandedPipelineData && expandedPipelineData?.config) {
|
||||
setShowSaveButton(ActionMode.Editing);
|
||||
const findRecordIndex = getRecordIndex(
|
||||
expandedPipelineData?.config,
|
||||
record,
|
||||
@@ -102,7 +99,7 @@ function PipelineExpandView({
|
||||
setExpandedPipelineData(modifiedProcessorData);
|
||||
}
|
||||
},
|
||||
[expandedPipelineData, setExpandedPipelineData, setShowSaveButton],
|
||||
[expandedPipelineData, setExpandedPipelineData],
|
||||
);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
@@ -145,14 +142,13 @@ function PipelineExpandView({
|
||||
|
||||
const reorderProcessorRow = useCallback(
|
||||
(updatedRow: ProcessorData[]) => (): void => {
|
||||
setShowSaveButton(ActionMode.Editing);
|
||||
if (expandedPipelineData) {
|
||||
const modifiedProcessorData = { ...expandedPipelineData };
|
||||
modifiedProcessorData.config = updatedRow;
|
||||
setExpandedPipelineData(modifiedProcessorData);
|
||||
}
|
||||
},
|
||||
[expandedPipelineData, setShowSaveButton, setExpandedPipelineData],
|
||||
[expandedPipelineData, setExpandedPipelineData],
|
||||
);
|
||||
|
||||
const onCancelReorderProcessorRow = useCallback(
|
||||
@@ -267,7 +263,6 @@ interface PipelineExpandViewProps {
|
||||
setActionType: (actionType?: ActionType) => void;
|
||||
processorEditAction: (record: ProcessorData) => () => void;
|
||||
isActionMode: string;
|
||||
setShowSaveButton: (actionMode: ActionMode) => void;
|
||||
expandedPipelineData?: PipelineData;
|
||||
setExpandedPipelineData: (data: PipelineData) => void;
|
||||
prevPipelineData: Array<PipelineData>;
|
||||
|
||||
@@ -7,6 +7,7 @@ import savePipeline from 'api/pipeline/post';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import cloneDeep from 'lodash-es/cloneDeep';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
@@ -14,7 +15,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActionMode,
|
||||
ActionType,
|
||||
Pipeline,
|
||||
PipelineData,
|
||||
ProcessorData,
|
||||
} from 'types/api/pipeline/def';
|
||||
@@ -85,7 +85,11 @@ function PipelineListsView({
|
||||
setActionType,
|
||||
isActionMode,
|
||||
setActionMode,
|
||||
pipelineData,
|
||||
savedPipelinesVersion,
|
||||
savedPipelines,
|
||||
setSavedPipelines,
|
||||
currentPipelines,
|
||||
setCurrentPipelines,
|
||||
refetchPipelineLists,
|
||||
}: PipelineListsViewProps): JSX.Element {
|
||||
const { t } = useTranslation(['pipeline', 'common']);
|
||||
@@ -93,34 +97,28 @@ function PipelineListsView({
|
||||
const { notifications } = useNotifications();
|
||||
const [pipelineSearchValue, setPipelineSearchValue] = useState<string>('');
|
||||
const { trackEvent } = useAnalytics();
|
||||
const [prevPipelineData, setPrevPipelineData] = useState<Array<PipelineData>>(
|
||||
cloneDeep(pipelineData?.pipelines || []),
|
||||
);
|
||||
const [currPipelineData, setCurrPipelineData] = useState<Array<PipelineData>>(
|
||||
cloneDeep(pipelineData?.pipelines || []),
|
||||
);
|
||||
|
||||
const [expandedPipelineId, setExpandedPipelineId] = useState<
|
||||
string | undefined
|
||||
>(undefined);
|
||||
const expandedPipelineData = useCallback(
|
||||
() => currPipelineData?.find((p) => p.id === expandedPipelineId),
|
||||
[currPipelineData, expandedPipelineId],
|
||||
() => currentPipelines?.find((p) => p.id === expandedPipelineId),
|
||||
[currentPipelines, expandedPipelineId],
|
||||
);
|
||||
|
||||
const setExpandedPipelineData = useCallback(
|
||||
(newData: PipelineData): void => {
|
||||
if (expandedPipelineId) {
|
||||
const pipelineIdx = currPipelineData?.findIndex(
|
||||
const pipelineIdx = currentPipelines?.findIndex(
|
||||
(p) => p.id === expandedPipelineId,
|
||||
);
|
||||
if (pipelineIdx >= 0) {
|
||||
const newPipelineData = [...currPipelineData];
|
||||
const newPipelineData = cloneDeep(currentPipelines);
|
||||
newPipelineData[pipelineIdx] = newData;
|
||||
setCurrPipelineData(newPipelineData);
|
||||
setCurrentPipelines(newPipelineData);
|
||||
}
|
||||
}
|
||||
},
|
||||
[expandedPipelineId, currPipelineData],
|
||||
[expandedPipelineId, currentPipelines, setCurrentPipelines],
|
||||
);
|
||||
|
||||
const [
|
||||
@@ -134,17 +132,16 @@ function PipelineListsView({
|
||||
] = useState<PipelineData>();
|
||||
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<Array<string>>();
|
||||
const [showSaveButton, setShowSaveButton] = useState<string>();
|
||||
const isEditingActionMode = isActionMode === ActionMode.Editing;
|
||||
|
||||
const visibleCurrPipelines = useMemo((): Array<PipelineData> => {
|
||||
if (pipelineSearchValue === '') {
|
||||
return currPipelineData;
|
||||
return currentPipelines;
|
||||
}
|
||||
return currPipelineData.filter((data) =>
|
||||
return currentPipelines.filter((data) =>
|
||||
getDataOnSearch(data as never, pipelineSearchValue),
|
||||
);
|
||||
}, [currPipelineData, pipelineSearchValue]);
|
||||
}, [currentPipelines, pipelineSearchValue]);
|
||||
|
||||
const handleAlert = useCallback(
|
||||
({ title, descrition, buttontext, onCancel, onOk }: AlertMessage) => {
|
||||
@@ -171,15 +168,18 @@ function PipelineListsView({
|
||||
|
||||
const pipelineDeleteHandler = useCallback(
|
||||
(record: PipelineData) => (): void => {
|
||||
setShowSaveButton(ActionMode.Editing);
|
||||
const filteredData = getElementFromArray(currPipelineData, record, 'id');
|
||||
const filteredData = getElementFromArray(
|
||||
cloneDeep(currentPipelines),
|
||||
record,
|
||||
'id',
|
||||
);
|
||||
filteredData.forEach((item, index) => {
|
||||
const obj = item;
|
||||
obj.orderId = index + 1;
|
||||
});
|
||||
setCurrPipelineData(filteredData);
|
||||
setCurrentPipelines(filteredData);
|
||||
},
|
||||
[currPipelineData],
|
||||
[currentPipelines, setCurrentPipelines],
|
||||
);
|
||||
|
||||
const pipelineDeleteAction = useCallback(
|
||||
@@ -204,21 +204,20 @@ function PipelineListsView({
|
||||
|
||||
const onSwitchPipelineChange = useCallback(
|
||||
(checked: boolean, record: PipelineData): void => {
|
||||
setShowSaveButton(ActionMode.Editing);
|
||||
const findRecordIndex = getRecordIndex(currPipelineData, record, 'id');
|
||||
const findRecordIndex = getRecordIndex(currentPipelines, record, 'id');
|
||||
const updateSwitch = {
|
||||
...currPipelineData[findRecordIndex],
|
||||
...currentPipelines[findRecordIndex],
|
||||
enabled: checked,
|
||||
};
|
||||
const editedPipelineData = getEditedDataSource(
|
||||
currPipelineData,
|
||||
cloneDeep(currentPipelines),
|
||||
record,
|
||||
'id',
|
||||
updateSwitch,
|
||||
);
|
||||
setCurrPipelineData(editedPipelineData);
|
||||
setCurrentPipelines(editedPipelineData);
|
||||
},
|
||||
[currPipelineData],
|
||||
[currentPipelines, setCurrentPipelines],
|
||||
);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
@@ -271,28 +270,13 @@ function PipelineListsView({
|
||||
onSwitchPipelineChange,
|
||||
]);
|
||||
|
||||
const updatePipelineSequence = useCallback(
|
||||
(updatedRow: PipelineData[]) => (): void => {
|
||||
setShowSaveButton(ActionMode.Editing);
|
||||
setCurrPipelineData(updatedRow);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const onCancelPipelineSequence = useCallback(
|
||||
(rawData: PipelineData[]) => (): void => {
|
||||
setCurrPipelineData(rawData);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const movePipelineRow = useCallback(
|
||||
(dragIndex: number, hoverIndex: number) => {
|
||||
if (currPipelineData && isEditingActionMode) {
|
||||
const rawData = currPipelineData;
|
||||
if (currentPipelines && isEditingActionMode) {
|
||||
const rawData = currentPipelines;
|
||||
|
||||
const updatedRows = getUpdatedRow(
|
||||
currPipelineData,
|
||||
cloneDeep(currentPipelines),
|
||||
visibleCurrPipelines[dragIndex].orderId - 1,
|
||||
visibleCurrPipelines[hoverIndex].orderId - 1,
|
||||
);
|
||||
@@ -305,19 +289,18 @@ function PipelineListsView({
|
||||
title: t('reorder_pipeline'),
|
||||
descrition: t('reorder_pipeline_description'),
|
||||
buttontext: t('reorder'),
|
||||
onOk: updatePipelineSequence(updatedRows),
|
||||
onCancel: onCancelPipelineSequence(rawData),
|
||||
onOk: (): void => setCurrentPipelines(updatedRows),
|
||||
onCancel: (): void => setCurrentPipelines(rawData),
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
currPipelineData,
|
||||
currentPipelines,
|
||||
isEditingActionMode,
|
||||
visibleCurrPipelines,
|
||||
handleAlert,
|
||||
t,
|
||||
updatePipelineSequence,
|
||||
onCancelPipelineSequence,
|
||||
setCurrentPipelines,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -328,10 +311,9 @@ function PipelineListsView({
|
||||
isActionMode={isActionMode}
|
||||
setActionType={setActionType}
|
||||
processorEditAction={processorEditAction}
|
||||
setShowSaveButton={setShowSaveButton}
|
||||
expandedPipelineData={expandedPipelineData()}
|
||||
setExpandedPipelineData={setExpandedPipelineData}
|
||||
prevPipelineData={prevPipelineData}
|
||||
prevPipelineData={savedPipelines}
|
||||
/>
|
||||
),
|
||||
[
|
||||
@@ -340,7 +322,7 @@ function PipelineListsView({
|
||||
isActionMode,
|
||||
expandedPipelineData,
|
||||
setActionType,
|
||||
prevPipelineData,
|
||||
savedPipelines,
|
||||
setExpandedPipelineData,
|
||||
],
|
||||
);
|
||||
@@ -390,7 +372,7 @@ function PipelineListsView({
|
||||
}, [isEditingActionMode, addNewPipelineHandler, t]);
|
||||
|
||||
const onSaveConfigurationHandler = useCallback(async () => {
|
||||
const modifiedPipelineData = currPipelineData.map((item: PipelineData) => {
|
||||
const modifiedPipelineData = currentPipelines.map((item: PipelineData) => {
|
||||
const pipelineData = { ...item };
|
||||
delete pipelineData?.id;
|
||||
return pipelineData;
|
||||
@@ -401,11 +383,10 @@ function PipelineListsView({
|
||||
if (response.statusCode === 200) {
|
||||
refetchPipelineLists();
|
||||
setActionMode(ActionMode.Viewing);
|
||||
setShowSaveButton(undefined);
|
||||
|
||||
const pipelinesInDB = response.payload?.pipelines || [];
|
||||
setCurrPipelineData(pipelinesInDB);
|
||||
setPrevPipelineData(pipelinesInDB);
|
||||
setCurrentPipelines(cloneDeep(pipelinesInDB));
|
||||
setSavedPipelines(cloneDeep(pipelinesInDB));
|
||||
|
||||
trackEvent('Logs: Pipelines: Saved Pipelines', {
|
||||
count: pipelinesInDB.length,
|
||||
@@ -419,21 +400,19 @@ function PipelineListsView({
|
||||
return pipelineData;
|
||||
});
|
||||
setActionMode(ActionMode.Editing);
|
||||
setShowSaveButton(ActionMode.Editing);
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: response.error || t('something_went_wrong'),
|
||||
});
|
||||
setCurrPipelineData(modifiedPipelineData);
|
||||
setPrevPipelineData(modifiedPipelineData);
|
||||
setCurrentPipelines(cloneDeep(modifiedPipelineData));
|
||||
setSavedPipelines(cloneDeep(modifiedPipelineData));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currPipelineData, notifications, refetchPipelineLists, setActionMode, t]);
|
||||
}, [currentPipelines, notifications, refetchPipelineLists, setActionMode, t]);
|
||||
|
||||
const onCancelConfigurationHandler = useCallback((): void => {
|
||||
setActionMode(ActionMode.Viewing);
|
||||
setShowSaveButton(undefined);
|
||||
prevPipelineData.forEach((item, index) => {
|
||||
savedPipelines.forEach((item, index) => {
|
||||
const obj = item;
|
||||
obj.orderId = index + 1;
|
||||
if (obj.config) {
|
||||
@@ -446,9 +425,9 @@ function PipelineListsView({
|
||||
}
|
||||
}
|
||||
});
|
||||
setCurrPipelineData(prevPipelineData);
|
||||
setCurrentPipelines(cloneDeep(savedPipelines));
|
||||
setExpandedRowKeys([]);
|
||||
}, [prevPipelineData, setActionMode]);
|
||||
}, [savedPipelines, setCurrentPipelines, setActionMode]);
|
||||
|
||||
const onRowHandler = (
|
||||
_data: PipelineData,
|
||||
@@ -473,25 +452,23 @@ function PipelineListsView({
|
||||
isActionType={isActionType}
|
||||
setActionType={setActionType}
|
||||
selectedPipelineData={selectedPipelineData}
|
||||
setShowSaveButton={setShowSaveButton}
|
||||
setCurrPipelineData={setCurrPipelineData}
|
||||
currPipelineData={currPipelineData}
|
||||
setCurrPipelineData={setCurrentPipelines}
|
||||
currPipelineData={currentPipelines}
|
||||
/>
|
||||
<AddNewProcessor
|
||||
isActionType={isActionType}
|
||||
setActionType={setActionType}
|
||||
selectedProcessorData={selectedProcessorData}
|
||||
setShowSaveButton={setShowSaveButton}
|
||||
expandedPipelineData={expandedPipelineData()}
|
||||
setExpandedPipelineData={setExpandedPipelineData}
|
||||
/>
|
||||
{prevPipelineData?.length > 0 || currPipelineData?.length > 0 ? (
|
||||
{savedPipelines?.length > 0 || currentPipelines?.length > 0 ? (
|
||||
<>
|
||||
<PipelinesSearchSection setPipelineSearchValue={setPipelineSearchValue} />
|
||||
<Container>
|
||||
<ModeAndConfiguration
|
||||
isActionMode={isActionMode}
|
||||
version={pipelineData?.version}
|
||||
version={savedPipelinesVersion}
|
||||
/>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<Table
|
||||
@@ -508,7 +485,7 @@ function PipelineListsView({
|
||||
</DndProvider>
|
||||
{isEditingActionMode && (
|
||||
<SaveConfigButton
|
||||
showSaveButton={Boolean(showSaveButton)}
|
||||
showSaveButton={!isEqual(currentPipelines, savedPipelines)}
|
||||
onSaveConfigurationHandler={onSaveConfigurationHandler}
|
||||
onCancelConfigurationHandler={onCancelConfigurationHandler}
|
||||
/>
|
||||
@@ -529,7 +506,13 @@ interface PipelineListsViewProps {
|
||||
setActionType: (actionType?: ActionType) => void;
|
||||
isActionMode: string;
|
||||
setActionMode: (actionMode: ActionMode) => void;
|
||||
pipelineData: Pipeline;
|
||||
savedPipelinesVersion: number | string;
|
||||
savedPipelines: Array<PipelineData>;
|
||||
setSavedPipelines: (value: React.SetStateAction<Array<PipelineData>>) => void;
|
||||
currentPipelines: Array<PipelineData>;
|
||||
setCurrentPipelines: (
|
||||
value: React.SetStateAction<Array<PipelineData>>,
|
||||
) => void;
|
||||
refetchPipelineLists: VoidFunction;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export const ButtonContainer = styled.div`
|
||||
|
||||
export const CustomButton = styled(Button)`
|
||||
&&& {
|
||||
margin-left: 1rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { MemoryRouter } from 'react-router-dom';
|
||||
import i18n from 'ReactI18';
|
||||
import store from 'store';
|
||||
|
||||
import CreatePipelineButton from '../Layouts/Pipeline/CreatePipelineButton';
|
||||
import PipelinesActions from '../Layouts/Pipeline/PipelinesActions';
|
||||
import { pipelineApiResponseMockData } from '../mocks/pipeline';
|
||||
|
||||
describe('PipelinePage container test', () => {
|
||||
@@ -14,7 +14,7 @@ describe('PipelinePage container test', () => {
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<CreatePipelineButton
|
||||
<PipelinesActions
|
||||
setActionType={jest.fn()}
|
||||
isActionMode="viewing-mode"
|
||||
setActionMode={jest.fn()}
|
||||
|
||||
@@ -91,6 +91,11 @@ function ServiceMetricTable({
|
||||
<ResourceAttributesFilter />
|
||||
|
||||
<ResizeTable
|
||||
pagination={{
|
||||
defaultPageSize: 10,
|
||||
showTotal: (total: number, range: number[]): string =>
|
||||
`${range[0]}-${range[1]} of ${total} items`,
|
||||
}}
|
||||
columns={tableColumns}
|
||||
loading={isLoading}
|
||||
dataSource={services}
|
||||
|
||||
@@ -49,6 +49,11 @@ function ServiceTraceTable({
|
||||
<ResourceAttributesFilter />
|
||||
|
||||
<ResizeTable
|
||||
pagination={{
|
||||
defaultPageSize: 10,
|
||||
showTotal: (total: number, range: number[]): string =>
|
||||
`${range[0]}-${range[1]} of ${total} items`,
|
||||
}}
|
||||
columns={tableColumns}
|
||||
loading={loading}
|
||||
dataSource={services}
|
||||
|
||||
@@ -74,6 +74,8 @@ function SideNav({
|
||||
isCurrentVersionError,
|
||||
} = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
const [licenseTag, setLicenseTag] = useState('');
|
||||
|
||||
const userSettingsMenuItem = {
|
||||
key: ROUTES.MY_SETTINGS,
|
||||
label: user?.name || 'User',
|
||||
@@ -199,10 +201,7 @@ function SideNav({
|
||||
|
||||
useEffect(() => {
|
||||
if (isCloudUser() || isEECloudUser()) {
|
||||
const updatedUserManagementMenuItems = [
|
||||
helpSupportMenuItem,
|
||||
manageLicenseMenuItem,
|
||||
];
|
||||
const updatedUserManagementMenuItems = [helpSupportMenuItem];
|
||||
|
||||
setUserManagementMenuItems(updatedUserManagementMenuItems);
|
||||
} else if (currentVersion && latestVersion) {
|
||||
@@ -242,6 +241,27 @@ function SideNav({
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFetching) {
|
||||
if (isCloudUserVal) {
|
||||
setLicenseTag('Cloud');
|
||||
} else if (isEnterprise) {
|
||||
setLicenseTag('Enterprise');
|
||||
} else {
|
||||
setLicenseTag('Free');
|
||||
}
|
||||
}
|
||||
}, [isCloudUserVal, isEnterprise, isFetching]);
|
||||
|
||||
const [isCurrentOrgSettings] = useComponentPermission(
|
||||
['current_org_settings'],
|
||||
role,
|
||||
);
|
||||
|
||||
const settingsRoute = isCurrentOrgSettings
|
||||
? ROUTES.ORG_SETTINGS
|
||||
: ROUTES.SETTINGS;
|
||||
|
||||
return (
|
||||
<div className={cx('sideNav', collapsed ? 'collapsed' : '')}>
|
||||
<div className="brand">
|
||||
@@ -260,7 +280,7 @@ function SideNav({
|
||||
|
||||
{!collapsed && (
|
||||
<>
|
||||
<div className="license tag">{!isEnterprise ? 'Free' : 'Enterprise'}</div>
|
||||
{!isFetching && <div className="license tag">{licenseTag}</div>}
|
||||
|
||||
<ToggleButton
|
||||
checked={isDarkMode}
|
||||
@@ -291,7 +311,9 @@ function SideNav({
|
||||
item={item}
|
||||
isActive={activeMenuKey === item.key}
|
||||
onClick={(): void => {
|
||||
if (item) {
|
||||
if (item.key === ROUTES.SETTINGS) {
|
||||
history.push(settingsRoute);
|
||||
} else if (item) {
|
||||
onClickHandler(item?.key as string);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -12,6 +12,7 @@ describe('CustomDateTimeModal', () => {
|
||||
visible
|
||||
onCreate={handleCreate}
|
||||
onCancel={handleCancel}
|
||||
setCustomDTPickerVisible={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DatePicker, Modal } from 'antd';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { useState } from 'react';
|
||||
import { Dispatch, SetStateAction, useState } from 'react';
|
||||
|
||||
export type DateTimeRangeType = [Dayjs | null, Dayjs | null] | null;
|
||||
|
||||
@@ -10,12 +10,12 @@ function CustomDateTimeModal({
|
||||
visible,
|
||||
onCreate,
|
||||
onCancel,
|
||||
setCustomDTPickerVisible,
|
||||
}: CustomDateTimeModalProps): JSX.Element {
|
||||
const [selectedDate, setDateTime] = useState<DateTimeRangeType>();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const onModalOkHandler = (date_time: any): void => {
|
||||
onCreate(date_time);
|
||||
setDateTime(date_time);
|
||||
};
|
||||
|
||||
@@ -25,7 +25,10 @@ function CustomDateTimeModal({
|
||||
};
|
||||
|
||||
const onOk = (): void => {
|
||||
if (selectedDate) onCreate(selectedDate);
|
||||
if (selectedDate) {
|
||||
onCreate(selectedDate);
|
||||
setCustomDTPickerVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -42,7 +45,6 @@ function CustomDateTimeModal({
|
||||
allowClear
|
||||
onOk={onModalOkHandler}
|
||||
showTime
|
||||
onCalendarChange={onModalOkHandler}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
@@ -52,6 +54,7 @@ interface CustomDateTimeModalProps {
|
||||
visible: boolean;
|
||||
onCreate: (dateTimeRange: DateTimeRangeType) => void;
|
||||
onCancel: () => void;
|
||||
setCustomDTPickerVisible: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export default CustomDateTimeModal;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.date-time-selection-container {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@@ -96,3 +96,13 @@ export const routesToSkip = [
|
||||
];
|
||||
|
||||
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];
|
||||
|
||||
export interface LocalStorageTimeRange {
|
||||
localstorageStartTime: string | null;
|
||||
localstorageEndTime: string | null;
|
||||
}
|
||||
|
||||
export interface TimeRange {
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import './DateTimeSelection.styles.scss';
|
||||
|
||||
import { SyncOutlined } from '@ant-design/icons';
|
||||
import { Button } from 'antd';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
@@ -13,6 +15,7 @@ import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import getTimeString from 'lib/getTimeString';
|
||||
import history from 'lib/history';
|
||||
import { isObject } from 'lodash-es';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
@@ -25,7 +28,13 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import AutoRefresh from '../AutoRefresh';
|
||||
import CustomDateTimeModal, { DateTimeRangeType } from '../CustomDateTimeModal';
|
||||
import { getDefaultOption, getOptions, Time } from './config';
|
||||
import {
|
||||
getDefaultOption,
|
||||
getOptions,
|
||||
LocalStorageTimeRange,
|
||||
Time,
|
||||
TimeRange,
|
||||
} from './config';
|
||||
import RefreshText from './Refresh';
|
||||
import { Form, FormContainer, FormItem } from './styles';
|
||||
|
||||
@@ -42,8 +51,35 @@ function DateTimeSelection({
|
||||
const searchStartTime = urlQuery.get('startTime');
|
||||
const searchEndTime = urlQuery.get('endTime');
|
||||
|
||||
const localstorageStartTime = getLocalStorageKey('startTime');
|
||||
const localstorageEndTime = getLocalStorageKey('endTime');
|
||||
const {
|
||||
localstorageStartTime,
|
||||
localstorageEndTime,
|
||||
} = ((): LocalStorageTimeRange => {
|
||||
const routes = getLocalStorageKey(LOCALSTORAGE.METRICS_TIME_IN_DURATION);
|
||||
|
||||
if (routes !== null) {
|
||||
const routesObject = JSON.parse(routes || '{}');
|
||||
const selectedTime = routesObject[location.pathname];
|
||||
|
||||
if (selectedTime) {
|
||||
let parsedSelectedTime: TimeRange;
|
||||
try {
|
||||
parsedSelectedTime = JSON.parse(selectedTime);
|
||||
} catch {
|
||||
parsedSelectedTime = selectedTime;
|
||||
}
|
||||
|
||||
if (isObject(parsedSelectedTime)) {
|
||||
return {
|
||||
localstorageStartTime: parsedSelectedTime.startTime,
|
||||
localstorageEndTime: parsedSelectedTime.endTime,
|
||||
};
|
||||
}
|
||||
return { localstorageStartTime: null, localstorageEndTime: null };
|
||||
}
|
||||
}
|
||||
return { localstorageStartTime: null, localstorageEndTime: null };
|
||||
})();
|
||||
|
||||
const getTime = useCallback((): [number, number] | undefined => {
|
||||
if (searchEndTime && searchStartTime) {
|
||||
@@ -116,6 +152,15 @@ function DateTimeSelection({
|
||||
const selectedTime = routesObject[pathName];
|
||||
|
||||
if (selectedTime) {
|
||||
let parsedSelectedTime: TimeRange;
|
||||
try {
|
||||
parsedSelectedTime = JSON.parse(selectedTime);
|
||||
} catch {
|
||||
parsedSelectedTime = selectedTime;
|
||||
}
|
||||
if (isObject(parsedSelectedTime)) {
|
||||
return 'custom';
|
||||
}
|
||||
return selectedTime;
|
||||
}
|
||||
}
|
||||
@@ -123,7 +168,7 @@ function DateTimeSelection({
|
||||
return defaultSelectedOption;
|
||||
};
|
||||
|
||||
const updateLocalStorageForRoutes = (value: Time): void => {
|
||||
const updateLocalStorageForRoutes = (value: Time | string): void => {
|
||||
const preRoutes = getLocalStorageKey(LOCALSTORAGE.METRICS_TIME_IN_DURATION);
|
||||
if (preRoutes !== null) {
|
||||
const preRoutesObject = JSON.parse(preRoutes);
|
||||
@@ -211,18 +256,16 @@ function DateTimeSelection({
|
||||
};
|
||||
|
||||
const onCustomDateHandler = (dateTimeRange: DateTimeRangeType): void => {
|
||||
console.log('dateTimeRange', dateTimeRange);
|
||||
if (dateTimeRange !== null) {
|
||||
const [startTimeMoment, endTimeMoment] = dateTimeRange;
|
||||
if (startTimeMoment && endTimeMoment) {
|
||||
setCustomDTPickerVisible(false);
|
||||
updateTimeInterval('custom', [
|
||||
startTimeMoment?.toDate().getTime() || 0,
|
||||
endTimeMoment?.toDate().getTime() || 0,
|
||||
]);
|
||||
setLocalStorageKey('startTime', startTimeMoment.toString());
|
||||
setLocalStorageKey('endTime', endTimeMoment.toString());
|
||||
updateLocalStorageForRoutes('custom');
|
||||
updateLocalStorageForRoutes(
|
||||
JSON.stringify({ startTime: startTimeMoment, endTime: endTimeMoment }),
|
||||
);
|
||||
if (!isLogsExplorerPage) {
|
||||
urlQuery.set(
|
||||
QueryParams.startTime,
|
||||
@@ -295,7 +338,7 @@ function DateTimeSelection({
|
||||
}, [location.pathname, updateTimeInterval, globalTimeLoading]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="date-time-selection-container">
|
||||
<Form
|
||||
form={formSelector}
|
||||
layout="inline"
|
||||
@@ -336,7 +379,7 @@ function DateTimeSelection({
|
||||
</FormContainer>
|
||||
</Form>
|
||||
|
||||
{!hasSelectedTimeError && (
|
||||
{!hasSelectedTimeError && selectedTime !== 'custom' && (
|
||||
<RefreshText
|
||||
{...{
|
||||
onLastRefreshHandler,
|
||||
@@ -351,8 +394,9 @@ function DateTimeSelection({
|
||||
onCancel={(): void => {
|
||||
setCustomDTPickerVisible(false);
|
||||
}}
|
||||
setCustomDTPickerVisible={setCustomDTPickerVisible}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ interface Props {
|
||||
}
|
||||
|
||||
export const RefreshTextContainer = styled.div<Props>`
|
||||
min-height: 2rem;
|
||||
visibility: ${({ refreshButtonHidden }): string =>
|
||||
refreshButtonHidden ? 'hidden' : 'visible'};
|
||||
`;
|
||||
|
||||
100
frontend/src/hooks/dashboard/useDashboardFromLocalStorage.tsx
Normal file
100
frontend/src/hooks/dashboard/useDashboardFromLocalStorage.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
interface LocalStoreDashboardVariables {
|
||||
[id: string]: {
|
||||
selectedValue: IDashboardVariable['selectedValue'];
|
||||
allSelected: boolean;
|
||||
};
|
||||
}
|
||||
interface DashboardLocalStorageVariables {
|
||||
[id: string]: LocalStoreDashboardVariables;
|
||||
}
|
||||
|
||||
interface UseDashboardVariablesFromLocalStorageReturn {
|
||||
currentDashboard: LocalStoreDashboardVariables;
|
||||
updateLocalStorageDashboardVariables: (
|
||||
id: string,
|
||||
selectedValue: IDashboardVariable['selectedValue'],
|
||||
allSelected: boolean,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const useDashboardVariablesFromLocalStorage = (
|
||||
dashboardId: string,
|
||||
): UseDashboardVariablesFromLocalStorageReturn => {
|
||||
const [
|
||||
allDashboards,
|
||||
setAllDashboards,
|
||||
] = useState<DashboardLocalStorageVariables>({});
|
||||
|
||||
const [
|
||||
currentDashboard,
|
||||
setCurrentDashboard,
|
||||
] = useState<LocalStoreDashboardVariables>({});
|
||||
|
||||
useEffect(() => {
|
||||
const localStoreDashboardVariablesString = getLocalStorageKey(
|
||||
LOCALSTORAGE.DASHBOARD_VARIABLES,
|
||||
);
|
||||
let localStoreDashboardVariables: DashboardLocalStorageVariables = {};
|
||||
if (localStoreDashboardVariablesString === null) {
|
||||
try {
|
||||
const serialzedData = JSON.stringify({
|
||||
[dashboardId]: {},
|
||||
});
|
||||
|
||||
setLocalStorageKey(LOCALSTORAGE.DASHBOARD_VARIABLES, serialzedData);
|
||||
} catch {
|
||||
console.error('Failed to seralise the data');
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
localStoreDashboardVariables = JSON.parse(
|
||||
localStoreDashboardVariablesString,
|
||||
);
|
||||
} catch {
|
||||
console.error('Failed to parse dashboards from local storage');
|
||||
localStoreDashboardVariables = {};
|
||||
} finally {
|
||||
setAllDashboards(localStoreDashboardVariables);
|
||||
}
|
||||
}
|
||||
setCurrentDashboard(defaultTo(localStoreDashboardVariables[dashboardId], {}));
|
||||
}, [dashboardId]);
|
||||
|
||||
const updateLocalStorageDashboardVariables = (
|
||||
id: string,
|
||||
selectedValue: IDashboardVariable['selectedValue'],
|
||||
allSelected: boolean,
|
||||
): void => {
|
||||
const newCurrentDashboard = {
|
||||
...currentDashboard,
|
||||
[id]: { selectedValue, allSelected },
|
||||
};
|
||||
|
||||
const newAllDashboards = {
|
||||
...allDashboards,
|
||||
[dashboardId]: newCurrentDashboard,
|
||||
};
|
||||
|
||||
try {
|
||||
const serializedData = JSON.stringify(newAllDashboards);
|
||||
setLocalStorageKey(LOCALSTORAGE.DASHBOARD_VARIABLES, serializedData);
|
||||
} catch {
|
||||
console.error('Failed to set dashboards in local storage');
|
||||
}
|
||||
|
||||
setAllDashboards(newAllDashboards);
|
||||
setCurrentDashboard(newCurrentDashboard);
|
||||
};
|
||||
|
||||
return {
|
||||
currentDashboard,
|
||||
updateLocalStorageDashboardVariables,
|
||||
};
|
||||
};
|
||||
@@ -88,9 +88,11 @@ const getChartData = ({
|
||||
const allLabels = modifiedData.map((e) => e.label);
|
||||
|
||||
const updatedDataSet = updatedSortedData.map((e, index) => {
|
||||
const label = allLabels[index];
|
||||
|
||||
const datasetBaseConfig = {
|
||||
index,
|
||||
label: allLabels[index],
|
||||
label,
|
||||
borderColor: colors[index % colors.length] || 'red',
|
||||
data: e.second,
|
||||
borderWidth: 1.5,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { getToolTipValue } from 'components/Graph/yAxisConfig';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import dayjs from 'dayjs';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
import { colors } from '../../getRandomColor';
|
||||
import { placement } from '../placement';
|
||||
import { generateColor } from '../utils/generateColor';
|
||||
|
||||
dayjs.extend(customParseFormat);
|
||||
|
||||
@@ -54,12 +55,14 @@ const generateTooltipContent = (
|
||||
const value = data[index][idx];
|
||||
const label = getLabelName(metric, queryName || '', legend || '');
|
||||
|
||||
const color = generateColor(label, themeColors.chartcolors);
|
||||
|
||||
if (Number.isFinite(value)) {
|
||||
const tooltipValue = getToolTipValue(value, yAxisUnit);
|
||||
|
||||
const dataObj = {
|
||||
show: item.show || false,
|
||||
color: colors[(index - 1) % colors.length],
|
||||
color,
|
||||
label,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
|
||||
20
frontend/src/lib/uPlotLib/utils/generateColor.ts
Normal file
20
frontend/src/lib/uPlotLib/utils/generateColor.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/* eslint-disable no-bitwise */
|
||||
export function hashFn(str: string): number {
|
||||
let hash = 5381;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = (hash * 33) ^ str.charCodeAt(i);
|
||||
}
|
||||
|
||||
return hash >>> 0;
|
||||
}
|
||||
|
||||
export function generateColor(
|
||||
key: string,
|
||||
colorMap: Record<string, string>,
|
||||
): string {
|
||||
const hashValue = hashFn(key);
|
||||
const keys = Object.keys(colorMap);
|
||||
const colorIndex = hashValue % keys.length;
|
||||
const selectedKey = keys[colorIndex];
|
||||
return colorMap[selectedKey];
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { themeColors } from 'constants/theme';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { colors } from 'lib/getRandomColor';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
|
||||
import { generateColor } from './generateColor';
|
||||
import getRenderer, { drawStyles, lineInterpolations } from './getRenderer';
|
||||
|
||||
const paths = (
|
||||
@@ -36,8 +37,6 @@ const getSeries = (
|
||||
const newGraphVisibilityStates = graphsVisibilityStates?.slice(1);
|
||||
|
||||
for (let i = 0; i < seriesList?.length; i += 1) {
|
||||
const color = colors[i % colors.length]; // Use modulo to loop through colors if there are more series than colors
|
||||
|
||||
const { metric = {}, queryName = '', legend = '' } = widgetMetaData[i] || {};
|
||||
|
||||
const label = getLabelName(
|
||||
@@ -46,6 +45,8 @@ const getSeries = (
|
||||
legend || '',
|
||||
);
|
||||
|
||||
const color = generateColor(label, themeColors.chartcolors);
|
||||
|
||||
const pointSize = seriesList[i].values.length > 1 ? 5 : 10;
|
||||
const showPoints = !(seriesList[i].values.length > 1);
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ function ServiceMap({ fgRef, serviceMap }: any): JSX.Element {
|
||||
|
||||
const graphData = { nodes, links };
|
||||
|
||||
let zoomLevel = 1;
|
||||
|
||||
return (
|
||||
<ForceGraph2D
|
||||
ref={fgRef}
|
||||
@@ -23,8 +25,9 @@ function ServiceMap({ fgRef, serviceMap }: any): JSX.Element {
|
||||
linkDirectionalParticles="value"
|
||||
linkDirectionalParticleSpeed={(d) => d.value}
|
||||
nodeCanvasObject={(node, ctx) => {
|
||||
const label = transformLabel(node.id);
|
||||
const { fontSize } = node;
|
||||
const label = transformLabel(node.id, zoomLevel);
|
||||
let { fontSize } = node;
|
||||
fontSize = (fontSize * 3) / zoomLevel;
|
||||
ctx.font = `${fontSize}px Roboto`;
|
||||
const { width } = node;
|
||||
|
||||
@@ -43,6 +46,9 @@ function ServiceMap({ fgRef, serviceMap }: any): JSX.Element {
|
||||
tooltip.innerHTML = getTooltip(node);
|
||||
}
|
||||
}}
|
||||
onZoom={(zoom) => {
|
||||
zoomLevel = zoom.k;
|
||||
}}
|
||||
nodePointerAreaPaint={(node, color, ctx) => {
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
|
||||
@@ -59,6 +59,7 @@ export const getGraphData = (serviceMap, isDarkMode): graphDataType => {
|
||||
width: MIN_WIDTH,
|
||||
color,
|
||||
nodeVal: MIN_WIDTH,
|
||||
name: node,
|
||||
};
|
||||
}
|
||||
if (service.errorRate > 0) {
|
||||
@@ -72,6 +73,7 @@ export const getGraphData = (serviceMap, isDarkMode): graphDataType => {
|
||||
width,
|
||||
color,
|
||||
nodeVal: width,
|
||||
name: node,
|
||||
};
|
||||
});
|
||||
return {
|
||||
@@ -123,9 +125,12 @@ export const getTooltip = (link: {
|
||||
</div>`;
|
||||
};
|
||||
|
||||
export const transformLabel = (label: string) => {
|
||||
const MAX_LENGTH = 13;
|
||||
const MAX_SHOW = 10;
|
||||
export const transformLabel = (label: string, zoomLevel: number) => {
|
||||
//? 13 is the minimum label length. Scaling factor of 0.9 which is slightly less than 1
|
||||
//? ensures smoother zoom transitions, gradually increasing MAX_LENGTH, displaying more of the label as
|
||||
//? zooming in.
|
||||
const MAX_LENGTH = 13 * (zoomLevel / 0.9);
|
||||
const MAX_SHOW = MAX_LENGTH - 3;
|
||||
if (label.length > MAX_LENGTH) {
|
||||
return `${label.slice(0, MAX_SHOW)}...`;
|
||||
}
|
||||
|
||||
@@ -2,17 +2,21 @@ import { RouteTabProps } from 'components/RouteTab/types';
|
||||
import ROUTES from 'constants/routes';
|
||||
import AlertChannels from 'container/AllAlertChannels';
|
||||
import GeneralSettings from 'container/GeneralSettings';
|
||||
import GeneralSettingsCloud from 'container/GeneralSettingsCloud';
|
||||
import IngestionSettings from 'container/IngestionSettings/IngestionSettings';
|
||||
import OrganizationSettings from 'container/OrganizationSettings';
|
||||
import { TFunction } from 'i18next';
|
||||
|
||||
export const commonRoutes = (t: TFunction): RouteTabProps['routes'] => [
|
||||
export const organizationSettings = (t: TFunction): RouteTabProps['routes'] => [
|
||||
{
|
||||
Component: GeneralSettings,
|
||||
name: t('routes:general').toString(),
|
||||
route: ROUTES.SETTINGS,
|
||||
key: ROUTES.SETTINGS,
|
||||
Component: OrganizationSettings,
|
||||
name: t('routes:organization_settings').toString(),
|
||||
route: ROUTES.ORG_SETTINGS,
|
||||
key: ROUTES.ORG_SETTINGS,
|
||||
},
|
||||
];
|
||||
|
||||
export const alertChannels = (t: TFunction): RouteTabProps['routes'] => [
|
||||
{
|
||||
Component: AlertChannels,
|
||||
name: t('routes:alert_channels').toString(),
|
||||
@@ -30,11 +34,20 @@ export const ingestionSettings = (t: TFunction): RouteTabProps['routes'] => [
|
||||
},
|
||||
];
|
||||
|
||||
export const organizationSettings = (t: TFunction): RouteTabProps['routes'] => [
|
||||
export const generalSettings = (t: TFunction): RouteTabProps['routes'] => [
|
||||
{
|
||||
Component: OrganizationSettings,
|
||||
name: t('routes:organization_settings').toString(),
|
||||
route: ROUTES.ORG_SETTINGS,
|
||||
key: ROUTES.ORG_SETTINGS,
|
||||
Component: GeneralSettings,
|
||||
name: t('routes:general').toString(),
|
||||
route: ROUTES.SETTINGS,
|
||||
key: ROUTES.SETTINGS,
|
||||
},
|
||||
];
|
||||
|
||||
export const generalSettingsCloud = (t: TFunction): RouteTabProps['routes'] => [
|
||||
{
|
||||
Component: GeneralSettingsCloud,
|
||||
name: t('routes:general').toString(),
|
||||
route: ROUTES.SETTINGS,
|
||||
key: ROUTES.SETTINGS,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -3,7 +3,9 @@ import { TFunction } from 'i18next';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
|
||||
import {
|
||||
commonRoutes,
|
||||
alertChannels,
|
||||
generalSettings,
|
||||
generalSettingsCloud,
|
||||
ingestionSettings,
|
||||
organizationSettings,
|
||||
} from './config';
|
||||
@@ -12,15 +14,20 @@ export const getRoutes = (
|
||||
isCurrentOrgSettings: boolean,
|
||||
t: TFunction,
|
||||
): RouteTabProps['routes'] => {
|
||||
let common = commonRoutes(t);
|
||||
const settings = [];
|
||||
|
||||
if (isCurrentOrgSettings) {
|
||||
common = [...common, ...organizationSettings(t)];
|
||||
settings.push(...organizationSettings(t));
|
||||
}
|
||||
|
||||
if (isCloudUser()) {
|
||||
common = [...common, ...ingestionSettings(t)];
|
||||
settings.push(...ingestionSettings(t));
|
||||
settings.push(...alertChannels(t));
|
||||
settings.push(...generalSettingsCloud(t));
|
||||
} else {
|
||||
settings.push(...alertChannels(t));
|
||||
settings.push(...generalSettings(t));
|
||||
}
|
||||
|
||||
return common;
|
||||
return settings;
|
||||
};
|
||||
|
||||
@@ -344,7 +344,7 @@ function SignUp({ version }: SignUpProps): JSX.Element {
|
||||
placeholder={t('placeholder_firstname')}
|
||||
required
|
||||
id="signupFirstName"
|
||||
disabled={isDetailsDisable}
|
||||
disabled={isDetailsDisable && form.getFieldValue('firstName')}
|
||||
/>
|
||||
</FormContainer.Item>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Button, Card, Skeleton, Typography } from 'antd';
|
||||
import updateCreditCardApi from 'api/billing/checkout';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import ROUTES from 'constants/routes';
|
||||
import FullViewHeader from 'container/FullViewHeader/FullViewHeader';
|
||||
import FullScreenHeader from 'container/FullScreenHeader/FullScreenHeader';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import useLicense from 'hooks/useLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
@@ -106,7 +106,7 @@ export default function WorkspaceBlocked(): JSX.Element {
|
||||
|
||||
return (
|
||||
<>
|
||||
<FullViewHeader overrideRoute={ROUTES.WORKSPACE_LOCKED} />
|
||||
<FullScreenHeader overrideRoute={ROUTES.WORKSPACE_LOCKED} />
|
||||
|
||||
<Card className="workspace-locked-container">
|
||||
{isLoadingLicenseData || !licensesData?.payload?.workSpaceBlock ? (
|
||||
|
||||
@@ -6,6 +6,7 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
|
||||
import useAxiosError from 'hooks/useAxiosError';
|
||||
import useTabVisibility from 'hooks/useTabFocus';
|
||||
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
|
||||
@@ -95,6 +96,10 @@ export function DashboardProvider({
|
||||
|
||||
const [selectedDashboard, setSelectedDashboard] = useState<Dashboard>();
|
||||
|
||||
const { currentDashboard } = useDashboardVariablesFromLocalStorage(
|
||||
dashboardId,
|
||||
);
|
||||
|
||||
const updatedTimeRef = useRef<Dayjs | null>(null); // Using ref to store the updated time
|
||||
const modalRef = useRef<any>(null);
|
||||
|
||||
@@ -103,11 +108,33 @@ export function DashboardProvider({
|
||||
const { t } = useTranslation(['dashboard']);
|
||||
const dashboardRef = useRef<Dashboard>();
|
||||
|
||||
const mergeDBWithLocalStorage = (
|
||||
data: Dashboard,
|
||||
localStorageVariables: any,
|
||||
): Dashboard => {
|
||||
const updatedData = data;
|
||||
if (data && localStorageVariables) {
|
||||
const updatedVariables = data.data.variables;
|
||||
Object.keys(data.data.variables).forEach((variable) => {
|
||||
const updatedVariable = {
|
||||
...data.data.variables[variable],
|
||||
...localStorageVariables[variable as any],
|
||||
};
|
||||
|
||||
updatedVariables[variable] = updatedVariable;
|
||||
});
|
||||
updatedData.data.variables = updatedVariables;
|
||||
}
|
||||
return updatedData;
|
||||
};
|
||||
// As we do not have order and ID's in the variables object, we have to process variables to add order and ID if they do not exist in the variables object
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const transformDashboardVariables = (data: Dashboard): Dashboard => {
|
||||
if (data && data.data && data.data.variables) {
|
||||
const clonedDashboardData = JSON.parse(JSON.stringify(data));
|
||||
const clonedDashboardData = mergeDBWithLocalStorage(
|
||||
JSON.parse(JSON.stringify(data)),
|
||||
currentDashboard,
|
||||
);
|
||||
const { variables } = clonedDashboardData.data;
|
||||
const existingOrders: Set<number> = new Set();
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface AlertDef {
|
||||
source?: string;
|
||||
disabled?: boolean;
|
||||
preferredChannels?: string[];
|
||||
broadcastToAll?: boolean;
|
||||
}
|
||||
|
||||
export interface RuleCondition {
|
||||
|
||||
2
go.mod
2
go.mod
@@ -5,7 +5,7 @@ go 1.21
|
||||
require (
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.15.0
|
||||
github.com/SigNoz/govaluate v0.0.0-20220522085550-d19c08c206cb
|
||||
github.com/SigNoz/signoz-otel-collector v0.88.8
|
||||
github.com/SigNoz/signoz-otel-collector v0.88.9
|
||||
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974
|
||||
github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230822164844-1b861a431974
|
||||
github.com/antonmedv/expr v1.15.3
|
||||
|
||||
4
go.sum
4
go.sum
@@ -98,8 +98,8 @@ github.com/SigNoz/govaluate v0.0.0-20220522085550-d19c08c206cb h1:bneLSKPf9YUSFm
|
||||
github.com/SigNoz/govaluate v0.0.0-20220522085550-d19c08c206cb/go.mod h1:JznGDNg9x1cujDKa22RaQOimOvvEfy3nxzDGd8XDgmA=
|
||||
github.com/SigNoz/prometheus v1.9.78 h1:bB3yuDrRzi/Mv00kWayR9DZbyjTuGfendSqISyDcXiY=
|
||||
github.com/SigNoz/prometheus v1.9.78/go.mod h1:MffmFu2qFILQrOHehx3D0XjYtaZMVfI+Ppeiv98x4Ww=
|
||||
github.com/SigNoz/signoz-otel-collector v0.88.8 h1:oa/0gSfkGhjzXtz1htzWBQx3p4VhBPs5iwMRxqfa2uo=
|
||||
github.com/SigNoz/signoz-otel-collector v0.88.8/go.mod h1:7I4FWwraVSnDywsPNbo8TdHDsPxShtYkGU5usr6dTtk=
|
||||
github.com/SigNoz/signoz-otel-collector v0.88.9 h1:7bbJSXrSZcQsdEVVLsjsNXm/bWe9MhKu8qfXp8MlXQM=
|
||||
github.com/SigNoz/signoz-otel-collector v0.88.9/go.mod h1:7I4FWwraVSnDywsPNbo8TdHDsPxShtYkGU5usr6dTtk=
|
||||
github.com/SigNoz/zap_otlp v0.1.0 h1:T7rRcFN87GavY8lDGZj0Z3Xv6OhJA6Pj3I9dNPmqvRc=
|
||||
github.com/SigNoz/zap_otlp v0.1.0/go.mod h1:lcHvbDbRgvDnPxo9lDlaL1JK2PyOyouP/C3ynnYIvyo=
|
||||
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974 h1:PKVgdf83Yw+lZJbFtNGBgqXiXNf3+kOXW2qZ7Ms7OaY=
|
||||
|
||||
90
pkg/query-service/app/having.go
Normal file
90
pkg/query-service/app/having.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
)
|
||||
|
||||
// applyHavingClause applies the having clause to the result
|
||||
// each query has its own having clause
|
||||
// there can be multiple having clauses for each query
|
||||
func applyHavingClause(result []*v3.Result, queryRangeParams *v3.QueryRangeParamsV3) {
|
||||
for _, result := range result {
|
||||
builderQueries := queryRangeParams.CompositeQuery.BuilderQueries
|
||||
|
||||
if builderQueries != nil && (builderQueries[result.QueryName].DataSource == v3.DataSourceMetrics) {
|
||||
havingClause := builderQueries[result.QueryName].Having
|
||||
|
||||
for i := 0; i < len(result.Series); i++ {
|
||||
for j := 0; j < len(result.Series[i].Points); j++ {
|
||||
if !evaluateHavingClause(havingClause, result.Series[i].Points[j].Value) {
|
||||
result.Series[i].Points = append(result.Series[i].Points[:j], result.Series[i].Points[j+1:]...)
|
||||
j--
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func evaluateHavingClause(having []v3.Having, value float64) bool {
|
||||
if len(having) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, h := range having {
|
||||
switch h.Operator {
|
||||
case v3.HavingOperatorEqual:
|
||||
if value == h.Value.(float64) {
|
||||
return true
|
||||
}
|
||||
case v3.HavingOperatorNotEqual:
|
||||
if value != h.Value.(float64) {
|
||||
return true
|
||||
}
|
||||
case v3.HavingOperatorGreaterThan:
|
||||
if value > h.Value.(float64) {
|
||||
return true
|
||||
}
|
||||
case v3.HavingOperatorGreaterThanOrEq:
|
||||
if value >= h.Value.(float64) {
|
||||
return true
|
||||
}
|
||||
case v3.HavingOperatorLessThan:
|
||||
if value < h.Value.(float64) {
|
||||
return true
|
||||
}
|
||||
case v3.HavingOperatorLessThanOrEq:
|
||||
if value <= h.Value.(float64) {
|
||||
return true
|
||||
}
|
||||
case v3.HavingOperatorIn, v3.HavingOperator(strings.ToLower(string(v3.HavingOperatorIn))):
|
||||
values, ok := h.Value.([]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for _, v := range values {
|
||||
if value == v.(float64) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
case v3.HavingOperatorNotIn, v3.HavingOperator(strings.ToLower(string(v3.HavingOperatorNotIn))):
|
||||
values, ok := h.Value.([]interface{})
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
found := false
|
||||
for _, v := range values {
|
||||
if value == v.(float64) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
283
pkg/query-service/app/having_test.go
Normal file
283
pkg/query-service/app/having_test.go
Normal file
@@ -0,0 +1,283 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
)
|
||||
|
||||
func TestApplyHavingCaluse(t *testing.T) {
|
||||
type testCase struct {
|
||||
name string
|
||||
results []*v3.Result
|
||||
params *v3.QueryRangeParamsV3
|
||||
want []*v3.Result
|
||||
}
|
||||
|
||||
testCases := []testCase{
|
||||
{
|
||||
name: "test having equal to",
|
||||
results: []*v3.Result{
|
||||
{
|
||||
QueryName: "A",
|
||||
Series: []*v3.Series{
|
||||
{
|
||||
Points: []v3.Point{
|
||||
{
|
||||
Value: 0.5,
|
||||
},
|
||||
{
|
||||
Value: 0.4,
|
||||
},
|
||||
{
|
||||
Value: 0.3,
|
||||
},
|
||||
{
|
||||
Value: 0.2,
|
||||
},
|
||||
{
|
||||
Value: 0.1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
params: &v3.QueryRangeParamsV3{
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
BuilderQueries: map[string]*v3.BuilderQuery{
|
||||
"A": {
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
Having: []v3.Having{
|
||||
{
|
||||
Operator: v3.HavingOperatorEqual,
|
||||
Value: 0.3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []*v3.Result{
|
||||
{
|
||||
Series: []*v3.Series{
|
||||
{
|
||||
Points: []v3.Point{
|
||||
{
|
||||
Value: 0.3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test having `in`",
|
||||
results: []*v3.Result{
|
||||
{
|
||||
QueryName: "A",
|
||||
Series: []*v3.Series{
|
||||
{
|
||||
Points: []v3.Point{
|
||||
{
|
||||
Value: 0.5,
|
||||
},
|
||||
{
|
||||
Value: 0.4,
|
||||
},
|
||||
{
|
||||
Value: 0.3,
|
||||
},
|
||||
{
|
||||
Value: 0.2,
|
||||
},
|
||||
{
|
||||
Value: 0.1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
params: &v3.QueryRangeParamsV3{
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
BuilderQueries: map[string]*v3.BuilderQuery{
|
||||
"A": {
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
Having: []v3.Having{
|
||||
{
|
||||
Operator: v3.HavingOperatorIn,
|
||||
Value: []interface{}{0.3, 0.4},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []*v3.Result{
|
||||
{
|
||||
Series: []*v3.Series{
|
||||
{
|
||||
Points: []v3.Point{
|
||||
{
|
||||
Value: 0.4,
|
||||
},
|
||||
{
|
||||
Value: 0.3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test having `not in` and multiple results",
|
||||
results: []*v3.Result{
|
||||
{
|
||||
QueryName: "A",
|
||||
Series: []*v3.Series{
|
||||
{
|
||||
Points: []v3.Point{
|
||||
{
|
||||
Value: 0.5,
|
||||
},
|
||||
{
|
||||
Value: 0.4,
|
||||
},
|
||||
{
|
||||
Value: 0.3,
|
||||
},
|
||||
{
|
||||
Value: 0.2,
|
||||
},
|
||||
{
|
||||
Value: 0.1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
QueryName: "B",
|
||||
Series: []*v3.Series{
|
||||
{
|
||||
Points: []v3.Point{
|
||||
{
|
||||
Value: 0.5,
|
||||
},
|
||||
{
|
||||
Value: 0.4,
|
||||
},
|
||||
{
|
||||
Value: 0.3,
|
||||
},
|
||||
{
|
||||
Value: 0.2,
|
||||
},
|
||||
{
|
||||
Value: 0.1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
params: &v3.QueryRangeParamsV3{
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
BuilderQueries: map[string]*v3.BuilderQuery{
|
||||
"A": {
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
Having: []v3.Having{
|
||||
{
|
||||
Operator: v3.HavingOperatorNotIn,
|
||||
Value: []interface{}{0.3, 0.4},
|
||||
},
|
||||
},
|
||||
},
|
||||
"B": {
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
Having: []v3.Having{
|
||||
{
|
||||
Operator: v3.HavingOperatorNotIn,
|
||||
Value: []interface{}{0.1},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []*v3.Result{
|
||||
{
|
||||
Series: []*v3.Series{
|
||||
{
|
||||
Points: []v3.Point{
|
||||
{
|
||||
Value: 0.5,
|
||||
},
|
||||
{
|
||||
Value: 0.2,
|
||||
},
|
||||
{
|
||||
Value: 0.1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Series: []*v3.Series{
|
||||
{
|
||||
Points: []v3.Point{
|
||||
{
|
||||
Value: 0.5,
|
||||
},
|
||||
{
|
||||
Value: 0.4,
|
||||
},
|
||||
{
|
||||
Value: 0.3,
|
||||
},
|
||||
{
|
||||
Value: 0.2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
applyHavingClause(tc.results, tc.params)
|
||||
|
||||
got := tc.results
|
||||
|
||||
if len(got) != len(tc.want) {
|
||||
t.Errorf("got %v, want %v", got, tc.want)
|
||||
}
|
||||
|
||||
for i := range got {
|
||||
if len(got[i].Series) != len(tc.want[i].Series) {
|
||||
t.Errorf("got %v, want %v", got, tc.want)
|
||||
}
|
||||
|
||||
for j := range got[i].Series {
|
||||
if len(got[i].Series[j].Points) != len(tc.want[i].Series[j].Points) {
|
||||
t.Errorf("got %v, want %v", len(got[i].Series[j].Points), len(tc.want[i].Series[j].Points))
|
||||
}
|
||||
|
||||
for k := range got[i].Series[j].Points {
|
||||
if got[i].Series[j].Points[k].Value != tc.want[i].Series[j].Points[k].Value {
|
||||
t.Errorf("got %v, want %v", got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -3107,78 +3106,6 @@ func (aH *APIHandler) QueryRangeV3(w http.ResponseWriter, r *http.Request) {
|
||||
aH.queryRangeV3(r.Context(), queryRangeParams, w, r)
|
||||
}
|
||||
|
||||
func applyMetricLimit(results []*v3.Result, queryRangeParams *v3.QueryRangeParamsV3) {
|
||||
// apply limit if any for metrics
|
||||
// use the grouping set points to apply the limit
|
||||
|
||||
for _, result := range results {
|
||||
builderQueries := queryRangeParams.CompositeQuery.BuilderQueries
|
||||
|
||||
if builderQueries != nil && (builderQueries[result.QueryName].DataSource == v3.DataSourceMetrics ||
|
||||
result.QueryName != builderQueries[result.QueryName].Expression) {
|
||||
limit := builderQueries[result.QueryName].Limit
|
||||
|
||||
orderByList := builderQueries[result.QueryName].OrderBy
|
||||
if limit >= 0 {
|
||||
if len(orderByList) == 0 {
|
||||
// If no orderBy is specified, sort by value in descending order
|
||||
orderByList = []v3.OrderBy{{ColumnName: constants.SigNozOrderByValue, Order: "desc"}}
|
||||
}
|
||||
sort.SliceStable(result.Series, func(i, j int) bool {
|
||||
for _, orderBy := range orderByList {
|
||||
if orderBy.ColumnName == constants.SigNozOrderByValue {
|
||||
|
||||
// For table type queries (we rely on the fact that one value for row), sort
|
||||
// based on final aggregation value
|
||||
if len(result.Series[i].Points) == 1 && len(result.Series[j].Points) == 1 {
|
||||
if orderBy.Order == "asc" {
|
||||
return result.Series[i].Points[0].Value < result.Series[j].Points[0].Value
|
||||
} else if orderBy.Order == "desc" {
|
||||
return result.Series[i].Points[0].Value > result.Series[j].Points[0].Value
|
||||
}
|
||||
}
|
||||
|
||||
// For graph type queries, sort based on GroupingSetsPoint
|
||||
if result.Series[i].GroupingSetsPoint == nil || result.Series[j].GroupingSetsPoint == nil {
|
||||
// Handle nil GroupingSetsPoint, if needed
|
||||
// Here, we assume non-nil values are always less than nil values
|
||||
return result.Series[i].GroupingSetsPoint != nil
|
||||
}
|
||||
if orderBy.Order == "asc" {
|
||||
return result.Series[i].GroupingSetsPoint.Value < result.Series[j].GroupingSetsPoint.Value
|
||||
} else if orderBy.Order == "desc" {
|
||||
return result.Series[i].GroupingSetsPoint.Value > result.Series[j].GroupingSetsPoint.Value
|
||||
}
|
||||
} else {
|
||||
// Sort based on Labels map
|
||||
labelI, existsI := result.Series[i].Labels[orderBy.ColumnName]
|
||||
labelJ, existsJ := result.Series[j].Labels[orderBy.ColumnName]
|
||||
|
||||
if !existsI || !existsJ {
|
||||
// Handle missing labels, if needed
|
||||
// Here, we assume non-existent labels are always less than existing ones
|
||||
return existsI
|
||||
}
|
||||
|
||||
if orderBy.Order == "asc" {
|
||||
return strings.Compare(labelI, labelJ) < 0
|
||||
} else if orderBy.Order == "desc" {
|
||||
return strings.Compare(labelI, labelJ) > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
// Preserve original order if no matching orderBy is found
|
||||
return i < j
|
||||
})
|
||||
|
||||
if limit > 0 && len(result.Series) > int(limit) {
|
||||
result.Series = result.Series[:limit]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (aH *APIHandler) liveTailLogs(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// get the param from url and add it to body
|
||||
@@ -3295,6 +3222,10 @@ func (aH *APIHandler) queryRangeV4(ctx context.Context, queryRangeParams *v3.Que
|
||||
return
|
||||
}
|
||||
|
||||
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder {
|
||||
postProcessResult(result, queryRangeParams)
|
||||
}
|
||||
|
||||
resp := v3.QueryRangeResponse{
|
||||
Result: result,
|
||||
}
|
||||
@@ -3322,3 +3253,48 @@ func (aH *APIHandler) QueryRangeV4(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
aH.queryRangeV4(r.Context(), queryRangeParams, w, r)
|
||||
}
|
||||
|
||||
// postProcessResult applies having clause, metric limit, reduce function to the result
|
||||
// This function is effective for metrics data source for now, but it can be extended to other data sources
|
||||
// if needed
|
||||
// Much of this work can be done in the ClickHouse query, but we decided to do it here because:
|
||||
// 1. Effective use of caching
|
||||
// 2. Easier to add new functions
|
||||
func postProcessResult(result []*v3.Result, queryRangeParams *v3.QueryRangeParamsV3) {
|
||||
// Having clause is not part of the clickhouse query, so we need to apply it here
|
||||
// It's not included in the query because it doesn't work nicely with caching
|
||||
// With this change, if you have a query with a having clause, and then you change the having clause
|
||||
// to something else, the query will still be cached.
|
||||
applyHavingClause(result, queryRangeParams)
|
||||
// We apply the metric limit here because it's not part of the clickhouse query
|
||||
// The limit in the context of the time series query is the number of time series
|
||||
// So for the limit to work, we need to know what series to keep and what to discard
|
||||
// For us to know that, we need to execute the query first, and then apply the limit
|
||||
// which we found expensive, because we are executing the query twice on the same data
|
||||
// So we decided to apply the limit here, after the query is executed
|
||||
// The function is named applyMetricLimit because it only applies to metrics data source
|
||||
// In traces and logs, the limit is achieved using subqueries
|
||||
applyMetricLimit(result, queryRangeParams)
|
||||
// Each series in the result produces N number of points, where N is (end - start) / step
|
||||
// For the panel type table, we need to show one point for each series in the row
|
||||
// We do that by applying a reduce function to each series
|
||||
applyReduceTo(result, queryRangeParams)
|
||||
// We apply the functions here it's easier to add new functions
|
||||
applyFunctions(result, queryRangeParams)
|
||||
}
|
||||
|
||||
// applyFunctions applies functions for each query in the composite query
|
||||
// The functions can be more than one, and they are applied in the order they are defined
|
||||
func applyFunctions(results []*v3.Result, queryRangeParams *v3.QueryRangeParamsV3) {
|
||||
for idx, result := range results {
|
||||
builderQueries := queryRangeParams.CompositeQuery.BuilderQueries
|
||||
|
||||
if builderQueries != nil && (builderQueries[result.QueryName].DataSource == v3.DataSourceMetrics) {
|
||||
functions := builderQueries[result.QueryName].Functions
|
||||
|
||||
for _, function := range functions {
|
||||
results[idx] = queryBuilder.ApplyFunction(function, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user