mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-11 10:29:01 +08:00
Merge branch 'develop' into issue-2511
This commit is contained in:
commit
6322578842
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@ -2,6 +2,6 @@
|
|||||||
# Owners are automatically requested for review for PRs that changes code
|
# Owners are automatically requested for review for PRs that changes code
|
||||||
# that they own.
|
# that they own.
|
||||||
* @ankitnayan
|
* @ankitnayan
|
||||||
/frontend/ @palashgdev @pranshuchittora
|
/frontend/ @palashgdev
|
||||||
/deploy/ @prashant-shahi
|
/deploy/ @prashant-shahi
|
||||||
**/query-service/ @srikanthccv
|
**/query-service/ @srikanthccv
|
||||||
|
6
.github/workflows/staging-deployment.yaml
vendored
6
.github/workflows/staging-deployment.yaml
vendored
@ -11,21 +11,23 @@ jobs:
|
|||||||
environment: staging
|
environment: staging
|
||||||
steps:
|
steps:
|
||||||
- name: Executing remote ssh commands using ssh key
|
- name: Executing remote ssh commands using ssh key
|
||||||
uses: appleboy/ssh-action@v0.1.6
|
uses: appleboy/ssh-action@v0.1.8
|
||||||
env:
|
env:
|
||||||
GITHUB_BRANCH: develop
|
GITHUB_BRANCH: develop
|
||||||
GITHUB_SHA: ${{ github.sha }}
|
GITHUB_SHA: ${{ github.sha }}
|
||||||
with:
|
with:
|
||||||
host: ${{ secrets.HOST_DNS }}
|
host: ${{ secrets.HOST_DNS }}
|
||||||
username: ${{ secrets.USERNAME }}
|
username: ${{ secrets.USERNAME }}
|
||||||
key: ${{ secrets.EC2_SSH_KEY }}
|
key: ${{ secrets.SSH_KEY }}
|
||||||
envs: GITHUB_BRANCH,GITHUB_SHA
|
envs: GITHUB_BRANCH,GITHUB_SHA
|
||||||
command_timeout: 60m
|
command_timeout: 60m
|
||||||
script: |
|
script: |
|
||||||
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
|
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
|
||||||
echo "GITHUB_SHA: ${GITHUB_SHA}"
|
echo "GITHUB_SHA: ${GITHUB_SHA}"
|
||||||
export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it
|
export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it
|
||||||
|
export OTELCOL_TAG="main"
|
||||||
docker system prune --force
|
docker system prune --force
|
||||||
|
docker pull signoz/signoz-otel-collector:main
|
||||||
cd ~/signoz
|
cd ~/signoz
|
||||||
git status
|
git status
|
||||||
git add .
|
git add .
|
||||||
|
4
.github/workflows/testing-deployment.yaml
vendored
4
.github/workflows/testing-deployment.yaml
vendored
@ -11,14 +11,14 @@ jobs:
|
|||||||
if: ${{ github.event.label.name == 'testing-deploy' }}
|
if: ${{ github.event.label.name == 'testing-deploy' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Executing remote ssh commands using ssh key
|
- name: Executing remote ssh commands using ssh key
|
||||||
uses: appleboy/ssh-action@v0.1.6
|
uses: appleboy/ssh-action@v0.1.8
|
||||||
env:
|
env:
|
||||||
GITHUB_BRANCH: ${{ github.head_ref || github.ref_name }}
|
GITHUB_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||||
GITHUB_SHA: ${{ github.sha }}
|
GITHUB_SHA: ${{ github.sha }}
|
||||||
with:
|
with:
|
||||||
host: ${{ secrets.HOST_DNS }}
|
host: ${{ secrets.HOST_DNS }}
|
||||||
username: ${{ secrets.USERNAME }}
|
username: ${{ secrets.USERNAME }}
|
||||||
key: ${{ secrets.EC2_SSH_KEY }}
|
key: ${{ secrets.SSH_KEY }}
|
||||||
envs: GITHUB_BRANCH,GITHUB_SHA
|
envs: GITHUB_BRANCH,GITHUB_SHA
|
||||||
command_timeout: 60m
|
command_timeout: 60m
|
||||||
script: |
|
script: |
|
||||||
|
@ -85,9 +85,9 @@ Hier findest du die vollständige Liste von unterstützten Programmiersprachen -
|
|||||||
|
|
||||||
### Bereitstellung mit Docker
|
### Bereitstellung mit Docker
|
||||||
|
|
||||||
Bitte folge den [hier](https://signoz.io/docs/deployment/docker/) aufgelisteten Schritten um deine Anwendung mit Docker bereitzustellen.
|
Bitte folge den [hier](https://signoz.io/docs/install/docker/) aufgelisteten Schritten um deine Anwendung mit Docker bereitzustellen.
|
||||||
|
|
||||||
Die [Anleitungen zur Fehlerbehebung](https://signoz.io/docs/deployment/troubleshooting) könnten hilfreich sein, falls du auf irgendwelche Schwierigkeiten stößt.
|
Die [Anleitungen zur Fehlerbehebung](https://signoz.io/docs/install/troubleshooting/) könnten hilfreich sein, falls du auf irgendwelche Schwierigkeiten stößt.
|
||||||
|
|
||||||
<p>  </p>
|
<p>  </p>
|
||||||
|
|
||||||
|
@ -130,9 +130,9 @@ You can find the complete list of languages here - https://opentelemetry.io/docs
|
|||||||
|
|
||||||
### Deploy using Docker
|
### Deploy using Docker
|
||||||
|
|
||||||
Please follow the steps listed [here](https://signoz.io/docs/deployment/docker/) to install using docker
|
Please follow the steps listed [here](https://signoz.io/docs/install/docker/) to install using docker
|
||||||
|
|
||||||
The [troubleshooting instructions](https://signoz.io/docs/deployment/troubleshooting) may be helpful if you face any issues.
|
The [troubleshooting instructions](https://signoz.io/docs/install/troubleshooting/) may be helpful if you face any issues.
|
||||||
|
|
||||||
<p>  </p>
|
<p>  </p>
|
||||||
|
|
||||||
|
@ -84,9 +84,9 @@ Você pode encontrar a lista completa de linguagens aqui - https://opentelemetry
|
|||||||
|
|
||||||
### Implantar usando Docker
|
### Implantar usando Docker
|
||||||
|
|
||||||
Siga as etapas listadas [aqui](https://signoz.io/docs/deployment/docker/) para instalar usando o Docker.
|
Siga as etapas listadas [aqui](https://signoz.io/docs/install/docker/) para instalar usando o Docker.
|
||||||
|
|
||||||
Esse [guia para solução de problemas](https://signoz.io/docs/deployment/troubleshooting) pode ser útil se você enfrentar quaisquer problemas.
|
Esse [guia para solução de problemas](https://signoz.io/docs/install/troubleshooting/) pode ser útil se você enfrentar quaisquer problemas.
|
||||||
|
|
||||||
<p>  </p>
|
<p>  </p>
|
||||||
|
|
||||||
|
@ -80,9 +80,9 @@ SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNo
|
|||||||
|
|
||||||
### 使用Docker部署
|
### 使用Docker部署
|
||||||
|
|
||||||
请按照[这里](https://signoz.io/docs/deployment/docker/)列出的步骤使用Docker来安装
|
请按照[这里](https://signoz.io/docs/install/docker/)列出的步骤使用Docker来安装
|
||||||
|
|
||||||
如果你遇到任何问题,这个[排查指南](https://signoz.io/docs/deployment/troubleshooting)会对你有帮助。
|
如果你遇到任何问题,这个[排查指南](https://signoz.io/docs/install/troubleshooting/)会对你有帮助。
|
||||||
|
|
||||||
<p>  </p>
|
<p>  </p>
|
||||||
|
|
||||||
|
@ -137,7 +137,7 @@ services:
|
|||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:0.17.0
|
image: signoz/query-service:0.18.1
|
||||||
command: ["-config=/root/config/prometheus.yml"]
|
command: ["-config=/root/config/prometheus.yml"]
|
||||||
# ports:
|
# ports:
|
||||||
# - "6060:6060" # pprof port
|
# - "6060:6060" # pprof port
|
||||||
@ -166,7 +166,7 @@ services:
|
|||||||
<<: *clickhouse-depend
|
<<: *clickhouse-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:0.17.0
|
image: signoz/frontend:0.18.1
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
@ -179,7 +179,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:0.66.6
|
image: signoz/signoz-otel-collector:0.66.7
|
||||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||||
user: root # required for reading docker container logs
|
user: root # required for reading docker container logs
|
||||||
volumes:
|
volumes:
|
||||||
@ -208,7 +208,7 @@ services:
|
|||||||
<<: *clickhouse-depend
|
<<: *clickhouse-depend
|
||||||
|
|
||||||
otel-collector-metrics:
|
otel-collector-metrics:
|
||||||
image: signoz/signoz-otel-collector:0.66.6
|
image: signoz/signoz-otel-collector:0.66.7
|
||||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||||
volumes:
|
volumes:
|
||||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||||
|
@ -41,7 +41,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`
|
# 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:
|
otel-collector:
|
||||||
container_name: otel-collector
|
container_name: otel-collector
|
||||||
image: signoz/signoz-otel-collector:0.66.6
|
image: signoz/signoz-otel-collector:0.66.7
|
||||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||||
# user: root # required for reading docker container logs
|
# user: root # required for reading docker container logs
|
||||||
volumes:
|
volumes:
|
||||||
@ -67,7 +67,7 @@ services:
|
|||||||
|
|
||||||
otel-collector-metrics:
|
otel-collector-metrics:
|
||||||
container_name: otel-collector-metrics
|
container_name: otel-collector-metrics
|
||||||
image: signoz/signoz-otel-collector:0.66.6
|
image: signoz/signoz-otel-collector:0.66.7
|
||||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||||
volumes:
|
volumes:
|
||||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||||
|
@ -153,7 +153,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`
|
# 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:
|
query-service:
|
||||||
image: signoz/query-service:${DOCKER_TAG:-0.17.0}
|
image: signoz/query-service:${DOCKER_TAG:-0.18.1}
|
||||||
container_name: query-service
|
container_name: query-service
|
||||||
command: ["-config=/root/config/prometheus.yml"]
|
command: ["-config=/root/config/prometheus.yml"]
|
||||||
# ports:
|
# ports:
|
||||||
@ -181,7 +181,7 @@ services:
|
|||||||
<<: *clickhouse-depend
|
<<: *clickhouse-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:${DOCKER_TAG:-0.17.0}
|
image: signoz/frontend:${DOCKER_TAG:-0.18.1}
|
||||||
container_name: frontend
|
container_name: frontend
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
@ -193,7 +193,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.6}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.7}
|
||||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||||
user: root # required for reading docker container logs
|
user: root # required for reading docker container logs
|
||||||
volumes:
|
volumes:
|
||||||
@ -219,7 +219,7 @@ services:
|
|||||||
<<: *clickhouse-depend
|
<<: *clickhouse-depend
|
||||||
|
|
||||||
otel-collector-metrics:
|
otel-collector-metrics:
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.6}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.7}
|
||||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||||
volumes:
|
volumes:
|
||||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||||
|
@ -125,7 +125,7 @@ check_ports_occupied() {
|
|||||||
|
|
||||||
echo "+++++++++++ ERROR ++++++++++++++++++++++"
|
echo "+++++++++++ ERROR ++++++++++++++++++++++"
|
||||||
echo "SigNoz requires ports 3301 & 4317 to be open. Please shut down any other service(s) that may be running on these ports."
|
echo "SigNoz requires ports 3301 & 4317 to be open. Please shut down any other service(s) that may be running on these ports."
|
||||||
echo "You can run SigNoz on another port following this guide https://signoz.io/docs/deployment/docker#troubleshooting"
|
echo "You can run SigNoz on another port following this guide https://signoz.io/docs/install/troubleshooting/"
|
||||||
echo "++++++++++++++++++++++++++++++++++++++++"
|
echo "++++++++++++++++++++++++++++++++++++++++"
|
||||||
echo ""
|
echo ""
|
||||||
exit 1
|
exit 1
|
||||||
@ -249,7 +249,7 @@ bye() { # Prints a friendly good bye message and exits the script.
|
|||||||
echo ""
|
echo ""
|
||||||
echo -e "$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml ps -a"
|
echo -e "$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml ps -a"
|
||||||
|
|
||||||
# echo "Please read our troubleshooting guide https://signoz.io/docs/deployment/docker#troubleshooting"
|
echo "Please read our troubleshooting guide https://signoz.io/docs/install/troubleshooting/"
|
||||||
echo "or reach us for support in #help channel in our Slack Community https://signoz.io/slack"
|
echo "or reach us for support in #help channel in our Slack Community https://signoz.io/slack"
|
||||||
echo "++++++++++++++++++++++++++++++++++++++++"
|
echo "++++++++++++++++++++++++++++++++++++++++"
|
||||||
|
|
||||||
@ -500,7 +500,7 @@ if [[ $status_code -ne 200 ]]; then
|
|||||||
|
|
||||||
echo -e "$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml ps -a"
|
echo -e "$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml ps -a"
|
||||||
|
|
||||||
echo "Please read our troubleshooting guide https://signoz.io/docs/deployment/docker/#troubleshooting-of-common-issues"
|
echo "Please read our troubleshooting guide https://signoz.io/docs/install/troubleshooting/"
|
||||||
echo "or reach us on SigNoz for support https://signoz.io/slack"
|
echo "or reach us on SigNoz for support https://signoz.io/slack"
|
||||||
echo "++++++++++++++++++++++++++++++++++++++++"
|
echo "++++++++++++++++++++++++++++++++++++++++"
|
||||||
|
|
||||||
|
@ -1,5 +1,20 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
/* eslint-disable object-shorthand */
|
||||||
|
/* eslint-disable func-names */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds custom matchers from the react testing library to all tests
|
* Adds custom matchers from the react testing library to all tests
|
||||||
*/
|
*/
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import 'jest-styled-components';
|
import 'jest-styled-components';
|
||||||
|
|
||||||
|
// Mock window.matchMedia
|
||||||
|
window.matchMedia =
|
||||||
|
window.matchMedia ||
|
||||||
|
function (): any {
|
||||||
|
return {
|
||||||
|
matches: false,
|
||||||
|
addListener: function () {},
|
||||||
|
removeListener: function () {},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -4,6 +4,7 @@ import Spinner from 'components/Spinner';
|
|||||||
import AppLayout from 'container/AppLayout';
|
import AppLayout from 'container/AppLayout';
|
||||||
import { useThemeConfig } from 'hooks/useDarkMode';
|
import { useThemeConfig } from 'hooks/useDarkMode';
|
||||||
import { NotificationProvider } from 'hooks/useNotifications';
|
import { NotificationProvider } from 'hooks/useNotifications';
|
||||||
|
import { ResourceProvider } from 'hooks/useResourceAttribute';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||||
import React, { Suspense } from 'react';
|
import React, { Suspense } from 'react';
|
||||||
@ -17,30 +18,32 @@ function App(): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider theme={themeConfig}>
|
<ConfigProvider theme={themeConfig}>
|
||||||
<NotificationProvider>
|
<Router history={history}>
|
||||||
<Router history={history}>
|
<NotificationProvider>
|
||||||
<PrivateRoute>
|
<PrivateRoute>
|
||||||
<QueryBuilderProvider>
|
<ResourceProvider>
|
||||||
<AppLayout>
|
<QueryBuilderProvider>
|
||||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
<AppLayout>
|
||||||
<Switch>
|
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||||
{routes.map(({ path, component, exact }) => (
|
<Switch>
|
||||||
<Route
|
{routes.map(({ path, component, exact }) => (
|
||||||
key={`${path}`}
|
<Route
|
||||||
exact={exact}
|
key={`${path}`}
|
||||||
path={path}
|
exact={exact}
|
||||||
component={component}
|
path={path}
|
||||||
/>
|
component={component}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
<Route path="*" component={NotFound} />
|
<Route path="*" component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</QueryBuilderProvider>
|
</QueryBuilderProvider>
|
||||||
|
</ResourceProvider>
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
</Router>
|
</NotificationProvider>
|
||||||
</NotificationProvider>
|
</Router>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
const apiV1 = '/api/v1/';
|
const apiV1 = '/api/v1/';
|
||||||
|
|
||||||
export const apiV2 = '/api/v2/';
|
export const apiV2 = '/api/v2/';
|
||||||
|
export const apiV3 = '/api/v3/';
|
||||||
export const apiAlertManager = '/api/alertmanager';
|
export const apiAlertManager = '/api/alertmanager';
|
||||||
|
|
||||||
export default apiV1;
|
export default apiV1;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import axios from 'api';
|
import axios from 'api';
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import createQueryParams from 'lib/createQueryParams';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { PayloadProps, Props } from 'types/api/errors/getAll';
|
import { PayloadProps, Props } from 'types/api/errors/getAll';
|
||||||
|
|
||||||
@ -9,11 +8,17 @@ const getAll = async (
|
|||||||
props: Props,
|
props: Props,
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(
|
const response = await axios.post(`/listErrors`, {
|
||||||
`/listErrors?${createQueryParams({
|
start: `${props.start}`,
|
||||||
...props,
|
end: `${props.end}`,
|
||||||
})}`,
|
order: props.order,
|
||||||
);
|
orderParam: props.orderParam,
|
||||||
|
limit: props.limit,
|
||||||
|
offset: props.offset,
|
||||||
|
exceptionType: props.exceptionType,
|
||||||
|
serviceName: props.serviceName,
|
||||||
|
tags: props.tags,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import axios from 'api';
|
import axios from 'api';
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import createQueryParams from 'lib/createQueryParams';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { PayloadProps, Props } from 'types/api/errors/getErrorCounts';
|
import { PayloadProps, Props } from 'types/api/errors/getErrorCounts';
|
||||||
|
|
||||||
@ -9,11 +8,13 @@ const getErrorCounts = async (
|
|||||||
props: Props,
|
props: Props,
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(
|
const response = await axios.post(`/countErrors`, {
|
||||||
`/countErrors?${createQueryParams({
|
start: `${props.start}`,
|
||||||
...props,
|
end: `${props.end}`,
|
||||||
})}`,
|
exceptionType: props.exceptionType,
|
||||||
);
|
serviceName: props.serviceName,
|
||||||
|
tags: props.tags,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
|
@ -9,7 +9,7 @@ import { ENVIRONMENT } from 'constants/env';
|
|||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
|
|
||||||
import apiV1, { apiAlertManager, apiV2 } from './apiV1';
|
import apiV1, { apiAlertManager, apiV2, apiV3 } from './apiV1';
|
||||||
import { Logout } from './utils';
|
import { Logout } from './utils';
|
||||||
|
|
||||||
const interceptorsResponse = (
|
const interceptorsResponse = (
|
||||||
@ -109,6 +109,17 @@ ApiV2Instance.interceptors.response.use(
|
|||||||
);
|
);
|
||||||
ApiV2Instance.interceptors.request.use(interceptorsRequestResponse);
|
ApiV2Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||||
|
|
||||||
|
// axios V3
|
||||||
|
export const ApiV3Instance = axios.create({
|
||||||
|
baseURL: `${ENVIRONMENT.baseURL}${apiV3}`,
|
||||||
|
});
|
||||||
|
ApiV3Instance.interceptors.response.use(
|
||||||
|
interceptorsResponse,
|
||||||
|
interceptorRejected,
|
||||||
|
);
|
||||||
|
ApiV3Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||||
|
//
|
||||||
|
|
||||||
AxiosAlertManagerInstance.interceptors.response.use(
|
AxiosAlertManagerInstance.interceptors.response.use(
|
||||||
interceptorsResponse,
|
interceptorsResponse,
|
||||||
interceptorRejected,
|
interceptorRejected,
|
||||||
|
33
frontend/src/api/queryBuilder/getAggregateAttribute.ts
Normal file
33
frontend/src/api/queryBuilder/getAggregateAttribute.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { ApiV3Instance } from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError, AxiosResponse } from 'axios';
|
||||||
|
// ** Helpers
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
// ** Types
|
||||||
|
import { IGetAggregateAttributePayload } from 'types/api/queryBuilder/getAggregatorAttribute';
|
||||||
|
import { IQueryAutocompleteResponse } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
|
||||||
|
export const getAggregateAttribute = async ({
|
||||||
|
aggregateOperator,
|
||||||
|
searchText,
|
||||||
|
dataSource,
|
||||||
|
}: IGetAggregateAttributePayload): Promise<
|
||||||
|
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
|
||||||
|
> => {
|
||||||
|
try {
|
||||||
|
const response: AxiosResponse<{
|
||||||
|
data: IQueryAutocompleteResponse;
|
||||||
|
}> = await ApiV3Instance.get(
|
||||||
|
`autocomplete/aggregate_attributes?aggregateOperator=${aggregateOperator}&dataSource=${dataSource}&searchText=${searchText}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.statusText,
|
||||||
|
payload: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return ErrorResponseHandler(e as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
34
frontend/src/api/queryBuilder/getAttributeKeys.ts
Normal file
34
frontend/src/api/queryBuilder/getAttributeKeys.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { ApiV3Instance } from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError, AxiosResponse } from 'axios';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
// ** Types
|
||||||
|
import { IGetAttributeKeysPayload } from 'types/api/queryBuilder/getAttributeKeys';
|
||||||
|
import { IQueryAutocompleteResponse } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
|
||||||
|
export const getAggregateKeys = async ({
|
||||||
|
aggregateOperator,
|
||||||
|
searchText,
|
||||||
|
dataSource,
|
||||||
|
aggregateAttribute,
|
||||||
|
tagType,
|
||||||
|
}: IGetAttributeKeysPayload): Promise<
|
||||||
|
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
|
||||||
|
> => {
|
||||||
|
try {
|
||||||
|
const response: AxiosResponse<{
|
||||||
|
data: IQueryAutocompleteResponse;
|
||||||
|
}> = await ApiV3Instance.get(
|
||||||
|
`autocomplete/attribute_keys?aggregateOperator=${aggregateOperator}&dataSource=${dataSource}&aggregateAttribute=${aggregateAttribute}&tagType=${tagType}&searchText=${searchText}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.statusText,
|
||||||
|
payload: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return ErrorResponseHandler(e as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
63
frontend/src/api/queryBuilder/getAttributesKeysValues.ts
Normal file
63
frontend/src/api/queryBuilder/getAttributesKeysValues.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { ApiV3Instance } from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
|
||||||
|
export type TagKeyValueProps = {
|
||||||
|
dataSource: string;
|
||||||
|
aggregateOperator?: string;
|
||||||
|
aggregateAttribute?: string;
|
||||||
|
searchText?: string;
|
||||||
|
attributeKey?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface AttributeKeyOptions {
|
||||||
|
key: string;
|
||||||
|
type: string;
|
||||||
|
dataType: 'string' | 'boolean' | 'number';
|
||||||
|
isColumn: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAttributesKeys = async (
|
||||||
|
props: TagKeyValueProps,
|
||||||
|
): Promise<SuccessResponse<AttributeKeyOptions[]> | ErrorResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await ApiV3Instance.get(
|
||||||
|
`/autocomplete/attribute_keys?aggregateOperator=${props.aggregateOperator}&dataSource=${props.dataSource}&aggregateAttribute=${props.aggregateAttribute}&searchText=${props.searchText}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data.data.attributeKeys,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface TagValuePayloadProps {
|
||||||
|
boolAttributeValues: null | string[];
|
||||||
|
numberAttributeValues: null | string[];
|
||||||
|
stringAttributeValues: null | string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAttributesValues = async (
|
||||||
|
props: TagKeyValueProps,
|
||||||
|
): Promise<SuccessResponse<TagValuePayloadProps> | ErrorResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await ApiV3Instance.get(
|
||||||
|
`/autocomplete/attribute_values?aggregateOperator=${props.aggregateOperator}&dataSource=${props.dataSource}&aggregateAttribute=${props.aggregateAttribute}&searchText=${props.searchText}&attributeKey=${props.attributeKey}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
@ -10,7 +10,7 @@ const getSpans = async (
|
|||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
const updatedSelectedTags = props.selectedTags.map((e) => ({
|
const updatedSelectedTags = props.selectedTags.map((e) => ({
|
||||||
Key: e.Key,
|
Key: `${e.Key}.(string)`,
|
||||||
Operator: e.Operator,
|
Operator: e.Operator,
|
||||||
StringValues: e.StringValues,
|
StringValues: e.StringValues,
|
||||||
NumberValues: e.NumberValues,
|
NumberValues: e.NumberValues,
|
||||||
|
@ -28,7 +28,7 @@ const getSpanAggregate = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const updatedSelectedTags = props.selectedTags.map((e) => ({
|
const updatedSelectedTags = props.selectedTags.map((e) => ({
|
||||||
Key: e.Key,
|
Key: `${e.Key}.(string)`,
|
||||||
Operator: e.Operator,
|
Operator: e.Operator,
|
||||||
StringValues: e.StringValues,
|
StringValues: e.StringValues,
|
||||||
NumberValues: e.NumberValues,
|
NumberValues: e.NumberValues,
|
||||||
|
@ -16,6 +16,4 @@ export const TableBodyContent = styled.div<TableBodyContentProps>`
|
|||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
|
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
`;
|
`;
|
||||||
|
@ -1,23 +1,40 @@
|
|||||||
/* eslint-disable react/no-unstable-nested-components */
|
import { grey } from '@ant-design/colors';
|
||||||
import { QuestionCircleFilled } from '@ant-design/icons';
|
import { QuestionCircleFilled } from '@ant-design/icons';
|
||||||
import { Tooltip } from 'antd';
|
import { Tooltip } from 'antd';
|
||||||
import React from 'react';
|
import { themeColors } from 'constants/theme';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { style } from './styles';
|
||||||
|
|
||||||
function TextToolTip({ text, url }: TextToolTipProps): JSX.Element {
|
function TextToolTip({ text, url }: TextToolTipProps): JSX.Element {
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
|
const overlay = useMemo(
|
||||||
|
() => (
|
||||||
|
<div>
|
||||||
|
{`${text} `}
|
||||||
|
{url && (
|
||||||
|
<a href={url} rel="noopener noreferrer" target="_blank">
|
||||||
|
here
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
[text, url],
|
||||||
|
);
|
||||||
|
|
||||||
|
const iconStyle = useMemo(
|
||||||
|
() => ({
|
||||||
|
...style,
|
||||||
|
color: isDarkMode ? themeColors.whiteCream : grey[0],
|
||||||
|
}),
|
||||||
|
[isDarkMode],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip overlay={overlay}>
|
||||||
overlay={(): JSX.Element => (
|
<QuestionCircleFilled style={iconStyle} />
|
||||||
<div>
|
|
||||||
{`${text} `}
|
|
||||||
{url && (
|
|
||||||
<a href={url} rel="noopener noreferrer" target="_blank">
|
|
||||||
here
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<QuestionCircleFilled style={{ fontSize: '1.3125rem' }} />
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
1
frontend/src/components/TextToolTip/styles.ts
Normal file
1
frontend/src/components/TextToolTip/styles.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const style = { fontSize: '1.3125rem' };
|
@ -1,5 +1,4 @@
|
|||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
export enum QueryParams {
|
||||||
export enum METRICS_PAGE_QUERY_PARAM {
|
|
||||||
interval = 'interval',
|
interval = 'interval',
|
||||||
startTime = 'startTime',
|
startTime = 'startTime',
|
||||||
endTime = 'endTime',
|
endTime = 'endTime',
|
||||||
@ -12,4 +11,5 @@ export enum METRICS_PAGE_QUERY_PARAM {
|
|||||||
selectedTags = 'selectedTags',
|
selectedTags = 'selectedTags',
|
||||||
aggregationOption = 'aggregationOption',
|
aggregationOption = 'aggregationOption',
|
||||||
entity = 'entity',
|
entity = 'entity',
|
||||||
|
resourceAttributes = 'resourceAttribute',
|
||||||
}
|
}
|
||||||
|
177
frontend/src/constants/queryBuilder.ts
Normal file
177
frontend/src/constants/queryBuilder.ts
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
// ** Helpers
|
||||||
|
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
|
||||||
|
import { LocalDataType } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import {
|
||||||
|
Having,
|
||||||
|
IBuilderFormula,
|
||||||
|
IBuilderQueryForm,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import {
|
||||||
|
BoolOperators,
|
||||||
|
DataSource,
|
||||||
|
LogsAggregatorOperator,
|
||||||
|
MetricAggregateOperator,
|
||||||
|
NumberOperators,
|
||||||
|
StringOperators,
|
||||||
|
TracesAggregatorOperator,
|
||||||
|
} from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
export const MAX_FORMULAS = 20;
|
||||||
|
export const MAX_QUERIES = 26;
|
||||||
|
|
||||||
|
export const formulasNames: string[] = Array.from(
|
||||||
|
Array(MAX_FORMULAS),
|
||||||
|
(_, i) => `F${i + 1}`,
|
||||||
|
);
|
||||||
|
const alpha: number[] = Array.from(Array(MAX_QUERIES), (_, i) => i + 65);
|
||||||
|
export const alphabet: string[] = alpha.map((str) => String.fromCharCode(str));
|
||||||
|
|
||||||
|
export enum QueryBuilderKeys {
|
||||||
|
GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE',
|
||||||
|
GET_AGGREGATE_KEYS = 'GET_AGGREGATE_KEYS',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mapOfOperators: Record<DataSource, string[]> = {
|
||||||
|
metrics: Object.values(MetricAggregateOperator),
|
||||||
|
logs: Object.values(LogsAggregatorOperator),
|
||||||
|
traces: Object.values(TracesAggregatorOperator),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapOfFilters: Record<DataSource, string[]> = {
|
||||||
|
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||||
|
metrics: ['Aggregation interval', 'Having'],
|
||||||
|
logs: ['Order by', 'Limit', 'Having', 'Aggregation interval'],
|
||||||
|
traces: ['Order by', 'Limit', 'Having', 'Aggregation interval'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialHavingValues: Having = {
|
||||||
|
columnName: '',
|
||||||
|
op: '',
|
||||||
|
value: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialAggregateAttribute: IBuilderQueryForm['aggregateAttribute'] = {
|
||||||
|
dataType: null,
|
||||||
|
key: '',
|
||||||
|
isColumn: null,
|
||||||
|
type: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialQueryBuilderFormValues: IBuilderQueryForm = {
|
||||||
|
dataSource: DataSource.METRICS,
|
||||||
|
queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
|
||||||
|
aggregateOperator: Object.values(MetricAggregateOperator)[0],
|
||||||
|
aggregateAttribute: initialAggregateAttribute,
|
||||||
|
tagFilters: { items: [], op: 'AND' },
|
||||||
|
expression: '',
|
||||||
|
disabled: false,
|
||||||
|
having: [],
|
||||||
|
stepInterval: 30,
|
||||||
|
limit: 10,
|
||||||
|
orderBy: [],
|
||||||
|
groupBy: [],
|
||||||
|
legend: '',
|
||||||
|
reduceTo: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialFormulaBuilderFormValues: IBuilderFormula = {
|
||||||
|
label: createNewBuilderItemName({
|
||||||
|
existNames: [],
|
||||||
|
sourceNames: formulasNames,
|
||||||
|
}),
|
||||||
|
expression: '',
|
||||||
|
disabled: false,
|
||||||
|
legend: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const operatorsByTypes: Record<LocalDataType, string[]> = {
|
||||||
|
string: Object.values(StringOperators),
|
||||||
|
number: Object.values(NumberOperators),
|
||||||
|
bool: Object.values(BoolOperators),
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IQueryBuilderState = 'search';
|
||||||
|
|
||||||
|
export const QUERY_BUILDER_SEARCH_VALUES = {
|
||||||
|
MULTIPLY: 'MULTIPLY_VALUE',
|
||||||
|
SINGLE: 'SINGLE_VALUE',
|
||||||
|
NON: 'NON_VALUE',
|
||||||
|
NOT_VALID: 'NOT_VALID',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OPERATORS = {
|
||||||
|
IN: 'IN',
|
||||||
|
NIN: 'NOT_IN',
|
||||||
|
LIKE: 'LIKE',
|
||||||
|
NLIKE: 'NOT_LIKE',
|
||||||
|
EQUALS: '=',
|
||||||
|
NOT_EQUALS: '!=',
|
||||||
|
EXISTS: 'EXISTS',
|
||||||
|
NOT_EXISTS: 'NOT_EXISTS',
|
||||||
|
CONTAINS: 'CONTAINS',
|
||||||
|
NOT_CONTAINS: 'NOT_CONTAINS',
|
||||||
|
GTE: '>=',
|
||||||
|
GT: '>',
|
||||||
|
LTE: '<=',
|
||||||
|
LT: '<',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QUERY_BUILDER_OPERATORS_BY_TYPES = {
|
||||||
|
string: [
|
||||||
|
OPERATORS.EQUALS,
|
||||||
|
OPERATORS.NOT_EQUALS,
|
||||||
|
OPERATORS.IN,
|
||||||
|
OPERATORS.NIN,
|
||||||
|
OPERATORS.LIKE,
|
||||||
|
OPERATORS.NLIKE,
|
||||||
|
OPERATORS.CONTAINS,
|
||||||
|
OPERATORS.NOT_CONTAINS,
|
||||||
|
OPERATORS.EXISTS,
|
||||||
|
OPERATORS.NOT_EXISTS,
|
||||||
|
],
|
||||||
|
number: [
|
||||||
|
OPERATORS.EQUALS,
|
||||||
|
OPERATORS.NOT_EQUALS,
|
||||||
|
OPERATORS.IN,
|
||||||
|
OPERATORS.NIN,
|
||||||
|
OPERATORS.EXISTS,
|
||||||
|
OPERATORS.NOT_EXISTS,
|
||||||
|
OPERATORS.GTE,
|
||||||
|
OPERATORS.GT,
|
||||||
|
OPERATORS.LTE,
|
||||||
|
OPERATORS.LT,
|
||||||
|
],
|
||||||
|
boolean: [
|
||||||
|
OPERATORS.EQUALS,
|
||||||
|
OPERATORS.NOT_EQUALS,
|
||||||
|
OPERATORS.EXISTS,
|
||||||
|
OPERATORS.NOT_EXISTS,
|
||||||
|
],
|
||||||
|
universal: [
|
||||||
|
OPERATORS.EQUALS,
|
||||||
|
OPERATORS.NOT_EQUALS,
|
||||||
|
OPERATORS.IN,
|
||||||
|
OPERATORS.NIN,
|
||||||
|
OPERATORS.EXISTS,
|
||||||
|
OPERATORS.NOT_EXISTS,
|
||||||
|
OPERATORS.LIKE,
|
||||||
|
OPERATORS.NLIKE,
|
||||||
|
OPERATORS.GTE,
|
||||||
|
OPERATORS.GT,
|
||||||
|
OPERATORS.LTE,
|
||||||
|
OPERATORS.LT,
|
||||||
|
OPERATORS.CONTAINS,
|
||||||
|
OPERATORS.NOT_CONTAINS,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HAVING_OPERATORS: string[] = [
|
||||||
|
OPERATORS.EQUALS,
|
||||||
|
OPERATORS.NOT_EQUALS,
|
||||||
|
OPERATORS.IN,
|
||||||
|
OPERATORS.NIN,
|
||||||
|
OPERATORS.GTE,
|
||||||
|
OPERATORS.GT,
|
||||||
|
OPERATORS.LTE,
|
||||||
|
OPERATORS.LT,
|
||||||
|
];
|
@ -18,6 +18,8 @@ import { ResizeTable } from 'components/ResizeTable';
|
|||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||||
|
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import createQueryParams from 'lib/createQueryParams';
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
@ -93,9 +95,11 @@ function AllErrors(): JSX.Element {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { queries } = useResourceAttribute();
|
||||||
|
|
||||||
const [{ isLoading, data }, errorCountResponse] = useQueries([
|
const [{ isLoading, data }, errorCountResponse] = useQueries([
|
||||||
{
|
{
|
||||||
queryKey: ['getAllErrors', updatedPath, maxTime, minTime],
|
queryKey: ['getAllErrors', updatedPath, maxTime, minTime, queries],
|
||||||
queryFn: (): Promise<SuccessResponse<PayloadProps> | ErrorResponse> =>
|
queryFn: (): Promise<SuccessResponse<PayloadProps> | ErrorResponse> =>
|
||||||
getAll({
|
getAll({
|
||||||
end: maxTime,
|
end: maxTime,
|
||||||
@ -106,6 +110,7 @@ function AllErrors(): JSX.Element {
|
|||||||
orderParam: getUpdatedParams,
|
orderParam: getUpdatedParams,
|
||||||
exceptionType: getUpdatedExceptionType,
|
exceptionType: getUpdatedExceptionType,
|
||||||
serviceName: getUpdatedServiceName,
|
serviceName: getUpdatedServiceName,
|
||||||
|
tags: convertRawQueriesToTraceSelectedTags(queries),
|
||||||
}),
|
}),
|
||||||
enabled: !loading,
|
enabled: !loading,
|
||||||
},
|
},
|
||||||
@ -116,6 +121,7 @@ function AllErrors(): JSX.Element {
|
|||||||
minTime,
|
minTime,
|
||||||
getUpdatedExceptionType,
|
getUpdatedExceptionType,
|
||||||
getUpdatedServiceName,
|
getUpdatedServiceName,
|
||||||
|
queries,
|
||||||
],
|
],
|
||||||
queryFn: (): Promise<ErrorResponse | SuccessResponse<number>> =>
|
queryFn: (): Promise<ErrorResponse | SuccessResponse<number>> =>
|
||||||
getErrorCounts({
|
getErrorCounts({
|
||||||
@ -123,6 +129,7 @@ function AllErrors(): JSX.Element {
|
|||||||
start: minTime,
|
start: minTime,
|
||||||
exceptionType: getUpdatedExceptionType,
|
exceptionType: getUpdatedExceptionType,
|
||||||
serviceName: getUpdatedServiceName,
|
serviceName: getUpdatedServiceName,
|
||||||
|
tags: convertRawQueriesToTraceSelectedTags(queries),
|
||||||
}),
|
}),
|
||||||
enabled: !loading,
|
enabled: !loading,
|
||||||
},
|
},
|
||||||
|
@ -1,23 +1,15 @@
|
|||||||
import { Button, Row } from 'antd';
|
import { Button, Row } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { QueryFields } from './utils';
|
|
||||||
|
|
||||||
interface SearchFieldsActionBarProps {
|
interface SearchFieldsActionBarProps {
|
||||||
fieldsQuery: QueryFields[][];
|
applyUpdate: VoidFunction;
|
||||||
applyUpdate: () => void;
|
clearFilters: VoidFunction;
|
||||||
clearFilters: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SearchFieldsActionBar({
|
export function SearchFieldsActionBar({
|
||||||
fieldsQuery,
|
|
||||||
applyUpdate,
|
applyUpdate,
|
||||||
clearFilters,
|
clearFilters,
|
||||||
}: SearchFieldsActionBarProps): JSX.Element | null {
|
}: SearchFieldsActionBarProps): JSX.Element | null {
|
||||||
if (fieldsQuery.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row style={{ justifyContent: 'flex-end', paddingRight: '2.4rem' }}>
|
<Row style={{ justifyContent: 'flex-end', paddingRight: '2.4rem' }}>
|
||||||
<Button
|
<Button
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import { reverseParser } from 'lib/logql';
|
||||||
import { flatten } from 'lodash-es';
|
import { flatten } from 'lodash-es';
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
@ -18,13 +19,13 @@ import {
|
|||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
export interface SearchFieldsProps {
|
export interface SearchFieldsProps {
|
||||||
updateParsedQuery: (query: QueryFields[]) => void;
|
|
||||||
onDropDownToggleHandler: (value: boolean) => VoidFunction;
|
onDropDownToggleHandler: (value: boolean) => VoidFunction;
|
||||||
|
updateQueryString: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SearchFields({
|
function SearchFields({
|
||||||
updateParsedQuery,
|
|
||||||
onDropDownToggleHandler,
|
onDropDownToggleHandler,
|
||||||
|
updateQueryString,
|
||||||
}: SearchFieldsProps): JSX.Element {
|
}: SearchFieldsProps): JSX.Element {
|
||||||
const {
|
const {
|
||||||
searchFilter: { parsedQuery },
|
searchFilter: { parsedQuery },
|
||||||
@ -90,15 +91,15 @@ function SearchFields({
|
|||||||
}
|
}
|
||||||
|
|
||||||
keyPrefixRef.current = hashCode(JSON.stringify(flatParsedQuery));
|
keyPrefixRef.current = hashCode(JSON.stringify(flatParsedQuery));
|
||||||
updateParsedQuery(flatParsedQuery);
|
updateQueryString(reverseParser(flatParsedQuery));
|
||||||
onDropDownToggleHandler(false)();
|
onDropDownToggleHandler(false)();
|
||||||
}, [onDropDownToggleHandler, fieldsQuery, updateParsedQuery, notifications]);
|
}, [fieldsQuery, notifications, onDropDownToggleHandler, updateQueryString]);
|
||||||
|
|
||||||
const clearFilters = useCallback((): void => {
|
const clearFilters = useCallback((): void => {
|
||||||
keyPrefixRef.current = hashCode(JSON.stringify([]));
|
keyPrefixRef.current = hashCode(JSON.stringify([]));
|
||||||
updateParsedQuery([]);
|
setFieldsQuery([]);
|
||||||
onDropDownToggleHandler(false)();
|
updateQueryString('');
|
||||||
}, [onDropDownToggleHandler, updateParsedQuery]);
|
}, [updateQueryString]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -113,7 +114,6 @@ function SearchFields({
|
|||||||
<SearchFieldsActionBar
|
<SearchFieldsActionBar
|
||||||
applyUpdate={applyUpdate}
|
applyUpdate={applyUpdate}
|
||||||
clearFilters={clearFilters}
|
clearFilters={clearFilters}
|
||||||
fieldsQuery={fieldsQuery}
|
|
||||||
/>
|
/>
|
||||||
<Suggestions applySuggestion={addSuggestedField} />
|
<Suggestions applySuggestion={addSuggestedField} />
|
||||||
</>
|
</>
|
||||||
|
@ -36,11 +36,7 @@ function SearchFilter({
|
|||||||
getLogsAggregate,
|
getLogsAggregate,
|
||||||
getLogsFields,
|
getLogsFields,
|
||||||
}: SearchFilterProps): JSX.Element {
|
}: SearchFilterProps): JSX.Element {
|
||||||
const {
|
const { updateQueryString, queryString } = useSearchParser();
|
||||||
updateParsedQuery,
|
|
||||||
updateQueryString,
|
|
||||||
queryString,
|
|
||||||
} = useSearchParser();
|
|
||||||
const [searchText, setSearchText] = useState(queryString);
|
const [searchText, setSearchText] = useState(queryString);
|
||||||
const [showDropDown, setShowDropDown] = useState(false);
|
const [showDropDown, setShowDropDown] = useState(false);
|
||||||
const searchRef = useRef<InputRef>(null);
|
const searchRef = useRef<InputRef>(null);
|
||||||
@ -187,8 +183,8 @@ function SearchFilter({
|
|||||||
content={
|
content={
|
||||||
<DropDownContainer>
|
<DropDownContainer>
|
||||||
<SearchFields
|
<SearchFields
|
||||||
|
updateQueryString={updateQueryString}
|
||||||
onDropDownToggleHandler={onDropDownToggleHandler}
|
onDropDownToggleHandler={onDropDownToggleHandler}
|
||||||
updateParsedQuery={updateParsedQuery as never}
|
|
||||||
/>
|
/>
|
||||||
</DropDownContainer>
|
</DropDownContainer>
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
|
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { parseQuery, reverseParser } from 'lib/logql';
|
import { parseQuery } from 'lib/logql';
|
||||||
import { ILogQLParsedQueryItem } from 'lib/logql/types';
|
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
@ -21,7 +20,6 @@ import { getGlobalTime } from './utils';
|
|||||||
export function useSearchParser(): {
|
export function useSearchParser(): {
|
||||||
queryString: string;
|
queryString: string;
|
||||||
parsedQuery: unknown;
|
parsedQuery: unknown;
|
||||||
updateParsedQuery: (arg0: ILogQLParsedQueryItem[]) => void;
|
|
||||||
updateQueryString: (arg0: string) => void;
|
updateQueryString: (arg0: string) => void;
|
||||||
} {
|
} {
|
||||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||||
@ -64,7 +62,7 @@ export function useSearchParser(): {
|
|||||||
},
|
},
|
||||||
// need to hide this warning as we don't want to update the query string on every change
|
// need to hide this warning as we don't want to update the query string on every change
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[dispatch, parsedQuery],
|
[dispatch, parsedQuery, selectedTime],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -75,32 +73,9 @@ export function useSearchParser(): {
|
|||||||
}
|
}
|
||||||
}, [queryString, updateQueryString, parsedFilters]);
|
}, [queryString, updateQueryString, parsedFilters]);
|
||||||
|
|
||||||
const updateParsedQuery = useCallback(
|
|
||||||
(updatedParsedPayload: ILogQLParsedQueryItem[]) => {
|
|
||||||
dispatch({
|
|
||||||
type: SET_SEARCH_QUERY_PARSED_PAYLOAD,
|
|
||||||
payload: updatedParsedPayload,
|
|
||||||
});
|
|
||||||
const reversedParsedQuery = reverseParser(updatedParsedPayload);
|
|
||||||
if (
|
|
||||||
!isEqual(queryString, reversedParsedQuery) ||
|
|
||||||
(queryString === '' && reversedParsedQuery === '')
|
|
||||||
) {
|
|
||||||
dispatch({
|
|
||||||
type: SET_SEARCH_QUERY_STRING,
|
|
||||||
payload: {
|
|
||||||
searchQueryString: reversedParsedQuery,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[dispatch, queryString],
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
queryString,
|
queryString,
|
||||||
parsedQuery,
|
parsedQuery,
|
||||||
updateParsedQuery,
|
|
||||||
updateQueryString,
|
updateQueryString,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
import { convertMetricKeyToTrace } from 'lib/resourceAttributes';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { QueryChipContainer, QueryChipItem } from './styles';
|
|
||||||
import { IResourceAttributeQuery } from './types';
|
|
||||||
|
|
||||||
interface IQueryChipProps {
|
|
||||||
queryData: IResourceAttributeQuery;
|
|
||||||
onClose: (id: string) => void;
|
|
||||||
disabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function QueryChip({
|
|
||||||
queryData,
|
|
||||||
onClose,
|
|
||||||
disabled,
|
|
||||||
}: IQueryChipProps): JSX.Element {
|
|
||||||
return (
|
|
||||||
<QueryChipContainer>
|
|
||||||
<QueryChipItem>{convertMetricKeyToTrace(queryData.tagKey)}</QueryChipItem>
|
|
||||||
<QueryChipItem>{queryData.operator}</QueryChipItem>
|
|
||||||
<QueryChipItem
|
|
||||||
closable={!disabled}
|
|
||||||
onClose={(): void => {
|
|
||||||
if (!disabled) onClose(queryData.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{queryData.tagValue.join(', ')}
|
|
||||||
</QueryChipItem>
|
|
||||||
</QueryChipContainer>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
import { createMachine } from 'xstate';
|
|
||||||
|
|
||||||
export const ResourceAttributesFilterMachine =
|
|
||||||
/** @xstate-layout N4IgpgJg5mDOIC5QBECGsAWAjA9qgThAAQDKYBAxhkQIIB2xAYgJYA2ALmPgHQAqqUANJgAngGIAcgFEAGr0SgADjljN2zHHQUgAHogAcAFgAM3AOz6ATAEYAzJdsA2Y4cOWAnABoQIxAFpDR2tuQ319AFYTcKdbFycAX3jvNExcAmIySmp6JjZOHn4hUTFNACFWAFd8bWVVdU1tPQQzY1MXY2tDdzNHM3dHd0NvXwR7biMTa313S0i+63DE5PRsPEJScnwqWgYiFg4uPgFhcQAlKRIpeSQQWrUNLRumx3Czbg8TR0sbS31jfUcw38fW47gBHmm4XCVms3SWIBSq3SGyyO1yBx4AHlFFxUOwcPhJLJrkoVPcGk9ENYFuF3i5YR0wtEHECEAEgiEmV8zH1DLYzHZ4Yi0utMltsrt9vluNjcfjCWVKtUbnd6o9QE1rMYBtxbGFvsZ3NrZj1WdYOfotUZLX0XEFHEKViKMpttjk9nlDrL8HiCWJzpcSbcyWrGoh3NCQj0zK53P1ph1WeFLLqnJZ2s5vmZLA6kginWsXaj3VLDoUAGqoSpgEp0cpVGohh5hhDWDy0sz8zruakzamWVm-Qyg362V5-AZOayO1KFlHitEejFHKCV6v+i5XRt1ZuU1s52zjNOOaZfdOWIY+RDZ0Hc6ZmKEXqyLPPCudit2Sz08ACSEFYNbSHI27kuquiIOEjiONwjJgrM3RWJYZisgEIJgnYPTmuEdi2OaiR5nQOAQHA2hvsiH4Sui0qFCcIGhnuLSmP0YJuJ2xjJsmKELG8XZTK0tjdHG06vgW5GupRS7St6vrKqSO4UhqVL8TBWp8o4eqdl0A5Xmy3G6gK56-B4uERDOSKiuJi6lgUAhrhUYB0buimtrEKZBDYrxaS0OZca8+ltheybOI4hivGZzrzp+VGHH+AGOQp4EIHy+ghNYnawtG4TsbYvk8QKfHGAJfQ9uF76WSW37xWBTSGJ0qXpd0vRZdEKGPqC2YeO2-zfO4+HxEAA */
|
|
||||||
createMachine({
|
|
||||||
tsTypes: {} as import('./ResourceAttributesFilter.Machine.typegen').Typegen0,
|
|
||||||
initial: 'Idle',
|
|
||||||
states: {
|
|
||||||
TagKey: {
|
|
||||||
on: {
|
|
||||||
NEXT: {
|
|
||||||
actions: 'onSelectOperator',
|
|
||||||
target: 'Operator',
|
|
||||||
},
|
|
||||||
onBlur: {
|
|
||||||
actions: 'onBlurPurge',
|
|
||||||
target: 'Idle',
|
|
||||||
},
|
|
||||||
RESET: {
|
|
||||||
target: 'Idle',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Operator: {
|
|
||||||
on: {
|
|
||||||
NEXT: {
|
|
||||||
actions: 'onSelectTagValue',
|
|
||||||
target: 'TagValue',
|
|
||||||
},
|
|
||||||
onBlur: {
|
|
||||||
actions: 'onBlurPurge',
|
|
||||||
target: 'Idle',
|
|
||||||
},
|
|
||||||
RESET: {
|
|
||||||
target: 'Idle',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
TagValue: {
|
|
||||||
on: {
|
|
||||||
onBlur: {
|
|
||||||
actions: ['onValidateQuery', 'onBlurPurge'],
|
|
||||||
target: 'Idle',
|
|
||||||
},
|
|
||||||
RESET: {
|
|
||||||
target: 'Idle',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Idle: {
|
|
||||||
on: {
|
|
||||||
NEXT: {
|
|
||||||
actions: 'onSelectTagKey',
|
|
||||||
description: 'Select Category',
|
|
||||||
target: 'TagKey',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
id: 'Dashboard Search And Filter',
|
|
||||||
});
|
|
@ -1,219 +0,0 @@
|
|||||||
import { CloseCircleFilled } from '@ant-design/icons';
|
|
||||||
import { useMachine } from '@xstate/react';
|
|
||||||
import { Button, Select, Spin } from 'antd';
|
|
||||||
import ROUTES from 'constants/routes';
|
|
||||||
import history from 'lib/history';
|
|
||||||
import { convertMetricKeyToTrace } from 'lib/resourceAttributes';
|
|
||||||
import { map } from 'lodash-es';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { ResetInitialData } from 'store/actions/metrics/resetInitialData';
|
|
||||||
import { SetResourceAttributeQueries } from 'store/actions/metrics/setResourceAttributeQueries';
|
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import MetricReducer from 'types/reducer/metrics';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
|
|
||||||
import QueryChip from './QueryChip';
|
|
||||||
import { ResourceAttributesFilterMachine } from './ResourceAttributesFilter.Machine';
|
|
||||||
import { QueryChipItem, SearchContainer } from './styles';
|
|
||||||
import { IOption, IResourceAttributeQuery } from './types';
|
|
||||||
import { createQuery, GetTagKeys, GetTagValues, OperatorSchema } from './utils';
|
|
||||||
|
|
||||||
function ResourceAttributesFilter(): JSX.Element | null {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const [disabled, setDisabled] = useState(
|
|
||||||
!(history.location.pathname === ROUTES.APPLICATION),
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unListen = history.listen(({ pathname }) => {
|
|
||||||
setDisabled(!(pathname === ROUTES.APPLICATION));
|
|
||||||
});
|
|
||||||
return (): void => {
|
|
||||||
if (!history.location.pathname.startsWith(`${ROUTES.APPLICATION}/`)) {
|
|
||||||
dispatch(ResetInitialData());
|
|
||||||
}
|
|
||||||
unListen();
|
|
||||||
};
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const { resourceAttributeQueries } = useSelector<AppState, MetricReducer>(
|
|
||||||
(state) => state.metrics,
|
|
||||||
);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [selectedValues, setSelectedValues] = useState<string[]>([]);
|
|
||||||
const [staging, setStaging] = useState<string[]>([]);
|
|
||||||
const [queries, setQueries] = useState<IResourceAttributeQuery[]>([]);
|
|
||||||
const [optionsData, setOptionsData] = useState<{
|
|
||||||
mode: undefined | 'tags' | 'multiple';
|
|
||||||
options: IOption[];
|
|
||||||
}>({
|
|
||||||
mode: undefined,
|
|
||||||
options: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const dispatchQueries = (updatedQueries: IResourceAttributeQuery[]): void => {
|
|
||||||
dispatch(SetResourceAttributeQueries(updatedQueries));
|
|
||||||
};
|
|
||||||
const handleLoading = (isLoading: boolean): void => {
|
|
||||||
setLoading(isLoading);
|
|
||||||
if (isLoading) {
|
|
||||||
setOptionsData({ mode: undefined, options: [] });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const [state, send] = useMachine(ResourceAttributesFilterMachine, {
|
|
||||||
actions: {
|
|
||||||
onSelectTagKey: () => {
|
|
||||||
handleLoading(true);
|
|
||||||
GetTagKeys()
|
|
||||||
.then((tagKeys) => setOptionsData({ options: tagKeys, mode: undefined }))
|
|
||||||
.finally(() => {
|
|
||||||
handleLoading(false);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onSelectOperator: () => {
|
|
||||||
setOptionsData({ options: OperatorSchema, mode: undefined });
|
|
||||||
},
|
|
||||||
onSelectTagValue: () => {
|
|
||||||
handleLoading(true);
|
|
||||||
|
|
||||||
GetTagValues(staging[0])
|
|
||||||
.then((tagValuesOptions) =>
|
|
||||||
setOptionsData({ options: tagValuesOptions, mode: 'multiple' }),
|
|
||||||
)
|
|
||||||
.finally(() => {
|
|
||||||
handleLoading(false);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onBlurPurge: () => {
|
|
||||||
setSelectedValues([]);
|
|
||||||
setStaging([]);
|
|
||||||
},
|
|
||||||
onValidateQuery: (): void => {
|
|
||||||
if (staging.length < 2 || selectedValues.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const generatedQuery = createQuery([...staging, selectedValues]);
|
|
||||||
if (generatedQuery) {
|
|
||||||
dispatchQueries([...queries, generatedQuery]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setQueries(resourceAttributeQueries);
|
|
||||||
}, [resourceAttributeQueries]);
|
|
||||||
|
|
||||||
const handleFocus = (): void => {
|
|
||||||
if (state.value === 'Idle') {
|
|
||||||
send('NEXT');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBlur = (): void => {
|
|
||||||
send('onBlur');
|
|
||||||
};
|
|
||||||
const handleChange = (value: never): void => {
|
|
||||||
if (!optionsData.mode) {
|
|
||||||
setStaging((prevStaging) => [...prevStaging, value]);
|
|
||||||
setSelectedValues([]);
|
|
||||||
send('NEXT');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedValues([...value]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = (id: string): void => {
|
|
||||||
dispatchQueries(queries.filter((queryData) => queryData.id !== id));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClearAll = (): void => {
|
|
||||||
send('RESET');
|
|
||||||
dispatchQueries([]);
|
|
||||||
setStaging([]);
|
|
||||||
setSelectedValues([]);
|
|
||||||
};
|
|
||||||
const disabledAndEmpty = !!(
|
|
||||||
!queries.length &&
|
|
||||||
!staging.length &&
|
|
||||||
!selectedValues.length &&
|
|
||||||
disabled
|
|
||||||
);
|
|
||||||
const disabledOrEmpty = !!(
|
|
||||||
queries.length ||
|
|
||||||
staging.length ||
|
|
||||||
selectedValues.length ||
|
|
||||||
disabled
|
|
||||||
);
|
|
||||||
|
|
||||||
if (disabledAndEmpty) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SearchContainer disabled={disabled}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
maxWidth: disabled ? '100%' : '70%',
|
|
||||||
display: 'flex',
|
|
||||||
overflowX: 'auto',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{map(
|
|
||||||
queries,
|
|
||||||
(query): JSX.Element => (
|
|
||||||
<QueryChip
|
|
||||||
disabled={disabled}
|
|
||||||
key={query.id}
|
|
||||||
queryData={query}
|
|
||||||
onClose={handleClose}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
{map(staging, (item, idx) => (
|
|
||||||
<QueryChipItem key={uuid()}>
|
|
||||||
{idx === 0 ? convertMetricKeyToTrace(item) : item}
|
|
||||||
</QueryChipItem>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{!disabled && (
|
|
||||||
<Select
|
|
||||||
placeholder={
|
|
||||||
disabledOrEmpty ? '' : 'Search and Filter based on resource attributes.'
|
|
||||||
}
|
|
||||||
disabled={disabled}
|
|
||||||
onChange={handleChange}
|
|
||||||
bordered={false}
|
|
||||||
value={selectedValues as never}
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
options={optionsData.options}
|
|
||||||
mode={optionsData?.mode}
|
|
||||||
showArrow={false}
|
|
||||||
onFocus={handleFocus}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
notFoundContent={
|
|
||||||
loading ? (
|
|
||||||
<span>
|
|
||||||
<Spin size="small" /> Loading...{' '}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span>
|
|
||||||
No resource attributes available to filter. Please refer docs to send
|
|
||||||
attributes.
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(queries.length || staging.length || selectedValues.length) && !disabled ? (
|
|
||||||
<Button onClick={handleClearAll} icon={<CloseCircleFilled />} type="text" />
|
|
||||||
) : null}
|
|
||||||
</SearchContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ResourceAttributesFilter;
|
|
@ -1,11 +0,0 @@
|
|||||||
export interface IOption {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IResourceAttributeQuery {
|
|
||||||
id: string;
|
|
||||||
tagKey: string;
|
|
||||||
operator: string;
|
|
||||||
tagValue: string[];
|
|
||||||
}
|
|
@ -1,64 +0,0 @@
|
|||||||
import {
|
|
||||||
getResourceAttributesTagKeys,
|
|
||||||
getResourceAttributesTagValues,
|
|
||||||
} from 'api/metrics/getResourceAttributes';
|
|
||||||
import { OperatorConversions } from 'constants/resourceAttributes';
|
|
||||||
import { convertMetricKeyToTrace } from 'lib/resourceAttributes';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
|
|
||||||
import { IOption, IResourceAttributeQuery } from './types';
|
|
||||||
|
|
||||||
export const OperatorSchema: IOption[] = OperatorConversions.map(
|
|
||||||
(operator) => ({
|
|
||||||
label: operator.label,
|
|
||||||
value: operator.label,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const GetTagKeys = async (): Promise<IOption[]> => {
|
|
||||||
// if (TagKeysCache) {
|
|
||||||
// return new Promise((resolve) => {
|
|
||||||
// resolve(TagKeysCache);
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
const { payload } = await getResourceAttributesTagKeys({
|
|
||||||
metricName: 'signoz_calls_total',
|
|
||||||
match: 'resource_',
|
|
||||||
});
|
|
||||||
if (!payload || !payload?.data) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return payload.data.map((tagKey: string) => ({
|
|
||||||
label: convertMetricKeyToTrace(tagKey),
|
|
||||||
value: tagKey,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GetTagValues = async (tagKey: string): Promise<IOption[]> => {
|
|
||||||
const { payload } = await getResourceAttributesTagValues({
|
|
||||||
tagKey,
|
|
||||||
metricName: 'signoz_calls_total',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!payload || !payload?.data) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return payload.data.map((tagValue: string) => ({
|
|
||||||
label: tagValue,
|
|
||||||
value: tagValue,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createQuery = (
|
|
||||||
selectedItems: Array<string | string[]> = [],
|
|
||||||
): IResourceAttributeQuery | null => {
|
|
||||||
if (selectedItems.length === 3) {
|
|
||||||
return {
|
|
||||||
id: uuid().slice(0, 8),
|
|
||||||
tagKey: selectedItems[0] as string,
|
|
||||||
operator: selectedItems[1] as string,
|
|
||||||
tagValue: selectedItems[2] as string[],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
@ -4,21 +4,20 @@ import {
|
|||||||
databaseCallsAvgDuration,
|
databaseCallsAvgDuration,
|
||||||
databaseCallsRPS,
|
databaseCallsRPS,
|
||||||
} from 'container/MetricsApplication/MetricsPageQueries/DBCallQueries';
|
} from 'container/MetricsApplication/MetricsPageQueries/DBCallQueries';
|
||||||
|
import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||||
import {
|
import {
|
||||||
convertRawQueriesToTraceSelectedTags,
|
convertRawQueriesToTraceSelectedTags,
|
||||||
resourceAttributesToTagFilterItems,
|
resourceAttributesToTagFilterItems,
|
||||||
} from 'lib/resourceAttributes';
|
} from 'hooks/useResourceAttribute/utils';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
import MetricReducer from 'types/reducer/metrics';
|
|
||||||
|
|
||||||
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
|
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
|
||||||
import { Button } from './styles';
|
import { Button } from './styles';
|
||||||
import {
|
import {
|
||||||
dbSystemTags,
|
dbSystemTags,
|
||||||
|
handleNonInQueryRange,
|
||||||
onGraphClickHandler,
|
onGraphClickHandler,
|
||||||
onViewTracePopupClick,
|
onViewTracePopupClick,
|
||||||
} from './util';
|
} from './util';
|
||||||
@ -26,22 +25,22 @@ import {
|
|||||||
function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
|
function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
|
||||||
const { servicename } = useParams<{ servicename?: string }>();
|
const { servicename } = useParams<{ servicename?: string }>();
|
||||||
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
|
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
|
||||||
const { resourceAttributeQueries } = useSelector<AppState, MetricReducer>(
|
const { queries } = useResourceAttribute();
|
||||||
(state) => state.metrics,
|
|
||||||
);
|
|
||||||
const tagFilterItems = useMemo(
|
const tagFilterItems = useMemo(
|
||||||
() => resourceAttributesToTagFilterItems(resourceAttributeQueries) || [],
|
() =>
|
||||||
[resourceAttributeQueries],
|
handleNonInQueryRange(resourceAttributesToTagFilterItems(queries)) || [],
|
||||||
|
[queries],
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedTraceTags: string = useMemo(
|
const selectedTraceTags: string = useMemo(
|
||||||
() =>
|
() =>
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
convertRawQueriesToTraceSelectedTags(resourceAttributeQueries).concat(
|
convertRawQueriesToTraceSelectedTags(queries).concat(...dbSystemTags) || [],
|
||||||
...dbSystemTags,
|
|
||||||
) || [],
|
|
||||||
),
|
),
|
||||||
[resourceAttributeQueries],
|
[queries],
|
||||||
);
|
);
|
||||||
|
|
||||||
const legend = '{{db_system}}';
|
const legend = '{{db_system}}';
|
||||||
|
|
||||||
const databaseCallsRPSWidget = useMemo(
|
const databaseCallsRPSWidget = useMemo(
|
||||||
|
@ -6,33 +6,34 @@ import {
|
|||||||
externalCallErrorPercent,
|
externalCallErrorPercent,
|
||||||
externalCallRpsByAddress,
|
externalCallRpsByAddress,
|
||||||
} from 'container/MetricsApplication/MetricsPageQueries/ExternalQueries';
|
} from 'container/MetricsApplication/MetricsPageQueries/ExternalQueries';
|
||||||
|
import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||||
import {
|
import {
|
||||||
convertRawQueriesToTraceSelectedTags,
|
convertRawQueriesToTraceSelectedTags,
|
||||||
resourceAttributesToTagFilterItems,
|
resourceAttributesToTagFilterItems,
|
||||||
} from 'lib/resourceAttributes';
|
} from 'hooks/useResourceAttribute/utils';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
import MetricReducer from 'types/reducer/metrics';
|
|
||||||
|
|
||||||
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
|
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
|
||||||
import { legend } from './constant';
|
import { legend } from './constant';
|
||||||
import { Button } from './styles';
|
import { Button } from './styles';
|
||||||
import { onGraphClickHandler, onViewTracePopupClick } from './util';
|
import {
|
||||||
|
handleNonInQueryRange,
|
||||||
|
onGraphClickHandler,
|
||||||
|
onViewTracePopupClick,
|
||||||
|
} from './util';
|
||||||
|
|
||||||
function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
|
function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
|
||||||
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
|
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
|
||||||
|
|
||||||
const { servicename } = useParams<{ servicename?: string }>();
|
const { servicename } = useParams<{ servicename?: string }>();
|
||||||
const { resourceAttributeQueries } = useSelector<AppState, MetricReducer>(
|
const { queries } = useResourceAttribute();
|
||||||
(state) => state.metrics,
|
|
||||||
);
|
|
||||||
|
|
||||||
const tagFilterItems = useMemo(
|
const tagFilterItems = useMemo(
|
||||||
() => resourceAttributesToTagFilterItems(resourceAttributeQueries) || [],
|
() =>
|
||||||
[resourceAttributeQueries],
|
handleNonInQueryRange(resourceAttributesToTagFilterItems(queries)) || [],
|
||||||
|
[queries],
|
||||||
);
|
);
|
||||||
|
|
||||||
const externalCallErrorWidget = useMemo(
|
const externalCallErrorWidget = useMemo(
|
||||||
@ -51,11 +52,8 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const selectedTraceTags = useMemo(
|
const selectedTraceTags = useMemo(
|
||||||
() =>
|
() => JSON.stringify(convertRawQueriesToTraceSelectedTags(queries) || []),
|
||||||
JSON.stringify(
|
[queries],
|
||||||
convertRawQueriesToTraceSelectedTags(resourceAttributeQueries) || [],
|
|
||||||
),
|
|
||||||
[resourceAttributeQueries],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const externalCallDurationWidget = useMemo(
|
const externalCallDurationWidget = useMemo(
|
||||||
|
@ -1,18 +1,21 @@
|
|||||||
import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js';
|
import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js';
|
||||||
import Graph from 'components/Graph';
|
import Graph from 'components/Graph';
|
||||||
import { METRICS_PAGE_QUERY_PARAM } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import FullView from 'container/GridGraphLayout/Graph/FullView/index.metricsBuilder';
|
import FullView from 'container/GridGraphLayout/Graph/FullView/index.metricsBuilder';
|
||||||
import convertToNanoSecondsToSecond from 'lib/convertToNanoSecondsToSecond';
|
import { routeConfig } from 'container/SideNav/config';
|
||||||
import { colors } from 'lib/getRandomColor';
|
import { getQueryString } from 'container/SideNav/helper';
|
||||||
import history from 'lib/history';
|
import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||||
import {
|
import {
|
||||||
convertRawQueriesToTraceSelectedTags,
|
convertRawQueriesToTraceSelectedTags,
|
||||||
resourceAttributesToTagFilterItems,
|
resourceAttributesToTagFilterItems,
|
||||||
} from 'lib/resourceAttributes';
|
} from 'hooks/useResourceAttribute/utils';
|
||||||
|
import convertToNanoSecondsToSecond from 'lib/convertToNanoSecondsToSecond';
|
||||||
|
import { colors } from 'lib/getRandomColor';
|
||||||
|
import history from 'lib/history';
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
import { UpdateTimeInterval } from 'store/actions';
|
import { UpdateTimeInterval } from 'store/actions';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
@ -25,11 +28,16 @@ import {
|
|||||||
import { Card, Col, GraphContainer, GraphTitle, Row } from '../styles';
|
import { Card, Col, GraphContainer, GraphTitle, Row } from '../styles';
|
||||||
import TopOperationsTable from '../TopOperationsTable';
|
import TopOperationsTable from '../TopOperationsTable';
|
||||||
import { Button } from './styles';
|
import { Button } from './styles';
|
||||||
import { onGraphClickHandler, onViewTracePopupClick } from './util';
|
import {
|
||||||
|
handleNonInQueryRange,
|
||||||
|
onGraphClickHandler,
|
||||||
|
onViewTracePopupClick,
|
||||||
|
} from './util';
|
||||||
|
|
||||||
function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
|
function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
|
||||||
const { servicename } = useParams<{ servicename?: string }>();
|
const { servicename } = useParams<{ servicename?: string }>();
|
||||||
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
|
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
|
||||||
|
const { search } = useLocation();
|
||||||
|
|
||||||
const handleSetTimeStamp = useCallback((selectTime: number) => {
|
const handleSetTimeStamp = useCallback((selectTime: number) => {
|
||||||
setSelectedTimeStamp(selectTime);
|
setSelectedTimeStamp(selectTime);
|
||||||
@ -54,20 +62,21 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
|
|||||||
[handleSetTimeStamp],
|
[handleSetTimeStamp],
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const { topOperations, serviceOverview, topLevelOperations } = useSelector<
|
||||||
topOperations,
|
AppState,
|
||||||
serviceOverview,
|
MetricReducer
|
||||||
resourceAttributeQueries,
|
>((state) => state.metrics);
|
||||||
topLevelOperations,
|
|
||||||
} = useSelector<AppState, MetricReducer>((state) => state.metrics);
|
const { queries } = useResourceAttribute();
|
||||||
|
|
||||||
const selectedTraceTags: string = JSON.stringify(
|
const selectedTraceTags: string = JSON.stringify(
|
||||||
convertRawQueriesToTraceSelectedTags(resourceAttributeQueries) || [],
|
convertRawQueriesToTraceSelectedTags(queries) || [],
|
||||||
);
|
);
|
||||||
|
|
||||||
const tagFilterItems = useMemo(
|
const tagFilterItems = useMemo(
|
||||||
() => resourceAttributesToTagFilterItems(resourceAttributeQueries) || [],
|
() =>
|
||||||
[resourceAttributeQueries],
|
handleNonInQueryRange(resourceAttributesToTagFilterItems(queries)) || [],
|
||||||
|
[queries],
|
||||||
);
|
);
|
||||||
|
|
||||||
const operationPerSecWidget = useMemo(
|
const operationPerSecWidget = useMemo(
|
||||||
@ -116,14 +125,19 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
|
|||||||
const currentTime = timestamp;
|
const currentTime = timestamp;
|
||||||
const tPlusOne = timestamp + 60 * 1000;
|
const tPlusOne = timestamp + 60 * 1000;
|
||||||
|
|
||||||
const urlParams = new URLSearchParams();
|
const urlParams = new URLSearchParams(search);
|
||||||
urlParams.set(METRICS_PAGE_QUERY_PARAM.startTime, currentTime.toString());
|
urlParams.set(QueryParams.startTime, currentTime.toString());
|
||||||
urlParams.set(METRICS_PAGE_QUERY_PARAM.endTime, tPlusOne.toString());
|
urlParams.set(QueryParams.endTime, tPlusOne.toString());
|
||||||
|
|
||||||
|
const avialableParams = routeConfig[ROUTES.TRACE];
|
||||||
|
const queryString = getQueryString(avialableParams, urlParams);
|
||||||
|
|
||||||
history.replace(
|
history.replace(
|
||||||
`${
|
`${
|
||||||
ROUTES.TRACE
|
ROUTES.TRACE
|
||||||
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"],"status":["error"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&isFilterExclude={"serviceName":false,"status":false}&userSelectedFilter={"serviceName":["${servicename}"],"status":["error"]}&spanAggregateCurrentPage=1`,
|
}?selected={"serviceName":["${servicename}"],"status":["error"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&isFilterExclude={"serviceName":false,"status":false}&userSelectedFilter={"serviceName":["${servicename}"],"status":["error"]}&spanAggregateCurrentPage=1&${queryString.join(
|
||||||
|
'',
|
||||||
|
)}`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js';
|
import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js';
|
||||||
import { METRICS_PAGE_QUERY_PARAM } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
|
import { routeConfig } from 'container/SideNav/config';
|
||||||
|
import { getQueryString } from 'container/SideNav/helper';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
|
import { IQueryBuilderTagFilterItems } from 'types/api/dashboard/getAll';
|
||||||
import { Tags } from 'types/reducer/trace';
|
import { Tags } from 'types/reducer/trace';
|
||||||
|
|
||||||
export const dbSystemTags: Tags[] = [
|
export const dbSystemTags: Tags[] = [
|
||||||
@ -30,16 +33,18 @@ export function onViewTracePopupClick({
|
|||||||
const currentTime = timestamp;
|
const currentTime = timestamp;
|
||||||
const tPlusOne = timestamp + 60 * 1000;
|
const tPlusOne = timestamp + 60 * 1000;
|
||||||
|
|
||||||
const urlParams = new URLSearchParams();
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
urlParams.set(METRICS_PAGE_QUERY_PARAM.startTime, currentTime.toString());
|
urlParams.set(QueryParams.startTime, currentTime.toString());
|
||||||
urlParams.set(METRICS_PAGE_QUERY_PARAM.endTime, tPlusOne.toString());
|
urlParams.set(QueryParams.endTime, tPlusOne.toString());
|
||||||
|
const avialableParams = routeConfig[ROUTES.TRACE];
|
||||||
|
const queryString = getQueryString(avialableParams, urlParams);
|
||||||
|
|
||||||
history.replace(
|
history.replace(
|
||||||
`${
|
`${
|
||||||
ROUTES.TRACE
|
ROUTES.TRACE
|
||||||
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&&isFilterExclude={"serviceName":false}&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"]}&spanAggregateCurrentPage=1${
|
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&&isFilterExclude={"serviceName":false}&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"]}&spanAggregateCurrentPage=1${
|
||||||
isExternalCall ? '&spanKind=3' : ''
|
isExternalCall ? '&spanKind=3' : ''
|
||||||
}`,
|
}&${queryString.join('&')}`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -60,7 +65,7 @@ export function onGraphClickHandler(
|
|||||||
const points = chart.getElementsAtEventForMode(
|
const points = chart.getElementsAtEventForMode(
|
||||||
event.native,
|
event.native,
|
||||||
'nearest',
|
'nearest',
|
||||||
{ intersect: true },
|
{ intersect: false },
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
const id = `${from}_button`;
|
const id = `${from}_button`;
|
||||||
@ -84,3 +89,16 @@ export function onGraphClickHandler(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const handleNonInQueryRange = (
|
||||||
|
tags: IQueryBuilderTagFilterItems[],
|
||||||
|
): IQueryBuilderTagFilterItems[] =>
|
||||||
|
tags.map((tag) => {
|
||||||
|
if (tag.op === 'Not IN') {
|
||||||
|
return {
|
||||||
|
...tag,
|
||||||
|
op: 'NIN',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return tag;
|
||||||
|
});
|
||||||
|
@ -1,27 +1,25 @@
|
|||||||
import { Tooltip, Typography } from 'antd';
|
import { Tooltip, Typography } from 'antd';
|
||||||
import { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
import { METRICS_PAGE_QUERY_PARAM } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
|
import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||||
|
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { convertRawQueriesToTraceSelectedTags } from 'lib/resourceAttributes';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
import MetricReducer from 'types/reducer/metrics';
|
|
||||||
|
|
||||||
function TopOperationsTable(props: TopOperationsTableProps): JSX.Element {
|
function TopOperationsTable(props: TopOperationsTableProps): JSX.Element {
|
||||||
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||||
(state) => state.globalTime,
|
(state) => state.globalTime,
|
||||||
);
|
);
|
||||||
const { resourceAttributeQueries } = useSelector<AppState, MetricReducer>(
|
const { queries } = useResourceAttribute();
|
||||||
(state) => state.metrics,
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedTraceTags: string = JSON.stringify(
|
const selectedTraceTags: string = JSON.stringify(
|
||||||
convertRawQueriesToTraceSelectedTags(resourceAttributeQueries) || [],
|
convertRawQueriesToTraceSelectedTags(queries) || [],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data } = props;
|
const { data } = props;
|
||||||
@ -31,14 +29,8 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element {
|
|||||||
const handleOnClick = (operation: string): void => {
|
const handleOnClick = (operation: string): void => {
|
||||||
const urlParams = new URLSearchParams();
|
const urlParams = new URLSearchParams();
|
||||||
const { servicename } = params;
|
const { servicename } = params;
|
||||||
urlParams.set(
|
urlParams.set(QueryParams.startTime, (minTime / 1000000).toString());
|
||||||
METRICS_PAGE_QUERY_PARAM.startTime,
|
urlParams.set(QueryParams.endTime, (maxTime / 1000000).toString());
|
||||||
(minTime / 1000000).toString(),
|
|
||||||
);
|
|
||||||
urlParams.set(
|
|
||||||
METRICS_PAGE_QUERY_PARAM.endTime,
|
|
||||||
(maxTime / 1000000).toString(),
|
|
||||||
);
|
|
||||||
|
|
||||||
history.push(
|
history.push(
|
||||||
`${
|
`${
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import RouteTab from 'components/RouteTab';
|
import RouteTab from 'components/RouteTab';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
|
import ResourceAttributesFilter from 'container/ResourceAttributesFilter';
|
||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
import { generatePath, useParams } from 'react-router-dom';
|
import { generatePath, useParams } from 'react-router-dom';
|
||||||
import { useLocation } from 'react-use';
|
import { useLocation } from 'react-use';
|
||||||
|
|
||||||
import { getWidgetQueryBuilder } from './MetricsApplication.factory';
|
import { getWidgetQueryBuilder } from './MetricsApplication.factory';
|
||||||
import ResourceAttributesFilter from './ResourceAttributesFilter';
|
|
||||||
import DBCall from './Tabs/DBCall';
|
import DBCall from './Tabs/DBCall';
|
||||||
import External from './Tabs/External';
|
import External from './Tabs/External';
|
||||||
import Overview from './Tabs/Overview';
|
import Overview from './Tabs/Overview';
|
||||||
|
70
frontend/src/container/MetricsTable/Metrics.test.tsx
Normal file
70
frontend/src/container/MetricsTable/Metrics.test.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { render, RenderResult, screen, waitFor } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
combineReducers,
|
||||||
|
legacy_createStore as createStore,
|
||||||
|
Store,
|
||||||
|
} from 'redux';
|
||||||
|
|
||||||
|
import { InitialValue } from '../../store/reducers/metric';
|
||||||
|
import Metrics from './index';
|
||||||
|
|
||||||
|
const rootReducer = combineReducers({
|
||||||
|
metrics: (state = InitialValue) => state,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockStore = createStore(rootReducer);
|
||||||
|
|
||||||
|
const renderWithReduxAndRouter = (mockStore: Store) => (
|
||||||
|
component: React.ReactElement,
|
||||||
|
): RenderResult =>
|
||||||
|
render(
|
||||||
|
<BrowserRouter>
|
||||||
|
<Provider store={mockStore}>{component}</Provider>
|
||||||
|
</BrowserRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('Metrics Component', () => {
|
||||||
|
it('renders without errors', async () => {
|
||||||
|
renderWithReduxAndRouter(mockStore)(<Metrics />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/application/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/p99 latency \(in ms\)/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/error rate \(% of total\)/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/operations per second/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders loading when required conditions are met', async () => {
|
||||||
|
const customStore = createStore(rootReducer, {
|
||||||
|
metrics: {
|
||||||
|
services: [],
|
||||||
|
loading: true,
|
||||||
|
error: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { container } = renderWithReduxAndRouter(customStore)(<Metrics />);
|
||||||
|
|
||||||
|
const spinner = container.querySelector('.ant-spin-nested-loading');
|
||||||
|
|
||||||
|
expect(spinner).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders no data when required conditions are met', async () => {
|
||||||
|
const customStore = createStore(rootReducer, {
|
||||||
|
metrics: {
|
||||||
|
services: [],
|
||||||
|
loading: false,
|
||||||
|
error: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
renderWithReduxAndRouter(customStore)(<Metrics />);
|
||||||
|
|
||||||
|
expect(screen.getByText('No data')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -11,6 +11,8 @@ import localStorageSet from 'api/browser/localstorage/set';
|
|||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
import { SKIP_ONBOARDING } from 'constants/onboarding';
|
import { SKIP_ONBOARDING } from 'constants/onboarding';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
|
import { routeConfig } from 'container/SideNav/config';
|
||||||
|
import { getQueryString } from 'container/SideNav/helper';
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
@ -88,11 +90,17 @@ function Metrics(): JSX.Element {
|
|||||||
.toString()
|
.toString()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(value.toString().toLowerCase()),
|
.includes(value.toString().toLowerCase()),
|
||||||
render: (text: string): JSX.Element => (
|
render: (metrics: string): JSX.Element => {
|
||||||
<Link to={`${ROUTES.APPLICATION}/${text}${search}`}>
|
const urlParams = new URLSearchParams(search);
|
||||||
<Name>{text}</Name>
|
const avialableParams = routeConfig[ROUTES.SERVICE_METRICS];
|
||||||
</Link>
|
const queryString = getQueryString(avialableParams, urlParams);
|
||||||
),
|
|
||||||
|
return (
|
||||||
|
<Link to={`${ROUTES.APPLICATION}/${metrics}?${queryString.join('')}`}>
|
||||||
|
<Name>{metrics}</Name>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
[filterDropdown, FilterIcon, search],
|
[filterDropdown, FilterIcon, search],
|
||||||
);
|
);
|
||||||
|
@ -129,7 +129,8 @@ function VariableItem({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getOptions();
|
getOptions();
|
||||||
}, [getOptions]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleChange = (value: string | string[]): void => {
|
const handleChange = (value: string | string[]): void => {
|
||||||
if (
|
if (
|
||||||
|
@ -2,31 +2,31 @@
|
|||||||
|
|
||||||
export interface Typegen0 {
|
export interface Typegen0 {
|
||||||
'@@xstate/typegen': true;
|
'@@xstate/typegen': true;
|
||||||
eventsCausingActions: {
|
|
||||||
onSelectOperator: 'NEXT';
|
|
||||||
onBlurPurge: 'onBlur';
|
|
||||||
onSelectTagValue: 'NEXT';
|
|
||||||
onValidateQuery: 'onBlur';
|
|
||||||
onSelectTagKey: 'NEXT';
|
|
||||||
};
|
|
||||||
internalEvents: {
|
internalEvents: {
|
||||||
'xstate.init': { type: 'xstate.init' };
|
'xstate.init': { type: 'xstate.init' };
|
||||||
};
|
};
|
||||||
invokeSrcNameMap: {};
|
invokeSrcNameMap: {};
|
||||||
missingImplementations: {
|
missingImplementations: {
|
||||||
actions:
|
actions:
|
||||||
| 'onSelectOperator'
|
|
||||||
| 'onBlurPurge'
|
| 'onBlurPurge'
|
||||||
|
| 'onSelectOperator'
|
||||||
|
| 'onSelectTagKey'
|
||||||
| 'onSelectTagValue'
|
| 'onSelectTagValue'
|
||||||
| 'onValidateQuery'
|
| 'onValidateQuery';
|
||||||
| 'onSelectTagKey';
|
|
||||||
services: never;
|
|
||||||
guards: never;
|
|
||||||
delays: never;
|
delays: never;
|
||||||
|
guards: never;
|
||||||
|
services: never;
|
||||||
|
};
|
||||||
|
eventsCausingActions: {
|
||||||
|
onBlurPurge: 'onBlur';
|
||||||
|
onSelectOperator: 'NEXT';
|
||||||
|
onSelectTagKey: 'NEXT';
|
||||||
|
onSelectTagValue: 'NEXT';
|
||||||
|
onValidateQuery: 'onBlur';
|
||||||
};
|
};
|
||||||
eventsCausingServices: {};
|
|
||||||
eventsCausingGuards: {};
|
|
||||||
eventsCausingDelays: {};
|
eventsCausingDelays: {};
|
||||||
matchesStates: 'TagKey' | 'Operator' | 'TagValue' | 'Idle';
|
eventsCausingGuards: {};
|
||||||
|
eventsCausingServices: {};
|
||||||
|
matchesStates: 'Idle' | 'Operator' | 'TagKey' | 'TagValue';
|
||||||
tags: never;
|
tags: never;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
//@ts-nocheck
|
|
||||||
|
|
||||||
import { Button, Tabs } from 'antd';
|
import { Button, Tabs } from 'antd';
|
||||||
import TextToolTip from 'components/TextToolTip';
|
import TextToolTip from 'components/TextToolTip';
|
||||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||||
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
|
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
|
||||||
|
import { QueryBuilder } from 'container/QueryBuilder';
|
||||||
import { cloneDeep, isEqual } from 'lodash-es';
|
import { cloneDeep, isEqual } from 'lodash-es';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { connect, useSelector } from 'react-redux';
|
import { connect, useSelector } from 'react-redux';
|
||||||
@ -31,6 +30,7 @@ import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
|
|||||||
import PromQLQueryContainer from './QueryBuilder/promQL';
|
import PromQLQueryContainer from './QueryBuilder/promQL';
|
||||||
import QueryBuilderQueryContainer from './QueryBuilder/queryBuilder';
|
import QueryBuilderQueryContainer from './QueryBuilder/queryBuilder';
|
||||||
import TabHeader from './TabHeader';
|
import TabHeader from './TabHeader';
|
||||||
|
import { IHandleUpdatedQuery } from './types';
|
||||||
import { getQueryKey } from './utils/getQueryKey';
|
import { getQueryKey } from './utils/getQueryKey';
|
||||||
import { showUnstagedStashConfirmBox } from './utils/userSettings';
|
import { showUnstagedStashConfirmBox } from './utils/userSettings';
|
||||||
|
|
||||||
@ -54,9 +54,7 @@ function QuerySection({
|
|||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const { widgets } = selectedDashboards.data;
|
const { widgets } = selectedDashboards.data;
|
||||||
|
|
||||||
const urlQuery = useMemo(() => {
|
const urlQuery = useMemo(() => new URLSearchParams(search), [search]);
|
||||||
return new URLSearchParams(search);
|
|
||||||
}, [search]);
|
|
||||||
|
|
||||||
const getWidget = useCallback(() => {
|
const getWidget = useCallback(() => {
|
||||||
const widgetId = urlQuery.get('widgetId');
|
const widgetId = urlQuery.get('widgetId');
|
||||||
@ -169,6 +167,9 @@ function QuerySection({
|
|||||||
}
|
}
|
||||||
selectedGraph={selectedGraph}
|
selectedGraph={selectedGraph}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
// TODO: uncomment for testing new QueryBuilder
|
||||||
|
// <QueryBuilder panelType={selectedGraph} />
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -11,12 +11,14 @@ export const Container = styled.div`
|
|||||||
export const RightContainerWrapper = styled(Col)`
|
export const RightContainerWrapper = styled(Col)`
|
||||||
&&& {
|
&&& {
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const LeftContainerWrapper = styled(Col)`
|
export const LeftContainerWrapper = styled(Col)`
|
||||||
&&& {
|
&&& {
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
import {
|
import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems';
|
||||||
IBuilderFormula,
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
IBuilderQuery,
|
|
||||||
} from 'types/api/queryBuilder/queryBuilderData';
|
export type QueryBuilderConfig =
|
||||||
|
| {
|
||||||
|
queryVariant: 'static';
|
||||||
|
initialDataSource: DataSource;
|
||||||
|
}
|
||||||
|
| { queryVariant: 'dropdown' };
|
||||||
|
|
||||||
export type QueryBuilderProps = {
|
export type QueryBuilderProps = {
|
||||||
queryData: IBuilderQuery[];
|
config?: QueryBuilderConfig;
|
||||||
queryFormula: IBuilderFormula[];
|
panelType?: ITEMS;
|
||||||
};
|
};
|
||||||
|
@ -1,23 +1,93 @@
|
|||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Col, Row } from 'antd';
|
||||||
|
import { MAX_FORMULAS, MAX_QUERIES } from 'constants/queryBuilder';
|
||||||
// ** Hooks
|
// ** Hooks
|
||||||
import { useQueryBuilder } from 'hooks/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/useQueryBuilder';
|
||||||
import React from 'react';
|
// ** Constants
|
||||||
|
import React, { memo, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
|
// ** Components
|
||||||
|
import { Formula, Query } from './components';
|
||||||
// ** Types
|
// ** Types
|
||||||
import { QueryBuilderProps } from './QueryBuilder.interfaces';
|
import { QueryBuilderProps } from './QueryBuilder.interfaces';
|
||||||
|
// ** Styles
|
||||||
|
|
||||||
// TODO: temporary eslint disable while variable isn't used
|
export const QueryBuilder = memo(function QueryBuilder({
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
config,
|
||||||
export function QueryBuilder(props: QueryBuilderProps): JSX.Element {
|
panelType,
|
||||||
// TODO: temporary doesn't use
|
}: QueryBuilderProps): JSX.Element {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
const {
|
||||||
const { queryBuilderData } = useQueryBuilder();
|
queryBuilderData,
|
||||||
|
setupInitialDataSource,
|
||||||
|
addNewQuery,
|
||||||
|
addNewFormula,
|
||||||
|
} = useQueryBuilder();
|
||||||
|
|
||||||
// Here we can use Form from antd library and fill context data or edit
|
useEffect(() => {
|
||||||
// Connect form with adding or removing items from the list
|
if (config && config.queryVariant === 'static') {
|
||||||
|
setupInitialDataSource(config.initialDataSource);
|
||||||
|
}
|
||||||
|
|
||||||
// Here will be map of query queryBuilderData.queryData and queryBuilderData.queryFormulas components
|
return (): void => {
|
||||||
// Each component can be part of antd Form list where we can add or remove items
|
setupInitialDataSource(null);
|
||||||
// Also need decide to make a copy of queryData for working with form or not and after it set the full new list with formulas or queries to the context
|
};
|
||||||
// With button to add him
|
}, [config, setupInitialDataSource]);
|
||||||
return <div>null</div>;
|
|
||||||
}
|
const isDisabledQueryButton = useMemo(
|
||||||
|
() => queryBuilderData.queryData.length >= MAX_QUERIES,
|
||||||
|
[queryBuilderData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const isDisabledFormulaButton = useMemo(
|
||||||
|
() => queryBuilderData.queryFormulas.length >= MAX_FORMULAS,
|
||||||
|
[queryBuilderData],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row gutter={[0, 20]} justify="start">
|
||||||
|
<Col span={24}>
|
||||||
|
<Row gutter={[0, 50]}>
|
||||||
|
{queryBuilderData.queryData.map((query, index) => (
|
||||||
|
<Col key={query.queryName} span={24}>
|
||||||
|
<Query
|
||||||
|
index={index}
|
||||||
|
isAvailableToDisable={queryBuilderData.queryData.length > 1}
|
||||||
|
queryVariant={config?.queryVariant || 'dropdown'}
|
||||||
|
query={query}
|
||||||
|
panelType={panelType}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
{queryBuilderData.queryFormulas.map((formula, index) => (
|
||||||
|
<Col key={formula.label} span={24}>
|
||||||
|
<Formula formula={formula} index={index} />
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Row gutter={[20, 0]}>
|
||||||
|
<Col>
|
||||||
|
<Button
|
||||||
|
disabled={isDisabledQueryButton}
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={addNewQuery}
|
||||||
|
>
|
||||||
|
Query
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Button
|
||||||
|
disabled={isDisabledFormulaButton}
|
||||||
|
onClick={addNewFormula}
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
>
|
||||||
|
Formula
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export type AdditionalFiltersProps = {
|
||||||
|
listOfAdditionalFilter: string[];
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
@ -0,0 +1,34 @@
|
|||||||
|
import { MinusSquareOutlined, PlusSquareOutlined } from '@ant-design/icons';
|
||||||
|
import { Col, Typography } from 'antd';
|
||||||
|
import styled, { css } from 'styled-components';
|
||||||
|
|
||||||
|
const IconCss = css`
|
||||||
|
margin-right: 0.6875rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const StyledIconOpen = styled(PlusSquareOutlined)`
|
||||||
|
${IconCss}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const StyledIconClose = styled(MinusSquareOutlined)`
|
||||||
|
${IconCss}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const StyledInner = styled(Col)`
|
||||||
|
width: fit-content;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.875rem;
|
||||||
|
min-height: 1.375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
${StyledIconOpen}, ${StyledIconClose} {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const StyledLink = styled(Typography.Link)`
|
||||||
|
pointer-events: none;
|
||||||
|
`;
|
@ -0,0 +1,53 @@
|
|||||||
|
import { Col, Row } from 'antd';
|
||||||
|
import React, { Fragment, memo, ReactNode, useState } from 'react';
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { AdditionalFiltersProps } from './AdditionalFiltersToggler.interfaces';
|
||||||
|
// ** Styles
|
||||||
|
import {
|
||||||
|
StyledIconClose,
|
||||||
|
StyledIconOpen,
|
||||||
|
StyledInner,
|
||||||
|
StyledLink,
|
||||||
|
} from './AdditionalFiltersToggler.styled';
|
||||||
|
|
||||||
|
export const AdditionalFiltersToggler = memo(function AdditionalFiltersToggler({
|
||||||
|
children,
|
||||||
|
listOfAdditionalFilter,
|
||||||
|
}: AdditionalFiltersProps): JSX.Element {
|
||||||
|
const [isOpenedFilters, setIsOpenedFilters] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleToggleOpenFilters = (): void => {
|
||||||
|
setIsOpenedFilters((prevState) => !prevState);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filtersTexts: ReactNode = listOfAdditionalFilter.map((str, index) => {
|
||||||
|
const isNextLast = index + 1 === listOfAdditionalFilter.length - 1;
|
||||||
|
if (index === listOfAdditionalFilter.length - 1) {
|
||||||
|
return (
|
||||||
|
<Fragment key={str}>
|
||||||
|
and <StyledLink>{str.toUpperCase()}</StyledLink>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span key={str}>
|
||||||
|
<StyledLink>{str.toUpperCase()}</StyledLink>
|
||||||
|
{isNextLast ? ' ' : ', '}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row>
|
||||||
|
<Col span={24}>
|
||||||
|
<StyledInner onClick={handleToggleOpenFilters}>
|
||||||
|
{isOpenedFilters ? <StyledIconClose /> : <StyledIconOpen />}
|
||||||
|
{!isOpenedFilters && <span>Add conditions for {filtersTexts}</span>}
|
||||||
|
</StyledInner>
|
||||||
|
</Col>
|
||||||
|
{isOpenedFilters && <Col span={24}>{children}</Col>}
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1 @@
|
|||||||
|
export { AdditionalFiltersToggler } from './AdditionalFiltersToggler';
|
@ -0,0 +1,6 @@
|
|||||||
|
import { SelectProps } from 'antd';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
export type QueryLabelProps = {
|
||||||
|
onChange: (value: DataSource) => void;
|
||||||
|
} & Omit<SelectProps, 'onChange'>;
|
@ -0,0 +1,35 @@
|
|||||||
|
import { Select } from 'antd';
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
import { SelectOption } from 'types/common/select';
|
||||||
|
// ** Helpers
|
||||||
|
import { transformToUpperCase } from 'utils/transformToUpperCase';
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { QueryLabelProps } from './DataSourceDropdown.interfaces';
|
||||||
|
|
||||||
|
const dataSourceMap = [DataSource.LOGS, DataSource.METRICS, DataSource.TRACES];
|
||||||
|
|
||||||
|
export const DataSourceDropdown = memo(function DataSourceDropdown(
|
||||||
|
props: QueryLabelProps,
|
||||||
|
): JSX.Element {
|
||||||
|
const { onChange, value, style } = props;
|
||||||
|
|
||||||
|
const dataSourceOptions: SelectOption<
|
||||||
|
DataSource,
|
||||||
|
string
|
||||||
|
>[] = dataSourceMap.map((source) => ({
|
||||||
|
label: transformToUpperCase(source),
|
||||||
|
value: source,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
defaultValue={dataSourceOptions[0].value}
|
||||||
|
options={dataSourceOptions}
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
style={style}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1 @@
|
|||||||
|
export { DataSourceDropdown } from './DataSourceDropdown';
|
@ -0,0 +1,6 @@
|
|||||||
|
import { CSSProperties } from 'react';
|
||||||
|
|
||||||
|
export type FilterLabelProps = {
|
||||||
|
label: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
};
|
@ -0,0 +1,13 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const StyledLabel = styled.div`
|
||||||
|
padding: 0 0.6875rem;
|
||||||
|
min-height: 2rem;
|
||||||
|
width: 100%;
|
||||||
|
display: inline-flex;
|
||||||
|
white-space: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 0.125rem;
|
||||||
|
border: 0.0625rem solid #434343;
|
||||||
|
background-color: #141414;
|
||||||
|
`;
|
@ -0,0 +1,12 @@
|
|||||||
|
import React, { memo } from 'react';
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { FilterLabelProps } from './FilterLabel.interfaces';
|
||||||
|
// ** Styles
|
||||||
|
import { StyledLabel } from './FilterLabel.styled';
|
||||||
|
|
||||||
|
export const FilterLabel = memo(function FilterLabel({
|
||||||
|
label,
|
||||||
|
}: FilterLabelProps): JSX.Element {
|
||||||
|
return <StyledLabel>{label}</StyledLabel>;
|
||||||
|
});
|
@ -0,0 +1 @@
|
|||||||
|
export { FilterLabel } from './FilterLabel';
|
@ -0,0 +1,3 @@
|
|||||||
|
import { IBuilderFormula } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
export type FormulaProps = { formula: IBuilderFormula; index: number };
|
@ -0,0 +1,73 @@
|
|||||||
|
import { Col, Input } from 'antd';
|
||||||
|
// ** Components
|
||||||
|
import { ListItemWrapper, ListMarker } from 'container/QueryBuilder/components';
|
||||||
|
// ** Hooks
|
||||||
|
import { useQueryBuilder } from 'hooks/useQueryBuilder';
|
||||||
|
import React, { ChangeEvent, useCallback } from 'react';
|
||||||
|
import { IBuilderFormula } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { FormulaProps } from './Formula.interfaces';
|
||||||
|
|
||||||
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
export function Formula({ index, formula }: FormulaProps): JSX.Element {
|
||||||
|
const { removeEntityByIndex, handleSetFormulaData } = useQueryBuilder();
|
||||||
|
|
||||||
|
const handleDelete = useCallback(() => {
|
||||||
|
removeEntityByIndex('queryFormulas', index);
|
||||||
|
}, [index, removeEntityByIndex]);
|
||||||
|
|
||||||
|
const handleToggleDisableFormula = useCallback((): void => {
|
||||||
|
const newFormula: IBuilderFormula = {
|
||||||
|
...formula,
|
||||||
|
disabled: !formula.disabled,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSetFormulaData(index, newFormula);
|
||||||
|
}, [index, formula, handleSetFormulaData]);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
const newFormula: IBuilderFormula = {
|
||||||
|
...formula,
|
||||||
|
[name]: value,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSetFormulaData(index, newFormula);
|
||||||
|
},
|
||||||
|
[index, formula, handleSetFormulaData],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItemWrapper onDelete={handleDelete}>
|
||||||
|
<Col span={24}>
|
||||||
|
<ListMarker
|
||||||
|
isDisabled={formula.disabled}
|
||||||
|
onDisable={handleToggleDisableFormula}
|
||||||
|
labelName={formula.label}
|
||||||
|
index={index}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={24}>
|
||||||
|
<TextArea
|
||||||
|
name="expression"
|
||||||
|
onChange={handleChange}
|
||||||
|
size="middle"
|
||||||
|
value={formula.expression}
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={24}>
|
||||||
|
<Input
|
||||||
|
name="legend"
|
||||||
|
onChange={handleChange}
|
||||||
|
size="middle"
|
||||||
|
value={formula.legend}
|
||||||
|
addonBefore="Legend Format"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</ListItemWrapper>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { Formula } from './Formula';
|
@ -0,0 +1,6 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export type ListItemWrapperProps = {
|
||||||
|
onDelete: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
@ -0,0 +1,26 @@
|
|||||||
|
import { CloseCircleOutlined } from '@ant-design/icons';
|
||||||
|
import { Row } from 'antd';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const StyledDeleteEntity = styled(CloseCircleOutlined)`
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.9375rem;
|
||||||
|
z-index: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.45;
|
||||||
|
width: 1.3125rem;
|
||||||
|
height: 1.3125rem;
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const StyledRow = styled(Row)`
|
||||||
|
padding-right: 3rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const StyledFilterRow = styled(Row)`
|
||||||
|
margin-bottom: 0.875rem;
|
||||||
|
`;
|
@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { ListItemWrapperProps } from './ListItemWrapper.interfaces';
|
||||||
|
// ** Styles
|
||||||
|
import { StyledDeleteEntity, StyledRow } from './ListItemWrapper.styled';
|
||||||
|
|
||||||
|
export function ListItemWrapper({
|
||||||
|
children,
|
||||||
|
onDelete,
|
||||||
|
}: ListItemWrapperProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<StyledRow gutter={[0, 15]}>
|
||||||
|
<StyledDeleteEntity onClick={onDelete} />
|
||||||
|
{children}
|
||||||
|
</StyledRow>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { ListItemWrapper } from './ListItemWrapper';
|
@ -1,8 +1,11 @@
|
|||||||
|
import { CSSProperties } from 'react';
|
||||||
|
|
||||||
export type ListMarkerProps = {
|
export type ListMarkerProps = {
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
labelName: string;
|
labelName: string;
|
||||||
index: number;
|
index: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
isAvailableToDisable: boolean;
|
isAvailableToDisable?: boolean;
|
||||||
toggleDisabled: (index: number) => void;
|
onDisable: (index: number) => void;
|
||||||
|
style?: CSSProperties;
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export const StyledButton = styled(Button)`
|
export const StyledButton = styled(Button)<{ $isAvailableToDisable: boolean }>`
|
||||||
min-width: 2rem;
|
min-width: 2rem;
|
||||||
height: 2.25rem;
|
height: 2.25rem;
|
||||||
padding: 0.125rem;
|
padding: ${(props): string =>
|
||||||
|
props.$isAvailableToDisable ? '0.43rem' : '0.43rem 0.68rem'};
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
pointer-events: ${(props): string =>
|
||||||
|
props.$isAvailableToDisable ? 'default' : 'none'};
|
||||||
`;
|
`;
|
||||||
|
@ -1,25 +1,26 @@
|
|||||||
import { EyeFilled, EyeInvisibleFilled } from '@ant-design/icons';
|
import { EyeFilled, EyeInvisibleFilled } from '@ant-design/icons';
|
||||||
import { ButtonProps } from 'antd';
|
import { ButtonProps } from 'antd';
|
||||||
import React from 'react';
|
import React, { memo } from 'react';
|
||||||
|
|
||||||
// ** Types
|
// ** Types
|
||||||
import { ListMarkerProps } from './ListMarker.interfaces';
|
import { ListMarkerProps } from './ListMarker.interfaces';
|
||||||
// ** Styles
|
// ** Styles
|
||||||
import { StyledButton } from './ListMarker.styled';
|
import { StyledButton } from './ListMarker.styled';
|
||||||
|
|
||||||
export function ListMarker({
|
export const ListMarker = memo(function ListMarker({
|
||||||
isDisabled,
|
isDisabled,
|
||||||
labelName,
|
labelName,
|
||||||
index,
|
index,
|
||||||
isAvailableToDisable,
|
isAvailableToDisable = true,
|
||||||
className,
|
className,
|
||||||
toggleDisabled,
|
onDisable,
|
||||||
|
style,
|
||||||
}: ListMarkerProps): JSX.Element {
|
}: ListMarkerProps): JSX.Element {
|
||||||
const buttonProps: Partial<ButtonProps> = isAvailableToDisable
|
const buttonProps: Partial<ButtonProps> = isAvailableToDisable
|
||||||
? {
|
? {
|
||||||
type: isDisabled ? 'default' : 'primary',
|
type: isDisabled ? 'default' : 'primary',
|
||||||
icon: isDisabled ? <EyeInvisibleFilled /> : <EyeFilled />,
|
icon: isDisabled ? <EyeInvisibleFilled /> : <EyeFilled />,
|
||||||
onClick: (): void => toggleDisabled(index),
|
onClick: (): void => onDisable(index),
|
||||||
}
|
}
|
||||||
: { type: 'primary' };
|
: { type: 'primary' };
|
||||||
|
|
||||||
@ -29,8 +30,10 @@ export function ListMarker({
|
|||||||
icon={buttonProps.icon}
|
icon={buttonProps.icon}
|
||||||
onClick={buttonProps.onClick}
|
onClick={buttonProps.onClick}
|
||||||
className={className}
|
className={className}
|
||||||
|
$isAvailableToDisable={isAvailableToDisable}
|
||||||
|
style={style}
|
||||||
>
|
>
|
||||||
{labelName}
|
{labelName}
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems';
|
||||||
|
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
export type QueryProps = {
|
||||||
|
index: number;
|
||||||
|
isAvailableToDisable: boolean;
|
||||||
|
query: IBuilderQueryForm;
|
||||||
|
queryVariant: 'static' | 'dropdown';
|
||||||
|
panelType?: ITEMS;
|
||||||
|
};
|
393
frontend/src/container/QueryBuilder/components/Query/Query.tsx
Normal file
393
frontend/src/container/QueryBuilder/components/Query/Query.tsx
Normal file
@ -0,0 +1,393 @@
|
|||||||
|
import { Col, Input, Row } from 'antd';
|
||||||
|
// ** Constants
|
||||||
|
import {
|
||||||
|
initialAggregateAttribute,
|
||||||
|
initialQueryBuilderFormValues,
|
||||||
|
mapOfFilters,
|
||||||
|
mapOfOperators,
|
||||||
|
} from 'constants/queryBuilder';
|
||||||
|
// ** Components
|
||||||
|
import {
|
||||||
|
AdditionalFiltersToggler,
|
||||||
|
DataSourceDropdown,
|
||||||
|
FilterLabel,
|
||||||
|
ListItemWrapper,
|
||||||
|
ListMarker,
|
||||||
|
} from 'container/QueryBuilder/components';
|
||||||
|
import {
|
||||||
|
AggregatorFilter,
|
||||||
|
GroupByFilter,
|
||||||
|
HavingFilter,
|
||||||
|
OperatorsSelect,
|
||||||
|
ReduceToFilter,
|
||||||
|
} from 'container/QueryBuilder/filters';
|
||||||
|
import AggregateEveryFilter from 'container/QueryBuilder/filters/AggregateEveryFilter';
|
||||||
|
import LimitFilter from 'container/QueryBuilder/filters/LimitFilter/LimitFilter';
|
||||||
|
import { OrderByFilter } from 'container/QueryBuilder/filters/OrderByFilter';
|
||||||
|
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||||
|
import { useQueryBuilder } from 'hooks/useQueryBuilder';
|
||||||
|
import { findDataTypeOfOperator } from 'lib/query/findDataTypeOfOperator';
|
||||||
|
// ** Hooks
|
||||||
|
import React, { memo, useCallback, useMemo } from 'react';
|
||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import {
|
||||||
|
Having,
|
||||||
|
IBuilderQueryForm,
|
||||||
|
TagFilter,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||||
|
import { transformToUpperCase } from 'utils/transformToUpperCase';
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { QueryProps } from './Query.interfaces';
|
||||||
|
|
||||||
|
export const Query = memo(function Query({
|
||||||
|
index,
|
||||||
|
isAvailableToDisable,
|
||||||
|
queryVariant,
|
||||||
|
query,
|
||||||
|
panelType,
|
||||||
|
}: QueryProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
handleSetQueryData,
|
||||||
|
removeEntityByIndex,
|
||||||
|
initialDataSource,
|
||||||
|
} = useQueryBuilder();
|
||||||
|
|
||||||
|
const currentListOfOperators = useMemo(
|
||||||
|
() => mapOfOperators[query.dataSource],
|
||||||
|
[query],
|
||||||
|
);
|
||||||
|
const listOfAdditionalFilters = useMemo(() => mapOfFilters[query.dataSource], [
|
||||||
|
query,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleChangeOperator = useCallback(
|
||||||
|
(value: string): void => {
|
||||||
|
const aggregateDataType: BaseAutocompleteData['dataType'] =
|
||||||
|
query.aggregateAttribute.dataType;
|
||||||
|
|
||||||
|
const newQuery: IBuilderQueryForm = {
|
||||||
|
...query,
|
||||||
|
aggregateOperator: value,
|
||||||
|
having: [],
|
||||||
|
groupBy: [],
|
||||||
|
orderBy: [],
|
||||||
|
limit: null,
|
||||||
|
tagFilters: { items: [], op: 'AND' },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!aggregateDataType) {
|
||||||
|
handleSetQueryData(index, newQuery);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (aggregateDataType) {
|
||||||
|
case 'string':
|
||||||
|
case 'bool': {
|
||||||
|
const typeOfValue = findDataTypeOfOperator(value);
|
||||||
|
|
||||||
|
handleSetQueryData(index, {
|
||||||
|
...newQuery,
|
||||||
|
...(typeOfValue === 'number'
|
||||||
|
? { aggregateAttribute: initialAggregateAttribute }
|
||||||
|
: {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'float64':
|
||||||
|
case 'int64': {
|
||||||
|
handleSetQueryData(index, newQuery);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
handleSetQueryData(index, newQuery);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[index, query, handleSetQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeAggregatorAttribute = useCallback(
|
||||||
|
(value: BaseAutocompleteData): void => {
|
||||||
|
const newQuery: IBuilderQueryForm = {
|
||||||
|
...query,
|
||||||
|
aggregateAttribute: value,
|
||||||
|
having: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSetQueryData(index, newQuery);
|
||||||
|
},
|
||||||
|
[index, query, handleSetQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeDataSource = useCallback(
|
||||||
|
(nextSource: DataSource): void => {
|
||||||
|
let newQuery: IBuilderQueryForm = {
|
||||||
|
...query,
|
||||||
|
dataSource: nextSource,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (nextSource !== query.dataSource) {
|
||||||
|
const initCopy = {
|
||||||
|
...(initialQueryBuilderFormValues as Partial<IBuilderQueryForm>),
|
||||||
|
};
|
||||||
|
delete initCopy.queryName;
|
||||||
|
|
||||||
|
newQuery = {
|
||||||
|
...newQuery,
|
||||||
|
...initCopy,
|
||||||
|
dataSource: initialDataSource || nextSource,
|
||||||
|
aggregateOperator: mapOfOperators[nextSource][0],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSetQueryData(index, newQuery);
|
||||||
|
},
|
||||||
|
[index, query, initialDataSource, handleSetQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleToggleDisableQuery = useCallback((): void => {
|
||||||
|
const newQuery: IBuilderQueryForm = {
|
||||||
|
...query,
|
||||||
|
disabled: !query.disabled,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSetQueryData(index, newQuery);
|
||||||
|
}, [index, query, handleSetQueryData]);
|
||||||
|
|
||||||
|
const handleChangeGroupByKeys = useCallback(
|
||||||
|
(values: BaseAutocompleteData[]): void => {
|
||||||
|
const newQuery: IBuilderQueryForm = {
|
||||||
|
...query,
|
||||||
|
groupBy: values,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSetQueryData(index, newQuery);
|
||||||
|
},
|
||||||
|
[index, query, handleSetQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeQueryLegend = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
const newQuery: IBuilderQueryForm = {
|
||||||
|
...query,
|
||||||
|
legend: e.target.value,
|
||||||
|
};
|
||||||
|
handleSetQueryData(index, newQuery);
|
||||||
|
},
|
||||||
|
[index, query, handleSetQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeReduceTo = useCallback(
|
||||||
|
(value: string): void => {
|
||||||
|
const newQuery: IBuilderQueryForm = {
|
||||||
|
...query,
|
||||||
|
reduceTo: value,
|
||||||
|
};
|
||||||
|
handleSetQueryData(index, newQuery);
|
||||||
|
},
|
||||||
|
[index, query, handleSetQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeHavingFilter = useCallback(
|
||||||
|
(having: Having[]) => {
|
||||||
|
const newQuery: IBuilderQueryForm = { ...query, having };
|
||||||
|
|
||||||
|
handleSetQueryData(index, newQuery);
|
||||||
|
},
|
||||||
|
[index, query, handleSetQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeleteQuery = useCallback(() => {
|
||||||
|
removeEntityByIndex('queryData', index);
|
||||||
|
}, [removeEntityByIndex, index]);
|
||||||
|
|
||||||
|
const isMatricsDataSource = useMemo(
|
||||||
|
() => query.dataSource === DataSource.METRICS,
|
||||||
|
[query.dataSource],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeOrderByKeys = useCallback(
|
||||||
|
(values: BaseAutocompleteData[]): void => {
|
||||||
|
const newQuery: IBuilderQueryForm = {
|
||||||
|
...query,
|
||||||
|
orderBy: values,
|
||||||
|
};
|
||||||
|
handleSetQueryData(index, newQuery);
|
||||||
|
},
|
||||||
|
[handleSetQueryData, index, query],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeLimit = useCallback(
|
||||||
|
(value: number | null): void => {
|
||||||
|
const newQuery: IBuilderQueryForm = {
|
||||||
|
...query,
|
||||||
|
limit: value,
|
||||||
|
};
|
||||||
|
handleSetQueryData(index, newQuery);
|
||||||
|
},
|
||||||
|
[index, query, handleSetQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeAggregateEvery = useCallback(
|
||||||
|
(value: number): void => {
|
||||||
|
const newQuery: IBuilderQueryForm = {
|
||||||
|
...query,
|
||||||
|
stepInterval: value,
|
||||||
|
};
|
||||||
|
handleSetQueryData(index, newQuery);
|
||||||
|
},
|
||||||
|
[index, query, handleSetQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeTagFilters = useCallback(
|
||||||
|
(value: TagFilter): void => {
|
||||||
|
const newQuery: IBuilderQueryForm = {
|
||||||
|
...query,
|
||||||
|
tagFilters: value,
|
||||||
|
};
|
||||||
|
handleSetQueryData(index, newQuery);
|
||||||
|
},
|
||||||
|
[index, query, handleSetQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItemWrapper onDelete={handleDeleteQuery}>
|
||||||
|
<Col span={24}>
|
||||||
|
<Row align="middle">
|
||||||
|
<Col>
|
||||||
|
<ListMarker
|
||||||
|
isDisabled={query.disabled}
|
||||||
|
onDisable={handleToggleDisableQuery}
|
||||||
|
labelName={query.queryName}
|
||||||
|
index={index}
|
||||||
|
isAvailableToDisable={isAvailableToDisable}
|
||||||
|
/>
|
||||||
|
{queryVariant === 'dropdown' ? (
|
||||||
|
<DataSourceDropdown
|
||||||
|
onChange={handleChangeDataSource}
|
||||||
|
value={query.dataSource}
|
||||||
|
style={{ marginRight: '0.5rem' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FilterLabel label={transformToUpperCase(query.dataSource)} />
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
<Col flex="1">
|
||||||
|
<Row gutter={[11, 5]}>
|
||||||
|
{isMatricsDataSource && (
|
||||||
|
<Col>
|
||||||
|
<FilterLabel label="WHERE" />
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
<Col flex="1">
|
||||||
|
<QueryBuilderSearch query={query} onChange={handleChangeTagFilters} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
<Col span={11}>
|
||||||
|
<Row gutter={[11, 5]}>
|
||||||
|
<Col flex="5.93rem">
|
||||||
|
<OperatorsSelect
|
||||||
|
value={query.aggregateOperator || currentListOfOperators[0]}
|
||||||
|
onChange={handleChangeOperator}
|
||||||
|
operators={currentListOfOperators}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col flex="1 1 12.5rem">
|
||||||
|
<AggregatorFilter
|
||||||
|
onChange={handleChangeAggregatorAttribute}
|
||||||
|
query={query}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col span={11} offset={2}>
|
||||||
|
<Row gutter={[11, 5]}>
|
||||||
|
<Col flex="5.93rem">
|
||||||
|
<FilterLabel label={panelType === 'VALUE' ? 'Reduce to' : 'Group by'} />
|
||||||
|
</Col>
|
||||||
|
<Col flex="1 1 12.5rem">
|
||||||
|
{panelType === 'VALUE' ? (
|
||||||
|
<ReduceToFilter query={query} onChange={handleChangeReduceTo} />
|
||||||
|
) : (
|
||||||
|
<GroupByFilter query={query} onChange={handleChangeGroupByKeys} />
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
<Col span={24}>
|
||||||
|
<AdditionalFiltersToggler listOfAdditionalFilter={listOfAdditionalFilters}>
|
||||||
|
<Row gutter={[0, 11]} justify="space-between">
|
||||||
|
{!isMatricsDataSource && (
|
||||||
|
<Col span={11}>
|
||||||
|
<Row gutter={[11, 5]}>
|
||||||
|
<Col flex="5.93rem">
|
||||||
|
<FilterLabel label="Limit" />
|
||||||
|
</Col>
|
||||||
|
<Col flex="1 1 12.5rem">
|
||||||
|
<LimitFilter query={query} onChange={handleChangeLimit} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
{query.aggregateOperator !== StringOperators.NOOP && (
|
||||||
|
<Col span={11}>
|
||||||
|
<Row gutter={[11, 5]}>
|
||||||
|
<Col flex="5.93rem">
|
||||||
|
<FilterLabel label="HAVING" />
|
||||||
|
</Col>
|
||||||
|
<Col flex="1 1 12.5rem">
|
||||||
|
<HavingFilter onChange={handleChangeHavingFilter} query={query} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
{!isMatricsDataSource && (
|
||||||
|
<Col span={11}>
|
||||||
|
<Row gutter={[11, 5]}>
|
||||||
|
<Col flex="5.93rem">
|
||||||
|
<FilterLabel label="Order by" />
|
||||||
|
</Col>
|
||||||
|
<Col flex="1 1 12.5rem">
|
||||||
|
<OrderByFilter query={query} onChange={handleChangeOrderByKeys} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Col span={11}>
|
||||||
|
<Row gutter={[11, 5]}>
|
||||||
|
<Col flex="5.93rem">
|
||||||
|
<FilterLabel label="Aggregate Every" />
|
||||||
|
</Col>
|
||||||
|
<Col flex="1 1 6rem">
|
||||||
|
<AggregateEveryFilter
|
||||||
|
query={query}
|
||||||
|
onChange={handleChangeAggregateEvery}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</AdditionalFiltersToggler>
|
||||||
|
</Col>
|
||||||
|
<Row style={{ width: '100%' }}>
|
||||||
|
<Input
|
||||||
|
onChange={handleChangeQueryLegend}
|
||||||
|
size="middle"
|
||||||
|
value={query.legend}
|
||||||
|
addonBefore="Legend Format"
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
</ListItemWrapper>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1 @@
|
|||||||
|
export { Query } from './Query';
|
@ -1,17 +0,0 @@
|
|||||||
import { SelectProps } from 'antd';
|
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
|
||||||
|
|
||||||
type StaticLabel = { variant: 'static'; dataSource: DataSource };
|
|
||||||
|
|
||||||
export type DropdownLabel = {
|
|
||||||
variant: 'dropdown';
|
|
||||||
onChange: (value: DataSource) => void;
|
|
||||||
} & Omit<SelectProps, 'onChange'>;
|
|
||||||
|
|
||||||
export type QueryLabelProps = StaticLabel | DropdownLabel;
|
|
||||||
|
|
||||||
export function isLabelDropdown(
|
|
||||||
label: QueryLabelProps,
|
|
||||||
): label is DropdownLabel {
|
|
||||||
return label.variant === 'dropdown';
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
import { Select } from 'antd';
|
|
||||||
import styled, { css } from 'styled-components';
|
|
||||||
|
|
||||||
// ** Types
|
|
||||||
import { DropdownLabel } from './QueryLabel.interfaces';
|
|
||||||
|
|
||||||
const LabelStyle = css`
|
|
||||||
width: fit-content;
|
|
||||||
min-width: 5.75rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const StyledSingleLabel = styled(Select)`
|
|
||||||
pointer-events: none;
|
|
||||||
${LabelStyle}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const StyledDropdownLabel = styled(Select)<DropdownLabel>`
|
|
||||||
${LabelStyle}
|
|
||||||
`;
|
|
@ -1,49 +0,0 @@
|
|||||||
import { Select } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
|
||||||
import { SelectOption } from 'types/common/select';
|
|
||||||
|
|
||||||
// ** Types
|
|
||||||
import { isLabelDropdown, QueryLabelProps } from './QueryLabel.interfaces';
|
|
||||||
// ** Styles
|
|
||||||
import { StyledSingleLabel } from './QueryLabel.styled';
|
|
||||||
|
|
||||||
const { Option } = Select;
|
|
||||||
|
|
||||||
const dataSourceMap = [DataSource.LOGS, DataSource.METRICS, DataSource.TRACES];
|
|
||||||
|
|
||||||
export function QueryLabel(props: QueryLabelProps): JSX.Element {
|
|
||||||
const isDropdown = isLabelDropdown(props);
|
|
||||||
|
|
||||||
if (!isDropdown) {
|
|
||||||
const { dataSource } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledSingleLabel
|
|
||||||
defaultValue={dataSource}
|
|
||||||
showArrow={false}
|
|
||||||
dropdownStyle={{ display: 'none' }}
|
|
||||||
>
|
|
||||||
<Option value={dataSource}>{dataSource}</Option>
|
|
||||||
</StyledSingleLabel>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { onChange } = props;
|
|
||||||
|
|
||||||
const dataSourceOptions: SelectOption<
|
|
||||||
DataSource,
|
|
||||||
string
|
|
||||||
>[] = dataSourceMap.map((source) => ({
|
|
||||||
label: source.charAt(0).toUpperCase() + source.slice(1),
|
|
||||||
value: source,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
defaultValue={dataSourceOptions[0].value}
|
|
||||||
options={dataSourceOptions}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
export { QueryLabel } from './QueryLabel';
|
|
@ -1,2 +1,7 @@
|
|||||||
|
export { AdditionalFiltersToggler } from './AdditionalFiltersToggler';
|
||||||
|
export { DataSourceDropdown } from './DataSourceDropdown';
|
||||||
|
export { FilterLabel } from './FilterLabel';
|
||||||
|
export { Formula } from './Formula';
|
||||||
|
export { ListItemWrapper } from './ListItemWrapper';
|
||||||
export { ListMarker } from './ListMarker';
|
export { ListMarker } from './ListMarker';
|
||||||
export { QueryLabel } from './QueryLabel';
|
export { Query } from './Query';
|
||||||
|
@ -0,0 +1,62 @@
|
|||||||
|
import { Input } from 'antd';
|
||||||
|
import getStep from 'lib/getStep';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
|
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||||
|
|
||||||
|
function AggregateEveryFilter({
|
||||||
|
onChange,
|
||||||
|
query,
|
||||||
|
}: AggregateEveryFilterProps): JSX.Element {
|
||||||
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
const stepInterval = useMemo(
|
||||||
|
() =>
|
||||||
|
getStep({
|
||||||
|
start: minTime,
|
||||||
|
end: maxTime,
|
||||||
|
inputFormat: 'ns',
|
||||||
|
}),
|
||||||
|
[maxTime, minTime],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleKeyDown = (event: {
|
||||||
|
keyCode: number;
|
||||||
|
which: number;
|
||||||
|
preventDefault: () => void;
|
||||||
|
}): void => {
|
||||||
|
const keyCode = event.keyCode || event.which;
|
||||||
|
const isBackspace = keyCode === 8;
|
||||||
|
const isNumeric =
|
||||||
|
(keyCode >= 48 && keyCode <= 57) || (keyCode >= 96 && keyCode <= 105);
|
||||||
|
|
||||||
|
if (!isNumeric && !isBackspace) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter in seconds"
|
||||||
|
disabled={!query.aggregateAttribute.key}
|
||||||
|
style={selectStyle}
|
||||||
|
defaultValue={stepInterval ?? query.stepInterval}
|
||||||
|
onChange={(event): void => onChange(Number(event.target.value))}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AggregateEveryFilterProps {
|
||||||
|
onChange: (values: number) => void;
|
||||||
|
query: IBuilderQueryForm;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AggregateEveryFilter;
|
@ -0,0 +1,7 @@
|
|||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
export type AgregatorFilterProps = {
|
||||||
|
onChange: (value: BaseAutocompleteData) => void;
|
||||||
|
query: IBuilderQueryForm;
|
||||||
|
};
|
@ -0,0 +1,83 @@
|
|||||||
|
// ** Components
|
||||||
|
import { AutoComplete, Spin } from 'antd';
|
||||||
|
// ** Api
|
||||||
|
import { getAggregateAttribute } from 'api/queryBuilder/getAggregateAttribute';
|
||||||
|
import { initialAggregateAttribute } from 'constants/queryBuilder';
|
||||||
|
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
|
||||||
|
import React, { memo, useMemo, useState } from 'react';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
import { SelectOption } from 'types/common/select';
|
||||||
|
import { transformToUpperCase } from 'utils/transformToUpperCase';
|
||||||
|
|
||||||
|
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||||
|
// ** Types
|
||||||
|
import { AgregatorFilterProps } from './AggregatorFilter.intefaces';
|
||||||
|
|
||||||
|
export const AggregatorFilter = memo(function AggregatorFilter({
|
||||||
|
onChange,
|
||||||
|
query,
|
||||||
|
}: AgregatorFilterProps): JSX.Element {
|
||||||
|
const [searchText, setSearchText] = useState<string>('');
|
||||||
|
|
||||||
|
const { data, isFetching } = useQuery(
|
||||||
|
[
|
||||||
|
'GET_AGGREGATE_ATTRIBUTE',
|
||||||
|
searchText,
|
||||||
|
query.aggregateOperator,
|
||||||
|
query.dataSource,
|
||||||
|
],
|
||||||
|
async () =>
|
||||||
|
getAggregateAttribute({
|
||||||
|
searchText,
|
||||||
|
aggregateOperator: query.aggregateOperator,
|
||||||
|
dataSource: query.dataSource,
|
||||||
|
}),
|
||||||
|
{ enabled: !!query.aggregateOperator && !!query.dataSource },
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSearchAttribute = (searchText: string): void =>
|
||||||
|
setSearchText(searchText);
|
||||||
|
|
||||||
|
const optionsData: SelectOption<string, string>[] =
|
||||||
|
data?.payload?.attributeKeys?.map((item) => ({
|
||||||
|
label: transformStringWithPrefix({
|
||||||
|
str: item.key,
|
||||||
|
prefix: item.type || '',
|
||||||
|
condition: !item.isColumn,
|
||||||
|
}),
|
||||||
|
value: item.key,
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
const handleChangeAttribute = (value: string): void => {
|
||||||
|
const currentAttributeObj = data?.payload?.attributeKeys?.find(
|
||||||
|
(item) => item.key === value,
|
||||||
|
) || { ...initialAggregateAttribute, key: value };
|
||||||
|
|
||||||
|
onChange(currentAttributeObj);
|
||||||
|
};
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() =>
|
||||||
|
transformStringWithPrefix({
|
||||||
|
str: query.aggregateAttribute.key,
|
||||||
|
prefix: query.aggregateAttribute.type || '',
|
||||||
|
condition: !query.aggregateAttribute.isColumn,
|
||||||
|
}),
|
||||||
|
[query],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AutoComplete
|
||||||
|
showSearch
|
||||||
|
placeholder={`${transformToUpperCase(query.dataSource)} name`}
|
||||||
|
style={selectStyle}
|
||||||
|
showArrow={false}
|
||||||
|
filterOption={false}
|
||||||
|
onSearch={handleSearchAttribute}
|
||||||
|
notFoundContent={isFetching ? <Spin size="small" /> : null}
|
||||||
|
options={optionsData}
|
||||||
|
value={value}
|
||||||
|
onChange={handleChangeAttribute}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1 @@
|
|||||||
|
export { AggregatorFilter } from './AggregatorFilter';
|
@ -0,0 +1,15 @@
|
|||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
export type GroupByFilterProps = {
|
||||||
|
query: IBuilderQueryForm;
|
||||||
|
onChange: (values: BaseAutocompleteData[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GroupByFilterValue = {
|
||||||
|
disabled: boolean | undefined;
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
title: string | undefined;
|
||||||
|
value: string;
|
||||||
|
};
|
@ -0,0 +1,107 @@
|
|||||||
|
import { Select, Spin } from 'antd';
|
||||||
|
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
||||||
|
// ** Constants
|
||||||
|
import { QueryBuilderKeys } from 'constants/queryBuilder';
|
||||||
|
// ** Components
|
||||||
|
// ** Helpers
|
||||||
|
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
|
||||||
|
import React, { memo, useMemo, useState } from 'react';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { MetricAggregateOperator } from 'types/common/queryBuilder';
|
||||||
|
import { SelectOption } from 'types/common/select';
|
||||||
|
|
||||||
|
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||||
|
import {
|
||||||
|
GroupByFilterProps,
|
||||||
|
GroupByFilterValue,
|
||||||
|
} from './GroupByFilter.interfaces';
|
||||||
|
|
||||||
|
export const GroupByFilter = memo(function GroupByFilter({
|
||||||
|
query,
|
||||||
|
onChange,
|
||||||
|
}: GroupByFilterProps): JSX.Element {
|
||||||
|
const [searchText, setSearchText] = useState<string>('');
|
||||||
|
|
||||||
|
const { data, isFetching } = useQuery(
|
||||||
|
[QueryBuilderKeys.GET_AGGREGATE_KEYS, searchText],
|
||||||
|
async () =>
|
||||||
|
getAggregateKeys({
|
||||||
|
aggregateAttribute: query.aggregateAttribute.key,
|
||||||
|
tagType: query.aggregateAttribute.type,
|
||||||
|
dataSource: query.dataSource,
|
||||||
|
aggregateOperator: query.aggregateOperator,
|
||||||
|
searchText,
|
||||||
|
}),
|
||||||
|
{ enabled: !!query.aggregateAttribute.key, keepPreviousData: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSearchKeys = (searchText: string): void => {
|
||||||
|
setSearchText(searchText);
|
||||||
|
};
|
||||||
|
|
||||||
|
const optionsData: SelectOption<string, string>[] =
|
||||||
|
data?.payload?.attributeKeys?.map((item) => ({
|
||||||
|
label: transformStringWithPrefix({
|
||||||
|
str: item.key,
|
||||||
|
prefix: item.type || '',
|
||||||
|
condition: !item.isColumn,
|
||||||
|
}),
|
||||||
|
value: item.key,
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
const handleChange = (values: GroupByFilterValue[]): void => {
|
||||||
|
const groupByValues: BaseAutocompleteData[] = values.map((item) => {
|
||||||
|
const iterationArray = data?.payload?.attributeKeys || query.groupBy;
|
||||||
|
const existGroup = iterationArray.find((group) => group.key === item.value);
|
||||||
|
if (existGroup) {
|
||||||
|
return existGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isColumn: null,
|
||||||
|
key: item.value,
|
||||||
|
dataType: null,
|
||||||
|
type: null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
onChange(groupByValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
const values: GroupByFilterValue[] = query.groupBy.map((item) => ({
|
||||||
|
label: transformStringWithPrefix({
|
||||||
|
str: item.key,
|
||||||
|
prefix: item.type || '',
|
||||||
|
condition: !item.isColumn,
|
||||||
|
}),
|
||||||
|
key: item.key,
|
||||||
|
value: item.key,
|
||||||
|
disabled: undefined,
|
||||||
|
title: undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const isDisabledSelect = useMemo(
|
||||||
|
() =>
|
||||||
|
!query.aggregateAttribute.key ||
|
||||||
|
query.aggregateOperator === MetricAggregateOperator.NOOP,
|
||||||
|
[query.aggregateAttribute.key, query.aggregateOperator],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
mode="tags"
|
||||||
|
style={selectStyle}
|
||||||
|
onSearch={handleSearchKeys}
|
||||||
|
showSearch
|
||||||
|
disabled={isDisabledSelect}
|
||||||
|
showArrow={false}
|
||||||
|
filterOption={false}
|
||||||
|
options={optionsData}
|
||||||
|
labelInValue
|
||||||
|
value={values}
|
||||||
|
notFoundContent={isFetching ? <Spin size="small" /> : null}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1 @@
|
|||||||
|
export { GroupByFilter } from './GroupByFilter';
|
@ -0,0 +1,9 @@
|
|||||||
|
import {
|
||||||
|
Having,
|
||||||
|
IBuilderQueryForm,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
export type HavingFilterProps = {
|
||||||
|
query: IBuilderQueryForm;
|
||||||
|
onChange: (having: Having[]) => void;
|
||||||
|
};
|
@ -0,0 +1,196 @@
|
|||||||
|
import { Select } from 'antd';
|
||||||
|
// ** Constants
|
||||||
|
import { HAVING_OPERATORS, initialHavingValues } from 'constants/queryBuilder';
|
||||||
|
// ** Hooks
|
||||||
|
import { useTagValidation } from 'hooks/queryBuilder/useTagValidation';
|
||||||
|
import {
|
||||||
|
transformFromStringToHaving,
|
||||||
|
transformHavingToStringValue,
|
||||||
|
} from 'lib/query/transformQueryBuilderData';
|
||||||
|
// ** Helpers
|
||||||
|
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
|
||||||
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { Having } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { SelectOption } from 'types/common/select';
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { HavingFilterProps } from './HavingFilter.interfaces';
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
export function HavingFilter({
|
||||||
|
query,
|
||||||
|
onChange,
|
||||||
|
}: HavingFilterProps): JSX.Element {
|
||||||
|
const { having } = query;
|
||||||
|
const [searchText, setSearchText] = useState<string>('');
|
||||||
|
const [options, setOptions] = useState<SelectOption<string, string>[]>([]);
|
||||||
|
const [localValues, setLocalValues] = useState<string[]>([]);
|
||||||
|
const [currentFormValue, setCurrentFormValue] = useState<Having>(
|
||||||
|
initialHavingValues,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { isMulti } = useTagValidation(
|
||||||
|
searchText,
|
||||||
|
currentFormValue.op,
|
||||||
|
currentFormValue.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
const aggregatorAttribute = useMemo(
|
||||||
|
() =>
|
||||||
|
transformStringWithPrefix({
|
||||||
|
str: query.aggregateAttribute.key,
|
||||||
|
prefix: query.aggregateAttribute.type || '',
|
||||||
|
condition: !query.aggregateAttribute.isColumn,
|
||||||
|
}),
|
||||||
|
[query],
|
||||||
|
);
|
||||||
|
|
||||||
|
const columnName = useMemo(
|
||||||
|
() => `${query.aggregateOperator.toUpperCase()}(${aggregatorAttribute})`,
|
||||||
|
[query, aggregatorAttribute],
|
||||||
|
);
|
||||||
|
|
||||||
|
const aggregatorOptions: SelectOption<string, string>[] = useMemo(
|
||||||
|
() => [{ label: columnName, value: columnName }],
|
||||||
|
[columnName],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getHavingObject = useCallback((currentSearch: string): Having => {
|
||||||
|
const textArr = currentSearch.split(' ');
|
||||||
|
const [columnName = '', op = '', ...value] = textArr;
|
||||||
|
|
||||||
|
return { columnName, op, value };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const generateOptions = useCallback(
|
||||||
|
(search: string): void => {
|
||||||
|
const [aggregator = '', op = '', ...restValue] = search.split(' ');
|
||||||
|
let newOptions: SelectOption<string, string>[] = [];
|
||||||
|
|
||||||
|
const isAggregatorExist = columnName
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(search.toLowerCase());
|
||||||
|
|
||||||
|
const isAggregatorChosen = aggregator === columnName;
|
||||||
|
|
||||||
|
if (isAggregatorExist || aggregator === '') {
|
||||||
|
newOptions = aggregatorOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((isAggregatorChosen && op === '') || op) {
|
||||||
|
const filteredOperators = HAVING_OPERATORS.filter((num) =>
|
||||||
|
num.toLowerCase().includes(op.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
|
newOptions = filteredOperators.map((opt) => ({
|
||||||
|
label: `${columnName} ${opt} ${restValue && restValue.join(' ')}`,
|
||||||
|
value: `${columnName} ${opt} ${restValue && restValue.join(' ')}`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
setOptions(newOptions);
|
||||||
|
},
|
||||||
|
[columnName, aggregatorOptions],
|
||||||
|
);
|
||||||
|
|
||||||
|
const isValidHavingValue = (search: string): boolean => {
|
||||||
|
const values = getHavingObject(search).value.join(' ');
|
||||||
|
if (values) {
|
||||||
|
const numRegexp = /^[^a-zA-Z]*$/;
|
||||||
|
|
||||||
|
return numRegexp.test(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = (search: string): void => {
|
||||||
|
const trimmedSearch = search.replace(/\s\s+/g, ' ').trimStart();
|
||||||
|
|
||||||
|
const currentSearch = isMulti
|
||||||
|
? trimmedSearch
|
||||||
|
: trimmedSearch.split(' ').slice(0, 3).join(' ');
|
||||||
|
const isValidSearch = isValidHavingValue(currentSearch);
|
||||||
|
|
||||||
|
if (isValidSearch) {
|
||||||
|
setSearchText(currentSearch);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetChanges = (): void => {
|
||||||
|
handleSearch('');
|
||||||
|
setCurrentFormValue(initialHavingValues);
|
||||||
|
setOptions(aggregatorOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (values: string[]): void => {
|
||||||
|
const having: Having[] = values.map(transformFromStringToHaving);
|
||||||
|
|
||||||
|
const isSelectable: boolean =
|
||||||
|
currentFormValue.value.length > 0 &&
|
||||||
|
currentFormValue.value.every((value) => !!value);
|
||||||
|
|
||||||
|
if (isSelectable) {
|
||||||
|
onChange(having);
|
||||||
|
resetChanges();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = (currentValue: string): void => {
|
||||||
|
const { columnName, op, value } = getHavingObject(currentValue);
|
||||||
|
|
||||||
|
const isCompletedValue = value.every((item) => !!item);
|
||||||
|
|
||||||
|
const isClearSearch = isCompletedValue && columnName && op;
|
||||||
|
|
||||||
|
handleSearch(isClearSearch ? '' : currentValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseSearchText = useCallback(
|
||||||
|
(text: string) => {
|
||||||
|
const { columnName, op, value } = getHavingObject(text);
|
||||||
|
setCurrentFormValue({ columnName, op, value });
|
||||||
|
|
||||||
|
generateOptions(text);
|
||||||
|
},
|
||||||
|
[generateOptions, getHavingObject],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeselect = (value: string): void => {
|
||||||
|
const result = localValues.filter((item) => item !== value);
|
||||||
|
setLocalValues(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
parseSearchText(searchText);
|
||||||
|
}, [searchText, parseSearchText]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLocalValues(transformHavingToStringValue(having));
|
||||||
|
}, [having]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
onSearch={handleSearch}
|
||||||
|
searchValue={searchText}
|
||||||
|
value={localValues}
|
||||||
|
data-testid="havingSelect"
|
||||||
|
disabled={!query.aggregateAttribute.key}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
notFoundContent={currentFormValue.value.length === 0 ? undefined : null}
|
||||||
|
placeholder="Count(operation) > 5"
|
||||||
|
onDeselect={handleDeselect}
|
||||||
|
onBlur={resetChanges}
|
||||||
|
onChange={handleChange}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
>
|
||||||
|
{options.map((opt) => (
|
||||||
|
<Option key={opt.value} value={opt.value} title="havingOption">
|
||||||
|
{opt.label}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,166 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
// Constants
|
||||||
|
import {
|
||||||
|
HAVING_OPERATORS,
|
||||||
|
initialQueryBuilderFormValues,
|
||||||
|
} from 'constants/queryBuilder';
|
||||||
|
import { transformFromStringToHaving } from 'lib/query/transformQueryBuilderData';
|
||||||
|
import React from 'react';
|
||||||
|
// ** Types
|
||||||
|
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
// ** Components
|
||||||
|
import { HavingFilter } from '../HavingFilter';
|
||||||
|
|
||||||
|
const valueWithAttributeAndOperator: IBuilderQueryForm = {
|
||||||
|
...initialQueryBuilderFormValues,
|
||||||
|
dataSource: DataSource.LOGS,
|
||||||
|
aggregateOperator: 'SUM',
|
||||||
|
aggregateAttribute: {
|
||||||
|
isColumn: false,
|
||||||
|
key: 'bytes',
|
||||||
|
type: 'tag',
|
||||||
|
dataType: 'float64',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Having filter behaviour', () => {
|
||||||
|
test('Having filter render is rendered', () => {
|
||||||
|
const mockFn = jest.fn();
|
||||||
|
const { unmount } = render(
|
||||||
|
<HavingFilter query={initialQueryBuilderFormValues} onChange={mockFn} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectId = 'havingSelect';
|
||||||
|
|
||||||
|
const select = screen.getByTestId(selectId);
|
||||||
|
|
||||||
|
expect(select).toBeInTheDocument();
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Having render is disabled initially', () => {
|
||||||
|
const mockFn = jest.fn();
|
||||||
|
const { unmount } = render(
|
||||||
|
<HavingFilter query={initialQueryBuilderFormValues} onChange={mockFn} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = screen.getByRole('combobox');
|
||||||
|
|
||||||
|
expect(input).toBeDisabled();
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Is having filter is enable', () => {
|
||||||
|
const mockFn = jest.fn();
|
||||||
|
const { unmount } = render(
|
||||||
|
<HavingFilter query={valueWithAttributeAndOperator} onChange={mockFn} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = screen.getByRole('combobox');
|
||||||
|
|
||||||
|
expect(input).toBeEnabled();
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Autocomplete in the having filter', async () => {
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
const constructedAttribute = 'SUM(tag_bytes)';
|
||||||
|
const optionTestTitle = 'havingOption';
|
||||||
|
|
||||||
|
const { unmount } = render(
|
||||||
|
<HavingFilter query={valueWithAttributeAndOperator} onChange={onChange} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
// get input
|
||||||
|
const input = screen.getByRole('combobox');
|
||||||
|
|
||||||
|
// click on the select
|
||||||
|
await user.click(input);
|
||||||
|
|
||||||
|
// show predefined options for operator with attribute SUM(tag_bytes)
|
||||||
|
const option = screen.getByTitle(optionTestTitle);
|
||||||
|
|
||||||
|
expect(option).toBeInTheDocument();
|
||||||
|
|
||||||
|
await user.click(option);
|
||||||
|
|
||||||
|
// autocomplete input
|
||||||
|
expect(input).toHaveValue(constructedAttribute);
|
||||||
|
|
||||||
|
// clear value from input and write from keyboard
|
||||||
|
await user.clear(input);
|
||||||
|
|
||||||
|
await user.type(input, 'bytes');
|
||||||
|
|
||||||
|
// show again predefined options for operator with attribute SUM(tag_bytes)
|
||||||
|
const sameAttributeOption = screen.getByTitle(optionTestTitle);
|
||||||
|
|
||||||
|
expect(sameAttributeOption).toBeInTheDocument();
|
||||||
|
|
||||||
|
await user.click(sameAttributeOption);
|
||||||
|
|
||||||
|
expect(input).toHaveValue(constructedAttribute);
|
||||||
|
|
||||||
|
// show operators after SUM(tag_bytes)
|
||||||
|
const operatorsOptions = screen.getAllByTitle(optionTestTitle);
|
||||||
|
|
||||||
|
expect(operatorsOptions.length).toEqual(HAVING_OPERATORS.length);
|
||||||
|
|
||||||
|
// show operators after SUM(tag_bytes) when type from keyboard
|
||||||
|
await user.clear(input);
|
||||||
|
|
||||||
|
await user.type(input, `${constructedAttribute} !=`);
|
||||||
|
|
||||||
|
// get filtered operators
|
||||||
|
const filteredOperators = screen.getAllByTitle(optionTestTitle);
|
||||||
|
|
||||||
|
expect(filteredOperators.length).toEqual(1);
|
||||||
|
|
||||||
|
// clear and show again all operators
|
||||||
|
await user.clear(input);
|
||||||
|
await user.type(input, constructedAttribute);
|
||||||
|
|
||||||
|
const returnedOptions = screen.getAllByTitle(optionTestTitle);
|
||||||
|
|
||||||
|
expect(returnedOptions.length).toEqual(HAVING_OPERATORS.length);
|
||||||
|
|
||||||
|
// check write value after operator
|
||||||
|
await user.clear(input);
|
||||||
|
await user.type(input, `${constructedAttribute} != 123`);
|
||||||
|
|
||||||
|
expect(input).toHaveValue(`${constructedAttribute} != 123`);
|
||||||
|
|
||||||
|
const optionWithValue = screen.getByTitle(optionTestTitle);
|
||||||
|
|
||||||
|
// onChange after complete writting in the input or autocomplete
|
||||||
|
await user.click(optionWithValue);
|
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onChange).toHaveBeenCalledWith([
|
||||||
|
transformFromStringToHaving(`${constructedAttribute} != 123`),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// onChange with multiple operator
|
||||||
|
await user.type(input, `${constructedAttribute} IN 123 123`);
|
||||||
|
|
||||||
|
expect(input).toHaveValue(`${constructedAttribute} IN 123 123`);
|
||||||
|
|
||||||
|
const optionWithMultipleValue = screen.getByTitle(optionTestTitle);
|
||||||
|
await user.click(optionWithMultipleValue);
|
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledTimes(2);
|
||||||
|
expect(onChange).toHaveBeenCalledWith([
|
||||||
|
transformFromStringToHaving(`${constructedAttribute} IN 123 123`),
|
||||||
|
]);
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1 @@
|
|||||||
|
export { HavingFilter } from './HavingFilter';
|
@ -0,0 +1,28 @@
|
|||||||
|
import { InputNumber } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||||
|
|
||||||
|
function LimitFilter({ onChange, query }: LimitFilterProps): JSX.Element {
|
||||||
|
const onChangeHandler = (value: number | null): void => {
|
||||||
|
onChange(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputNumber
|
||||||
|
min={1}
|
||||||
|
type="number"
|
||||||
|
disabled={!query.aggregateAttribute.key}
|
||||||
|
style={selectStyle}
|
||||||
|
onChange={onChangeHandler}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LimitFilterProps {
|
||||||
|
onChange: (values: number | null) => void;
|
||||||
|
query: IBuilderQueryForm;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LimitFilter;
|
@ -0,0 +1,7 @@
|
|||||||
|
import { SelectProps } from 'antd';
|
||||||
|
|
||||||
|
export type OperatorsSelectProps = Omit<SelectProps, 'onChange' | 'value'> & {
|
||||||
|
operators: string[];
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
value: string;
|
||||||
|
};
|
@ -0,0 +1,34 @@
|
|||||||
|
import { Select } from 'antd';
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
// ** Types
|
||||||
|
import { SelectOption } from 'types/common/select';
|
||||||
|
// ** Helpers
|
||||||
|
import { transformToUpperCase } from 'utils/transformToUpperCase';
|
||||||
|
|
||||||
|
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||||
|
import { OperatorsSelectProps } from './OperatorsSelect.interfaces';
|
||||||
|
|
||||||
|
export const OperatorsSelect = memo(function OperatorsSelect({
|
||||||
|
operators,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
...props
|
||||||
|
}: OperatorsSelectProps): JSX.Element {
|
||||||
|
const operatorsOptions: SelectOption<string, string>[] = operators.map(
|
||||||
|
(operator) => ({
|
||||||
|
label: transformToUpperCase(operator),
|
||||||
|
value: operator,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
options={operatorsOptions}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
style={selectStyle}
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1 @@
|
|||||||
|
export { OperatorsSelect } from './OperatorsSelect';
|
@ -0,0 +1,15 @@
|
|||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
export type OrderByFilterProps = {
|
||||||
|
query: IBuilderQueryForm;
|
||||||
|
onChange: (values: BaseAutocompleteData[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OrderByFilterValue = {
|
||||||
|
disabled: boolean | undefined;
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
title: string | undefined;
|
||||||
|
value: string;
|
||||||
|
};
|
@ -0,0 +1,141 @@
|
|||||||
|
import { Select, Spin } from 'antd';
|
||||||
|
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
||||||
|
import { QueryBuilderKeys } from 'constants/queryBuilder';
|
||||||
|
import { IOption } from 'hooks/useResourceAttribute/types';
|
||||||
|
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
|
||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { MetricAggregateOperator } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||||
|
import {
|
||||||
|
OrderByFilterProps,
|
||||||
|
OrderByFilterValue,
|
||||||
|
} from './OrderByFilter.interfaces';
|
||||||
|
import { getLabelFromValue, mapLabelValuePairs } from './utils';
|
||||||
|
|
||||||
|
export function OrderByFilter({
|
||||||
|
query,
|
||||||
|
onChange,
|
||||||
|
}: OrderByFilterProps): JSX.Element {
|
||||||
|
const [searchText, setSearchText] = useState<string>('');
|
||||||
|
const [selectedValue, setSelectedValue] = useState<OrderByFilterValue[]>([]);
|
||||||
|
|
||||||
|
const { data, isFetching } = useQuery(
|
||||||
|
[QueryBuilderKeys.GET_AGGREGATE_KEYS, searchText],
|
||||||
|
async () =>
|
||||||
|
getAggregateKeys({
|
||||||
|
aggregateAttribute: query.aggregateAttribute.key,
|
||||||
|
tagType: query.aggregateAttribute.type,
|
||||||
|
dataSource: query.dataSource,
|
||||||
|
aggregateOperator: query.aggregateOperator,
|
||||||
|
searchText,
|
||||||
|
}),
|
||||||
|
{ enabled: !!query.aggregateAttribute.key, keepPreviousData: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSearchKeys = useCallback(
|
||||||
|
(searchText: string): void => setSearchText(searchText),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const generateOptionsData = (
|
||||||
|
attributeKeys: BaseAutocompleteData[] | undefined,
|
||||||
|
selectedValue: OrderByFilterValue[],
|
||||||
|
query: IBuilderQueryForm,
|
||||||
|
): IOption[] => {
|
||||||
|
const selectedValueLabels = getLabelFromValue(selectedValue);
|
||||||
|
|
||||||
|
const noAggregationOptions = attributeKeys
|
||||||
|
? mapLabelValuePairs(attributeKeys)
|
||||||
|
.flat()
|
||||||
|
.filter(
|
||||||
|
(option) => !selectedValueLabels.includes(option.label.split(' ')[0]),
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const aggregationOptions = mapLabelValuePairs(query.groupBy)
|
||||||
|
.flat()
|
||||||
|
.concat([
|
||||||
|
{
|
||||||
|
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) asc`,
|
||||||
|
value: `${query.aggregateOperator}(${query.aggregateAttribute.key}) asc`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) desc`,
|
||||||
|
value: `${query.aggregateOperator}(${query.aggregateAttribute.key}) desc`,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.filter(
|
||||||
|
(option) => !selectedValueLabels.includes(option.label.split(' ')[0]),
|
||||||
|
);
|
||||||
|
|
||||||
|
return query.aggregateOperator === MetricAggregateOperator.NOOP
|
||||||
|
? noAggregationOptions
|
||||||
|
: aggregationOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
const optionsData = useMemo(
|
||||||
|
() => generateOptionsData(data?.payload?.attributeKeys, selectedValue, query),
|
||||||
|
[data?.payload?.attributeKeys, query, selectedValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = (values: OrderByFilterValue[]): void => {
|
||||||
|
setSelectedValue(values);
|
||||||
|
const orderByValues: BaseAutocompleteData[] = values?.map((item) => {
|
||||||
|
const iterationArray = data?.payload?.attributeKeys || query.orderBy;
|
||||||
|
const existingOrderValues = iterationArray.find(
|
||||||
|
(group) => group.key === item.value,
|
||||||
|
);
|
||||||
|
if (existingOrderValues) {
|
||||||
|
return existingOrderValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isColumn: null,
|
||||||
|
key: item.value,
|
||||||
|
dataType: null,
|
||||||
|
type: null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
onChange(orderByValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
const values: OrderByFilterValue[] = query.orderBy.map((item) => ({
|
||||||
|
label: transformStringWithPrefix({
|
||||||
|
str: item.key,
|
||||||
|
prefix: item.type || '',
|
||||||
|
condition: !item.isColumn,
|
||||||
|
}),
|
||||||
|
key: item.key,
|
||||||
|
value: item.key,
|
||||||
|
disabled: undefined,
|
||||||
|
title: undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const isDisabledSelect = useMemo(
|
||||||
|
() =>
|
||||||
|
!query.aggregateAttribute.key ||
|
||||||
|
query.aggregateOperator === MetricAggregateOperator.NOOP,
|
||||||
|
[query.aggregateAttribute.key, query.aggregateOperator],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
mode="tags"
|
||||||
|
style={selectStyle}
|
||||||
|
onSearch={handleSearchKeys}
|
||||||
|
showSearch
|
||||||
|
disabled={isDisabledSelect}
|
||||||
|
showArrow={false}
|
||||||
|
filterOption={false}
|
||||||
|
options={optionsData}
|
||||||
|
labelInValue
|
||||||
|
value={values}
|
||||||
|
notFoundContent={isFetching ? <Spin size="small" /> : null}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { OrderByFilter } from './OrderByFilter';
|
@ -0,0 +1,32 @@
|
|||||||
|
import { IOption } from 'hooks/useResourceAttribute/types';
|
||||||
|
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
|
||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
|
||||||
|
import { OrderByFilterValue } from './OrderByFilter.interfaces';
|
||||||
|
|
||||||
|
export function mapLabelValuePairs(
|
||||||
|
arr: BaseAutocompleteData[],
|
||||||
|
): Array<IOption>[] {
|
||||||
|
return arr.map((item) => {
|
||||||
|
const label = transformStringWithPrefix({
|
||||||
|
str: item.key,
|
||||||
|
prefix: item.type || '',
|
||||||
|
condition: !item.isColumn,
|
||||||
|
});
|
||||||
|
const value = item.key;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: `${label} asc`,
|
||||||
|
value: `${value} asc`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `${label} desc`,
|
||||||
|
value: `${value} desc`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLabelFromValue(arr: OrderByFilterValue[]): string[] {
|
||||||
|
return arr.map((value) => value.label.split(' ')[0]);
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export const selectStyle = { width: '100%' };
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user