Merge pull request #2955 from SigNoz/release/v0.21.0

Release/v0.21.0
This commit is contained in:
Ankit Nayan 2023-06-22 00:56:11 +05:30 committed by GitHub
commit 2ee7817685
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
158 changed files with 4290 additions and 1406 deletions

View File

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install dependencies
run: cd frontend && yarn install
- name: Run ESLint
@ -31,7 +31,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Run tests
shell: bash
run: |
@ -45,7 +45,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Build EE query-service image
shell: bash
run: |

View File

@ -39,11 +39,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -54,7 +54,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -68,4 +68,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2

View File

@ -12,11 +12,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Codebase
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: signoz/gh-bot
- name: Use Node v16
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: 16
- name: Setup Cache & Install Dependencies

View File

@ -13,7 +13,7 @@ jobs:
DOCKER_TAG: pull-${{ github.event.number }}
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Build query-service image
env:
@ -69,12 +69,14 @@ jobs:
--restart='OnFailure' -i --rm --command -- curl -X POST -F \
'locust_count=6' -F 'hatch_rate=2' http://locust-master:8089/swarm
- name: Get short commit SHA and display tunnel URL
- name: Get short commit SHA, display tunnel URL and IP Address of the worker node
id: get-subdomain
run: |
subdomain="pr-$(git rev-parse --short HEAD)"
echo "URL for tunnelling: https://$subdomain.loca.lt"
echo "::set-output name=subdomain::$subdomain"
echo "subdomain=$subdomain" >> $GITHUB_OUTPUT
worker_ip="$(curl -4 -s ipconfig.io/ip)"
echo "Worker node IP address: $worker_ip"
- name: Start tunnel
env:

View File

@ -9,8 +9,8 @@ jobs:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "16.x"
- name: Install dependencies

View File

@ -14,6 +14,6 @@ jobs:
name: Ensure Pull Request has a linked issue.
steps:
- name: Verify Linked Issue
uses: srikanthccv/verify-linked-issue-action@v0.70
uses: srikanthccv/verify-linked-issue-action@v0.71
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -14,19 +14,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2
with:
version: latest
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: benjlevesque/short-sha@v1.2
- uses: benjlevesque/short-sha@v2.2
id: short-sha
- name: Get branch name
id: branch-name
@ -49,19 +49,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2
with:
version: latest
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: benjlevesque/short-sha@v1.2
- uses: benjlevesque/short-sha@v2.2
id: short-sha
- name: Get branch name
id: branch-name
@ -84,7 +84,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install dependencies
working-directory: frontend
run: yarn install
@ -97,15 +97,15 @@ jobs:
run: npm run lint
continue-on-error: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2
with:
version: latest
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: benjlevesque/short-sha@v1.2
- uses: benjlevesque/short-sha@v2.2
id: short-sha
- name: Get branch name
id: branch-name

View File

@ -12,6 +12,12 @@ on:
jobs:
update_release_draft:
permissions:
# write permission is required to create a github release
contents: write
# write permission is required for autolabeler
# otherwise, read permission is required at least
pull-requests: write
runs-on: ubuntu-latest
steps:
# (Optional) GitHub Enterprise requires GHE_HOST variable set

View File

@ -8,9 +8,15 @@ jobs:
remove:
runs-on: ubuntu-latest
steps:
- name: Remove label
uses: buildsville/add-remove-label@v1
- name: Remove label ok-to-test from PR
uses: buildsville/add-remove-label@v2.0.0
with:
label: ok-to-test,testing-deploy
label: ok-to-test
type: remove
token: ${{ secrets.GITHUB_TOKEN }}
- name: Remove label testing-deploy from PR
uses: buildsville/add-remove-label@v2.0.0
with:
label: testing-deploy
type: remove
token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -3,7 +3,7 @@ on:
pull_request:
branches:
- main
- v*
- develop
paths:
- 'frontend/**'
defaults:
@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Sonar analysis

View File

@ -54,7 +54,7 @@ build-push-frontend:
@echo "--> Building and pushing frontend docker image"
@echo "------------------"
@cd $(FRONTEND_DIRECTORY) && \
docker buildx build --file Dockerfile --progress plane --push --platform linux/arm64,linux/amd64 \
docker buildx build --file Dockerfile --progress plain --push --platform linux/arm64,linux/amd64 \
--tag $(REPONAME)/$(FRONTEND_DOCKER_IMAGE):$(DOCKER_TAG) .
# Steps to build and push docker image of query service
@ -73,7 +73,7 @@ build-push-query-service:
@echo "------------------"
@echo "--> Building and pushing query-service docker image"
@echo "------------------"
@docker buildx build --file $(QUERY_SERVICE_DIRECTORY)/Dockerfile --progress plane \
@docker buildx build --file $(QUERY_SERVICE_DIRECTORY)/Dockerfile --progress plain \
--push --platform linux/arm64,linux/amd64 --build-arg LD_FLAGS="$(LD_FLAGS)" \
--tag $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) .
@ -98,7 +98,7 @@ build-push-ee-query-service:
@echo "--> Building and pushing query-service docker image"
@echo "------------------"
@docker buildx build --file $(EE_QUERY_SERVICE_DIRECTORY)/Dockerfile \
--progress plane --push --platform linux/arm64,linux/amd64 \
--progress plain --push --platform linux/arm64,linux/amd64 \
--build-arg LD_FLAGS="$(LD_FLAGS)" --tag $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) .
dev-setup:
@ -136,9 +136,18 @@ clear-swarm-data:
@docker run --rm -v "$(PWD)/$(SWARM_DIRECTORY)/data:/pwd" busybox \
sh -c "cd /pwd && rm -rf alertmanager/* clickhouse*/* signoz/* zookeeper-*/*"
clear-standalone-ch:
@docker run --rm -v "$(PWD)/$(STANDALONE_DIRECTORY)/data:/pwd" busybox \
sh -c "cd /pwd && rm -rf clickhouse*/* zookeeper-*/*"
clear-swarm-ch:
@docker run --rm -v "$(PWD)/$(SWARM_DIRECTORY)/data:/pwd" busybox \
sh -c "cd /pwd && rm -rf clickhouse*/* zookeeper-*/*"
test:
go test ./pkg/query-service/app/metrics/...
go test ./pkg/query-service/cache/...
go test ./pkg/query-service/app/...
go test ./pkg/query-service/app/querier/...
go test ./pkg/query-service/converter/...
go test ./pkg/query-service/formatter/...

View File

@ -137,7 +137,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.20.2
image: signoz/query-service:0.21.0
command: ["-config=/root/config/prometheus.yml"]
# ports:
# - "6060:6060" # pprof port
@ -166,7 +166,7 @@ services:
<<: *clickhouse-depend
frontend:
image: signoz/frontend:0.20.2
image: signoz/frontend:0.21.0
deploy:
restart_policy:
condition: on-failure
@ -179,7 +179,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:0.76.1
image: signoz/signoz-otel-collector:0.79.1
command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
user: root # required for reading docker container logs
volumes:
@ -208,7 +208,7 @@ services:
<<: *clickhouse-depend
otel-collector-metrics:
image: signoz/signoz-otel-collector:0.76.1
image: signoz/signoz-otel-collector:0.79.1
command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@ -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`
otel-collector:
container_name: otel-collector
image: signoz/signoz-otel-collector:0.76.1
image: signoz/signoz-otel-collector:0.79.1
command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
# user: root # required for reading docker container logs
volumes:
@ -67,7 +67,7 @@ services:
otel-collector-metrics:
container_name: otel-collector-metrics
image: signoz/signoz-otel-collector:0.76.1
image: signoz/signoz-otel-collector:0.79.1
command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@ -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`
query-service:
image: signoz/query-service:${DOCKER_TAG:-0.20.2}
image: signoz/query-service:${DOCKER_TAG:-0.21.0}
container_name: query-service
command: ["-config=/root/config/prometheus.yml"]
# ports:
@ -181,7 +181,7 @@ services:
<<: *clickhouse-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.20.2}
image: signoz/frontend:${DOCKER_TAG:-0.21.0}
container_name: frontend
restart: on-failure
depends_on:
@ -193,7 +193,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.76.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.1}
command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
user: root # required for reading docker container logs
volumes:
@ -219,7 +219,7 @@ services:
<<: *clickhouse-depend
otel-collector-metrics:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.76.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.1}
command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@ -277,7 +277,7 @@ func loggingMiddleware(next http.Handler) http.Handler {
path, _ := route.GetPathTemplate()
startTime := time.Now()
next.ServeHTTP(w, r)
zap.S().Info(path, "\ttimeTaken: ", time.Now().Sub(startTime))
zap.L().Info(path+"\ttimeTaken:"+time.Now().Sub(startTime).String(), zap.Duration("timeTaken", time.Now().Sub(startTime)), zap.String("path", path))
})
}
@ -289,7 +289,7 @@ func loggingMiddlewarePrivate(next http.Handler) http.Handler {
path, _ := route.GetPathTemplate()
startTime := time.Now()
next.ServeHTTP(w, r)
zap.S().Info(path, "\tprivatePort: true", "\ttimeTaken: ", time.Now().Sub(startTime))
zap.L().Info(path+"\tprivatePort: true \ttimeTaken"+time.Now().Sub(startTime).String(), zap.Duration("timeTaken", time.Now().Sub(startTime)), zap.String("path", path), zap.Bool("tprivatePort", true))
})
}

View File

@ -3,25 +3,73 @@ package main
import (
"context"
"flag"
"log"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
"go.signoz.io/signoz/ee/query-service/app"
"go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/constants"
baseconst "go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/version"
"google.golang.org/grpc"
zapotlpencoder "github.com/SigNoz/zap_otlp/zap_otlp_encoder"
zapotlpsync "github.com/SigNoz/zap_otlp/zap_otlp_sync"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func initZapLog() *zap.Logger {
func initZapLog(enableQueryServiceLogOTLPExport bool) *zap.Logger {
config := zap.NewDevelopmentConfig()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
config.EncoderConfig.EncodeDuration = zapcore.StringDurationEncoder
otlpEncoder := zapotlpencoder.NewOTLPEncoder(config.EncoderConfig)
consoleEncoder := zapcore.NewConsoleEncoder(config.EncoderConfig)
defaultLogLevel := zapcore.DebugLevel
config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
config.EncoderConfig.TimeKey = "timestamp"
config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
logger, _ := config.Build()
res := resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("query-service"),
)
core := zapcore.NewTee(
zapcore.NewCore(consoleEncoder, os.Stdout, defaultLogLevel),
)
if enableQueryServiceLogOTLPExport == true {
conn, err := grpc.DialContext(ctx, constants.OTLPTarget, grpc.WithBlock(), grpc.WithInsecure(), grpc.WithTimeout(time.Second*30))
if err != nil {
log.Println("failed to connect to otlp collector to export query service logs with error:", err)
} else {
logExportBatchSizeInt, err := strconv.Atoi(baseconst.LogExportBatchSize)
if err != nil {
logExportBatchSizeInt = 1000
}
ws := zapcore.AddSync(zapotlpsync.NewOtlpSyncer(conn, zapotlpsync.Options{
BatchSize: logExportBatchSizeInt,
ResourceSchema: semconv.SchemaURL,
Resource: res,
}))
core = zapcore.NewTee(
zapcore.NewCore(consoleEncoder, os.Stdout, defaultLogLevel),
zapcore.NewCore(otlpEncoder, zapcore.NewMultiWriteSyncer(ws), defaultLogLevel),
)
}
}
logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel))
return logger
}
@ -34,12 +82,15 @@ func main() {
// the url used to build link in the alert messages in slack and other systems
var ruleRepoURL string
var enableQueryServiceLogOTLPExport bool
flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)")
flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)")
flag.StringVar(&ruleRepoURL, "rules.repo-url", baseconst.AlertHelpPage, "(host address used to build rule link in alert messages)")
flag.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)")
flag.Parse()
loggerMgr := initZapLog()
loggerMgr := initZapLog(enableQueryServiceLogOTLPExport)
zap.ReplaceGlobals(loggerMgr)
defer loggerMgr.Sync() // flushes buffer, if any

56
frontend/CONTRIBUTIONS.md Normal file
View File

@ -0,0 +1,56 @@
# **Frontend Guidelines**
Embrace the spirit of collaboration and contribute to the success of our open-source project by adhering to these frontend development guidelines with precision and passion.
### React and Components
- Strive to create small and modular components, ensuring they are divided into individual pieces for improved maintainability and reusability.
- Avoid passing inline objects or functions as props to React components, as they are recreated with each render cycle.
Utilize careful memoization of functions and variables, balancing optimization efforts to prevent potential performance issues. [When to useMemo and useCallback](https://kentcdodds.com/blog/usememo-and-usecallback) by Kent C. Dodds is quite helpful for this scenario.
- Minimize the use of inline functions whenever possible to enhance code readability and improve overall comprehension.
- Employ the appropriate usage of useMemo and useCallback hooks for effective memoization of values and functions.
- Determine the appropriate placement of components:
- Pages should contain an aggregation of all components and containers.
- Commonly used components should reside in the 'components' directory.
- Parent components responsible for data manipulation should be placed in the 'container' directory.
- Strategically decide where to store data, either in global state or local components:
- Begin by storing data in local components and gradually transition to global state as necessary.
- Avoid importing default namespace `React` as the project is using `v18` and `import React from 'react'` is not needed anymore.
- When a function requires more than three arguments (except when memoized), encapsulate them within an object to enhance readability and reduce potential parameter complexity.
### API and Services
- Avoid incorporating business logic within API/Service files to maintain flexibility for consumers to handle it according to their specific needs.
- Employ the use of the useQuery hook for fetching data and the useMutation hook for updating data, ensuring a consistent and efficient approach.
- Utilize the useQueryClient hook when updating the cache, facilitating smooth and effective management of data within the application.
**Note -** In our project, we are utilizing React Query v3. To gain a comprehensive understanding of its features and implementation, we recommend referring to the [official documentation](https://tanstack.com/query/v3/docs/react/overview) as a valuable resource.
### Styling
- Refrain from using inline styling within React components to maintain separation of concerns and promote a more maintainable codebase.
- Opt for using the rem unit instead of px values to ensure better scalability and responsiveness across different devices and screen sizes.
### Linting and Setup
- It is crucial to refrain from disabling ESLint and TypeScript errors within the project. If there is a specific rule that needs to be disabled, provide a clear and justified explanation for doing so. Maintaining the integrity of the linting and type-checking processes ensures code quality and consistency throughout the codebase.
- In our project, we rely on several essential ESLint plugins, namely:
- [plugin:@typescript-eslint](https://typescript-eslint.io/rules/)
- [airbnb styleguide](https://github.com/airbnb/javascript)
- [plugin:sonarjs](https://github.com/SonarSource/eslint-plugin-sonarjs)
To ensure compliance with our coding standards and best practices, we encourage you to refer to the documentation of these plugins. Familiarizing yourself with the ESLint rules they provide will help maintain code quality and consistency throughout the project.
### Naming Conventions
- Ensure that component names are written in Capital Case, while the folder names should be in lowercase.
- Keep all other elements, such as variables, functions, and file names, in lowercase.
### Miscellaneous
- Ensure that functions are modularized and follow the Single Responsibility Principle (SRP). The function's name should accurately convey its purpose and functionality.
- Semantic division of functions into smaller units should be prioritized for improved readability and maintainability.
Aim to keep functions concise and avoid exceeding a maximum length of 40 lines to enhance code understandability and ease of maintenance.
- Eliminate the use of hard-coded strings or enums, favoring a more flexible and maintainable approach.
- Strive to internationalize all strings within the codebase to support localization and improve accessibility for users across different languages.
- Minimize the usage of multiple if statements or switch cases within a function. Consider creating a mapper and separating logic into multiple functions for better code organization.

View File

@ -0,0 +1,11 @@
{
"options_menu": {
"options": "Options",
"format": "Format",
"row": "Row",
"default": "Default",
"column": "Column",
"maxLines": "Max lines per Row",
"addColumn": "Add a column"
}
}

View File

@ -0,0 +1,11 @@
{
"options_menu": {
"options": "Options",
"format": "Format",
"row": "Row",
"default": "Default",
"column": "Column",
"maxLines": "Max lines per Row",
"addColumn": "Add a column"
}
}

View File

@ -15,6 +15,11 @@ export const ServiceMapPage = Loadable(
() => import(/* webpackChunkName: "ServiceMapPage" */ 'modules/Servicemap'),
);
export const TracesExplorer = Loadable(
() =>
import(/* webpackChunkName: "Traces Explorer Page" */ 'pages/TracesExplorer'),
);
export const TraceFilter = Loadable(
() => import(/* webpackChunkName: "Trace Filter Page" */ 'pages/Trace'),
);
@ -101,6 +106,10 @@ export const Logs = Loadable(
() => import(/* webpackChunkName: "Logs" */ 'pages/Logs'),
);
export const LogsExplorer = Loadable(
() => import(/* webpackChunkName: "Logs Explorer" */ 'pages/LogsExplorer'),
);
export const Login = Loadable(
() => import(/* webpackChunkName: "Login" */ 'pages/Login'),
);

View File

@ -16,6 +16,7 @@ import {
ListAllALertsPage,
Login,
Logs,
LogsExplorer,
MySettings,
NewDashboardPage,
OrganizationSettings,
@ -29,6 +30,7 @@ import {
StatusPage,
TraceDetail,
TraceFilter,
TracesExplorer,
UnAuthorized,
UsageExplorerPage,
} from './pageComponents';
@ -139,6 +141,13 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'TRACE',
},
{
path: ROUTES.TRACES_EXPLORER,
exact: true,
component: TracesExplorer,
isPrivate: true,
key: 'TRACES_EXPLORER',
},
{
path: ROUTES.CHANNELS_NEW,
exact: true,
@ -209,6 +218,13 @@ const routes: AppRoutes[] = [
key: 'LOGS',
isPrivate: true,
},
{
path: ROUTES.LOGS_EXPLORER,
exact: true,
component: LogsExplorer,
key: 'LOGS_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.LOGIN,
exact: true,

View File

@ -16,7 +16,7 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
return {
statusCode,
payload: null,
error: 'Not Found',
error: data.errorType,
message: null,
};
}

View File

@ -1,5 +1,6 @@
// ** Helpers
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
import {
BaseAutocompleteData,
@ -18,6 +19,7 @@ import { EQueryType } from 'types/common/dashboard';
import {
BoolOperators,
DataSource,
LogsAggregatorOperator,
MetricAggregateOperator,
NumberOperators,
PanelTypeKeys,
@ -25,6 +27,7 @@ import {
QueryBuilderData,
ReduceOperators,
StringOperators,
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select';
import { v4 as uuid } from 'uuid';
@ -100,14 +103,17 @@ export const initialHavingValues: HavingForm = {
};
export const initialAutocompleteData: BaseAutocompleteData = {
id: uuid(),
id: createIdFromObjectFields(
{ dataType: null, key: '', isColumn: null, type: null },
baseAutoCompleteIdKeysOrder,
),
dataType: null,
key: '',
isColumn: null,
type: null,
};
export const initialQueryBuilderFormValues: IBuilderQuery = {
const initialQueryBuilderFormValues: IBuilderQuery = {
dataSource: DataSource.METRICS,
queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
aggregateOperator: MetricAggregateOperator.NOOP,
@ -127,6 +133,27 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
reduceTo: 'sum',
};
const initialQueryBuilderFormLogsValues: IBuilderQuery = {
...initialQueryBuilderFormValues,
aggregateOperator: LogsAggregatorOperator.COUNT,
dataSource: DataSource.LOGS,
};
const initialQueryBuilderFormTracesValues: IBuilderQuery = {
...initialQueryBuilderFormValues,
aggregateOperator: TracesAggregatorOperator.COUNT,
dataSource: DataSource.TRACES,
};
export const initialQueryBuilderFormValuesMap: Record<
DataSource,
IBuilderQuery
> = {
metrics: initialQueryBuilderFormValues,
logs: initialQueryBuilderFormLogsValues,
traces: initialQueryBuilderFormTracesValues,
};
export const initialFormulaBuilderFormValues: IBuilderFormula = {
queryName: createNewBuilderItemName({
existNames: [],
@ -161,17 +188,39 @@ export const initialSingleQueryMap: Record<
IClickHouseQuery | IPromQLQuery
> = { clickhouse_sql: initialClickHouseData, promql: initialQueryPromQLData };
export const initialQuery: QueryState = {
export const initialQueryState: QueryState = {
id: uuid(),
builder: initialQueryBuilderData,
clickhouse_sql: [initialClickHouseData],
promql: [initialQueryPromQLData],
};
export const initialQueryWithType: Query = {
...initialQuery,
const initialQueryWithType: Query = {
...initialQueryState,
queryType: EQueryType.QUERY_BUILDER,
};
const initialQueryLogsWithType: Query = {
...initialQueryWithType,
builder: {
...initialQueryWithType.builder,
queryData: [initialQueryBuilderFormValuesMap.logs],
},
};
const initialQueryTracesWithType: Query = {
...initialQueryWithType,
builder: {
...initialQueryWithType.builder,
queryData: [initialQueryBuilderFormValuesMap.traces],
},
};
export const initialQueriesMap: Record<DataSource, Query> = {
metrics: initialQueryWithType,
logs: initialQueryLogsWithType,
traces: initialQueryTracesWithType,
};
export const operatorsByTypes: Record<LocalDataType, string[]> = {
string: Object.values(StringOperators),
number: Object.values(NumberOperators),

View File

@ -1 +1,2 @@
export const COMPOSITE_QUERY = 'compositeQuery';
export const PANEL_TYPES_QUERY = 'panelTypes';

View File

@ -1,3 +1,5 @@
export const REACT_QUERY_KEY = {
GET_ALL_LICENCES: 'GET_ALL_LICENCES',
GET_QUERY_RANGE: 'GET_QUERY_RANGE',
GET_ALL_DASHBOARDS: 'GET_ALL_DASHBOARDS',
};

View File

@ -5,6 +5,7 @@ const ROUTES = {
SERVICE_MAP: '/service-map',
TRACE: '/trace',
TRACE_DETAIL: '/trace/:id',
TRACES_EXPLORER: '/traces-explorer',
SETTINGS: '/settings',
INSTRUMENTATION: '/get-started',
USAGE_EXPLORER: '/usage-explorer',
@ -27,6 +28,7 @@ const ROUTES = {
UN_AUTHORIZED: '/un-authorized',
NOT_FOUND: '/not-found',
LOGS: '/logs',
LOGS_EXPLORER: '/logs-explorer',
HOME_PAGE: '/',
PASSWORD_RESET: '/password-reset',
LIST_LICENSES: '/licenses',

View File

@ -36,8 +36,11 @@ const themeColors = {
royalGrey: '#888888',
matterhornGrey: '#555555',
whiteCream: '#ffffffd5',
white: '#ffffff',
black: '#000000',
lightBlack: '#141414',
lightgrey: '#ddd',
lightWhite: '#ffffffd9',
borderLightGrey: '#d9d9d9',
borderDarkGrey: '#424242',
};

View File

@ -0,0 +1,7 @@
import { CSSProperties } from 'react';
export const ITEMS_PER_PAGE_OPTIONS = [25, 50, 100, 200];
export const defaultSelectStyle: CSSProperties = {
minWidth: '6rem',
};

View File

@ -0,0 +1,69 @@
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
import { Button, Select } from 'antd';
import { memo, useMemo } from 'react';
import { defaultSelectStyle, ITEMS_PER_PAGE_OPTIONS } from './config';
import { Container } from './styles';
interface ControlsProps {
count: number;
countPerPage: number;
isLoading: boolean;
handleNavigatePrevious: () => void;
handleNavigateNext: () => void;
handleCountItemsPerPageChange: (e: number) => void;
}
function Controls(props: ControlsProps): JSX.Element | null {
const {
count,
isLoading,
countPerPage,
handleNavigatePrevious,
handleNavigateNext,
handleCountItemsPerPageChange,
} = props;
const isNextAndPreviousDisabled = useMemo(
() => isLoading || countPerPage === 0 || count === 0 || count < countPerPage,
[isLoading, countPerPage, count],
);
return (
<Container>
<Button
loading={isLoading}
size="small"
type="link"
disabled={isNextAndPreviousDisabled}
onClick={handleNavigatePrevious}
>
<LeftOutlined /> Previous
</Button>
<Button
loading={isLoading}
size="small"
type="link"
disabled={isNextAndPreviousDisabled}
onClick={handleNavigateNext}
>
Next <RightOutlined />
</Button>
<Select
style={defaultSelectStyle}
loading={isLoading}
value={countPerPage}
onChange={handleCountItemsPerPageChange}
>
{ITEMS_PER_PAGE_OPTIONS.map((count) => (
<Select.Option
key={count}
value={count}
>{`${count} / page`}</Select.Option>
))}
</Select>
</Container>
);
}
export default memo(Controls);

View File

@ -0,0 +1,7 @@
import styled from 'styled-components';
export const Container = styled.div`
display: flex;
align-items: center;
gap: 0.5rem;
`;

View File

@ -1,5 +1,5 @@
import {
initialQueryBuilderFormValues,
initialQueryBuilderFormValuesMap,
initialQueryPromQLData,
PANEL_TYPES,
} from 'constants/queryBuilder';
@ -11,11 +11,6 @@ import {
defaultMatchType,
} from 'types/api/alerts/def';
import { EQueryType } from 'types/common/dashboard';
import {
DataSource,
LogsAggregatorOperator,
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
const defaultAlertDescription =
'This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})';
@ -32,7 +27,7 @@ export const alertDefaults: AlertDef = {
condition: {
compositeQuery: {
builderQueries: {
A: initialQueryBuilderFormValues,
A: initialQueryBuilderFormValuesMap.metrics,
},
promQueries: { A: initialQueryPromQLData },
chQueries: {
@ -61,11 +56,7 @@ export const logAlertDefaults: AlertDef = {
condition: {
compositeQuery: {
builderQueries: {
A: {
...initialQueryBuilderFormValues,
aggregateOperator: LogsAggregatorOperator.COUNT,
dataSource: DataSource.LOGS,
},
A: initialQueryBuilderFormValuesMap.logs,
},
promQueries: { A: initialQueryPromQLData },
chQueries: {
@ -95,11 +86,7 @@ export const traceAlertDefaults: AlertDef = {
condition: {
compositeQuery: {
builderQueries: {
A: {
...initialQueryBuilderFormValues,
aggregateOperator: TracesAggregatorOperator.COUNT,
dataSource: DataSource.TRACES,
},
A: initialQueryBuilderFormValuesMap.traces,
},
promQueries: { A: initialQueryPromQLData },
chQueries: {
@ -129,11 +116,7 @@ export const exceptionAlertDefaults: AlertDef = {
condition: {
compositeQuery: {
builderQueries: {
A: {
...initialQueryBuilderFormValues,
aggregateOperator: TracesAggregatorOperator.COUNT,
dataSource: DataSource.TRACES,
},
A: initialQueryBuilderFormValuesMap.traces,
},
promQueries: { A: initialQueryPromQLData },
chQueries: {

View File

@ -1,16 +1,11 @@
import { Form, Row } from 'antd';
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames';
import FormAlertRules from 'container/FormAlertRules';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useUrlQuery from 'hooks/useUrlQuery';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { useState } from 'react';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { AlertDef } from 'types/api/alerts/def';
import {
alertDefaults,
ALERTS_VALUES_MAP,
exceptionAlertDefaults,
logAlertDefaults,
traceAlertDefaults,
@ -18,18 +13,12 @@ import {
import SelectAlertType from './SelectAlertType';
function CreateRules(): JSX.Element {
const [initValues, setInitValues] = useState<AlertDef>(alertDefaults);
const [initValues, setInitValues] = useState<AlertDef | null>(null);
const [alertType, setAlertType] = useState<AlertTypes>(
AlertTypes.METRICS_BASED_ALERT,
);
const [formInstance] = Form.useForm();
const urlQuery = useUrlQuery();
const compositeQuery = urlQuery.get(COMPOSITE_QUERY);
const { redirectWithQueryBuilderData } = useQueryBuilder();
const onSelectType = (typ: AlertTypes): void => {
setAlertType(typ);
switch (typ) {
@ -45,15 +34,9 @@ function CreateRules(): JSX.Element {
default:
setInitValues(alertDefaults);
}
const value = ALERTS_VALUES_MAP[typ].condition.compositeQuery;
const compositeQuery = mapQueryDataFromApi(value);
redirectWithQueryBuilderData(compositeQuery);
};
if (!compositeQuery) {
if (!initValues) {
return (
<Row wrap={false}>
<SelectAlertType onSelect={onSelectType} />

View File

@ -0,0 +1,113 @@
import { Button, Typography } from 'antd';
import createDashboard from 'api/dashboard/create';
import getAll from 'api/dashboard/getAll';
import axios from 'axios';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useNotifications } from 'hooks/useNotifications';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation, useQuery } from 'react-query';
import { ExportPanelProps } from '.';
import {
DashboardSelect,
NewDashboardButton,
SelectWrapper,
Title,
Wrapper,
} from './styles';
import { getSelectOptions } from './utils';
function ExportPanel({ onExport }: ExportPanelProps): JSX.Element {
const { notifications } = useNotifications();
const { t } = useTranslation(['dashboard']);
const [selectedDashboardId, setSelectedDashboardId] = useState<string | null>(
null,
);
const { data, isLoading, refetch } = useQuery({
queryFn: getAll,
queryKey: REACT_QUERY_KEY.GET_ALL_DASHBOARDS,
});
const {
mutate: createNewDashboard,
isLoading: createDashboardLoading,
} = useMutation(createDashboard, {
onSuccess: () => {
refetch();
},
onError: (error) => {
if (axios.isAxiosError(error)) {
notifications.error({
message: error.message,
});
}
},
});
const options = useMemo(() => getSelectOptions(data?.payload || []), [data]);
const handleExportClick = useCallback((): void => {
const currentSelectedDashboard = data?.payload?.find(
({ uuid }) => uuid === selectedDashboardId,
);
onExport(currentSelectedDashboard || null);
}, [data, selectedDashboardId, onExport]);
const handleSelect = useCallback(
(selectedDashboardValue: string): void => {
setSelectedDashboardId(selectedDashboardValue);
},
[setSelectedDashboardId],
);
const handleNewDashboard = useCallback(async () => {
createNewDashboard({
title: t('new_dashboard_title', {
ns: 'dashboard',
}),
uploadedGrafana: false,
});
}, [t, createNewDashboard]);
return (
<Wrapper direction="vertical">
<Title>Export Panel</Title>
<SelectWrapper direction="horizontal">
<DashboardSelect
placeholder="Select Dashboard"
options={options}
loading={isLoading || createDashboardLoading}
disabled={isLoading || createDashboardLoading}
value={selectedDashboardId}
onSelect={handleSelect}
/>
<Button
type="primary"
disabled={isLoading || !options?.length || !selectedDashboardId}
onClick={handleExportClick}
>
Export
</Button>
</SelectWrapper>
<Typography>
Or create dashboard with this panel -
<NewDashboardButton
disabled={createDashboardLoading}
loading={createDashboardLoading}
type="link"
onClick={handleNewDashboard}
>
New Dashboard
</NewDashboardButton>
</Typography>
</Wrapper>
);
}
export default ExportPanel;

View File

@ -0,0 +1,9 @@
export const MENU_KEY = {
EXPORT: 'export',
CREATE_ALERTS: 'create-alerts',
};
export const MENU_LABEL = {
EXPORT: 'Export Panel',
CREATE_ALERTS: 'Create Alerts',
};

View File

@ -0,0 +1,70 @@
import { Button, Dropdown, MenuProps, Modal } from 'antd';
import { useCallback, useMemo, useState } from 'react';
import { Dashboard } from 'types/api/dashboard/getAll';
import { MENU_KEY, MENU_LABEL } from './config';
import ExportPanelContainer from './ExportPanel';
function ExportPanel({ onExport }: ExportPanelProps): JSX.Element {
const [isExport, setIsExport] = useState<boolean>(false);
const onModalToggle = useCallback((value: boolean) => {
setIsExport(value);
}, []);
const onMenuClickHandler: MenuProps['onClick'] = useCallback(
(e: OnClickProps) => {
if (e.key === MENU_KEY.EXPORT) {
onModalToggle(true);
}
},
[onModalToggle],
);
const menu: MenuProps = useMemo(
() => ({
items: [
{
key: MENU_KEY.EXPORT,
label: MENU_LABEL.EXPORT,
},
{
key: MENU_KEY.CREATE_ALERTS,
label: MENU_LABEL.CREATE_ALERTS,
},
],
onClick: onMenuClickHandler,
}),
[onMenuClickHandler],
);
const onCancel = (value: boolean) => (): void => {
onModalToggle(value);
};
return (
<>
<Dropdown trigger={['click']} menu={menu}>
<Button>Actions</Button>
</Dropdown>
<Modal
onOk={onCancel(false)}
onCancel={onCancel(false)}
open={isExport}
centered
>
<ExportPanelContainer onExport={onExport} />
</Modal>
</>
);
}
interface OnClickProps {
key: string;
}
export interface ExportPanelProps {
onExport: (dashboard: Dashboard | null) => void;
}
export default ExportPanel;

View File

@ -0,0 +1,33 @@
import { Button, Select, SelectProps, Space, Typography } from 'antd';
import { FunctionComponent } from 'react';
import styled from 'styled-components';
export const DashboardSelect: FunctionComponent<SelectProps> = styled(
Select,
)<SelectProps>`
width: 100%;
`;
export const SelectWrapper = styled(Space)`
width: 100%;
margin-bottom: 1rem;
.ant-space-item:first-child {
width: 100%;
max-width: 20rem;
}
`;
export const Wrapper = styled(Space)`
width: 100%;
`;
export const NewDashboardButton = styled(Button)`
&&& {
padding: 0 0.125rem;
}
`;
export const Title = styled(Typography.Text)`
font-size: 1rem;
`;

View File

@ -0,0 +1,10 @@
import { SelectProps } from 'antd';
import { PayloadProps as AllDashboardsData } from 'types/api/dashboard/getAll';
export const getSelectOptions = (
data: AllDashboardsData,
): SelectProps['options'] =>
data.map(({ uuid, data }) => ({
label: data.title,
value: uuid,
}));

View File

@ -1,6 +1,7 @@
import { Form, Select } from 'antd';
import { useTranslation } from 'react-i18next';
import { AlertDef, Labels } from 'types/api/alerts/def';
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
import ChannelSelect from './ChannelSelect';
import LabelSelect from './labels';
@ -54,7 +55,15 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
</SeveritySelect>
</Form.Item>
<Form.Item label={t('field_alert_name')} labelAlign="left" name="alert">
<Form.Item
required
name="alert"
labelAlign="left"
label={t('field_alert_name')}
rules={[
{ required: true, message: requireErrorMessage(t('field_alert_name')) },
]}
>
<InputSmall
onChange={(e): void => {
setAlertDef({
@ -97,10 +106,10 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
<FormItemMedium label="Notification Channels">
<ChannelSelect
currentValue={alertDef.preferredChannels}
onSelectChannels={(s: string[]): void => {
onSelectChannels={(preferredChannels): void => {
setAlertDef({
...alertDef,
preferredChannels: s,
preferredChannels,
});
}}
/>

View File

@ -1,16 +1,15 @@
import { InfoCircleOutlined } from '@ant-design/icons';
import { StaticLineProps } from 'components/Graph';
import Spinner from 'components/Spinner';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import GridGraphComponent from 'container/GridGraphComponent';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import getChartData from 'lib/getChartData';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
@ -18,7 +17,7 @@ import { ChartContainer, FailedMessageContainer } from './styles';
export interface ChartPreviewProps {
name: string;
query: Query | undefined;
query: Query | null;
graphType?: GRAPH_TYPES;
selectedTime?: timePreferenceType;
selectedInterval?: Time;
@ -26,9 +25,6 @@ export interface ChartPreviewProps {
threshold?: number | undefined;
userQueryKey?: string;
}
interface QueryResponseError {
message?: string;
}
function ChartPreview({
name,
@ -76,39 +72,30 @@ function ChartPreview({
}
}, [query]);
const queryResponse = useQuery({
queryKey: [
'chartPreview',
userQueryKey || JSON.stringify(query),
selectedInterval,
],
queryFn: () =>
GetMetricQueryRange({
query: query || {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: {
queryFormulas: [],
queryData: [],
},
clickhouse_sql: [],
},
globalSelectedInterval: selectedInterval,
graphType,
selectedTime,
}),
retry: false,
enabled: canQuery,
});
const queryResponse = useGetQueryRange(
{
query: query || initialQueriesMap.metrics,
globalSelectedInterval: selectedInterval,
graphType,
selectedTime,
},
{
queryKey: [
'chartPreview',
userQueryKey || JSON.stringify(query),
selectedInterval,
],
retry: false,
enabled: canQuery,
},
);
const chartDataSet = queryResponse.isError
? null
: getChartData({
queryData: [
{
queryData: queryResponse?.data?.payload?.data?.result
? queryResponse?.data?.payload?.data?.result
: [],
queryData: queryResponse?.data?.payload?.data?.result ?? [],
},
],
});
@ -119,11 +106,12 @@ function ChartPreview({
{(queryResponse?.isError || queryResponse?.error) && (
<FailedMessageContainer color="red" title="Failed to refresh the chart">
<InfoCircleOutlined />{' '}
{(queryResponse?.error as QueryResponseError).message ||
t('preview_chart_unexpected_error')}
{queryResponse.error.message || t('preview_chart_unexpected_error')}
</FailedMessageContainer>
)}
{queryResponse.isLoading && <Spinner size="large" tip="Loading..." />}
{queryResponse.isLoading && (
<Spinner size="large" tip="Loading..." height="70vh" />
)}
{chartDataSet && !queryResponse.isError && (
<GridGraphComponent
title={name}

View File

@ -4,8 +4,11 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import { QueryBuilder } from 'container/QueryBuilder';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { EQueryType } from 'types/common/dashboard';
import AppReducer from 'types/reducer/app';
import ChQuerySection from './ChQuerySection';
import PromqlSection from './PromqlSection';
@ -20,8 +23,14 @@ function QuerySection({
// init namespace for translations
const { t } = useTranslation('alerts');
const { featureResponse } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const handleQueryCategoryChange = (queryType: string): void => {
setQueryCategory(queryType as EQueryType);
featureResponse.refetch().then(() => {
setQueryCategory(queryType as EQueryType);
});
};
const renderPromqlUI = (): JSX.Element => <PromqlSection />;
@ -38,10 +47,6 @@ function QuerySection({
/>
);
const handleRunQuery = (): void => {
runQuery();
};
const tabs = [
{
label: t('tab_qb'),
@ -76,7 +81,7 @@ function QuerySection({
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<Button type="primary" onClick={handleRunQuery}>
<Button type="primary" onClick={runQuery}>
Run Query
</Button>
</span>
@ -95,7 +100,7 @@ function QuerySection({
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<Button type="primary" onClick={handleRunQuery}>
<Button type="primary" onClick={runQuery}>
Run Query
</Button>
</span>
@ -132,7 +137,7 @@ interface QuerySectionProps {
queryCategory: EQueryType;
setQueryCategory: (n: EQueryType) => void;
alertType: AlertTypes;
runQuery: () => void;
runQuery: VoidFunction;
}
export default QuerySection;

View File

@ -140,12 +140,14 @@ function RuleOptions({
{queryCategory === EQueryType.PROM
? renderPromRuleOptions()
: renderThresholdRuleOpts()}
<InputNumber
addonBefore={t('field_threshold')}
value={alertDef?.condition?.target}
onChange={onChange}
type="number"
/>
<Form.Item name={['condition', 'target']}>
<InputNumber
addonBefore={t('field_threshold')}
value={alertDef?.condition?.target}
onChange={onChange}
type="number"
/>
</Form.Item>
</FormContainer>
</>
);

View File

@ -48,7 +48,12 @@ function FormAlertRules({
// init namespace for translations
const { t } = useTranslation('alerts');
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const {
currentQuery,
stagedQuery,
handleRunQuery,
redirectWithQueryBuilderData,
} = useQueryBuilder();
// use query client
const ruleCache = useQueryClient();
@ -65,35 +70,14 @@ function FormAlertRules({
const sq = useMemo(() => mapQueryDataFromApi(initQuery), [initQuery]);
// manualStagedQuery requires manual staging of query
// when user clicks run query button. Useful for clickhouse tab where
// run query button is provided.
const [manualStagedQuery, setManualStagedQuery] = useState<Query>();
// this use effect initiates staged query and
// other queries based on server data.
// useful when fetching of initial values (from api)
// is delayed
const { compositeQuery } = useShareBuilderUrl({ defaultValue: sq });
useShareBuilderUrl({ defaultValue: sq });
useEffect(() => {
if (compositeQuery && !manualStagedQuery) {
setManualStagedQuery(compositeQuery);
}
setAlertDef(initialValue);
}, [
initialValue,
initQuery,
redirectWithQueryBuilderData,
currentQuery,
manualStagedQuery,
compositeQuery,
]);
}, [initialValue]);
const onRunQuery = (): void => {
setManualStagedQuery(currentQuery);
redirectWithQueryBuilderData(currentQuery);
handleRunQuery();
};
const onCancelHandler = useCallback(() => {
@ -115,8 +99,6 @@ function FormAlertRules({
}
const query: Query = { ...currentQuery, queryType: val };
setManualStagedQuery(query);
redirectWithQueryBuilderData(query);
};
const { notifications } = useNotifications();
@ -201,10 +183,6 @@ function FormAlertRules({
const isFormValid = useCallback((): boolean => {
if (!alertDef.alert || alertDef.alert === '') {
notifications.error({
message: 'Error',
description: t('alertname_required'),
});
return false;
}
@ -217,14 +195,7 @@ function FormAlertRules({
}
return validateQBParams();
}, [
t,
validateQBParams,
validateChQueryParams,
alertDef,
validatePromParams,
notifications,
]);
}, [validateQBParams, validateChQueryParams, alertDef, validatePromParams]);
const preparePostData = (): AlertDef => {
const postableAlert: AlertDef = {
@ -328,9 +299,7 @@ function FormAlertRules({
title: t('confirm_save_title'),
centered: true,
content,
onOk() {
saveRule();
},
onOk: saveRule,
});
}, [t, saveRule, currentQuery]);
@ -381,7 +350,7 @@ function FormAlertRules({
headline={<PlotTag queryType={currentQuery.queryType} />}
name=""
threshold={alertDef.condition?.target}
query={manualStagedQuery}
query={stagedQuery}
selectedInterval={toChartInterval(alertDef.evalWindow)}
/>
);
@ -391,7 +360,7 @@ function FormAlertRules({
headline={<PlotTag queryType={currentQuery.queryType} />}
name="Chart Preview"
threshold={alertDef.condition?.target}
query={manualStagedQuery}
query={stagedQuery}
/>
);
@ -400,23 +369,25 @@ function FormAlertRules({
headline={<PlotTag queryType={currentQuery.queryType} />}
name="Chart Preview"
threshold={alertDef.condition?.target}
query={manualStagedQuery}
query={stagedQuery}
selectedInterval={toChartInterval(alertDef.evalWindow)}
/>
);
const isNewRule = ruleId === 0;
const isAlertNameMissing = !formInstance.getFieldValue('alert');
const isAlertAvialableToSave =
isAlertAvialable &&
isNewRule &&
currentQuery.queryType === EQueryType.QUERY_BUILDER;
currentQuery.queryType === EQueryType.QUERY_BUILDER &&
alertType !== AlertTypes.METRICS_BASED_ALERT;
return (
<>
{Element}
<PanelContainer>
<StyledLeftContainer flex="5 1 600px">
<StyledLeftContainer flex="5 1 600px" md={18}>
<MainFormContainer
initialValues={initialValue}
layout="vertical"
@ -448,7 +419,7 @@ function FormAlertRules({
type="primary"
onClick={onSaveHandler}
icon={<SaveOutlined />}
disabled={isAlertAvialableToSave}
disabled={isAlertNameMissing || isAlertAvialableToSave}
>
{isNewRule ? t('button_createrule') : t('button_savechanges')}
</ActionButton>

View File

@ -84,8 +84,8 @@ function LabelSelect({
handleBlur();
}, [handleBlur]);
const handleChange = (e: ChangeEvent<HTMLInputElement>): void => {
setCurrentVal(e.target?.value);
const handleLabelChange = (event: ChangeEvent<HTMLInputElement>): void => {
setCurrentVal(event.target?.value.replace(':', ''));
};
const handleClose = (key: string): void => {
@ -133,9 +133,9 @@ function LabelSelect({
<div style={{ display: 'flex', width: '100%' }}>
<Input
placeholder={renderPlaceholder()}
onChange={handleChange}
onChange={handleLabelChange}
onKeyUp={(e): void => {
if (e.key === 'Enter' || e.code === 'Enter') {
if (e.key === 'Enter' || e.code === 'Enter' || e.key === ':') {
send('NEXT');
}
}}

View File

@ -7,16 +7,13 @@ import {
timeItems,
timePreferance,
} from 'container/NewWidget/RightContainer/timeItems';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import getChartData from 'lib/getChartData';
import { useCallback, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
import { AppState } from 'store/reducers';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TimeContainer } from './styles';
@ -44,18 +41,24 @@ function FullView({
name: getSelectedTime()?.name || '',
enum: widget?.timePreferance || 'GLOBAL_TIME',
});
const response = useQuery<
SuccessResponse<MetricRangePayloadProps> | ErrorResponse
>(
`FullViewGetMetricsQueryRange-${selectedTime.enum}-${globalSelectedTime}-${widget.id}`,
const queryKey = useMemo(
() =>
GetMetricQueryRange({
selectedTime: selectedTime.enum,
graphType: widget.panelTypes,
query: widget.query,
globalSelectedInterval: globalSelectedTime,
variables: getDashboardVariables(),
}),
`FullViewGetMetricsQueryRange-${selectedTime.enum}-${globalSelectedTime}-${widget.id}`,
[selectedTime, globalSelectedTime, widget],
);
const response = useGetQueryRange(
{
selectedTime: selectedTime.enum,
graphType: widget.panelTypes,
query: widget.query,
globalSelectedInterval: globalSelectedTime,
variables: getDashboardVariables(),
},
{
queryKey,
},
);
const chartDataSet = useMemo(

View File

@ -3,6 +3,7 @@ import { ChartData } from 'chart.js';
import Spinner from 'components/Spinner';
import GridGraphComponent from 'container/GridGraphComponent';
import { UpdateDashboard } from 'container/GridGraphLayout/utils';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useNotifications } from 'hooks/useNotifications';
import usePreviousValue from 'hooks/usePreviousValue';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
@ -20,7 +21,6 @@ import {
import { Layout } from 'react-grid-layout';
import { useTranslation } from 'react-i18next';
import { useInView } from 'react-intersection-observer';
import { useQuery } from 'react-query';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
@ -28,7 +28,6 @@ import {
DeleteWidget,
DeleteWidgetProps,
} from 'store/actions/dashboard/deleteWidget';
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { Widgets } from 'types/api/dashboard/getAll';
@ -55,7 +54,7 @@ function GridCardGraph({
const { ref: graphRef, inView: isGraphVisible } = useInView({
threshold: 0,
triggerOnce: true,
initialInView: true,
initialInView: false,
});
const { notifications } = useNotifications();
@ -81,33 +80,28 @@ function GridCardGraph({
const selectedData = selectedDashboard?.data;
const { variables } = selectedData;
const queryResponse = useQuery(
[
`GetMetricsQueryRange-${widget?.timePreferance}-${globalSelectedInterval}-${widget.id}`,
{
const queryResponse = useGetQueryRange(
{
selectedTime: widget?.timePreferance,
graphType: widget?.panelTypes,
query: widget?.query,
globalSelectedInterval,
variables: getDashboardVariables(),
},
{
queryKey: [
`GetMetricsQueryRange-${widget?.timePreferance}-${globalSelectedInterval}-${widget?.id}`,
widget,
maxTime,
minTime,
globalSelectedInterval,
variables,
},
],
() =>
GetMetricQueryRange({
selectedTime: widget?.timePreferance,
graphType: widget.panelTypes,
query: widget.query,
globalSelectedInterval,
variables: getDashboardVariables(),
}),
{
],
keepPreviousData: true,
enabled: isGraphVisible,
refetchOnMount: false,
onError: (error) => {
if (error instanceof Error) {
setErrorMessage(error.message);
}
setErrorMessage(error.message);
},
},
);
@ -179,7 +173,7 @@ function GridCardGraph({
{
data: selectedDashboard.data,
generateWidgetId: uuid,
graphType: widget.panelTypes,
graphType: widget?.panelTypes,
selectedDashboard,
layout,
widgetData: widget,
@ -193,7 +187,7 @@ function GridCardGraph({
setTimeout(() => {
history.push(
`${history.location.pathname}/new?graphType=${widget.panelTypes}&widgetId=${uuid}`,
`${history.location.pathname}/new?graphType=${widget?.panelTypes}&widgetId=${uuid}`,
);
}, 1500);
});
@ -259,10 +253,10 @@ function GridCardGraph({
/>
</div>
<GridGraphComponent
GRAPH_TYPES={widget.panelTypes}
GRAPH_TYPES={widget?.panelTypes}
data={prevChartDataSetRef}
isStacked={widget.isStacked}
opacity={widget.opacity}
isStacked={widget?.isStacked}
opacity={widget?.opacity}
title={' '}
name={name}
yAxisUnit={yAxisUnit}

View File

@ -126,7 +126,7 @@ function WidgetHeader({
{
key: keyMethodMapping.clone.key,
icon: <CopyOutlined />,
disabled: false,
disabled: !editWidget,
label: 'Clone',
},
{

View File

@ -124,8 +124,7 @@ function GridGraph(props: Props): JSX.Element {
}
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [dispatch, isAddWidget, layouts, selectedDashboard, widgets]);
const { featureResponse } = useSelector<AppState, AppReducer>(
(state) => state.app,

View File

@ -1,15 +1,10 @@
import { NotificationInstance } from 'antd/es/notification/interface';
import updateDashboardApi from 'api/dashboard/update';
import {
initialClickHouseData,
initialQueryBuilderFormValues,
initialQueryPromQLData,
} from 'constants/queryBuilder';
import { initialQueriesMap } from 'constants/queryBuilder';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { Layout } from 'react-grid-layout';
import store from 'store';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import { EQueryType } from 'types/common/dashboard';
export const UpdateDashboard = async (
{
@ -41,23 +36,7 @@ export const UpdateDashboard = async (
nullZeroValues: widgetData?.nullZeroValues || '',
opacity: '',
panelTypes: graphType,
query: widgetData?.query || {
queryType: EQueryType.QUERY_BUILDER,
promql: [initialQueryPromQLData],
clickhouse_sql: [initialClickHouseData],
builder: {
queryFormulas: [],
queryData: [initialQueryBuilderFormValues],
},
},
queryData: {
data: {
queryData: widgetData?.queryData.data.queryData || [],
},
error: false,
errorMessage: '',
loading: false,
},
query: widgetData?.query || initialQueriesMap.metrics,
timePreferance: widgetData?.timePreferance || 'GLOBAL_TIME',
title: widgetData ? copyTitle : '',
},

View File

@ -91,7 +91,7 @@ function HeaderContainer(): JSX.Element {
const onClickSignozCloud = (): void => {
window.open(
'https://signoz.io/pricing/?utm_source=product_navbar&utm_medium=frontend',
'https://signoz.io/oss-to-cloud/?utm_source=product_navbar&utm_medium=frontend&utm_campaign=oss_users',
'_blank',
);
};

View File

@ -2,6 +2,7 @@
import { PlusOutlined } from '@ant-design/icons';
import { Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import saveAlertApi from 'api/alerts/save';
import { ResizeTable } from 'components/ResizeTable';
import TextToolTip from 'components/TextToolTip';
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames';
@ -67,7 +68,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
.catch(handleError);
}, [featureResponse, handleError]);
const onEditHandler = (record: GettableAlert): void => {
const onEditHandler = (record: GettableAlert) => (): void => {
featureResponse
.refetch()
.then(() => {
@ -84,6 +85,44 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
.catch(handleError);
};
const onCloneHandler = (
originalAlert: GettableAlert,
) => async (): Promise<void> => {
const copyAlert = {
...originalAlert,
alert: originalAlert.alert.concat(' - Copy'),
};
const apiReq = { data: copyAlert };
const response = await saveAlertApi(apiReq);
if (response.statusCode === 200) {
notificationsApi.success({
message: 'Success',
description: 'Alert cloned successfully',
});
const { data: refetchData, status } = await refetch();
if (status === 'success' && refetchData.payload) {
setData(refetchData.payload || []);
setTimeout(() => {
const clonedAlert = refetchData.payload[refetchData.payload.length - 1];
history.push(`${ROUTES.EDIT_ALERTS}?ruleId=${clonedAlert.id}`);
}, 2000);
}
if (status === 'error') {
notificationsApi.error({
message: t('something_went_wrong'),
});
}
} else {
notificationsApi.error({
message: 'Error',
description: response.error || t('something_went_wrong'),
});
}
};
const columns: ColumnsType<GettableAlert> = [
{
title: 'Status',
@ -107,9 +146,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
return 0;
},
render: (value, record): JSX.Element => (
<Typography.Link onClick={(): void => onEditHandler(record)}>
{value}
</Typography.Link>
<Typography.Link onClick={onEditHandler(record)}>{value}</Typography.Link>
),
},
{
@ -165,9 +202,12 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
<>
<ToggleAlertState disabled={record.disabled} setData={setData} id={id} />
<ColumnButton onClick={(): void => onEditHandler(record)} type="link">
<ColumnButton onClick={onEditHandler(record)} type="link">
Edit
</ColumnButton>
<ColumnButton onClick={onCloneHandler(record)} type="link">
Clone
</ColumnButton>
<DeleteAlert notifications={notificationsApi} setData={setData} id={id} />
</>

View File

@ -71,23 +71,8 @@ function ImportJSON({
setDashboardCreating(true);
const dashboardData = JSON.parse(editorValue) as DashboardData;
// removing the queryData
const parsedWidgets: DashboardData = {
...dashboardData,
widgets: dashboardData.widgets?.map((e) => ({
...e,
queryData: {
...e.queryData,
data: e.queryData.data,
error: false,
errorMessage: '',
loading: false,
},
})),
};
const response = await createDashboard({
...parsedWidgets,
...dashboardData,
uploadedGrafana,
});

View File

@ -1 +0,0 @@
export const ITEMS_PER_PAGE_OPTIONS = [25, 50, 100, 200];

View File

@ -1,16 +1,11 @@
import {
CloudDownloadOutlined,
FastBackwardOutlined,
LeftOutlined,
RightOutlined,
} from '@ant-design/icons';
import { Button, Divider, Dropdown, MenuProps, Select } from 'antd';
import { CloudDownloadOutlined, FastBackwardOutlined } from '@ant-design/icons';
import { Button, Divider, Dropdown, MenuProps } from 'antd';
import { Excel } from 'antd-table-saveas-excel';
import Controls from 'container/Controls';
import { getGlobalTime } from 'container/LogsSearchFilter/utils';
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
import dayjs from 'dayjs';
import { FlatLogData } from 'lib/logs/flatLogData';
import { defaultSelectStyle } from 'pages/Logs/config';
import * as Papa from 'papaparse';
import { memo, useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
@ -26,7 +21,6 @@ import {
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
import { ITEMS_PER_PAGE_OPTIONS } from './config';
import { Container, DownloadLogButton } from './styles';
function LogControls(): JSX.Element | null {
@ -149,15 +143,6 @@ function LogControls(): JSX.Element | null {
const isLoading = isLogsLoading || isLoadingAggregate;
const isNextAndPreviousDisabled = useMemo(
() =>
isLoading ||
logLinesPerPage === 0 ||
logs.length === 0 ||
logs.length < logLinesPerPage,
[isLoading, logLinesPerPage, logs.length],
);
if (liveTail !== 'STOPPED') {
return null;
}
@ -179,37 +164,14 @@ function LogControls(): JSX.Element | null {
<FastBackwardOutlined /> Go to latest
</Button>
<Divider type="vertical" />
<Button
loading={isLoading}
size="small"
type="link"
disabled={isNextAndPreviousDisabled}
onClick={handleNavigatePrevious}
>
<LeftOutlined /> Previous
</Button>
<Button
loading={isLoading}
size="small"
type="link"
disabled={isNextAndPreviousDisabled}
onClick={handleNavigateNext}
>
Next <RightOutlined />
</Button>
<Select
style={defaultSelectStyle}
loading={isLoading}
value={logLinesPerPage}
onChange={handleLogLinesPerPageChange}
>
{ITEMS_PER_PAGE_OPTIONS.map((count) => (
<Select.Option
key={count}
value={count}
>{`${count} / page`}</Select.Option>
))}
</Select>
<Controls
isLoading={isLoading}
count={logs.length}
countPerPage={logLinesPerPage}
handleNavigatePrevious={handleNavigatePrevious}
handleNavigateNext={handleNavigateNext}
handleCountItemsPerPageChange={handleLogLinesPerPageChange}
/>
</Container>
);
}

View File

@ -32,7 +32,7 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
const dispatch = useDispatch<Dispatch<AppActions>>();
const flattenLogData: Record<string, any> | null = useMemo(
const flattenLogData: Record<string, string> | null = useMemo(
() => (logData ? flattenObject(logData) : null),
[logData],
);

View File

@ -0,0 +1,11 @@
import { Card } from 'antd';
import styled from 'styled-components';
export const CardStyled = styled(Card)`
position: relative;
margin: 0.5rem 0 3.1rem 0;
.ant-card-body {
height: 20vh;
min-height: 200px;
}
`;

View File

@ -0,0 +1,66 @@
import Graph from 'components/Graph';
import Spinner from 'components/Spinner';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getExplorerChartData } from 'lib/explorer/getExplorerChartData';
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { CardStyled } from './LogsExplorerChart.styled';
export function LogsExplorerChart(): JSX.Element {
const { stagedQuery } = useQueryBuilder();
const { selectedTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const panelTypeParam = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
const { data, isFetching } = useGetQueryRange(
{
query: stagedQuery || initialQueriesMap.metrics,
graphType: panelTypeParam,
globalSelectedInterval: selectedTime,
selectedTime: 'GLOBAL_TIME',
},
{
queryKey: [
REACT_QUERY_KEY.GET_QUERY_RANGE,
selectedTime,
stagedQuery,
panelTypeParam,
],
enabled: !!stagedQuery,
},
);
const graphData = useMemo(() => {
if (data?.payload.data && data.payload.data.result.length > 0) {
return getExplorerChartData([data.payload.data.result[0]]);
}
return getExplorerChartData([]);
}, [data]);
return (
<CardStyled>
{isFetching ? (
<Spinner size="default" height="100%" />
) : (
<Graph
name="logsExplorerChart"
data={graphData}
type="bar"
containerHeight="100%"
animate
/>
)}
</CardStyled>
);
}

View File

@ -0,0 +1 @@
export { LogsExplorerChart } from './LogsExplorerChart';

View File

@ -0,0 +1,9 @@
import { Tabs } from 'antd';
import { themeColors } from 'constants/theme';
import styled from 'styled-components';
export const TabsStyled = styled(Tabs)`
& .ant-tabs-nav {
background-color: ${themeColors.lightBlack};
}
`;

View File

@ -0,0 +1,75 @@
import { TabsProps } from 'antd';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PANEL_TYPES_QUERY } from 'constants/queryBuilderQueryNames';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useUrlQuery from 'hooks/useUrlQuery';
import { useCallback, useEffect, useMemo } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { TabsStyled } from './LogsExplorerViews.styled';
export function LogsExplorerViews(): JSX.Element {
const location = useLocation();
const urlQuery = useUrlQuery();
const history = useHistory();
const { currentQuery } = useQueryBuilder();
const panelTypeParams = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
const isMultipleQueries = useMemo(
() =>
currentQuery.builder.queryData.length > 1 ||
currentQuery.builder.queryFormulas.length > 0,
[currentQuery],
);
const tabsItems: TabsProps['items'] = useMemo(
() => [
{
label: 'List View',
key: PANEL_TYPES.LIST,
disabled: isMultipleQueries,
},
{ label: 'TimeSeries', key: PANEL_TYPES.TIME_SERIES },
{ label: 'Table', key: PANEL_TYPES.TABLE },
],
[isMultipleQueries],
);
const handleChangeView = useCallback(
(panelType: string) => {
urlQuery.set(PANEL_TYPES_QUERY, JSON.stringify(panelType) as GRAPH_TYPES);
const path = `${location.pathname}?${urlQuery}`;
history.push(path);
},
[history, location, urlQuery],
);
const currentTabKey = useMemo(
() =>
Object.values(PANEL_TYPES).includes(panelTypeParams)
? panelTypeParams
: PANEL_TYPES.LIST,
[panelTypeParams],
);
useEffect(() => {
if (panelTypeParams === 'list' && isMultipleQueries) {
handleChangeView(PANEL_TYPES.TIME_SERIES);
}
}, [panelTypeParams, isMultipleQueries, handleChangeView]);
return (
<div>
<TabsStyled
items={tabsItems}
defaultActiveKey={currentTabKey}
activeKey={currentTabKey}
onChange={handleChangeView}
/>
</div>
);
}

View File

@ -0,0 +1 @@
export { LogsExplorerViews } from './LogsExplorerViews';

View File

@ -6,7 +6,7 @@ import {
QueryOperatorsMultiVal,
QueryOperatorsSingleVal,
} from 'lib/logql/tokens';
import { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { ILogsReducer } from 'types/reducer/logs';
@ -56,6 +56,8 @@ function QueryField({
onUpdate,
onDelete,
}: QueryFieldProps): JSX.Element | null {
const [isDropDownOpen, setIsDropDownOpen] = useState(false);
const {
fields: { selected },
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
@ -136,9 +138,12 @@ function QueryField({
<Select
mode="tags"
style={{ width: '100%' }}
open={isDropDownOpen}
onChange={(e): void => handleChange(2, e as never)}
defaultValue={(query[2] && query[2].value) || []}
notFoundContent={null}
onInputKeyDown={(): void => setIsDropDownOpen(true)}
onSelect={(): void => setIsDropDownOpen(false)}
/>
) : (
<Input

View File

@ -10,12 +10,6 @@ export const getWidgetQueryBuilder = (query: Widgets['query']): Widgets => ({
opacity: '0',
panelTypes: PANEL_TYPES.TIME_SERIES,
query,
queryData: {
data: { queryData: [] },
error: false,
errorMessage: '',
loading: false,
},
timePreferance: 'GLOBAL_TIME',
title: '',
});

View File

@ -1,6 +1,6 @@
import {
initialFormulaBuilderFormValues,
initialQueryBuilderFormValues,
initialQueryBuilderFormValuesMap,
} from 'constants/queryBuilder';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
@ -18,7 +18,7 @@ export const getQueryBuilderQueries = ({
queryFormulas: [],
queryData: [
{
...initialQueryBuilderFormValues,
...initialQueryBuilderFormValuesMap.metrics,
aggregateOperator: MetricAggregateOperator.SUM_RATE,
disabled: false,
groupBy,
@ -53,7 +53,7 @@ export const getQueryBuilderQuerieswithFormula = ({
],
queryData: [
{
...initialQueryBuilderFormValues,
...initialQueryBuilderFormValuesMap.metrics,
aggregateOperator: MetricAggregateOperator.SUM_RATE,
disabled,
groupBy,
@ -66,7 +66,7 @@ export const getQueryBuilderQuerieswithFormula = ({
},
},
{
...initialQueryBuilderFormValues,
...initialQueryBuilderFormValuesMap.metrics,
aggregateOperator: MetricAggregateOperator.SUM_RATE,
disabled,
groupBy,

View File

@ -14,6 +14,7 @@ import { useParams } from 'react-router-dom';
import { Widgets } from 'types/api/dashboard/getAll';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
import { Button } from './styles';
@ -56,6 +57,7 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
tagFilterItems,
}),
clickhouse_sql: [],
id: uuid(),
}),
[getWidgetQueryBuilder, servicename, tagFilterItems],
);
@ -69,6 +71,7 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
tagFilterItems,
}),
clickhouse_sql: [],
id: uuid(),
}),
[getWidgetQueryBuilder, servicename, tagFilterItems],
);

View File

@ -15,6 +15,7 @@ import { useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Widgets } from 'types/api/dashboard/getAll';
import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
import { legend } from './constant';
@ -48,6 +49,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
tagFilterItems,
}),
clickhouse_sql: [],
id: uuid(),
}),
[getWidgetQueryBuilder, servicename, tagFilterItems],
);
@ -67,6 +69,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
tagFilterItems,
}),
clickhouse_sql: [],
id: uuid(),
}),
[getWidgetQueryBuilder, servicename, tagFilterItems],
);
@ -82,6 +85,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
tagFilterItems,
}),
clickhouse_sql: [],
id: uuid(),
}),
[getWidgetQueryBuilder, servicename, tagFilterItems],
);
@ -97,6 +101,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
tagFilterItems,
}),
clickhouse_sql: [],
id: uuid(),
}),
[getWidgetQueryBuilder, servicename, tagFilterItems],
);

View File

@ -21,6 +21,7 @@ import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
import { EQueryType } from 'types/common/dashboard';
import MetricReducer from 'types/reducer/metrics';
import { v4 as uuid } from 'uuid';
import {
errorPercentage,
@ -91,6 +92,7 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
topLevelOperations,
}),
clickhouse_sql: [],
id: uuid(),
}),
[getWidgetQueryBuilder, servicename, topLevelOperations, tagFilterItems],
);
@ -106,6 +108,7 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
topLevelOperations,
}),
clickhouse_sql: [],
id: uuid(),
}),
[servicename, topLevelOperations, tagFilterItems, getWidgetQueryBuilder],
);

View File

@ -11,6 +11,8 @@ import { useParams } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getErrorRate } from './utils';
function TopOperationsTable(props: TopOperationsTableProps): JSX.Element {
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
@ -89,10 +91,10 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element {
dataIndex: 'errorCount',
key: 'errorCount',
width: 50,
sorter: (a: TopOperationList, b: TopOperationList): number =>
a.errorCount - b.errorCount,
render: (value: number, record: TopOperationList): string =>
`${((value / record.numCalls) * 100).toFixed(2)} %`,
sorter: (first: TopOperationList, second: TopOperationList): number =>
getErrorRate(first) - getErrorRate(second),
render: (_, record: TopOperationList): string =>
`${getErrorRate(record).toFixed(2)} %`,
},
];

View File

@ -0,0 +1,4 @@
import { TopOperationList } from './TopOperationsTable';
export const getErrorRate = (list: TopOperationList): number =>
(list.errorCount / list.numCalls) * 100;

View File

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { initialQueryWithType } from 'constants/queryBuilder';
import { initialQueriesMap } from 'constants/queryBuilder';
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
@ -47,7 +47,7 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element {
history.push(
`${history.location.pathname}/new?graphType=${name}&widgetId=${
emptyLayout.i
}&${COMPOSITE_QUERY}=${JSON.stringify(initialQueryWithType)}`,
}&${COMPOSITE_QUERY}=${JSON.stringify(initialQueriesMap.metrics)}`,
);
} catch (error) {
notifications.error({

View File

@ -10,6 +10,7 @@ import { UpdateDashboardVariables } from 'store/actions/dashboard/updatedDashboa
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import DashboardReducer from 'types/reducer/dashboards';
import VariableItem from './VariableItem';
@ -29,6 +30,8 @@ function DashboardVariableSelection({
const [lastUpdatedVar, setLastUpdatedVar] = useState<string>('');
const { notifications } = useNotifications();
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const onVarChanged = (name: string): void => {
setLastUpdatedVar(name);
setUpdate(!update);
@ -36,19 +39,15 @@ function DashboardVariableSelection({
const onValueUpdate = (
name: string,
value:
| string
| string[]
| number
| number[]
| boolean
| boolean[]
| null
| undefined,
value: IDashboardVariable['selectedValue'],
): void => {
const updatedVariablesData = { ...variables };
updatedVariablesData[name].selectedValue = value;
updateDashboardVariables(updatedVariablesData, notifications);
if (role !== 'VIEWER') {
updateDashboardVariables(updatedVariablesData, notifications);
}
onVarChanged(name);
};
const onAllSelectedUpdate = (
@ -57,7 +56,10 @@ function DashboardVariableSelection({
): void => {
const updatedVariablesData = { ...variables };
updatedVariablesData[name].allSelected = value;
updateDashboardVariables(updatedVariablesData, notifications);
if (role !== 'VIEWER') {
updateDashboardVariables(updatedVariablesData, notifications);
}
onVarChanged(name);
};

View File

@ -6,28 +6,14 @@ import { useTranslation } from 'react-i18next';
import { useCopyToClipboard } from 'react-use';
import { DashboardData } from 'types/api/dashboard/getAll';
import { cleardQueryData, downloadObjectAsJson } from './util';
import { downloadObjectAsJson } from './util';
function ShareModal({
isJSONModalVisible,
onToggleHandler,
selectedData,
}: ShareModalProps): JSX.Element {
const getParsedValue = (): string => {
const updatedData: DashboardData = {
...selectedData,
widgets: selectedData.widgets?.map((widget) => ({
...widget,
queryData: {
...widget.queryData,
loading: false,
error: false,
errorMessage: '',
},
})),
};
return JSON.stringify(updatedData, null, 2);
};
const getParsedValue = (): string => JSON.stringify(selectedData, null, 2);
const [jsonValue, setJSONValue] = useState<string>(getParsedValue());
const [isViewJSON, setIsViewJSON] = useState<boolean>(false);
@ -53,7 +39,6 @@ function ShareModal({
}
}, [state.error, state.value, t, notifications]);
const selectedDataCleaned = cleardQueryData(selectedData);
const GetFooterComponent = useMemo(() => {
if (!isViewJSON) {
return (
@ -69,7 +54,7 @@ function ShareModal({
<Button
type="primary"
onClick={(): void => {
downloadObjectAsJson(selectedDataCleaned, selectedData.title);
downloadObjectAsJson(selectedData, selectedData.title);
}}
>
{t('download_json')}
@ -82,7 +67,7 @@ function ShareModal({
{t('copy_to_clipboard')}
</Button>
);
}, [isViewJSON, jsonValue, selectedData, selectedDataCleaned, setCopy, t]);
}, [isViewJSON, jsonValue, selectedData, setCopy, t]);
return (
<Modal

View File

@ -1,5 +1,3 @@
import { DashboardData } from 'types/api/dashboard/getAll';
export function downloadObjectAsJson(
exportObj: unknown,
exportName: string,
@ -14,18 +12,3 @@ export function downloadObjectAsJson(
downloadAnchorNode.click();
downloadAnchorNode.remove();
}
export function cleardQueryData(param: DashboardData): DashboardData {
return {
...param,
widgets: param.widgets?.map((widget) => ({
...widget,
queryData: {
...widget.queryData,
data: {
queryData: [],
},
},
})),
};
}

View File

@ -1,11 +1,13 @@
import { Button, Tabs, Typography } from 'antd';
import TextToolTip from 'components/TextToolTip';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { WidgetGraphProps } from 'container/NewWidget/types';
import { QueryBuilder } from 'container/QueryBuilder';
import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import useUrlQuery from 'hooks/useUrlQuery';
import { useCallback, useEffect, useState } from 'react';
import { useCallback } from 'react';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
@ -18,21 +20,31 @@ import AppActions from 'types/actions';
import { Widgets } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import AppReducer from 'types/reducer/app';
import DashboardReducer from 'types/reducer/dashboards';
import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
import PromQLQueryContainer from './QueryBuilder/promQL';
function QuerySection({ updateQuery, selectedGraph }: QueryProps): JSX.Element {
function QuerySection({
updateQuery,
selectedGraph,
selectedTime,
}: QueryProps): JSX.Element {
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const urlQuery = useUrlQuery();
const { featureResponse } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const [isInit, setIsInit] = useState<boolean>(false);
const { dashboards } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
const { dashboards, isLoadingQueryResult } = useSelector<
AppState,
DashboardReducer
>((state) => state.dashboards);
const getWidgetQueryRange = useGetWidgetQueryRange({
graphType: selectedGraph,
selectedTime: selectedTime.enum,
});
const [selectedDashboards] = dashboards;
const { widgets } = selectedDashboards.data;
@ -46,23 +58,11 @@ function QuerySection({ updateQuery, selectedGraph }: QueryProps): JSX.Element {
const { query } = selectedWidget;
const { compositeQuery } = useShareBuilderUrl({ defaultValue: query });
useEffect(() => {
if (!isInit && compositeQuery) {
setIsInit(true);
updateQuery({
updatedQuery: compositeQuery,
widgetId: urlQuery.get('widgetId') || '',
yAxisUnit: selectedWidget.yAxisUnit,
});
}
}, [isInit, compositeQuery, selectedWidget, urlQuery, updateQuery]);
useShareBuilderUrl({ defaultValue: query });
const handleStageQuery = useCallback(
(updatedQuery: Query): void => {
updateQuery({
updatedQuery,
widgetId: urlQuery.get('widgetId') || '',
yAxisUnit: selectedWidget.yAxisUnit,
});
@ -76,7 +76,9 @@ function QuerySection({ updateQuery, selectedGraph }: QueryProps): JSX.Element {
const handleQueryCategoryChange = (qCategory: string): void => {
const currentQueryType = qCategory as EQueryType;
handleStageQuery({ ...currentQuery, queryType: currentQueryType });
featureResponse.refetch().then(() => {
handleStageQuery({ ...currentQuery, queryType: currentQueryType });
});
};
const handleRunQuery = (): void => {
@ -115,7 +117,7 @@ function QuerySection({ updateQuery, selectedGraph }: QueryProps): JSX.Element {
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<TextToolTip text="This will temporarily save the current query and graph state. This will persist across tab change" />
<Button
loading={isLoadingQueryResult}
loading={getWidgetQueryRange.isFetching}
type="primary"
onClick={handleRunQuery}
>
@ -142,6 +144,7 @@ const mapDispatchToProps = (
interface QueryProps extends DispatchProps {
selectedGraph: GRAPH_TYPES;
selectedTime: WidgetGraphProps['selectedTime'];
}
export default connect(null, mapDispatchToProps)(QuerySection);

View File

@ -1,6 +1,8 @@
import { Card, Typography } from 'antd';
import Spinner from 'components/Spinner';
import GridGraphComponent from 'container/GridGraphComponent';
import { NewWidgetProps } from 'container/NewWidget';
import { WidgetGraphProps } from 'container/NewWidget/types';
import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange';
import getChartData from 'lib/getChartData';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
@ -12,6 +14,7 @@ import { NotFoundContainer } from './styles';
function WidgetGraph({
selectedGraph,
yAxisUnit,
selectedTime,
}: WidgetGraphProps): JSX.Element {
const { dashboards } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
@ -27,20 +30,28 @@ function WidgetGraph({
const selectedWidget = widgets.find((e) => e.id === widgetId);
const getWidgetQueryRange = useGetWidgetQueryRange({
graphType: selectedGraph,
selectedTime: selectedTime.enum,
});
if (selectedWidget === undefined) {
return <Card>Invalid widget</Card>;
}
const { queryData, title, opacity, isStacked } = selectedWidget;
const { title, opacity, isStacked } = selectedWidget;
if (queryData.error) {
if (getWidgetQueryRange.error) {
return (
<NotFoundContainer>
<Typography>{queryData.errorMessage}</Typography>
<Typography>{getWidgetQueryRange.error.message}</Typography>
</NotFoundContainer>
);
}
if (queryData.data.queryData.length === 0) {
if (getWidgetQueryRange.isLoading) {
return <Spinner size="large" tip="Loading..." />;
}
if (getWidgetQueryRange.data?.payload.data.result.length === 0) {
return (
<NotFoundContainer>
<Typography>No Data</Typography>
@ -49,7 +60,9 @@ function WidgetGraph({
}
const chartDataSet = getChartData({
queryData: [queryData.data],
queryData: [
{ queryData: getWidgetQueryRange.data?.payload.data.result ?? [] },
],
});
return (
@ -65,6 +78,4 @@ function WidgetGraph({
);
}
type WidgetGraphProps = NewWidgetProps;
export default WidgetGraph;

View File

@ -1,24 +1,28 @@
import { InfoCircleOutlined } from '@ant-design/icons';
import { Typography } from 'antd';
import { Card } from 'container/GridGraphLayout/styles';
import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { memo } from 'react';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import DashboardReducer from 'types/reducer/dashboards';
import { NewWidgetProps } from '../../index';
import { WidgetGraphProps } from '../../types';
import PlotTag from './PlotTag';
import { AlertIconContainer, Container, NotFoundContainer } from './styles';
import { AlertIconContainer, Container } from './styles';
import WidgetGraphComponent from './WidgetGraph';
function WidgetGraph({
selectedGraph,
yAxisUnit,
selectedTime,
}: WidgetGraphProps): JSX.Element {
const { dashboards, isQueryFired } = useSelector<AppState, DashboardReducer>(
const { currentQuery } = useQueryBuilder();
const { dashboards } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
const [selectedDashboard] = dashboards;
const { search } = useLocation();
@ -31,33 +35,31 @@ function WidgetGraph({
const selectedWidget = widgets.find((e) => e.id === widgetId);
const getWidgetQueryRange = useGetWidgetQueryRange({
graphType: selectedGraph,
selectedTime: selectedTime.enum,
});
if (selectedWidget === undefined) {
return <Card>Invalid widget</Card>;
}
const { queryData } = selectedWidget;
return (
<Container>
<PlotTag queryType={selectedWidget.query.queryType} />
{queryData.error && (
<AlertIconContainer color="red" title={queryData.errorMessage}>
<PlotTag queryType={currentQuery.queryType} />
{getWidgetQueryRange.error && (
<AlertIconContainer color="red" title={getWidgetQueryRange.error.message}>
<InfoCircleOutlined />
</AlertIconContainer>
)}
{!isQueryFired && (
<NotFoundContainer>
<Typography>No Data</Typography>
</NotFoundContainer>
)}
{isQueryFired && (
<WidgetGraphComponent selectedGraph={selectedGraph} yAxisUnit={yAxisUnit} />
)}
<WidgetGraphComponent
selectedTime={selectedTime}
selectedGraph={selectedGraph}
yAxisUnit={yAxisUnit}
/>
</Container>
);
}
type WidgetGraphProps = NewWidgetProps;
export default memo(WidgetGraph);

View File

@ -1,6 +1,6 @@
import { memo } from 'react';
import { NewWidgetProps } from '../index';
import { WidgetGraphProps } from '../types';
import QuerySection from './QuerySection';
import { QueryContainer } from './styles';
import WidgetGraph from './WidgetGraph';
@ -8,12 +8,17 @@ import WidgetGraph from './WidgetGraph';
function LeftContainer({
selectedGraph,
yAxisUnit,
}: NewWidgetProps): JSX.Element {
selectedTime,
}: WidgetGraphProps): JSX.Element {
return (
<>
<WidgetGraph selectedGraph={selectedGraph} yAxisUnit={yAxisUnit} />
<WidgetGraph
selectedTime={selectedTime}
selectedGraph={selectedGraph}
yAxisUnit={yAxisUnit}
/>
<QueryContainer>
<QuerySection selectedGraph={selectedGraph} />
<QuerySection selectedTime={selectedTime} selectedGraph={selectedGraph} />
</QueryContainer>
</>
);

View File

@ -1,25 +1,18 @@
import { LockFilled } from '@ant-design/icons';
import { Button, Modal, Tooltip, Typography } from 'antd';
import { FeatureKeys } from 'constants/features';
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames';
import ROUTES from 'constants/routes';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import history from 'lib/history';
import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { connect, useDispatch, useSelector } from 'react-redux';
import { generatePath, useLocation, useParams } from 'react-router-dom';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import {
GetQueryResults,
GetQueryResultsProps,
} from 'store/actions/dashboard/getQueryResults';
import {
SaveDashboard,
SaveDashboardProps,
@ -27,10 +20,10 @@ import {
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { FLUSH_DASHBOARD } from 'types/actions/dashboard';
import { Widgets } from 'types/api/dashboard/getAll';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import AppReducer from 'types/reducer/app';
import DashboardReducer from 'types/reducer/dashboards';
import { GlobalReducer } from 'types/reducer/globalTime';
import LeftContainer from './LeftContainer';
import QueryTypeTag from './LeftContainer/QueryTypeTag';
@ -43,21 +36,15 @@ import {
PanelContainer,
RightContainerWrapper,
} from './styles';
import { NewWidgetProps } from './types';
function NewWidget({
selectedGraph,
saveSettingOfPanel,
getQueryResults,
}: Props): JSX.Element {
const urlQuery = useUrlQuery();
function NewWidget({ selectedGraph, saveSettingOfPanel }: Props): JSX.Element {
const dispatch = useDispatch();
const { dashboards } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
const { selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const { currentQuery } = useQueryBuilder();
const { featureResponse } = useSelector<AppState, AppReducer>(
(state) => state.app,
@ -161,27 +148,6 @@ function NewWidget({
history.push(generatePath(ROUTES.DASHBOARD, { dashboardId }));
}, [dashboardId, dispatch]);
const getQueryResult = useCallback(() => {
const compositeQuery = urlQuery.get(COMPOSITE_QUERY);
if ((selectedWidget?.id.length !== 0 && compositeQuery) || compositeQuery) {
getQueryResults({
query: JSON.parse(compositeQuery),
selectedTime: selectedTime.enum,
widgetId: selectedWidget?.id || '',
graphType,
globalSelectedInterval,
variables: getDashboardVariables(),
});
}
}, [
selectedTime.enum,
selectedWidget?.id,
getQueryResults,
globalSelectedInterval,
graphType,
urlQuery,
]);
const setGraphHandler = (type: ITEMS): void => {
const params = new URLSearchParams(search);
params.set('graphType', type);
@ -189,10 +155,6 @@ function NewWidget({
setGraphType(type);
};
useEffect(() => {
getQueryResult();
}, [getQueryResult]);
const onSaveDashboard = useCallback((): void => {
setSaveModal(true);
}, []);
@ -201,15 +163,53 @@ function NewWidget({
FeatureKeys.QUERY_BUILDER_PANELS,
);
const isNewTraceLogsAvailable = useMemo(
() =>
isQueryBuilderActive &&
currentQuery.queryType === EQueryType.QUERY_BUILDER &&
currentQuery.builder.queryData.find(
(query) => query.dataSource !== DataSource.METRICS,
) !== undefined,
[
currentQuery.builder.queryData,
currentQuery.queryType,
isQueryBuilderActive,
],
);
const isSaveDisabled = useMemo(() => {
// new created dashboard
if (selectedWidget?.id === 'empty') {
return isNewTraceLogsAvailable;
}
const isTraceOrLogsQueryBuilder =
currentQuery.builder.queryData.find(
(query) =>
query.dataSource === DataSource.TRACES ||
query.dataSource === DataSource.LOGS,
) !== undefined;
if (isTraceOrLogsQueryBuilder) {
return false;
}
return isNewTraceLogsAvailable;
}, [
currentQuery.builder.queryData,
selectedWidget?.id,
isNewTraceLogsAvailable,
]);
return (
<Container>
<ButtonContainer>
{isQueryBuilderActive && (
{isSaveDisabled && (
<Tooltip title={MESSAGE.PANEL}>
<Button
icon={<LockFilled />}
type="primary"
disabled={isQueryBuilderActive}
disabled={isSaveDisabled}
onClick={onSaveDashboard}
>
Save
@ -217,12 +217,8 @@ function NewWidget({
</Tooltip>
)}
{!isQueryBuilderActive && (
<Button
type="primary"
disabled={isQueryBuilderActive}
onClick={onSaveDashboard}
>
{!isSaveDisabled && (
<Button type="primary" disabled={isSaveDisabled} onClick={onSaveDashboard}>
Save
</Button>
)}
@ -231,7 +227,11 @@ function NewWidget({
<PanelContainer>
<LeftContainerWrapper flex={5}>
<LeftContainer selectedGraph={graphType} yAxisUnit={yAxisUnit} />
<LeftContainer
selectedTime={selectedTime}
selectedGraph={graphType}
yAxisUnit={yAxisUnit}
/>
</LeftContainerWrapper>
<RightContainerWrapper flex={1}>
@ -270,34 +270,24 @@ function NewWidget({
width={600}
>
<Typography>
Your graph built with{' '}
<QueryTypeTag queryType={selectedWidget?.query.queryType} /> query will be
saved. Press OK to confirm.
Your graph built with <QueryTypeTag queryType={currentQuery.queryType} />{' '}
query will be saved. Press OK to confirm.
</Typography>
</Modal>
</Container>
);
}
export interface NewWidgetProps {
selectedGraph: GRAPH_TYPES;
yAxisUnit: Widgets['yAxisUnit'];
}
interface DispatchProps {
saveSettingOfPanel: (
props: SaveDashboardProps,
) => (dispatch: Dispatch<AppActions>) => void;
getQueryResults: (
props: GetQueryResultsProps,
) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
saveSettingOfPanel: bindActionCreators(SaveDashboard, dispatch),
getQueryResults: bindActionCreators(GetQueryResults, dispatch),
});
type Props = DispatchProps & NewWidgetProps;

View File

@ -0,0 +1,13 @@
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { Widgets } from 'types/api/dashboard/getAll';
import { timePreferance } from './RightContainer/timeItems';
export interface NewWidgetProps {
selectedGraph: GRAPH_TYPES;
yAxisUnit: Widgets['yAxisUnit'];
}
export interface WidgetGraphProps extends NewWidgetProps {
selectedTime: timePreferance;
}

View File

@ -0,0 +1,43 @@
import { SearchOutlined } from '@ant-design/icons';
import { Input } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useTranslation } from 'react-i18next';
import { OptionsMenuConfig } from '..';
import { FieldTitle } from '../styles';
import { AddColumnSelect, AddColumnWrapper, SearchIconWrapper } from './styles';
function AddColumnField({ config }: AddColumnFieldProps): JSX.Element | null {
const { t } = useTranslation(['trace']);
const isDarkMode = useIsDarkMode();
if (!config) return null;
return (
<AddColumnWrapper direction="vertical">
<FieldTitle>{t('options_menu.addColumn')}</FieldTitle>
<Input.Group compact>
<AddColumnSelect
allowClear
maxTagCount={0}
size="small"
mode="multiple"
placeholder="Search"
options={config.options}
value={config.value}
onChange={config.onChange}
/>
<SearchIconWrapper $isDarkMode={isDarkMode}>
<SearchOutlined />
</SearchIconWrapper>
</Input.Group>
</AddColumnWrapper>
);
}
interface AddColumnFieldProps {
config: OptionsMenuConfig['addColumn'];
}
export default AddColumnField;

View File

@ -0,0 +1,28 @@
import { Card, Select, SelectProps, Space } from 'antd';
import { themeColors } from 'constants/theme';
import { FunctionComponent } from 'react';
import styled from 'styled-components';
export const SearchIconWrapper = styled(Card)<{ $isDarkMode: boolean }>`
width: 15%;
border-color: ${({ $isDarkMode }): string =>
$isDarkMode ? themeColors.borderDarkGrey : themeColors.borderLightGrey};
.ant-card-body {
display: flex;
justify-content: center;
align-items: center;
padding: 0.25rem;
font-size: 0.875rem;
}
`;
export const AddColumnSelect: FunctionComponent<SelectProps> = styled(
Select,
)<SelectProps>`
width: 85%;
`;
export const AddColumnWrapper = styled(Space)`
width: 100%;
`;

View File

@ -0,0 +1,33 @@
import { useTranslation } from 'react-i18next';
import { OptionsMenuConfig } from '..';
import { FieldTitle } from '../styles';
import { FormatFieldWrapper, RadioButton, RadioGroup } from './styles';
function FormatField({ config }: FormatFieldProps): JSX.Element | null {
const { t } = useTranslation(['trace']);
if (!config) return null;
return (
<FormatFieldWrapper direction="vertical">
<FieldTitle>{t('options_menu.format')}</FieldTitle>
<RadioGroup
size="small"
buttonStyle="solid"
value={config.value}
onChange={config.onChange}
>
<RadioButton value="row">{t('options_menu.row')}</RadioButton>
<RadioButton value="default">{t('options_menu.default')}</RadioButton>
<RadioButton value="column">{t('options_menu.column')}</RadioButton>
</RadioGroup>
</FormatFieldWrapper>
);
}
interface FormatFieldProps {
config: OptionsMenuConfig['format'];
}
export default FormatField;

View File

@ -0,0 +1,17 @@
import { Radio, Space } from 'antd';
import styled from 'styled-components';
export const FormatFieldWrapper = styled(Space)`
width: 100%;
margin-bottom: 1.125rem;
`;
export const RadioGroup = styled(Radio.Group)`
display: flex;
text-align: center;
`;
export const RadioButton = styled(Radio.Button)`
font-size: 0.75rem;
flex: 1;
`;

View File

@ -0,0 +1,29 @@
import { useTranslation } from 'react-i18next';
import { OptionsMenuConfig } from '..';
import { FieldTitle } from '../styles';
import { MaxLinesFieldWrapper, MaxLinesInput } from './styles';
function MaxLinesField({ config }: MaxLinesFieldProps): JSX.Element | null {
const { t } = useTranslation(['trace']);
if (!config) return null;
return (
<MaxLinesFieldWrapper>
<FieldTitle>{t('options_menu.maxLines')}</FieldTitle>
<MaxLinesInput
controls
size="small"
value={config.value}
onChange={config.onChange}
/>
</MaxLinesFieldWrapper>
);
}
interface MaxLinesFieldProps {
config: OptionsMenuConfig['maxLines'];
}
export default MaxLinesField;

View File

@ -0,0 +1,12 @@
import { InputNumber } from 'antd';
import styled from 'styled-components';
export const MaxLinesFieldWrapper = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`;
export const MaxLinesInput = styled(InputNumber)`
max-width: 46px;
`;

View File

@ -0,0 +1,57 @@
import { SettingFilled, SettingOutlined } from '@ant-design/icons';
import {
InputNumberProps,
Popover,
RadioProps,
SelectProps,
Space,
} from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import AddColumnField from './AddColumnField';
import FormatField from './FormatField';
import MaxLinesField from './MaxLinesField';
import { OptionsContainer, OptionsContentWrapper } from './styles';
function OptionsMenu({ config }: OptionsMenuProps): JSX.Element {
const { t } = useTranslation(['trace']);
const isDarkMode = useIsDarkMode();
const OptionsContent = useMemo(
() => (
<OptionsContentWrapper direction="vertical">
{config?.format && <FormatField config={config.format} />}
{config?.maxLines && <MaxLinesField config={config.maxLines} />}
{config?.addColumn && <AddColumnField config={config.addColumn} />}
</OptionsContentWrapper>
),
[config],
);
const SettingIcon = isDarkMode ? SettingOutlined : SettingFilled;
return (
<OptionsContainer>
<Popover placement="bottom" trigger="click" content={OptionsContent}>
<Space align="center">
{t('options_menu.options')}
<SettingIcon />
</Space>
</Popover>
</OptionsContainer>
);
}
export type OptionsMenuConfig = {
format?: Pick<RadioProps, 'value' | 'onChange'>;
maxLines?: Pick<InputNumberProps, 'value' | 'onChange'>;
addColumn?: Pick<SelectProps, 'options' | 'value' | 'onChange'>;
};
interface OptionsMenuProps {
config: OptionsMenuConfig;
}
export default OptionsMenu;

View File

@ -0,0 +1,19 @@
import { Card, Space, Typography } from 'antd';
import styled from 'styled-components';
export const OptionsContainer = styled(Card)`
.ant-card-body {
display: flex;
padding: 0.25rem 0.938rem;
cursor: pointer;
}
`;
export const OptionsContentWrapper = styled(Space)`
min-width: 11rem;
padding: 0.25rem 0.5rem;
`;
export const FieldTitle = styled(Typography.Text)`
font-size: 0.75rem;
`;

View File

@ -55,12 +55,18 @@ function PendingInvitesContainer(): JSX.Element {
queryKey: ['getPendingInvites', user?.accessJwt],
});
const toggleModal = (value: boolean): void => {
setIsInviteTeamMemberModalOpen(value);
};
const [dataSource, setDataSource] = useState<DataProps[]>([]);
const toggleModal = useCallback(
(value: boolean): void => {
setIsInviteTeamMemberModalOpen(value);
if (!value) {
form.resetFields();
}
},
[form],
);
const { hash } = useLocation();
const getParsedInviteData = useCallback(
@ -79,7 +85,7 @@ function PendingInvitesContainer(): JSX.Element {
if (hash === INVITE_MEMBERS_HASH) {
toggleModal(true);
}
}, [hash]);
}, [hash, toggleModal]);
useEffect(() => {
if (
@ -225,7 +231,13 @@ function PendingInvitesContainer(): JSX.Element {
});
}
},
[getParsedInviteData, getPendingInvitesResponse, notifications, t],
[
getParsedInviteData,
getPendingInvitesResponse,
notifications,
t,
toggleModal,
],
);
return (

View File

@ -1,4 +1,5 @@
import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems';
import { ReactNode } from 'react';
import { DataSource } from 'types/common/queryBuilder';
export type QueryBuilderConfig =
@ -11,4 +12,5 @@ export type QueryBuilderConfig =
export type QueryBuilderProps = {
config?: QueryBuilderConfig;
panelType: ITEMS;
actions?: ReactNode;
};

View File

@ -0,0 +1,6 @@
import { Col } from 'antd';
import styled from 'styled-components';
export const ActionsWrapperStyled = styled(Col)`
padding-right: 1rem;
`;

View File

@ -11,15 +11,16 @@ import { Formula, Query } from './components';
// ** Types
import { QueryBuilderProps } from './QueryBuilder.interfaces';
// ** Styles
import { ActionsWrapperStyled } from './QueryBuilder.styled';
export const QueryBuilder = memo(function QueryBuilder({
config,
panelType,
actions,
}: QueryBuilderProps): JSX.Element {
const {
currentQuery,
setupInitialDataSource,
resetQueryBuilderInfo,
addNewBuilderQuery,
addNewFormula,
handleSetPanelType,
@ -35,13 +36,6 @@ export const QueryBuilder = memo(function QueryBuilder({
handleSetPanelType(panelType);
}, [handleSetPanelType, panelType]);
useEffect(
() => (): void => {
resetQueryBuilderInfo();
},
[resetQueryBuilderInfo],
);
const isDisabledQueryButton = useMemo(
() => currentQuery.builder.queryData.length >= MAX_QUERIES,
[currentQuery],
@ -60,7 +54,7 @@ export const QueryBuilder = memo(function QueryBuilder({
);
return (
<Row gutter={[0, 20]} justify="start">
<Row style={{ width: '100%' }} gutter={[0, 20]} justify="start">
<Col span={24}>
<Row gutter={[0, 50]}>
{currentQuery.builder.queryData.map((query, index) => (
@ -81,28 +75,31 @@ export const QueryBuilder = memo(function QueryBuilder({
</Row>
</Col>
<Row gutter={[20, 0]}>
<Col>
<Button
disabled={isDisabledQueryButton}
type="primary"
icon={<PlusOutlined />}
onClick={addNewBuilderQuery}
>
Query
</Button>
</Col>
<Col>
<Button
disabled={isDisabledFormulaButton}
onClick={addNewFormula}
type="primary"
icon={<PlusOutlined />}
>
Formula
</Button>
</Col>
</Row>
<ActionsWrapperStyled span={24}>
<Row gutter={[20, 0]}>
<Col>
<Button
disabled={isDisabledQueryButton}
type="primary"
icon={<PlusOutlined />}
onClick={addNewBuilderQuery}
>
Query
</Button>
</Col>
<Col>
<Button
disabled={isDisabledFormulaButton}
onClick={addNewFormula}
type="primary"
icon={<PlusOutlined />}
>
Formula
</Button>
</Col>
{actions}
</Row>
</ActionsWrapperStyled>
</Row>
);
});

View File

@ -1,4 +1,4 @@
import { Col, Row } from 'antd';
import { Col, Row, Typography } from 'antd';
import { Fragment, memo, ReactNode, useState } from 'react';
// ** Types
@ -46,7 +46,9 @@ export const AdditionalFiltersToggler = memo(function AdditionalFiltersToggler({
<Col span={24}>
<StyledInner onClick={handleToggleOpenFilters}>
{isOpenedFilters ? <StyledIconClose /> : <StyledIconOpen />}
{!isOpenedFilters && <span>Add conditions for {filtersTexts}</span>}
{!isOpenedFilters && (
<Typography>Add conditions for {filtersTexts}</Typography>
)}
</StyledInner>
</Col>
{isOpenedFilters && <Col span={24}>{children}</Col>}

View File

@ -1,3 +1,4 @@
import { Typography } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { memo } from 'react';
@ -11,5 +12,9 @@ export const FilterLabel = memo(function FilterLabel({
}: FilterLabelProps): JSX.Element {
const isDarkMode = useIsDarkMode();
return <StyledLabel isDarkMode={isDarkMode}>{label}</StyledLabel>;
return (
<StyledLabel isDarkMode={isDarkMode}>
<Typography>{label}</Typography>
</StyledLabel>
);
});

View File

@ -3,19 +3,17 @@ import userEvent from '@testing-library/user-event';
// Constants
import {
HAVING_OPERATORS,
initialQueryBuilderFormValues,
initialQueryBuilderFormValuesMap,
} from 'constants/queryBuilder';
import { transformFromStringToHaving } from 'lib/query/transformQueryBuilderData';
// ** Types
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
// ** Components
import { HavingFilter } from '../HavingFilter';
const valueWithAttributeAndOperator: IBuilderQuery = {
...initialQueryBuilderFormValues,
dataSource: DataSource.LOGS,
...initialQueryBuilderFormValuesMap.logs,
aggregateOperator: 'SUM',
aggregateAttribute: {
isColumn: false,
@ -29,7 +27,10 @@ describe('Having filter behaviour', () => {
test('Having filter render is rendered', () => {
const mockFn = jest.fn();
const { unmount } = render(
<HavingFilter query={initialQueryBuilderFormValues} onChange={mockFn} />,
<HavingFilter
query={initialQueryBuilderFormValuesMap.metrics}
onChange={mockFn}
/>,
);
const selectId = 'havingSelect';
@ -44,7 +45,10 @@ describe('Having filter behaviour', () => {
test('Having render is disabled initially', () => {
const mockFn = jest.fn();
const { unmount } = render(
<HavingFilter query={initialQueryBuilderFormValues} onChange={mockFn} />,
<HavingFilter
query={initialQueryBuilderFormValuesMap.metrics}
onChange={mockFn}
/>,
);
const input = screen.getByRole('combobox');

View File

@ -30,6 +30,7 @@ export const routeConfig: Record<string, QueryParams[]> = {
[ROUTES.SETTINGS]: [QueryParams.resourceAttributes],
[ROUTES.SIGN_UP]: [QueryParams.resourceAttributes],
[ROUTES.SOMETHING_WENT_WRONG]: [QueryParams.resourceAttributes],
[ROUTES.TRACES_EXPLORER]: [QueryParams.resourceAttributes],
[ROUTES.TRACE]: [QueryParams.resourceAttributes],
[ROUTES.TRACE_DETAIL]: [QueryParams.resourceAttributes],
[ROUTES.UN_AUTHORIZED]: [QueryParams.resourceAttributes],

View File

@ -1,8 +1,8 @@
export const getQueryString = (
avialableParams: string[],
availableParams: string[],
params: URLSearchParams,
): string[] =>
avialableParams.map((param) => {
availableParams.map((param) => {
if (params.has(param)) {
return `${param}=${params.get(param)}`;
}

View File

@ -1,5 +1,5 @@
import { CheckCircleTwoTone, WarningOutlined } from '@ant-design/icons';
import { Menu, Space, Typography } from 'antd';
import { Menu, MenuProps } from 'antd';
import getLocalStorageKey from 'api/browser/localstorage/get';
import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
import ROUTES from 'constants/routes';
@ -27,7 +27,6 @@ import {
Sider,
SlackButton,
SlackMenuItemContainer,
Tags,
VersionContainer,
} from './styles';
@ -42,6 +41,7 @@ function SideNav(): JSX.Element {
>((state) => state.app);
const { pathname, search } = useLocation();
const { t } = useTranslation('');
const onCollapse = useCallback(() => {
@ -55,9 +55,9 @@ function SideNav(): JSX.Element {
const onClickHandler = useCallback(
(to: string) => {
const params = new URLSearchParams(search);
const avialableParams = routeConfig[to];
const availableParams = routeConfig[to];
const queryString = getQueryString(avialableParams, params);
const queryString = getQueryString(availableParams || [], params);
if (pathname !== to) {
history.push(`${to}?${queryString.join('&')}`);
@ -66,6 +66,10 @@ function SideNav(): JSX.Element {
[pathname, search],
);
const onClickMenuHandler: MenuProps['onClick'] = (e) => {
onClickHandler(e.key);
};
const onClickSlackHandler = (): void => {
window.open('https://signoz.io/slack', '_blank');
};
@ -104,30 +108,17 @@ function SideNav(): JSX.Element {
},
];
const currentMenu = useMemo(
() => menus.find((menu) => pathname.startsWith(menu.to)),
[pathname],
);
const currentMenu = useMemo(() => {
const routeKeys = Object.keys(ROUTES) as (keyof typeof ROUTES)[];
const currentRouteKey = routeKeys.find((key) => {
const route = ROUTES[key];
return pathname === route;
});
const items = [
...menus.map(({ to, Icon, name, tags, children }) => ({
key: to,
icon: <Icon />,
onClick: (): void => onClickHandler(to),
label: (
<Space>
<div>{name}</div>
{tags &&
tags.map((e) => (
<Tags key={e}>
<Typography.Text>{e}</Typography.Text>
</Tags>
))}
</Space>
),
children,
})),
];
if (!currentRouteKey) return null;
return ROUTES[currentRouteKey];
}, [pathname]);
const sidebarItems = (props: SidebarItem, index: number): SidebarItem => ({
key: `${index}`,
@ -141,10 +132,11 @@ function SideNav(): JSX.Element {
<Menu
theme="dark"
defaultSelectedKeys={[ROUTES.APPLICATION]}
selectedKeys={currentMenu ? [currentMenu?.to] : []}
selectedKeys={currentMenu ? [currentMenu] : []}
mode="vertical"
style={styles}
items={items}
items={menus}
onClick={onClickMenuHandler}
/>
{sidebar.map((props, index) => (
<SlackMenuItemContainer
@ -155,7 +147,7 @@ function SideNav(): JSX.Element {
<Menu
theme="dark"
defaultSelectedKeys={[ROUTES.APPLICATION]}
selectedKeys={currentMenu ? [currentMenu?.to] : []}
selectedKeys={currentMenu ? [currentMenu] : []}
mode="inline"
style={styles}
items={[sidebarItems(props, index)]}

View File

@ -1,84 +0,0 @@
import {
AlertOutlined,
AlignLeftOutlined,
ApiOutlined,
BarChartOutlined,
BugOutlined,
DashboardFilled,
DeploymentUnitOutlined,
LineChartOutlined,
MenuOutlined,
SettingOutlined,
} from '@ant-design/icons';
import type { MenuProps } from 'antd';
import ROUTES from 'constants/routes';
const menus: SidebarMenu[] = [
{
Icon: BarChartOutlined,
to: ROUTES.APPLICATION,
name: 'Services',
},
{
Icon: MenuOutlined,
to: ROUTES.TRACE,
name: 'Traces',
},
{
Icon: AlignLeftOutlined,
to: ROUTES.LOGS,
name: 'Logs',
// tags: ['Beta'],
// children: [
// {
// key: ROUTES.LOGS,
// label: 'Search',
// },
// ],
},
{
Icon: DashboardFilled,
to: ROUTES.ALL_DASHBOARD,
name: 'Dashboards',
},
{
Icon: AlertOutlined,
to: ROUTES.LIST_ALL_ALERT,
name: 'Alerts',
},
{
Icon: BugOutlined,
to: ROUTES.ALL_ERROR,
name: 'Exceptions',
},
{
to: ROUTES.SERVICE_MAP,
name: 'Service Map',
Icon: DeploymentUnitOutlined,
},
{
Icon: LineChartOutlined,
to: ROUTES.USAGE_EXPLORER,
name: 'Usage Explorer',
},
{
Icon: SettingOutlined,
to: ROUTES.SETTINGS,
name: 'Settings',
},
{
Icon: ApiOutlined,
to: ROUTES.INSTRUMENTATION,
name: 'Get Started',
},
];
interface SidebarMenu {
to: string;
name: string;
Icon: typeof ApiOutlined;
tags?: string[];
children?: Required<MenuProps>['items'][number][];
}
export default menus;

View File

@ -0,0 +1,113 @@
import {
AlertOutlined,
AlignLeftOutlined,
ApiOutlined,
BarChartOutlined,
BugOutlined,
DashboardFilled,
DeploymentUnitOutlined,
LineChartOutlined,
MenuOutlined,
SettingOutlined,
} from '@ant-design/icons';
import { MenuProps, Space, Typography } from 'antd';
import ROUTES from 'constants/routes';
import { Tags } from './styles';
type MenuItem = Required<MenuProps>['items'][number];
export const createLabelWithTags = (
label: string,
tags: string[],
): JSX.Element => (
<Space>
<div>{label}</div>
{tags.map((tag) => (
<Tags key={tag}>
<Typography.Text>{tag}</Typography.Text>
</Tags>
))}
</Space>
);
const menus: SidebarMenu[] = [
{
key: ROUTES.APPLICATION,
label: 'Services',
icon: <BarChartOutlined />,
},
{
key: ROUTES.TRACE,
label: 'Traces',
icon: <MenuOutlined />,
// children: [
// {
// key: ROUTES.TRACE,
// label: 'Traces',
// },
// TODO: uncomment when will be ready explorer
// {
// key: ROUTES.TRACES_EXPLORER,
// label: "Explorer",
// },
// ],
},
{
key: ROUTES.LOGS,
label: 'Logs',
icon: <AlignLeftOutlined />,
// children: [
// {
// key: ROUTES.LOGS,
// label: 'Search',
// },
// TODO: uncomment when will be ready explorer
// {
// key: ROUTES.LOGS_EXPLORER,
// label: 'Views',
// },
// ],
},
{
key: ROUTES.ALL_DASHBOARD,
label: 'Dashboards',
icon: <DashboardFilled />,
},
{
key: ROUTES.LIST_ALL_ALERT,
label: 'Alerts',
icon: <AlertOutlined />,
},
{
key: ROUTES.ALL_ERROR,
label: 'Exceptions',
icon: <BugOutlined />,
},
{
key: ROUTES.SERVICE_MAP,
label: 'Service Map',
icon: <DeploymentUnitOutlined />,
},
{
key: ROUTES.USAGE_EXPLORER,
label: 'Usage Explorer',
icon: <LineChartOutlined />,
},
{
key: ROUTES.SETTINGS,
label: 'Settings',
icon: <SettingOutlined />,
},
{
key: ROUTES.INSTRUMENTATION,
label: 'Get Started',
icon: <ApiOutlined />,
},
];
type SidebarMenu = MenuItem & {
tags?: string[];
};
export default menus;

View File

@ -5,6 +5,7 @@ import { Link, RouteComponentProps, withRouter } from 'react-router-dom';
const breadcrumbNameMap = {
[ROUTES.APPLICATION]: 'Services',
[ROUTES.TRACE]: 'Traces',
[ROUTES.TRACES_EXPLORER]: 'Traces Explorer',
[ROUTES.SERVICE_MAP]: 'Service Map',
[ROUTES.USAGE_EXPLORER]: 'Usage Explorer',
[ROUTES.INSTRUMENTATION]: 'Get Started',
@ -19,6 +20,7 @@ const breadcrumbNameMap = {
[ROUTES.LIST_ALL_ALERT]: 'Alerts',
[ROUTES.ALL_DASHBOARD]: 'Dashboard',
[ROUTES.LOGS]: 'Logs',
[ROUTES.LOGS_EXPLORER]: 'Logs Explorer',
};
function ShowBreadcrumbs(props: RouteComponentProps): JSX.Element {

View File

@ -0,0 +1,25 @@
import Controls from 'container/Controls';
import { memo } from 'react';
import { Container } from './styles';
function TraceExplorerControls(): JSX.Element | null {
const handleCountItemsPerPageChange = (): void => {};
const handleNavigatePrevious = (): void => {};
const handleNavigateNext = (): void => {};
return (
<Container>
<Controls
isLoading={false}
count={0}
countPerPage={0}
handleNavigatePrevious={handleNavigatePrevious}
handleNavigateNext={handleNavigateNext}
handleCountItemsPerPageChange={handleCountItemsPerPageChange}
/>
</Container>
);
}
export default memo(TraceExplorerControls);

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