Merge pull request #956 from SigNoz/release/v0.7.5

Release/v0.7.5
This commit is contained in:
Ankit Nayan 2022-04-07 17:12:16 +05:30 committed by GitHub
commit aa5100261d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
110 changed files with 2603 additions and 1506 deletions

33
.editorconfig Normal file
View File

@ -0,0 +1,33 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
# Matches multiple files with brace expansion notation
# Set default charset
[*.{js,py}]
charset = utf-8
# 4 space indentation
[*.py]
indent_style = space
indent_size = 4
# Tab indentation (no size specified)
[Makefile]
indent_style = tab
# Indentation override for all JS under lib directory
[lib/**.js]
indent_style = space
indent_size = 2
# Matches the exact files either package.json or .travis.yml
[{package.json,.travis.yml}]
indent_style = space
indent_size = 2

View File

@ -115,11 +115,9 @@ down-arm:
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.arm.yaml down -v
clear-standalone-data:
@cd $(STANDALONE_DIRECTORY)
@docker run --rm -v "data:/pwd" busybox \
@docker run --rm -v "$(PWD)/$(STANDALONE_DIRECTORY)/data:/pwd" busybox \
sh -c "cd /pwd && rm -rf alertmanager/* clickhouse/* signoz/*"
clear-swarm-data:
@cd $(SWARM_DIRECTORY)
@docker run --rm -v "data:/pwd" busybox \
@docker run --rm -v "$(PWD)/$(SWARM_DIRECTORY)/data:/pwd" busybox \
sh -c "cd /pwd && rm -rf alertmanager/* clickhouse/* signoz/*"

View File

@ -1,11 +1,8 @@
<?xml version="1.0"?>
<yandex>
<logger>
<level>trace</level>
<log>/var/log/clickhouse-server/clickhouse-server.log</log>
<errorlog>/var/log/clickhouse-server/clickhouse-server.err.log</errorlog>
<size>1000M</size>
<count>10</count>
<level>information</level>
<console>1</console>
</logger>
<http_port>8123</http_port>
@ -45,6 +42,34 @@
</client>
</openSSL>
<!-- Example config for tiered storage -->
<!-- <storage_configuration>
<disks>
<default>
<keep_free_space_bytes>10485760</keep_free_space_bytes>
</default>
<s3>
<type>s3</type>
<endpoint>https://BUCKET-NAME.s3.amazonaws.com/data/</endpoint>
<access_key_id>ACCESS-KEY-ID</access_key_id>
<secret_access_key>SECRET-ACCESS-KEY</secret_access_key>
</s3>
</disks>
<policies>
<tiered>
<volumes>
<default>
<disk>default</disk>
</default>
<s3>
<disk>s3</disk>
</s3>
</volumes>
</tiered>
</policies>
</storage_configuration> -->
<!-- Default root page on http[s] server. For example load UI from https://tabix.io/ when opening http://localhost:8123 -->
<!--
<http_server_default_response><![CDATA[<html ng-app="SMI2"><head><base href="http://ui.tabix.io/"></head><body><div ui-view="" class="content-ui"></div><script src="http://loader.tabix.io/master.js"></script></body></html>]]></http_server_default_response>

View File

@ -3,12 +3,19 @@ version: "3.9"
services:
clickhouse:
image: yandex/clickhouse-server:21.12.3.32
# ports:
# - "9000:9000"
# - "8123:8123"
volumes:
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
- ./data/clickhouse/:/var/lib/clickhouse/
deploy:
restart_policy:
condition: on-failure
logging:
options:
max-size: 50m
max-file: "3"
healthcheck:
# "clickhouse", "client", "-u ${CLICKHOUSE_USER}", "--password ${CLICKHOUSE_PASSWORD}", "-q 'SELECT 1'"
test: ["CMD", "wget", "--spider", "-q", "localhost:8123/ping"]
@ -17,7 +24,7 @@ services:
retries: 3
alertmanager:
image: signoz/alertmanager:0.6.0
image: signoz/alertmanager:0.6.1
volumes:
- ./data/alertmanager:/data
command:
@ -30,7 +37,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.7.4
image: signoz/query-service:0.7.5
command: ["-config=/root/config/prometheus.yml"]
ports:
- "8080:8080"
@ -45,6 +52,11 @@ services:
- TELEMETRY_ENABLED=true
- DEPLOYMENT_TYPE=docker-swarm
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
interval: 30s
timeout: 5s
retries: 3
deploy:
restart_policy:
condition: on-failure
@ -52,8 +64,12 @@ services:
- clickhouse
frontend:
image: signoz/frontend:0.7.4
image: signoz/frontend:0.7.5
deploy:
restart_policy:
condition: on-failure
depends_on:
- alertmanager
- query-service
ports:
- "3301:3301"

View File

@ -1,11 +1,8 @@
<?xml version="1.0"?>
<yandex>
<logger>
<level>trace</level>
<log>/var/log/clickhouse-server/clickhouse-server.log</log>
<errorlog>/var/log/clickhouse-server/clickhouse-server.err.log</errorlog>
<size>1000M</size>
<count>10</count>
<level>information</level>
<console>1</console>
</logger>
<http_port>8123</http_port>
@ -46,30 +43,31 @@
</openSSL>
<!-- Example config for tiered storage -->
<!-- <storage_configuration> -->
<!-- <disks> -->
<!-- <default> -->
<!-- </default> -->
<!-- <s3> -->
<!-- <type>s3</type> -->
<!-- <endpoint>http://172.17.0.1:9100/test/random/</endpoint> -->
<!-- <access_key_id>ash</access_key_id> -->
<!-- <secret_access_key>password</secret_access_key> -->
<!-- </s3> -->
<!-- </disks> -->
<!-- <policies> -->
<!-- <tiered> -->
<!-- <volumes> -->
<!-- <default> -->
<!-- <disk>default</disk> -->
<!-- </default> -->
<!-- <s3> -->
<!-- <disk>s3</disk> -->
<!-- </s3> -->
<!-- </volumes> -->
<!-- </tiered> -->
<!-- </policies> -->
<!-- </storage_configuration> -->
<!-- <storage_configuration>
<disks>
<default>
<keep_free_space_bytes>10485760</keep_free_space_bytes>
</default>
<s3>
<type>s3</type>
<endpoint>https://BUCKET-NAME.s3.amazonaws.com/data/</endpoint>
<access_key_id>ACCESS-KEY-ID</access_key_id>
<secret_access_key>SECRET-ACCESS-KEY</secret_access_key>
</s3>
</disks>
<policies>
<tiered>
<volumes>
<default>
<disk>default</disk>
</default>
<s3>
<disk>s3</disk>
</s3>
</volumes>
</tiered>
</policies>
</storage_configuration> -->
<!-- Default root page on http[s] server. For example load UI from https://tabix.io/ when opening http://localhost:8123 -->

View File

@ -3,10 +3,17 @@ version: "2.4"
services:
clickhouse:
image: altinity/clickhouse-server:21.12.3.32.altinitydev.arm
# ports:
# - "9000:9000"
# - "8123:8123"
volumes:
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
- ./data/clickhouse/:/var/lib/clickhouse/
restart: on-failure
logging:
options:
max-size: 50m
max-file: "3"
healthcheck:
# "clickhouse", "client", "-u ${CLICKHOUSE_USER}", "--password ${CLICKHOUSE_PASSWORD}", "-q 'SELECT 1'"
test: ["CMD", "wget", "--spider", "-q", "localhost:8123/ping"]
@ -15,17 +22,19 @@ services:
retries: 3
alertmanager:
image: signoz/alertmanager:0.6.0
image: signoz/alertmanager:0.6.1
volumes:
- ./data/alertmanager:/data
depends_on:
- query-service
query-service:
condition: service_healthy
restart: on-failure
command:
- --queryService.url=http://query-service:8080
- --storage.path=/data
query-service:
image: signoz/query-service:0.7.4
image: signoz/query-service:0.7.5
container_name: query-service
command: ["-config=/root/config/prometheus.yml"]
volumes:
@ -40,14 +49,21 @@ services:
- DEPLOYMENT_TYPE=docker-standalone-arm
restart: on-failure
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
interval: 30s
timeout: 5s
retries: 3
depends_on:
clickhouse:
condition: service_healthy
frontend:
image: signoz/frontend:0.7.4
image: signoz/frontend:0.7.5
container_name: frontend
restart: on-failure
depends_on:
- alertmanager
- query-service
ports:
- "3301:3301"

View File

@ -3,10 +3,17 @@ version: "2.4"
services:
clickhouse:
image: yandex/clickhouse-server:21.12.3.32
# ports:
# - "9000:9000"
# - "8123:8123"
volumes:
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
- ./data/clickhouse/:/var/lib/clickhouse/
restart: on-failure
logging:
options:
max-size: 50m
max-file: "3"
healthcheck:
# "clickhouse", "client", "-u ${CLICKHOUSE_USER}", "--password ${CLICKHOUSE_PASSWORD}", "-q 'SELECT 1'"
test: ["CMD", "wget", "--spider", "-q", "localhost:8123/ping"]
@ -15,11 +22,13 @@ services:
retries: 3
alertmanager:
image: signoz/alertmanager:0.6.0
image: signoz/alertmanager:0.6.1
volumes:
- ./data/alertmanager:/data
depends_on:
- query-service
query-service:
condition: service_healthy
restart: on-failure
command:
- --queryService.url=http://query-service:8080
- --storage.path=/data
@ -27,7 +36,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:0.7.4
image: signoz/query-service:0.7.5
container_name: query-service
command: ["-config=/root/config/prometheus.yml"]
volumes:
@ -41,14 +50,21 @@ services:
- TELEMETRY_ENABLED=true
- DEPLOYMENT_TYPE=docker-standalone-amd
restart: on-failure
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
interval: 30s
timeout: 5s
retries: 3
depends_on:
clickhouse:
condition: service_healthy
frontend:
image: signoz/frontend:0.7.4
image: signoz/frontend:0.7.5
container_name: frontend
restart: on-failure
depends_on:
- alertmanager
- query-service
ports:
- "3301:3301"

View File

@ -247,7 +247,7 @@ bye() { # Prints a friendly good bye message and exits the script.
echo "or reach us for support in #help channel in our Slack Community https://signoz.io/slack"
echo "++++++++++++++++++++++++++++++++++++++++"
if [[ email == "" ]]; then
if [[ $email == "" ]]; then
echo -e "\n📨 Please share your email to receive support with the installation"
read -rp 'Email: ' email

View File

@ -1,2 +1,2 @@
node_modules
build
build

6
frontend/babel.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
};

View File

@ -9,12 +9,19 @@ const config: Config.InitialOptions = {
moduleNameMapper: {
'\\.(css|less)$': '<rootDir>/__mocks__/cssMock.ts',
},
notify: true,
notifyMode: 'always',
testMatch: ['<rootDir>/src/**/?(*.)(test).(ts|js)?(x)'],
transform: {
'\\.(js|jsx|ts|tsx)?$': 'babel-jest',
globals: {
extensionsToTreatAsEsm: ['.ts'],
'ts-jest': {
useESM: true,
},
},
testMatch: ['<rootDir>/src/**/?(*.)(test).(ts|js)?(x)'],
preset: 'ts-jest/presets/js-with-ts-esm',
transform: {
'^.+\\.(ts|tsx)?$': 'ts-jest',
'^.+\\.(js|jsx)$': 'babel-jest',
},
transformIgnorePatterns: ['node_modules/(?!(lodash-es)/)'],
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'],
moduleDirectories: ['node_modules', 'src'],

View File

@ -57,7 +57,7 @@
"i18next": "^21.6.12",
"i18next-browser-languagedetector": "^6.1.3",
"i18next-http-backend": "^1.3.2",
"jest": "26.6.0",
"jest": "^27.5.1",
"less": "^4.1.2",
"less-loader": "^10.2.0",
"lodash-es": "^4.17.21",
@ -68,6 +68,7 @@
"react-graph-vis": "^1.0.5",
"react-grid-layout": "^1.2.5",
"react-i18next": "^11.16.1",
"react-query": "^3.34.19",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"react-use": "^17.3.2",
@ -107,6 +108,7 @@
"@babel/preset-typescript": "^7.12.17",
"@jest/globals": "^27.5.1",
"@testing-library/cypress": "^8.0.0",
"@testing-library/react-hooks": "^7.0.2",
"@types/color": "^3.0.3",
"@types/compression-webpack-plugin": "^9.0.0",
"@types/copy-webpack-plugin": "^8.0.1",
@ -155,6 +157,7 @@
"portfinder-sync": "^0.0.2",
"prettier": "2.2.1",
"react-hot-loader": "^4.13.0",
"ts-jest": "^27.1.4",
"ts-node": "^10.2.1",
"typescript-plugin-css-modules": "^3.4.0",
"webpack-bundle-analyzer": "^4.5.0",

View File

@ -1,3 +1,27 @@
{
"monitor_signup": "Monitor your applications. Find what is causing issues."
"monitor_signup": "Monitor your applications. Find what is causing issues.",
"version": "Version",
"latest_version": "Latest version",
"current_version": "Current version",
"release_notes": "Release Notes",
"read_how_to_upgrade": "Read instructions on how to upgrade",
"latest_version_signoz": "You are running the latest version of SigNoz.",
"stale_version": "You are on an older version and may be loosing on the latest features we have shipped. We recommend to upgrade to the latest version",
"oops_something_went_wrong_version": "Oops.. facing issues with fetching updated version information",
"n_a": "N/A",
"routes": {
"general": "General",
"alert_channels": "Alert Channels"
},
"settings": {
"total_retention_period": "Total Retention Period",
"move_to_s3": "Move to S3\n(should be lower than total retention period)",
"retention_success_message": "Congrats. The retention periods for {{name}} has been updated successfully.",
"retention_error_message": "There was an issue in changing the retention period for {{name}}. Please try again or reach out to support@signoz.io",
"retention_failed_message": "There was an issue in changing the retention period. Please try again or reach out to support@signoz.io",
"retention_comparison_error": "Total retention period for {{name}} cant be lower or equal to the period after which data is moved to s3.",
"retention_null_value_error": "Retention Period for {{name}} is not set yet. Please set by choosing below",
"retention_confirmation": "Are you sure you want to change the retention period?",
"retention_confirmation_description": "This will change the amount of storage needed for saving metrics & traces."
}
}

View File

@ -85,3 +85,7 @@ export const EditAlertChannelsAlerts = Loadable(
export const AllAlertChannels = Loadable(
() => import(/* webpackChunkName: "All Channels" */ 'pages/AllAlertChannels'),
);
export const StatusPage = Loadable(
() => import(/* webpackChunkName: "All Status" */ 'pages/Status'),
);

View File

@ -17,6 +17,7 @@ import {
ServicesTablePage,
SettingsPage,
SignupPage,
StatusPage,
TraceDetail,
TraceFilter,
UsageExplorerPage,
@ -113,6 +114,11 @@ const routes: AppRoutes[] = [
exact: true,
component: AllAlertChannels,
},
{
path: ROUTES.VERSION,
exact: true,
component: StatusPage,
},
];
interface AppRoutes {

View File

@ -0,0 +1,24 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/disks/getDisks';
const getDisks = async (): Promise<
SuccessResponse<PayloadProps> | ErrorResponse
> => {
try {
const response = await axios.get(`/disks`);
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getDisks;

View File

@ -9,7 +9,11 @@ const setRetention = async (
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post<PayloadProps>(
`/settings/ttl?duration=${props.duration}&type=${props.type}`,
`/settings/ttl?duration=${props.totalDuration}&type=${props.type}${
props.coldStorage
? `&coldStorage=${props.coldStorage};toColdDuration=${props.toColdDuration}`
: ''
}`,
);
return {

View File

@ -0,0 +1,28 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/trace/getTagValue';
const getTagValue = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post<PayloadProps>(`/getTagValues`, {
start: props.start.toString(),
end: props.end.toString(),
tagKey: props.tagKey,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getTagValue;

View File

@ -0,0 +1,25 @@
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import axios, { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/user/getLatestVersion';
const getLatestVersion = async (): Promise<
SuccessResponse<PayloadProps> | ErrorResponse
> => {
try {
const response = await axios.get(
`https://api.github.com/repos/signoz/signoz/releases/latest`,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getLatestVersion;

View File

@ -0,0 +1,17 @@
import { grey } from '@ant-design/colors';
import { Chart } from 'chart.js';
export const emptyGraph = {
id: 'emptyChart',
afterDraw(chart: Chart): void {
const { height, width, ctx } = chart;
chart.clear();
ctx.save();
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = '1.5rem sans-serif';
ctx.fillStyle = `${grey.primary}`;
ctx.fillText('No data to display', width / 2, height / 2);
ctx.restore();
},
};

View File

@ -0,0 +1,19 @@
/* eslint-disable no-restricted-syntax */
import { ChartData } from 'chart.js';
export const hasData = (data: ChartData): boolean => {
const { datasets = [] } = data;
let hasData = false;
try {
for (const dataset of datasets) {
if (dataset.data.length > 0 && dataset.data.some((item) => item !== 0)) {
hasData = true;
break;
}
}
} catch (error) {
console.error(error);
}
return hasData;
};

View File

@ -27,7 +27,9 @@ import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import { hasData } from './hasData';
import { legend } from './Plugin';
import { emptyGraph } from './Plugin/EmptyGraph';
import { LegendsContainer } from './styles';
import { useXAxisTimeUnit } from './xAxisConfig';
import { getYAxisFormattedValue } from './yAxisConfig';
@ -128,6 +130,7 @@ function Graph({
grid: {
display: true,
color: getGridColor(),
drawTicks: true,
},
adapters: {
date: chartjsAdapter,
@ -180,12 +183,18 @@ function Graph({
}
},
};
const chartHasData = hasData(data);
const chartPlugins = [];
if (chartHasData) {
chartPlugins.push(legend(name, data.datasets.length > 3));
} else {
chartPlugins.push(emptyGraph);
}
lineChartRef.current = new Chart(chartRef.current, {
type,
data,
options,
plugins: [legend(name, data.datasets.length > 3)],
plugins: chartPlugins,
});
}
}, [

View File

@ -1,3 +1,7 @@
/**
* @jest-environment jsdom
*/
import { expect } from '@jest/globals';
import { render } from '@testing-library/react';
import React from 'react';

View File

@ -3,7 +3,7 @@
exports[`Not Found page test should render Not Found page without errors 1`] = `
<DocumentFragment>
<div
class="sc-gtsrHT VomVY"
class="sc-gsDKAQ cLXpIa"
>
<svg
fill="none"
@ -272,21 +272,21 @@ exports[`Not Found page test should render Not Found page without errors 1`] = `
</defs>
</svg>
<div
class="sc-hKFxyN dunFuJ"
class="sc-hKwDye foaleg"
>
<p
class="sc-dlnjwi cydxLA"
class="sc-dkPtRN fcyVIq"
>
Ah, seems like we reached a dead end!
</p>
<p
class="sc-dlnjwi cydxLA"
class="sc-dkPtRN fcyVIq"
>
Page Not Found
</p>
</div>
<a
class="sc-bdnxRM bYqcho"
class="sc-bdvvtL dbTZkj"
href="/application"
tabindex="0"
>

View File

@ -17,6 +17,7 @@ const ROUTES = {
ALL_CHANNELS: '/settings/channels',
CHANNELS_NEW: '/setting/channels/new',
CHANNELS_EDIT: '/setting/channels/edit/:id',
VERSION: '/status',
};
export default ROUTES;

View File

@ -1,11 +1,24 @@
import { notification } from 'antd';
import getLatestVersion from 'api/user/getLatestVersion';
import getVersion from 'api/user/getVersion';
import ROUTES from 'constants/routes';
import TopNav from 'container/Header';
import SideNav from 'container/SideNav';
import useFetch from 'hooks/useFetch';
import history from 'lib/history';
import React, { ReactNode, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import React, { ReactNode, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import {
UPDATE_CURRENT_ERROR,
UPDATE_CURRENT_VERSION,
UPDATE_LATEST_VERSION,
UPDATE_LATEST_VERSION_ERROR,
} from 'types/actions/app';
import AppReducer from 'types/reducer/app';
import { Content, Layout } from './styles';
@ -13,11 +26,24 @@ import { Content, Layout } from './styles';
function AppLayout(props: AppLayoutProps): JSX.Element {
const { isLoggedIn } = useSelector<AppState, AppReducer>((state) => state.app);
const { pathname } = useLocation();
const { t } = useTranslation();
const [isSignUpPage, setIsSignUpPage] = useState(ROUTES.SIGN_UP === pathname);
const { payload: versionPayload, loading, error: getVersionError } = useFetch(
getVersion,
);
const {
payload: latestVersionPayload,
loading: latestLoading,
error: latestError,
} = useFetch(getLatestVersion);
const { children } = props;
const dispatch = useDispatch<Dispatch<AppActions>>();
useEffect(() => {
if (!isLoggedIn) {
setIsSignUpPage(true);
@ -27,6 +53,72 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
}
}, [isLoggedIn, isSignUpPage]);
const latestCurrentCounter = useRef(0);
const latestVersionCounter = useRef(0);
useEffect(() => {
if (isLoggedIn && pathname === ROUTES.SIGN_UP) {
history.push(ROUTES.APPLICATION);
}
if (!latestLoading && latestError && latestCurrentCounter.current === 0) {
latestCurrentCounter.current = 1;
dispatch({
type: UPDATE_LATEST_VERSION_ERROR,
payload: {
isError: true,
},
});
notification.error({
message: t('oops_something_went_wrong_version'),
});
}
if (!loading && getVersionError && latestVersionCounter.current === 0) {
latestVersionCounter.current = 1;
dispatch({
type: UPDATE_CURRENT_ERROR,
payload: {
isError: true,
},
});
notification.error({
message: t('oops_something_went_wrong_version'),
});
}
if (!latestLoading && versionPayload) {
dispatch({
type: UPDATE_CURRENT_VERSION,
payload: {
currentVersion: versionPayload.version,
},
});
}
if (!loading && latestVersionPayload) {
dispatch({
type: UPDATE_LATEST_VERSION,
payload: {
latestVersion: latestVersionPayload.name,
},
});
}
}, [
dispatch,
loading,
latestLoading,
versionPayload,
latestVersionPayload,
isLoggedIn,
pathname,
getVersionError,
latestError,
t,
]);
return (
<Layout>
{!isSignUpPage && <SideNav />}

View File

@ -18,12 +18,14 @@ function CreateAlertChannels({
preType = 'slack',
}: CreateAlertChannelsProps): JSX.Element {
const [formInstance] = Form.useForm();
const [selectedConfig, setSelectedConfig] = useState<
Partial<SlackChannel & WebhookChannel>
>({
text: ` {{ range .Alerts -}}
*Alert:* {{ .Annotations.title }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}
text: `{{ range .Alerts -}}
*Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}
*Summary:* {{ .Annotations.summary }}
*Description:* {{ .Annotations.description }}
*Details:*

View File

@ -1,108 +1,118 @@
import { DownOutlined } from '@ant-design/icons';
import { Button, Menu } from 'antd';
import React from 'react';
import { Col, Row, Select } from 'antd';
import { find } from 'lodash-es';
import React, { useEffect, useRef, useState } from 'react';
import { SettingPeroid } from '.';
import {
Dropdown,
Input,
RetentionContainer,
TextContainer,
Typography,
RetentionFieldInputContainer,
RetentionFieldLabel,
} from './styles';
import {
convertHoursValueToRelevantUnit,
SettingPeriod,
TimeUnits,
} from './utils';
const { Option } = Select;
function Retention({
retentionValue,
setRentionValue,
selectedRetentionPeroid,
setSelectedRetentionPeroid,
setRetentionValue,
text,
}: RetentionProps): JSX.Element {
const options: Option[] = [
{
key: 'hr',
value: 'Hrs',
},
{
key: 'day',
value: 'Days',
},
{
key: 'month',
value: 'Months',
},
];
const onClickHandler = (
e: { key: string },
func: React.Dispatch<React.SetStateAction<SettingPeroid>>,
): void => {
const selected = e.key as SettingPeroid;
func(selected);
};
const menu = (
<Menu onClick={(e): void => onClickHandler(e, setSelectedRetentionPeroid)}>
{options.map((option) => (
<Menu.Item key={option.key}>{option.value}</Menu.Item>
))}
</Menu>
hide,
}: RetentionProps): JSX.Element | null {
const {
value: initialValue,
timeUnitValue: initialTimeUnitValue,
} = convertHoursValueToRelevantUnit(Number(retentionValue));
const [selectedTimeUnit, setSelectTimeUnit] = useState(initialTimeUnitValue);
const [selectedValue, setSelectedValue] = useState<number | null>(
initialValue,
);
const interacted = useRef(false);
useEffect(() => {
if (!interacted.current) setSelectedValue(initialValue);
}, [initialValue]);
const currentSelectedOption = (option: SettingPeroid): string | undefined => {
return options.find((e) => e.key === option)?.value;
useEffect(() => {
if (!interacted.current) setSelectTimeUnit(initialTimeUnitValue);
}, [initialTimeUnitValue]);
const menuItems = TimeUnits.map((option) => (
<Option key={option.value} value={option.value}>
{option.key}
</Option>
));
const currentSelectedOption = (option: SettingPeriod): void => {
const selectedValue = find(TimeUnits, (e) => e.value === option)?.value;
if (selectedValue) setSelectTimeUnit(selectedValue);
};
useEffect(() => {
const inverseMultiplier = find(
TimeUnits,
(timeUnit) => timeUnit.value === selectedTimeUnit,
)?.multiplier;
if (!selectedValue) setRetentionValue(null);
if (selectedValue && inverseMultiplier) {
setRetentionValue(selectedValue * (1 / inverseMultiplier));
}
}, [selectedTimeUnit, selectedValue, setRetentionValue]);
const onChangeHandler = (
e: React.ChangeEvent<HTMLInputElement>,
func: React.Dispatch<React.SetStateAction<string>>,
func: React.Dispatch<React.SetStateAction<number | null>>,
): void => {
interacted.current = true;
const { value } = e.target;
const integerValue = parseInt(value, 10);
if (value.length > 0 && integerValue.toString() === value) {
const parsedValue = Math.abs(integerValue).toString();
const parsedValue = Math.abs(integerValue);
func(parsedValue);
}
if (value.length === 0) {
func('');
func(null);
}
};
if (hide) {
return null;
}
return (
<RetentionContainer>
<TextContainer>
<Typography>{text}</Typography>
</TextContainer>
<Input
value={retentionValue}
onChange={(e): void => onChangeHandler(e, setRentionValue)}
/>
<Dropdown overlay={menu}>
<Button>
{currentSelectedOption(selectedRetentionPeroid)} <DownOutlined />
</Button>
</Dropdown>
<Row justify="space-between">
<Col flex={1} style={{ display: 'flex' }}>
<RetentionFieldLabel>{text}</RetentionFieldLabel>
</Col>
<Col flex="150px">
<RetentionFieldInputContainer>
<Input
value={selectedValue && selectedValue >= 0 ? selectedValue : ''}
onChange={(e): void => onChangeHandler(e, setSelectedValue)}
style={{ width: 75 }}
/>
<Select
value={selectedTimeUnit}
onChange={currentSelectedOption}
style={{ width: 100 }}
>
{menuItems}
</Select>
</RetentionFieldInputContainer>
</Col>
</Row>
</RetentionContainer>
);
}
interface Option {
key: SettingPeroid;
value: string;
}
interface RetentionProps {
retentionValue: string;
retentionValue: number | null;
text: string;
setRentionValue: React.Dispatch<React.SetStateAction<string>>;
selectedRetentionPeroid: SettingPeroid;
setSelectedRetentionPeroid: React.Dispatch<
React.SetStateAction<SettingPeroid>
>;
setRetentionValue: React.Dispatch<React.SetStateAction<number | null>>;
hide: boolean;
}
export default Retention;

View File

@ -1,43 +1,73 @@
import { Button, Modal, notification, Typography } from 'antd';
import getRetentionperoidApi from 'api/settings/getRetention';
/* eslint-disable sonarjs/cognitive-complexity */
import { Button, Col, Modal, notification, Row, Typography } from 'antd';
import getDisks from 'api/disks/getDisks';
import getRetentionPeriodApi from 'api/settings/getRetention';
import setRetentionApi from 'api/settings/setRetention';
import Spinner from 'components/Spinner';
import TextToolTip from 'components/TextToolTip';
import useFetch from 'hooks/useFetch';
import convertIntoHr from 'lib/convertIntoHr';
import getSettingsPeroid from 'lib/getSettingsPeroid';
import React, { useCallback, useEffect, useState } from 'react';
import { find } from 'lodash-es';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IDiskType } from 'types/api/disks/getDisks';
import { PayloadProps } from 'types/api/settings/getRetention';
import Retention from './Retention';
import {
ButtonContainer,
Container,
ErrorText,
ErrorTextContainer,
ToolTipContainer,
} from './styles';
import { ButtonContainer, ErrorText, ErrorTextContainer } from './styles';
function GeneralSettings(): JSX.Element {
const [
selectedMetricsPeroid,
setSelectedMetricsPeroid,
] = useState<SettingPeroid>('month');
const { t } = useTranslation();
const [notifications, Element] = notification.useNotification();
const [retentionPeroidMetrics, setRetentionPeroidMetrics] = useState<string>(
'',
);
const [modal, setModal] = useState<boolean>(false);
const [postApiLoading, setPostApiLoading] = useState<boolean>(false);
const [selectedTracePeroid, setSelectedTracePeroid] = useState<SettingPeroid>(
'hr',
);
const [availableDisks, setAvailableDisks] = useState<IDiskType[] | null>(null);
const [retentionPeroidTrace, setRetentionPeroidTrace] = useState<string>('');
const [isDefaultMetrics, setIsDefaultMetrics] = useState<boolean>(false);
const [isDefaultTrace, setIsDefaultTrace] = useState<boolean>(false);
useEffect(() => {
getDisks().then((response) => setAvailableDisks(response.payload));
}, []);
const { payload, loading, error, errorMessage } = useFetch<
PayloadProps,
undefined
>(getRetentionPeriodApi, undefined);
const [currentTTLValues, setCurrentTTLValues] = useState(payload);
useEffect(() => {
setCurrentTTLValues(payload);
}, [payload]);
const [metricsTotalRetentionPeriod, setMetricsTotalRetentionPeriod] = useState<
number | null
>(null);
const [metricsS3RetentionPeriod, setMetricsS3RetentionPeriod] = useState<
number | null
>(null);
const [tracesTotalRetentionPeriod, setTracesTotalRetentionPeriod] = useState<
number | null
>(null);
const [tracesS3RetentionPeriod, setTracesS3RetentionPeriod] = useState<
number | null
>(null);
useEffect(() => {
if (currentTTLValues) {
setMetricsTotalRetentionPeriod(currentTTLValues.metrics_ttl_duration_hrs);
setMetricsS3RetentionPeriod(
currentTTLValues.metrics_move_ttl_duration_hrs
? currentTTLValues.metrics_move_ttl_duration_hrs
: null,
);
setTracesTotalRetentionPeriod(currentTTLValues.traces_ttl_duration_hrs);
setTracesS3RetentionPeriod(
currentTTLValues.traces_move_ttl_duration_hrs
? currentTTLValues.traces_move_ttl_duration_hrs
: null,
);
}
console.log({ changed: currentTTLValues });
}, [currentTTLValues]);
const onModalToggleHandler = (): void => {
setModal((modal) => !modal);
@ -47,182 +77,236 @@ function GeneralSettings(): JSX.Element {
onModalToggleHandler();
}, []);
const { payload, loading, error, errorMessage } = useFetch<
PayloadProps,
undefined
>(getRetentionperoidApi, undefined);
const s3Enabled = useMemo(
() => !!find(availableDisks, (disks: IDiskType) => disks?.type === 's3'),
[availableDisks],
);
const checkMetricTraceDefault = (trace: number, metric: number): void => {
if (metric === -1) {
setIsDefaultMetrics(true);
} else {
setIsDefaultMetrics(false);
const renderConfig = [
{
name: 'Metrics',
retentionFields: [
{
name: t('settings.total_retention_period'),
value: metricsTotalRetentionPeriod,
setValue: setMetricsTotalRetentionPeriod,
},
{
name: t('settings.move_to_s3'),
value: metricsS3RetentionPeriod,
setValue: setMetricsS3RetentionPeriod,
hide: !s3Enabled,
},
],
},
{
name: 'Traces',
retentionFields: [
{
name: t('settings.total_retention_period'),
value: tracesTotalRetentionPeriod,
setValue: setTracesTotalRetentionPeriod,
},
{
name: t('settings.move_to_s3'),
value: tracesS3RetentionPeriod,
setValue: setTracesS3RetentionPeriod,
hide: !s3Enabled,
},
],
},
].map((category): JSX.Element | null => {
if (
Array.isArray(category.retentionFields) &&
category.retentionFields.length > 0
) {
return (
<Col flex="40%" style={{ minWidth: 475 }} key={category.name}>
<Typography.Title level={3}>{category.name}</Typography.Title>
{category.retentionFields.map((retentionField) => (
<Retention
key={retentionField.name}
text={retentionField.name}
retentionValue={retentionField.value}
setRetentionValue={retentionField.setValue}
hide={!!retentionField.hide}
/>
))}
</Col>
);
}
if (trace === -1) {
setIsDefaultTrace(true);
} else {
setIsDefaultTrace(false);
}
};
useEffect(() => {
if (!loading && payload !== undefined) {
const {
metrics_ttl_duration_hrs: metricTllDuration,
traces_ttl_duration_hrs: traceTllDuration,
} = payload;
checkMetricTraceDefault(traceTllDuration, metricTllDuration);
const traceValue = getSettingsPeroid(traceTllDuration);
const metricsValue = getSettingsPeroid(metricTllDuration);
setRetentionPeroidTrace(traceValue.value.toString());
setSelectedTracePeroid(traceValue.peroid);
setRetentionPeroidMetrics(metricsValue.value.toString());
setSelectedMetricsPeroid(metricsValue.peroid);
}
}, [setSelectedMetricsPeroid, loading, payload]);
return null;
});
const onOkHandler = async (): Promise<void> => {
try {
setPostApiLoading(true);
const retentionTraceValue =
retentionPeroidTrace === '0' && (payload?.traces_ttl_duration_hrs || 0) < 0
? payload?.traces_ttl_duration_hrs || 0
: parseInt(retentionPeroidTrace, 10);
const retentionMetricsValue =
retentionPeroidMetrics === '0' &&
(payload?.metrics_ttl_duration_hrs || 0) < 0
? payload?.metrics_ttl_duration_hrs || 0
: parseInt(retentionPeroidMetrics, 10);
const [tracesResponse, metricsResponse] = await Promise.all([
setRetentionApi({
duration: `${convertIntoHr(retentionTraceValue, selectedTracePeroid)}h`,
type: 'traces',
}),
setRetentionApi({
duration: `${convertIntoHr(
retentionMetricsValue,
selectedMetricsPeroid,
)}h`,
type: 'metrics',
}),
]);
const apiCalls = [];
if (
tracesResponse.statusCode === 200 &&
metricsResponse.statusCode === 200
!(
currentTTLValues?.metrics_move_ttl_duration_hrs ===
metricsS3RetentionPeriod &&
currentTTLValues.metrics_ttl_duration_hrs === metricsTotalRetentionPeriod
)
) {
notifications.success({
message: 'Success!',
placement: 'topRight',
description: 'Congrats. The retention periods were updated correctly.',
});
checkMetricTraceDefault(retentionTraceValue, retentionMetricsValue);
onModalToggleHandler();
apiCalls.push(() =>
setRetentionApi({
type: 'metrics',
totalDuration: `${metricsTotalRetentionPeriod || -1}h`,
coldStorage: s3Enabled ? 's3' : null,
toColdDuration: `${metricsS3RetentionPeriod || -1}h`,
}),
);
} else {
notifications.error({
message: 'Error',
description:
'There was an issue in changing the retention period. Please try again or reach out to support@signoz.io',
placement: 'topRight',
});
apiCalls.push(() => Promise.resolve(null));
}
if (
!(
currentTTLValues?.traces_move_ttl_duration_hrs ===
tracesS3RetentionPeriod &&
currentTTLValues.traces_ttl_duration_hrs === tracesTotalRetentionPeriod
)
) {
apiCalls.push(() =>
setRetentionApi({
type: 'traces',
totalDuration: `${tracesTotalRetentionPeriod || -1}h`,
coldStorage: s3Enabled ? 's3' : null,
toColdDuration: `${tracesS3RetentionPeriod || -1}h`,
}),
);
} else {
apiCalls.push(() => Promise.resolve(null));
}
const apiCallSequence = ['metrics', 'traces'];
const apiResponses = await Promise.all(apiCalls.map((api) => api()));
apiResponses.forEach((apiResponse, idx) => {
const name = apiCallSequence[idx];
if (apiResponse) {
if (apiResponse.statusCode === 200) {
notifications.success({
message: 'Success!',
placement: 'topRight',
description: t('settings.retention_success_message', { name }),
});
} else {
notifications.error({
message: 'Error',
description: t('settings.retention_error_message', { name }),
placement: 'topRight',
});
}
}
});
onModalToggleHandler();
setPostApiLoading(false);
} catch (error) {
notifications.error({
message: 'Error',
description:
'There was an issue in changing the retention period. Please try again or reach out to support@signoz.io',
description: t('settings.retention_failed_message'),
placement: 'topRight',
});
}
// Updates the currentTTL Values in order to avoid pushing the same values.
setCurrentTTLValues({
metrics_ttl_duration_hrs: metricsTotalRetentionPeriod || -1,
metrics_move_ttl_duration_hrs: metricsS3RetentionPeriod || -1,
traces_ttl_duration_hrs: tracesTotalRetentionPeriod || -1,
traces_move_ttl_duration_hrs: tracesS3RetentionPeriod || -1,
});
setModal(false);
};
const [isDisabled, errorText] = useMemo((): [boolean, string] => {
// Various methods to return dynamic error message text.
const messages = {
compareError: (name: string | number): string =>
t('settings.retention_comparison_error', { name }),
nullValueError: (name: string | number): string =>
t('settings.retention_null_value_error', { name }),
};
// Defaults to button not disabled and empty error message text.
let isDisabled = false;
let errorText = '';
if (s3Enabled) {
if (
(metricsTotalRetentionPeriod || metricsS3RetentionPeriod) &&
Number(metricsTotalRetentionPeriod) <= Number(metricsS3RetentionPeriod)
) {
isDisabled = true;
errorText = messages.compareError('metrics');
} else if (
(tracesTotalRetentionPeriod || tracesS3RetentionPeriod) &&
Number(tracesTotalRetentionPeriod) <= Number(tracesS3RetentionPeriod)
) {
isDisabled = true;
errorText = messages.compareError('traces');
}
}
if (!metricsTotalRetentionPeriod || !tracesTotalRetentionPeriod) {
isDisabled = true;
if (!metricsTotalRetentionPeriod && !tracesTotalRetentionPeriod) {
errorText = messages.nullValueError('metrics and traces');
} else if (!metricsTotalRetentionPeriod) {
errorText = messages.nullValueError('metrics');
} else if (!tracesTotalRetentionPeriod) {
errorText = messages.nullValueError('traces');
}
}
if (
currentTTLValues?.metrics_ttl_duration_hrs === metricsTotalRetentionPeriod &&
currentTTLValues.metrics_move_ttl_duration_hrs ===
metricsS3RetentionPeriod &&
currentTTLValues.traces_ttl_duration_hrs === tracesTotalRetentionPeriod &&
currentTTLValues.traces_move_ttl_duration_hrs === tracesS3RetentionPeriod
) {
isDisabled = true;
}
return [isDisabled, errorText];
}, [
currentTTLValues,
metricsS3RetentionPeriod,
metricsTotalRetentionPeriod,
s3Enabled,
t,
tracesS3RetentionPeriod,
tracesTotalRetentionPeriod,
]);
if (error) {
return <Typography>{errorMessage}</Typography>;
}
if (loading || payload === undefined) {
if (loading || currentTTLValues === undefined) {
return <Spinner tip="Loading.." height="70vh" />;
}
const getErrorText = (): string => {
const getValue = (value: string): string =>
`Retention Peroid for ${value} is not set yet. Please set by choosing below`;
if (!isDefaultMetrics && !isDefaultTrace) {
return '';
}
if (isDefaultMetrics && !isDefaultTrace) {
return `${getValue('Metrics')}`;
}
if (!isDefaultMetrics && isDefaultTrace) {
return `${getValue('Trace')}`;
}
return `${getValue('Trace , Metrics')}`;
};
const isDisabledHandler = (): boolean => {
return !!(retentionPeroidTrace === '' || retentionPeroidMetrics === '');
};
const errorText = getErrorText();
return (
<Container>
<Col xs={24} md={22} xl={20} xxl={18} style={{ margin: 'auto' }}>
{Element}
<ErrorTextContainer>
<TextToolTip
{...{
text: `More details on how to set retention period`,
url: 'https://signoz.io/docs/userguide/retention-period/',
}}
/>
{errorText && <ErrorText>{errorText}</ErrorText>}
</ErrorTextContainer>
{errorText ? (
<ErrorTextContainer>
<ErrorText>{errorText}</ErrorText>
<TextToolTip
{...{
text: `More details on how to set retention period`,
url: 'https://signoz.io/docs/userguide/retention-period/',
}}
/>
</ErrorTextContainer>
) : (
<ToolTipContainer>
<TextToolTip
{...{
text: `More details on how to set retention period`,
url: 'https://signoz.io/docs/userguide/retention-period/',
}}
/>
</ToolTipContainer>
)}
<Retention
text="Retention Period for Metrics"
selectedRetentionPeroid={selectedMetricsPeroid}
setRentionValue={setRetentionPeroidMetrics}
retentionValue={retentionPeroidMetrics}
setSelectedRetentionPeroid={setSelectedMetricsPeroid}
/>
<Retention
text="Retention Period for Traces"
selectedRetentionPeroid={selectedTracePeroid}
setRentionValue={setRetentionPeroidTrace}
retentionValue={retentionPeroidTrace}
setSelectedRetentionPeroid={setSelectedTracePeroid}
/>
<Row justify="space-around">{renderConfig}</Row>
<Modal
title="Are you sure you want to change the retention period?"
title={t('settings.retention_confirmation')}
focusTriggerAfterClose
forceRender
destroyOnClose
@ -233,24 +317,18 @@ function GeneralSettings(): JSX.Element {
visible={modal}
confirmLoading={postApiLoading}
>
<Typography>
This will change the amount of storage needed for saving metrics & traces.
</Typography>
<Typography>{t('settings.retention_confirmation_description')}</Typography>
</Modal>
<ButtonContainer>
<Button
onClick={onClickSaveHandler}
disabled={isDisabledHandler()}
type="primary"
>
<Button onClick={onClickSaveHandler} disabled={isDisabled} type="primary">
Save
</Button>
</ButtonContainer>
</Container>
</Col>
);
}
export type SettingPeroid = 'hr' | 'day' | 'month';
export type SettingPeriod = 'hr' | 'day' | 'month';
export default GeneralSettings;

View File

@ -1,16 +1,13 @@
import {
Col,
Dropdown as DropDownComponent,
Input as InputComponent,
Typography as TypographyComponent,
} from 'antd';
import styled from 'styled-components';
export const RetentionContainer = styled.div`
width: 50%;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
export const RetentionContainer = styled(Col)`
margin: 0.75rem 0;
`;
export const Input = styled(InputComponent)`
@ -37,13 +34,6 @@ export const ButtonContainer = styled.div`
}
`;
export const Container = styled.div`
&&& {
display: flex;
flex-direction: column;
}
`;
export const Dropdown = styled(DropDownComponent)`
&&& {
display: flex;
@ -66,6 +56,7 @@ export const ErrorTextContainer = styled.div`
margin-bottom: 2rem;
display: flex;
align-items: center;
gap: 1rem;
> article {
margin-right: 1rem;
@ -90,3 +81,12 @@ export const ErrorText = styled(TypographyComponent)`
font-style: italic;
}
`;
export const RetentionFieldLabel = styled(TypographyComponent)`
vertical-align: middle;
white-space: pre-wrap;
`;
export const RetentionFieldInputContainer = styled.div`
display: inline-flex;
`;

View File

@ -0,0 +1,42 @@
export type SettingPeriod = 'hr' | 'day' | 'month';
export interface ITimeUnit {
value: SettingPeriod;
key: string;
multiplier: number;
}
export const TimeUnits: ITimeUnit[] = [
{
value: 'hr',
key: 'Hours',
multiplier: 1,
},
{
value: 'day',
key: 'Days',
multiplier: 1 / 24,
},
{
value: 'month',
key: 'Months',
multiplier: 1 / (24 * 30),
},
];
export const convertHoursValueToRelevantUnit = (
value: number,
): { value: number; timeUnitValue: SettingPeriod } => {
if (value)
for (let idx = TimeUnits.length - 1; idx >= 0; idx -= 1) {
const timeUnit = TimeUnits[idx];
const convertedValue = timeUnit.multiplier * value;
if (
convertedValue >= 1 &&
convertedValue === parseInt(`${convertedValue}`, 10)
) {
return { value: convertedValue, timeUnitValue: timeUnit.value };
}
}
return { value, timeUnitValue: TimeUnits[0].value };
};

View File

@ -1,91 +0,0 @@
import Graph, { GraphOnClickHandler } from 'components/Graph';
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
import GetMaxMinTime from 'lib/getMaxMinTime';
import { colors } from 'lib/getRandomColor';
import getStartAndEndTime from 'lib/getStartAndEndTime';
import getTimeString from 'lib/getTimeString';
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
function EmptyGraph({
selectedTime,
widget,
onClickHandler,
}: EmptyGraphProps): JSX.Element {
const { minTime, maxTime, loading } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const maxMinTime = GetMaxMinTime({
graphType: widget.panelTypes,
maxTime,
minTime,
});
const { end, start } = getStartAndEndTime({
type: selectedTime.enum,
maxTime: maxMinTime.maxTime,
minTime: maxMinTime.minTime,
});
const dateFunction = useCallback(() => {
if (!loading) {
const dates: Date[] = [];
const startString = getTimeString(start);
const endString = getTimeString(end);
const parsedStart = parseInt(startString, 10);
const parsedEnd = parseInt(endString, 10);
let startDate = parsedStart;
const endDate = parsedEnd;
while (endDate >= startDate) {
const newDate = new Date(startDate);
startDate += 20000;
dates.push(newDate);
}
return dates;
}
return [];
}, [start, end, loading]);
const date = dateFunction();
return (
<Graph
name=""
{...{
type: 'line',
onClickHandler,
data: {
datasets: [
{
data: new Array(date?.length).fill(0),
borderColor: colors[0],
showLine: true,
borderWidth: 1.5,
spanGaps: true,
pointRadius: 0,
},
],
labels: date,
},
}}
/>
);
}
interface EmptyGraphProps {
selectedTime: timePreferance;
widget: Widgets;
onClickHandler: GraphOnClickHandler | undefined;
}
export default EmptyGraph;

View File

@ -23,14 +23,12 @@ import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import EmptyGraph from './EmptyGraph';
import { NotFoundContainer, TimeContainer } from './styles';
function FullView({
widget,
fullViewOptions = true,
onClickHandler,
noDataGraph = false,
name,
yAxisUnit,
}: FullViewProps): JSX.Element {
@ -166,38 +164,6 @@ function FullView({
);
}
if (state.loading === false && state.payload.datasets.length === 0) {
return (
<>
{fullViewOptions && (
<TimeContainer>
<TimePreference
{...{
selectedTime,
setSelectedTime,
}}
/>
<Button onClick={onFetchDataHandler} type="primary">
Refresh
</Button>
</TimeContainer>
)}
{noDataGraph ? (
<EmptyGraph
onClickHandler={onClickHandler}
widget={widget}
selectedTime={selectedTime}
/>
) : (
<NotFoundContainer>
<Typography>No Data</Typography>
</NotFoundContainer>
)}
</>
);
}
return (
<>
{fullViewOptions && (
@ -243,7 +209,6 @@ interface FullViewProps {
widget: Widgets;
fullViewOptions?: boolean;
onClickHandler?: GraphOnClickHandler;
noDataGraph?: boolean;
name: string;
yAxisUnit?: string;
}
@ -251,7 +216,6 @@ interface FullViewProps {
FullView.defaultProps = {
fullViewOptions: undefined,
onClickHandler: undefined,
noDataGraph: undefined,
yAxisUnit: undefined,
};

View File

@ -11,6 +11,7 @@ const breadcrumbNameMap = {
[ROUTES.INSTRUMENTATION]: 'Add instrumentation',
[ROUTES.SETTINGS]: 'Settings',
[ROUTES.DASHBOARD]: 'Dashboard',
[ROUTES.VERSION]: 'Status',
};
function ShowBreadcrumbs(props: RouteComponentProps): JSX.Element {

View File

@ -1,4 +1,6 @@
import React, { useCallback } from 'react';
import { ExclamationCircleOutlined } from '@ant-design/icons';
import { Modal } from 'antd';
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
@ -8,14 +10,29 @@ import AppActions from 'types/actions';
import { Data } from '../index';
import { TableLinkText } from './styles';
function DeleteButton({ deleteDashboard, id }: DeleteButtonProps): JSX.Element {
const onClickHandler = useCallback(() => {
deleteDashboard({
uuid: id,
});
}, [id, deleteDashboard]);
const { confirm } = Modal;
return <TableLinkText onClick={onClickHandler}>Delete</TableLinkText>;
function DeleteButton({ deleteDashboard, id }: DeleteButtonProps): JSX.Element {
const openConfirmationDialog = (): void => {
confirm({
title: 'Do you really want to delete this dashboard?',
icon: <ExclamationCircleOutlined style={{ color: '#e42b35' }} />,
onOk() {
deleteDashboard({
uuid: id,
});
},
okText: 'Delete',
okButtonProps: { danger: true },
centered: true,
});
};
return (
<TableLinkText type="danger" onClick={openConfirmationDialog}>
Delete
</TableLinkText>
);
}
interface DispatchProps {

View File

@ -157,7 +157,7 @@ function ListOfAllDashboard(): JSX.Element {
<TextToolTip
{...{
text: `More details on how to create dashboards`,
url: 'https://signoz.io/docs/userguide/metrics-dashboard',
url: 'https://signoz.io/docs/userguide/dashboards',
}}
/>

View File

@ -36,7 +36,7 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
history.replace(
`${
ROUTES.TRACE
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"],"status":["ok","error"]}&filterToFetchData=["duration","status","serviceName"]&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"]}&isSelectedFilterSkipped=true`,
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=[]&&isFilterExclude={"serviceName":false}&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"]}&spanAggregateCurrentPage=1&spanAggregateOrder=ascend`,
);
};
@ -88,7 +88,7 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
history.replace(
`${
ROUTES.TRACE
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"],"status":["error"]}&filterToFetchData=["duration","status","serviceName"]&userSelectedFilter={"status":["error"],"serviceName":["${servicename}"]}&isSelectedFilterSkipped=true`,
}?${urlParams.toString()}?selected={"serviceName":["${servicename}"],"status":["error"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=[]&isFilterExclude={"serviceName":false,"status":false}&userSelectedFilter={"serviceName":["${servicename}"],"status":["error"]}&spanAggregateCurrentPage=1&spanAggregateOrder=ascend`,
);
};
@ -179,7 +179,6 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
<GraphContainer>
<FullView
name="request_per_sec"
noDataGraph
fullViewOptions={false}
onClickHandler={(event, element, chart, data): void => {
onClickhandler(event, element, chart, data, 'Request');
@ -214,7 +213,6 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
<GraphContainer>
<FullView
name="error_percentage_%"
noDataGraph
fullViewOptions={false}
onClickHandler={(ChartEvent, activeElements, chart, data): void => {
onClickhandler(ChartEvent, activeElements, chart, data, 'Error');

View File

@ -17,7 +17,6 @@ function DBCall({ getWidget }: DBCallProps): JSX.Element {
<GraphContainer>
<FullView
name="database_call_rps"
noDataGraph
fullViewOptions={false}
widget={getWidget([
{
@ -37,7 +36,6 @@ function DBCall({ getWidget }: DBCallProps): JSX.Element {
<GraphContainer>
<FullView
name="database_call_avg_duration"
noDataGraph
fullViewOptions={false}
widget={getWidget([
{

View File

@ -21,7 +21,6 @@ function External({ getWidget }: ExternalProps): JSX.Element {
<FullView
name="external_call_error_percentage"
fullViewOptions={false}
noDataGraph
widget={getWidget([
{
query: `max((sum(rate(signoz_external_call_latency_count{service_name="${servicename}", status_code="STATUS_CODE_ERROR"}[1m]) OR rate(signoz_external_call_latency_count{service_name="${servicename}", http_status_code=~"5.."}[1m]) OR vector(0)) by (http_url))*100/sum(rate(signoz_external_call_latency_count{service_name="${servicename}"}[1m])) by (http_url)) < 1000 OR vector(0)`,
@ -40,7 +39,6 @@ function External({ getWidget }: ExternalProps): JSX.Element {
<GraphContainer>
<FullView
name="external_call_duration"
noDataGraph
fullViewOptions={false}
widget={getWidget([
{
@ -62,7 +60,6 @@ function External({ getWidget }: ExternalProps): JSX.Element {
<GraphContainer>
<FullView
name="external_call_rps_by_address"
noDataGraph
fullViewOptions={false}
widget={getWidget([
{
@ -81,7 +78,6 @@ function External({ getWidget }: ExternalProps): JSX.Element {
<GraphTitle>External Call duration(by Address)</GraphTitle>
<GraphContainer>
<FullView
noDataGraph
name="external_call_duration_by_address"
fullViewOptions={false}
widget={getWidget([

View File

@ -33,7 +33,7 @@ function TopEndpointsTable(props: TopEndpointsTableProps): JSX.Element {
history.push(
`${
ROUTES.TRACE
}?${urlParams.toString()}&selected={"status":["error","ok"],"serviceName":["${servicename}"],"operation":["${operation}"]}&filterToFetchData=["duration","status","serviceName","operation"]&isSelectedFilterSkipped=true&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"],"operation":["${operation}"]}&isSelectedFilterSkipped=true`,
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"],"operation":["${operation}"]}&filterToFetchData=["duration","status","serviceName","operation"]&spanAggregateCurrentPage=1&selectedTags=[]&&isFilterExclude={"serviceName":false,"operation":false}&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"],"operation":["${operation}"]}&spanAggregateCurrentPage=1&spanAggregateOrder=ascend`,
);
};

View File

@ -114,7 +114,7 @@ function Query({
<TextToolTip
{...{
text: `More details on how to plot metrics graphs`,
url: 'https://signoz.io/docs/userguide/prometheus-metrics/',
url: 'https://signoz.io/docs/userguide/send-metrics/#related-videos',
}}
/>
</ButtonContainer>

View File

@ -1,3 +1,4 @@
import { CheckCircleTwoTone, WarningOutlined } from '@ant-design/icons';
import { Menu, Typography } from 'antd';
import getLocalStorageKey from 'api/browser/localstorage/get';
import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
@ -5,6 +6,7 @@ import ROUTES from 'constants/routes';
import history from 'lib/history';
import setTheme, { AppMode } from 'lib/theme/setTheme';
import React, { useCallback, useLayoutEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { connect, useDispatch, useSelector } from 'react-redux';
import { NavLink, useLocation } from 'react-router-dom';
import { bindActionCreators } from 'redux';
@ -19,11 +21,13 @@ import menus from './menuItems';
import Slack from './Slack';
import {
Logo,
RedDot,
Sider,
SlackButton,
SlackMenuItemContainer,
ThemeSwitcherWrapper,
ToggleButton,
VersionContainer,
} from './styles';
function SideNav({ toggleDarkMode }: Props): JSX.Element {
@ -31,9 +35,15 @@ function SideNav({ toggleDarkMode }: Props): JSX.Element {
const [collapsed, setCollapsed] = useState<boolean>(
getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true',
);
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const {
isDarkMode,
currentVersion,
latestVersion,
isCurrentVersionError,
} = useSelector<AppState, AppReducer>((state) => state.app);
const { pathname } = useLocation();
const { t } = useTranslation('');
const toggleTheme = useCallback(() => {
const preMode: AppMode = isDarkMode ? 'lightMode' : 'darkMode';
@ -77,6 +87,38 @@ function SideNav({ toggleDarkMode }: Props): JSX.Element {
window.open('https://signoz.io/slack', '_blank');
};
const onClickVersionHandler = (): void => {
history.push(ROUTES.VERSION);
};
const isNotCurrentVersion = currentVersion !== latestVersion;
const sidebar = [
{
onClick: onClickSlackHandler,
icon: <Slack />,
text: <SlackButton>Support</SlackButton>,
},
{
onClick: onClickVersionHandler,
icon: isNotCurrentVersion ? (
<WarningOutlined style={{ color: '#E87040' }} />
) : (
<CheckCircleTwoTone twoToneColor={['#D5F2BB', '#1f1f1f']} />
),
text: (
<VersionContainer>
{!isCurrentVersionError ? (
<SlackButton>{currentVersion}</SlackButton>
) : (
<SlackButton>{t('n_a')}</SlackButton>
)}
{isNotCurrentVersion && <RedDot />}
</VersionContainer>
),
},
];
return (
<Sider collapsible collapsed={collapsed} onCollapse={onCollapse} width={200}>
<ThemeSwitcherWrapper>
@ -87,7 +129,7 @@ function SideNav({ toggleDarkMode }: Props): JSX.Element {
/>
</ThemeSwitcherWrapper>
<NavLink to={ROUTES.APPLICATION}>
<Logo src="/signoz.svg" alt="SigNoz" collapsed={collapsed} />
<Logo index={0} src="/signoz.svg" alt="SigNoz" collapsed={collapsed} />
</NavLink>
<Menu
@ -105,11 +147,21 @@ function SideNav({ toggleDarkMode }: Props): JSX.Element {
<Typography>{name}</Typography>
</Menu.Item>
))}
<SlackMenuItemContainer collapsed={collapsed}>
<Menu.Item onClick={onClickSlackHandler} icon={<Slack />}>
<SlackButton>Support</SlackButton>
</Menu.Item>
</SlackMenuItemContainer>
{sidebar.map((props, index) => (
<SlackMenuItemContainer
index={index + 1}
key={`${index + 1}`}
collapsed={collapsed}
>
<Menu.Item
eventKey={index.toString()}
onClick={props.onClick}
icon={props.icon}
>
{props.text}
</Menu.Item>
</SlackMenuItemContainer>
))}
</Menu>
</Sider>
);

View File

@ -19,6 +19,7 @@ export const Logo = styled.img<LogoProps>`
interface LogoProps {
collapsed: boolean;
index: number;
}
export const Sider = styled(SiderComponent)`
@ -50,9 +51,10 @@ export const SlackButton = styled(Typography)`
export const SlackMenuItemContainer = styled.div<LogoProps>`
position: fixed;
bottom: 48px;
bottom: ${({ index }): string => `${index * 48 + (index + 16)}px`};
background: #262626;
width: ${({ collapsed }): string => (!collapsed ? '200px' : '80px')};
transition: inherit;
&&& {
li {
@ -60,11 +62,14 @@ export const SlackMenuItemContainer = styled.div<LogoProps>`
collapsed &&
css`
padding-left: 24px;
padding-top: 6px;
`}
}
svg {
margin-left: ${({ collapsed }): string => (collapsed ? '0' : '24px')};
width: 28px;
height: 28px;
${({ collapsed }): StyledCSS =>
collapsed &&
@ -73,5 +78,24 @@ export const SlackMenuItemContainer = styled.div<LogoProps>`
margin: 0 auto;
`}
}
.ant-menu-title-content {
margin: 0;
}
}
`;
export const RedDot = styled.div`
width: 12px;
height: 12px;
background: #d32029;
border-radius: 50%;
margin-left: 1rem;
margin-top: 0.5rem;
`;
export const VersionContainer = styled.div`
&&& {
display: flex;
}
`;

View File

@ -93,11 +93,15 @@ function CheckBoxComponent(props: CheckBoxProps): JSX.Element {
if (response.statusCode === 200) {
const updatedFilter = getFilter(response.payload);
updatedFilter.forEach((value, key) => {
if (key !== 'duration' && name !== key) {
preUserSelectedMap.set(key, Object.keys(value));
}
});
// updatedFilter.forEach((value, key) => {
// if (key !== 'duration' && name !== key) {
// preUserSelectedMap.set(key, Object.keys(value));
// }
// if (key === 'duration') {
// newSelectedMap.set('duration', [value.maxDuration, value.minDuration]);
// }
// });
updatedFilter.set(name, {
[`${keyValue}`]: '-1',
@ -115,6 +119,7 @@ function CheckBoxComponent(props: CheckBoxProps): JSX.Element {
selectedFilter: newSelectedMap,
userSelected: preUserSelectedMap,
isFilterExclude: preIsFilterExclude,
order: spansAggregate.order,
},
});
@ -125,9 +130,9 @@ function CheckBoxComponent(props: CheckBoxProps): JSX.Element {
filterToFetchData,
spansAggregate.currentPage,
selectedTags,
updatedFilter,
preIsFilterExclude,
preUserSelectedMap,
spansAggregate.order,
);
} else {
setIsLoading(false);

View File

@ -18,16 +18,26 @@ function CommonCheckBox(props: CommonCheckBoxProps): JSX.Element {
return (
<>
{statusObj.map((e) => (
<CheckBoxComponent
key={e}
{...{
name,
keyValue: e,
value: status[e],
}}
/>
))}
{statusObj
.sort((a, b) => {
const countA = +status[a];
const countB = +status[b];
if (countA === countB) {
return a.length - b.length;
}
return countA - countB;
})
.map((e) => (
<CheckBoxComponent
key={e}
{...{
name,
keyValue: e,
value: status[e],
}}
/>
))}
</>
);
}

View File

@ -5,7 +5,7 @@ import getFilters from 'api/trace/getFilters';
import dayjs from 'dayjs';
import durationPlugin from 'dayjs/plugin/duration';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import React, { useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Dispatch } from 'redux';
import { getFilter, updateURL } from 'store/actions/trace/util';
@ -20,11 +20,13 @@ import { Container, InputContainer, Text } from './styles';
dayjs.extend(durationPlugin);
const getMs = (value: string): string => {
return dayjs
.duration({
milliseconds: parseInt(value, 10) / 1000000,
})
.format('SSS');
return parseFloat(
dayjs
.duration({
milliseconds: parseInt(value, 10) / 1000000,
})
.format('SSS'),
).toFixed(2);
};
function Duration(): JSX.Element {
@ -43,9 +45,10 @@ function Duration(): JSX.Element {
(state) => state.globalTime,
);
const getDuration = ():
| { maxDuration: string; minDuration: string }
| Record<string, string> => {
const preLocalMaxDuration = useRef<number>();
const preLocalMinDuration = useRef<number>();
const getDuration = useMemo(() => {
const selectedDuration = selectedFilter.get('duration');
if (selectedDuration) {
@ -56,17 +59,29 @@ function Duration(): JSX.Element {
}
return filter.get('duration') || {};
};
}, [selectedFilter, filter]);
const duration = getDuration();
const [preMax, setPreMax] = useState<string>('');
const [preMin, setPreMin] = useState<string>('');
const maxDuration = duration.maxDuration || '0';
const minDuration = duration.minDuration || '0';
useEffect(() => {
const duration = getDuration || {};
const [localMax, setLocalMax] = useState<string>(maxDuration);
const [localMin, setLocalMin] = useState<string>(minDuration);
const maxDuration = duration.maxDuration || '0';
const minDuration = duration.minDuration || '0';
const defaultValue = [parseFloat(minDuration), parseFloat(maxDuration)];
if (preLocalMaxDuration.current === undefined) {
preLocalMaxDuration.current = parseFloat(maxDuration);
}
if (preLocalMinDuration.current === undefined) {
preLocalMinDuration.current = parseFloat(minDuration);
}
setPreMax(maxDuration);
setPreMin(minDuration);
}, [getDuration]);
const defaultValue = [parseFloat(preMin), parseFloat(preMax)];
const updatedUrl = async (min: number, max: number): Promise<void> => {
const preSelectedFilter = new Map(selectedFilter);
@ -74,7 +89,6 @@ function Duration(): JSX.Element {
preSelectedFilter.set('duration', [String(max), String(min)]);
console.log('on the update Url');
const response = await getFilters({
end: String(globalTime.maxTime),
getFilters: filterToFetchData,
@ -87,8 +101,9 @@ function Duration(): JSX.Element {
const preFilter = getFilter(response.payload);
preFilter.forEach((value, key) => {
if (key !== 'duration') {
preUserSelected.set(key, Object.keys(value));
const values = Object.keys(value);
if (key !== 'duration' && values.length) {
preUserSelected.set(key, values);
}
});
@ -102,6 +117,7 @@ function Duration(): JSX.Element {
selectedTags,
userSelected: preUserSelected,
isFilterExclude,
order: spansAggregate.order,
},
});
@ -110,9 +126,9 @@ function Duration(): JSX.Element {
filterToFetchData,
spansAggregate.currentPage,
selectedTags,
preFilter,
isFilterExclude,
userSelectedFilter,
spansAggregate.order,
);
}
};
@ -120,13 +136,12 @@ function Duration(): JSX.Element {
const onRangeSliderHandler = (number: [number, number]): void => {
const [min, max] = number;
setLocalMin(min.toString());
setLocalMax(max.toString());
setPreMin(min.toString());
setPreMax(max.toString());
};
const debouncedFunction = useDebouncedFn(
(min, max) => {
console.log('debounce function');
updatedUrl(min as number, max as number);
},
500,
@ -137,11 +152,9 @@ function Duration(): JSX.Element {
event,
) => {
const { value } = event.target;
const min = parseFloat(localMin);
const min = parseFloat(preMin);
const max = parseFloat(value) * 1000000;
console.log('on change in max');
onRangeSliderHandler([min, max]);
debouncedFunction(min, max);
};
@ -151,9 +164,8 @@ function Duration(): JSX.Element {
) => {
const { value } = event.target;
const min = parseFloat(value) * 1000000;
const max = parseFloat(localMax);
const max = parseFloat(preMax);
onRangeSliderHandler([min, max]);
console.log('on change in min');
debouncedFunction(min, max);
};
@ -170,7 +182,7 @@ function Duration(): JSX.Element {
<Input
addonAfter="ms"
onChange={onChangeMinHandler}
value={getMs(localMin)}
value={getMs(preMin)}
/>
<InputContainer>
@ -179,27 +191,27 @@ function Duration(): JSX.Element {
<Input
addonAfter="ms"
onChange={onChangeMaxHandler}
value={getMs(localMax)}
value={getMs(preMax)}
/>
</Container>
<Container>
<Slider
defaultValue={[defaultValue[0], defaultValue[1]]}
min={parseFloat((filter.get('duration') || {}).minDuration)}
max={parseFloat((filter.get('duration') || {}).maxDuration)}
min={parseFloat((preLocalMinDuration.current || 0).toString())}
max={parseFloat((preLocalMaxDuration.current || 0).toString())}
range
tipFormatter={(value): JSX.Element => {
if (value === undefined) {
return <div />;
}
return <div>{`${getMs(value.toString())}ms`}</div>;
return <div>{`${getMs(value?.toString())}ms`}</div>;
}}
onChange={([min, max]): void => {
onRangeSliderHandler([min, max]);
}}
onAfterChange={onRangeHandler}
value={[parseFloat(localMin), parseFloat(localMax)]}
value={[parseFloat(preMin), parseFloat(preMax)]}
/>
</Container>
</div>

View File

@ -21,7 +21,7 @@ import {
ButtonContainer,
Container,
IconContainer,
TextCotainer,
TextContainer,
} from './styles';
const { Text } = Typography;
@ -64,16 +64,7 @@ function PanelHeading(props: PanelHeadingProps): JSX.Element {
const getprepdatedSelectedFilter = new Map(selectedFilter);
const getPreUserSelected = new Map(userSelectedFilter);
if (!isDefaultOpen) {
updatedFilterData = [PanelName];
} else {
// removing the selected filter
updatedFilterData = [
...filterToFetchData.filter((name) => name !== PanelName),
];
getprepdatedSelectedFilter.delete(PanelName);
getPreUserSelected.delete(PanelName);
}
updatedFilterData = [PanelName];
const response = await getFilters({
end: String(global.maxTime),
@ -86,33 +77,14 @@ function PanelHeading(props: PanelHeadingProps): JSX.Element {
if (response.statusCode === 200) {
const updatedFilter = getFilter(response.payload);
// is closed
if (!isDefaultOpen) {
// getprepdatedSelectedFilter.set(
// props.name,
// Object.keys(updatedFilter.get(props.name) || {}),
// );
if (!getPreUserSelected.has(PanelName)) {
getPreUserSelected.set(
PanelName,
Object.keys(updatedFilter.get(PanelName) || {}),
Object.keys(updatedFilter.get(PanelName) || []),
);
updatedFilterData = [...filterToFetchData, PanelName];
}
// now append the non prop.name trace filter enum over the list
// selectedFilter.forEach((value, key) => {
// if (key !== props.name) {
// getprepdatedSelectedFilter.set(key, value);
// }
// });
getPreUserSelected.forEach((value, key) => {
if (key !== PanelName) {
getPreUserSelected.set(key, value);
}
});
updatedFilterData = [...filterToFetchData, PanelName];
filter.forEach((value, key) => {
if (key !== PanelName) {
updatedFilter.set(key, value);
@ -129,6 +101,7 @@ function PanelHeading(props: PanelHeadingProps): JSX.Element {
selectedTags,
userSelected: getPreUserSelected,
isFilterExclude,
order: spansAggregate.order,
},
});
@ -137,9 +110,9 @@ function PanelHeading(props: PanelHeadingProps): JSX.Element {
updatedFilterData,
spansAggregate.currentPage,
selectedTags,
updatedFilter,
isFilterExclude,
getPreUserSelected,
spansAggregate.order,
);
} else {
notification.error({
@ -155,6 +128,43 @@ function PanelHeading(props: PanelHeadingProps): JSX.Element {
}
};
/**
* @description this function removes the selected filter
*/
const onCloseHandler = (): void => {
const preSelectedFilter = new Map(selectedFilter);
// removing the filter from filter to fetch the data
const preFilterToFetchTheData = [
...filterToFetchData.filter((name) => name !== PanelName),
];
// preSelectedFilter.delete(PanelName);
dispatch({
type: UPDATE_ALL_FILTERS,
payload: {
current: spansAggregate.currentPage,
filter,
filterToFetchData: preFilterToFetchTheData,
selectedFilter: preSelectedFilter,
selectedTags,
userSelected: userSelectedFilter,
isFilterExclude,
order: spansAggregate.order,
},
});
updateURL(
preSelectedFilter,
preFilterToFetchTheData,
spansAggregate.currentPage,
selectedTags,
isFilterExclude,
userSelectedFilter,
spansAggregate.order,
);
};
const onClearAllHandler = async (): Promise<void> => {
try {
setIsLoading(true);
@ -177,18 +187,19 @@ function PanelHeading(props: PanelHeadingProps): JSX.Element {
});
if (response.statusCode === 200 && response.payload) {
const getUpatedFilter = getFilter(response.payload);
const getUpdatedFilter = getFilter(response.payload);
dispatch({
type: UPDATE_ALL_FILTERS,
payload: {
current: spansAggregate.currentPage,
filter: getUpatedFilter,
filter: getUpdatedFilter,
filterToFetchData,
selectedFilter: updatedFilter,
selectedTags,
userSelected: preUserSelected,
isFilterExclude: postIsFilterExclude,
order: spansAggregate.order,
},
});
@ -197,9 +208,9 @@ function PanelHeading(props: PanelHeadingProps): JSX.Element {
filterToFetchData,
spansAggregate.currentPage,
selectedTags,
getUpatedFilter,
postIsFilterExclude,
preUserSelected,
spansAggregate.order,
);
} else {
notification.error({
@ -280,7 +291,7 @@ function PanelHeading(props: PanelHeadingProps): JSX.Element {
aria-disabled={filterLoading || isLoading}
aria-expanded={IsPanelOpen}
>
<TextCotainer onClick={onExpandHandler}>
<TextContainer onClick={isDefaultOpen ? onCloseHandler : onExpandHandler}>
<IconContainer>
{!IsPanelOpen ? <RightOutlined /> : <DownOutlined />}
</IconContainer>
@ -288,7 +299,7 @@ function PanelHeading(props: PanelHeadingProps): JSX.Element {
<Text style={{ textTransform: 'capitalize' }} ellipsis>
{AllPanelHeading.find((e) => e.key === PanelName)?.displayValue || ''}
</Text>
</TextCotainer>
</TextContainer>
{PanelName !== 'duration' && (
<ButtonContainer>

View File

@ -30,7 +30,7 @@ export const IconContainer = styled.div`
}
`;
export const TextCotainer = styled.div`
export const TextContainer = styled.div`
&&& {
display: flex;
cursor: pointer;

View File

@ -0,0 +1,70 @@
import { Select } from 'antd';
import { DefaultOptionType } from 'antd/lib/select';
import getTagValue from 'api/trace/getTagValue';
import useFetch from 'hooks/useFetch';
import React from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { PayloadProps, Props } from 'types/api/trace/getTagValue';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace';
import { Value } from '.';
import { SelectComponent } from './styles';
function TagValue(props: TagValueProps): JSX.Element {
const { tag, setLocalSelectedTags, index, tagKey } = props;
const {
Key: selectedKey,
Operator: selectedOperator,
Values: selectedValues,
} = tag;
const globalReducer = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const valueSuggestion = useFetch<PayloadProps, Props>(getTagValue, {
end: globalReducer.maxTime,
start: globalReducer.minTime,
tagKey,
});
return (
<SelectComponent
value={selectedValues[0]}
onSelect={(value: unknown): void => {
if (typeof value === 'string') {
setLocalSelectedTags((tags) => [
...tags.slice(0, index),
{
Key: selectedKey,
Operator: selectedOperator,
Values: [...selectedValues, value],
},
...tags.slice(index + 1, tags.length),
]);
}
}}
loading={valueSuggestion.loading || false}
>
{valueSuggestion.payload &&
valueSuggestion.payload.map((suggestion) => (
<Select.Option key={suggestion.tagValues} value={suggestion.tagValues}>
{suggestion.tagValues}
</Select.Option>
))}
</SelectComponent>
);
}
interface TagValueProps {
index: number;
tag: FlatArray<TraceReducer['selectedTags'], 1>;
setLocalSelectedTags: React.Dispatch<
React.SetStateAction<TraceReducer['selectedTags']>
>;
tagKey: string;
}
export default TagValue;

View File

@ -5,13 +5,9 @@ import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { TraceReducer } from 'types/reducer/trace';
import {
Container,
IconContainer,
SelectComponent,
ValueSelect,
} from './styles';
import { Container, IconContainer, SelectComponent } from './styles';
import TagsKey from './TagKey';
import TagValue from './TagValue';
const { Option } = Select;
@ -68,7 +64,6 @@ function SingleTags(props: AllTagsProps): JSX.Element {
tag={tag}
setLocalSelectedTags={setLocalSelectedTags}
/>
<SelectComponent
onChange={onChangeOperatorHandler}
value={AllMenu.find((e) => e.key === selectedOperator)?.value || ''}
@ -80,21 +75,16 @@ function SingleTags(props: AllTagsProps): JSX.Element {
))}
</SelectComponent>
<ValueSelect
value={selectedValues}
onChange={(value): void => {
setLocalSelectedTags((tags) => [
...tags.slice(0, index),
{
Key: selectedKey,
Operator: selectedOperator,
Values: value as string[],
},
...tags.slice(index + 1, tags.length),
]);
}}
mode="tags"
/>
{selectedKey[0] ? (
<TagValue
index={index}
tag={tag}
setLocalSelectedTags={setLocalSelectedTags}
tagKey={selectedKey[0]}
/>
) : (
<SelectComponent />
)}
<IconContainer role="button" onClick={(): void => onDeleteTagHandler(index)}>
<CloseOutlined />
@ -112,4 +102,10 @@ interface AllTagsProps {
>;
}
export interface Value {
key: string;
label: string;
value: string;
}
export default SingleTags;

View File

@ -15,12 +15,6 @@ export const SelectComponent = styled(Select)`
}
`;
export const ValueSelect = styled(Select)`
&&& {
width: 100%;
}
`;
export const Container = styled.div`
&&& {
display: flex;

View File

@ -5,7 +5,7 @@ import { connect, useSelector } from 'react-redux';
import { bindActionCreators } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { UpdateTagIsError } from 'store/actions/trace/updateIsTagsError';
import { UpdateTagVisiblity } from 'store/actions/trace/updateTagPanelVisiblity';
import { UpdateTagVisibility } from 'store/actions/trace/updateTagPanelVisiblity';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { TraceReducer } from 'types/reducer/trace';
@ -27,7 +27,7 @@ const { Paragraph } = Typography;
function AllTags({
updateTagIsError,
onChangeHandler,
updateTagVisiblity,
updateTagVisibility,
updateFilters,
}: AllTagsProps): JSX.Element {
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
@ -63,7 +63,7 @@ function AllTags({
onChangeHandler(parsedQuery.payload);
updateFilters(localSelectedTags);
updateTagIsError(false);
updateTagVisiblity(false);
updateTagVisibility(false);
}
};
@ -75,7 +75,7 @@ function AllTags({
return (
<ErrorContainer>
<Paragraph style={{ color: '#E89A3C' }}>
Unrecognised query format. Please reset your query by clicking `X` in the
Unrecognized query format. Please reset your query by clicking `X` in the
search bar above.
</Paragraph>
@ -116,14 +116,16 @@ function AllTags({
</Wrapper>
<ButtonContainer>
<Button onClick={onResetHandler}>Reset</Button>
<Button
type="primary"
onClick={onRunQueryHandler}
icon={<CaretRightFilled />}
>
Run Query
</Button>
<Space align="start">
<Button onClick={onResetHandler}>Reset</Button>
<Button
type="primary"
onClick={onRunQueryHandler}
icon={<CaretRightFilled />}
>
Run Query
</Button>
</Space>
</ButtonContainer>
</Container>
);
@ -131,14 +133,14 @@ function AllTags({
interface DispatchProps {
updateTagIsError: (value: boolean) => void;
updateTagVisiblity: (value: boolean) => void;
updateTagVisibility: (value: boolean) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateTagIsError: bindActionCreators(UpdateTagIsError, dispatch),
updateTagVisiblity: bindActionCreators(UpdateTagVisiblity, dispatch),
updateTagVisibility: bindActionCreators(UpdateTagVisibility, dispatch),
});
interface AllTagsProps extends DispatchProps {

View File

@ -6,7 +6,7 @@ export const Container = styled(Card)`
min-height: 20vh;
width: 100%;
z-index: 2;
position: absolute;
position: absolute !important;
.ant-card-body {
padding-bottom: 0;
@ -35,20 +35,16 @@ export const Wrapper = styled.div`
}
`;
export const ButtonContainer = styled.div`
export const ButtonContainer = styled(Card)`
display: flex;
justify-content: flex-end;
align-items: center;
background-color: #303030;
padding-top: 11px;
padding-bottom: 11px;
padding-right: 38.01px;
margin-top: 1rem;
padding-top: 11px !important;
padding-bottom: 11px !important;
padding-right: 38.01px !important;
> button:nth-child(1) {
margin-right: 1rem;
}
margin-top: 1rem !important;
`;
export const CurrentTagsContainer = styled.div`

View File

@ -1,12 +1,11 @@
import { CaretRightFilled } from '@ant-design/icons';
import { Space } from 'antd';
import useClickOutside from 'hooks/useClickOutside';
import React, { useEffect, useRef, useState } from 'react';
import { connect, useDispatch, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { UpdateTagIsError } from 'store/actions/trace/updateIsTagsError';
import { UpdateTagVisiblity } from 'store/actions/trace/updateTagPanelVisiblity';
import { UpdateTagVisibility } from 'store/actions/trace/updateTagPanelVisiblity';
import { updateURL } from 'store/actions/trace/util';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
@ -18,7 +17,7 @@ import { Container, SearchComponent } from './styles';
import { parseQueryToTags, parseTagsToQuery } from './util';
function Search({
updateTagVisiblity,
updateTagVisibility,
updateTagIsError,
}: SearchProps): JSX.Element {
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
@ -66,7 +65,7 @@ function Search({
!(e.ariaSelected === 'true') &&
traces.isTagModalOpen
) {
updateTagVisiblity(false);
updateTagVisibility(false);
}
});
@ -75,7 +74,7 @@ function Search({
};
const setIsTagsModalHandler = (value: boolean): void => {
updateTagVisiblity(value);
updateTagVisibility(value);
};
const onFocusHandler: React.FocusEventHandler<HTMLInputElement> = (e) => {
@ -96,6 +95,7 @@ function Search({
selectedFilter: traces.selectedFilter,
userSelected: traces.userSelectedFilter,
isFilterExclude: traces.isFilterExclude,
order: traces.spansAggregate.order,
},
});
@ -104,60 +104,58 @@ function Search({
traces.filterToFetchData,
traces.spansAggregate.currentPage,
selectedTags,
traces.filter,
traces.isFilterExclude,
traces.userSelectedFilter,
traces.spansAggregate.order,
);
};
return (
<Space direction="vertical" style={{ width: '100%' }}>
<Container ref={tagRef}>
<SearchComponent
onChange={(event): void => onChangeHandler(event.target.value)}
value={value}
allowClear
disabled={traces.filterLoading}
onFocus={onFocusHandler}
placeholder="Click to filter by tags"
type="search"
enterButton={<CaretRightFilled />}
onSearch={(string): void => {
if (string.length === 0) {
updateTagVisiblity(false);
updateFilters([]);
return;
}
<Container ref={tagRef}>
<SearchComponent
onChange={(event): void => onChangeHandler(event.target.value)}
value={value}
allowClear
disabled={traces.filterLoading}
onFocus={onFocusHandler}
placeholder="Click to filter by tags"
type="search"
enterButton={<CaretRightFilled />}
onSearch={(string): void => {
if (string.length === 0) {
updateTagVisibility(false);
updateFilters([]);
return;
}
const { isError, payload } = parseQueryToTags(string);
const { isError, payload } = parseQueryToTags(string);
if (isError) {
updateTagIsError(true);
} else {
updateTagIsError(false);
updateTagVisiblity(false);
updateFilters(payload);
}
}}
/>
if (isError) {
updateTagIsError(true);
} else {
updateTagIsError(false);
updateTagVisibility(false);
updateFilters(payload);
}
}}
/>
{traces.isTagModalOpen && (
<Tags updateFilters={updateFilters} onChangeHandler={onChangeHandler} />
)}
</Container>
</Space>
{traces.isTagModalOpen && (
<Tags updateFilters={updateFilters} onChangeHandler={onChangeHandler} />
)}
</Container>
);
}
interface DispatchProps {
updateTagVisiblity: (value: boolean) => void;
updateTagVisibility: (value: boolean) => void;
updateTagIsError: (value: boolean) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateTagVisiblity: bindActionCreators(UpdateTagVisiblity, dispatch),
updateTagVisibility: bindActionCreators(UpdateTagVisibility, dispatch),
updateTagIsError: bindActionCreators(UpdateTagIsError, dispatch),
});

View File

@ -6,6 +6,7 @@ const { Search } = Input;
export const Container = styled.div`
display: flex;
position: relative;
width: 100%;
`;
export const SearchComponent = styled(Search)`

View File

@ -3,35 +3,36 @@ import Table, { ColumnsType } from 'antd/lib/table';
import ROUTES from 'constants/routes';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import history from 'lib/history';
import React from 'react';
import { connect, useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import { bindActionCreators } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import {
GetSpansAggregate,
GetSpansAggregateProps,
} from 'store/actions/trace/getInitialSpansAggregate';
import { useDispatch, useSelector } from 'react-redux';
import { Dispatch } from 'redux';
import { updateURL } from 'store/actions/trace/util';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { GlobalReducer } from 'types/reducer/globalTime';
import {
UPDATE_SPAN_ORDER,
UPDATE_SPANS_AGGREGATE_PAGE_NUMBER,
} from 'types/actions/trace';
import { TraceReducer } from 'types/reducer/trace';
dayjs.extend(duration);
function TraceTable({ getSpansAggregate }: TraceProps): JSX.Element {
function TraceTable(): JSX.Element {
const {
spansAggregate,
selectedFilter,
selectedTags,
filterLoading,
userSelectedFilter,
filter,
isFilterExclude,
filterToFetchData,
} = useSelector<AppState, TraceReducer>((state) => state.traces);
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const dispatch = useDispatch<Dispatch<AppActions>>();
const { loading, total } = spansAggregate;
const { loading, total, order: spansAggregateOrder } = spansAggregate;
type TableType = FlatArray<TraceReducer['spansAggregate']['data'], 1>;
@ -39,30 +40,17 @@ function TraceTable({ getSpansAggregate }: TraceProps): JSX.Element {
return `${ROUTES.TRACE}/${record.traceID}?spanId=${record.spanID}`;
};
const getValue = (value: string, record: TableType): JSX.Element => {
return (
<Link to={getLink(record)}>
<Typography>{value}</Typography>
</Link>
);
const getValue = (value: string): JSX.Element => {
return <Typography>{value}</Typography>;
};
const getHttpMethodOrStatus = (
value: TableType['httpMethod'],
record: TableType,
): JSX.Element => {
if (value.length === 0) {
return (
<Link to={getLink(record)}>
<Typography>-</Typography>
</Link>
);
return <Typography>-</Typography>;
}
return (
<Link to={getLink(record)}>
<Tag color="magenta">{value}</Tag>
</Link>
);
return <Tag color="magenta">{value}</Tag>;
};
const columns: ColumnsType<TableType> = [
@ -71,13 +59,9 @@ function TraceTable({ getSpansAggregate }: TraceProps): JSX.Element {
dataIndex: 'timestamp',
key: 'timestamp',
sorter: true,
render: (value: TableType['timestamp'], record): JSX.Element => {
render: (value: TableType['timestamp']): JSX.Element => {
const day = dayjs(value);
return (
<Link to={getLink(record)}>
<Typography>{day.format('YYYY/MM/DD HH:mm:ss')}</Typography>
</Link>
);
return <Typography>{day.format('YYYY/MM/DD HH:mm:ss')}</Typography>;
},
},
{
@ -96,14 +80,13 @@ function TraceTable({ getSpansAggregate }: TraceProps): JSX.Element {
title: 'Duration',
dataIndex: 'durationNano',
key: 'durationNano',
render: (value: TableType['durationNano'], record): JSX.Element => (
<Link to={getLink(record)}>
<Typography>
{`${dayjs
.duration({ milliseconds: value / 1000000 })
.asMilliseconds()} ms`}
</Typography>
</Link>
render: (value: TableType['durationNano']): JSX.Element => (
<Typography>
{`${dayjs
.duration({ milliseconds: value / 1000000 })
.asMilliseconds()
.toFixed(2)} ms`}
</Typography>
),
},
{
@ -126,17 +109,33 @@ function TraceTable({ getSpansAggregate }: TraceProps): JSX.Element {
sort,
) => {
if (!Array.isArray(sort)) {
const { order = 'ascend' } = sort;
const { order = spansAggregateOrder } = sort;
if (props.current && props.pageSize) {
getSpansAggregate({
maxTime: globalTime.maxTime,
minTime: globalTime.minTime,
selectedFilter,
current: props.current,
pageSize: props.pageSize,
selectedTags,
order: order === 'ascend' ? 'ascending' : 'descending',
const spanOrder = order || spansAggregateOrder;
dispatch({
type: UPDATE_SPAN_ORDER,
payload: {
order: spanOrder,
},
});
dispatch({
type: UPDATE_SPANS_AGGREGATE_PAGE_NUMBER,
payload: {
currentPage: props.current,
},
});
updateURL(
selectedFilter,
filterToFetchData,
props.current,
selectedTags,
isFilterExclude,
userSelectedFilter,
spanOrder,
);
}
}
};
@ -147,10 +146,17 @@ function TraceTable({ getSpansAggregate }: TraceProps): JSX.Element {
dataSource={spansAggregate.data}
loading={loading || filterLoading}
columns={columns}
rowKey="timestamp"
rowKey={(record): string => `${record.traceID}-${record.spanID}`}
style={{
cursor: 'pointer',
}}
onRow={(record): React.HTMLAttributes<TableType> => ({
onClick: (event): void => {
event.preventDefault();
event.stopPropagation();
history.push(getLink(record));
},
})}
pagination={{
current: spansAggregate.currentPage,
pageSize: spansAggregate.pageSize,
@ -158,20 +164,9 @@ function TraceTable({ getSpansAggregate }: TraceProps): JSX.Element {
position: ['bottomLeft'],
total,
}}
sortDirections={['ascend', 'descend']}
/>
);
}
interface DispatchProps {
getSpansAggregate: (props: GetSpansAggregateProps) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getSpansAggregate: bindActionCreators(GetSpansAggregate, dispatch),
});
type TraceProps = DispatchProps;
export default connect(null, mapDispatchToProps)(TraceTable);
export default TraceTable;

View File

@ -1,42 +1,51 @@
/**
* @jest-environment jsdom
*/
import { expect } from '@jest/globals';
import { render } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import TraceFlameGraph from 'container/TraceFlameGraph';
import React, { useState } from 'react';
import { Provider } from 'react-redux';
import store from 'store';
test('loads and displays greeting', () => {
const onSpanHover = useState('');
const { rerender } = renderHook(() => useState(''));
const { asFragment } = render(
<TraceFlameGraph
{...{
hoveredSpanId: '',
intervalUnit: { multiplier: 0, name: 'm' },
onSpanHover: onSpanHover[1],
onSpanSelect: (): void => {},
selectedSpanId: '',
traceMetaData: {
globalEnd: 0,
globalStart: 0,
levels: 0,
spread: 0,
totalSpans: 0,
},
treeData: {
children: [],
id: '',
name: '',
serviceColour: '',
serviceName: '',
startTime: 0,
tags: [],
time: 0,
value: 0,
event: [],
hasError: false,
parent: undefined,
},
}}
/>,
<Provider store={store}>
<TraceFlameGraph
{...{
hoveredSpanId: '',
intervalUnit: { multiplier: 0, name: 'm' },
onSpanHover: rerender,
onSpanSelect: (): void => {},
selectedSpanId: '',
traceMetaData: {
globalEnd: 0,
globalStart: 0,
levels: 0,
spread: 0,
totalSpans: 0,
},
treeData: {
children: [],
id: '',
name: '',
serviceColour: '#a0e',
serviceName: '',
startTime: 0,
tags: [],
time: 100,
value: 100,
event: [],
hasError: false,
parent: undefined,
},
}}
/>
</Provider>,
);
expect(asFragment()).toMatchSnapshot();
});

View File

@ -1,3 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`loads and displays greeting 1`] = `<DocumentFragment />`;
exports[`loads and displays greeting 1`] = `
<DocumentFragment>
<div
class="sc-gsDKAQ jFDWPs"
height="0"
>
<div
class="sc-bdvvtL fyFVjh"
title="
0 m"
width="Infinity"
/>
</div>
</DocumentFragment>
`;

View File

@ -0,0 +1,110 @@
import { WarningFilled } from '@ant-design/icons';
import { Button, Card, Form, Space, Typography } from 'antd';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import { InputComponent } from './styles';
const { Title } = Typography;
function Version(): JSX.Element {
const [form] = Form.useForm();
const { t } = useTranslation();
const onClickUpgradeHandler = useCallback((link: string) => {
window.open(link, '_blank');
}, []);
const {
currentVersion,
latestVersion,
isCurrentVersionError,
isLatestVersionError,
} = useSelector<AppState, AppReducer>((state) => state.app);
const isLatestVersion = currentVersion === latestVersion;
const isError = isCurrentVersionError || isLatestVersionError;
return (
<Card>
<Title ellipsis level={4}>
{t('version')}
</Title>
<Form
wrapperCol={{
span: 14,
}}
labelCol={{
span: 3,
}}
layout="horizontal"
form={form}
labelAlign="left"
>
<Form.Item label={t('current_version')}>
<InputComponent
readOnly
value={isCurrentVersionError ? t('n_a').toString() : currentVersion}
placeholder={t('current_version')}
/>
</Form.Item>
<Form.Item label={t('latest_version')}>
<InputComponent
readOnly
value={isLatestVersionError ? t('n_a').toString() : latestVersion}
placeholder={t('latest_version')}
/>
<Button
onClick={(): void =>
onClickUpgradeHandler('https://github.com/SigNoz/signoz/releases')
}
type="link"
>
{t('release_notes')}
</Button>
</Form.Item>
</Form>
{!isError && isLatestVersion && (
<div>
<Space align="start">
<span></span>
<Typography.Paragraph italic>
{t('latest_version_signoz')}
</Typography.Paragraph>
</Space>
</div>
)}
{!isError && !isLatestVersion && (
<div>
<Space align="start">
<span>
<WarningFilled style={{ color: '#E87040' }} />
</span>
<Typography.Paragraph italic>{t('stale_version')}</Typography.Paragraph>
</Space>
</div>
)}
{!isError && !isLatestVersion && (
<Button
onClick={(): void =>
onClickUpgradeHandler(
'https://signoz.io/docs/operate/docker-standalone/#upgrade',
)
}
>
{t('read_how_to_upgrade')}
</Button>
)}
</Card>
);
}
export default Version;

View File

@ -0,0 +1,8 @@
import { Input } from 'antd';
import styled from 'styled-components';
export const InputComponent = styled(Input)`
&&& {
max-width: 183px;
}
`;

View File

@ -4,6 +4,7 @@ import './ReactI18';
import AppRoutes from 'AppRoutes';
import React from 'react';
import ReactDOM from 'react-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import reportWebVitals from 'reportWebVitals';
import store from 'store';
@ -12,12 +13,16 @@ if (process.env.NODE_ENV === 'development') {
reportWebVitals(console.log);
}
const queryClient = new QueryClient();
ReactDOM.render(
<Provider store={store}>
<React.StrictMode>
<AppRoutes />
</React.StrictMode>
</Provider>,
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<React.StrictMode>
<AppRoutes />
</React.StrictMode>
</Provider>
</QueryClientProvider>,
document.querySelector('#root'),
);

View File

@ -1,6 +1,6 @@
import { expect } from '@jest/globals';
import dayjs from 'dayjs';
import getStep, { DefaultStepSize } from 'lib/getStep';
import getStep, { DefaultStepSize, MaxDataPoints } from 'lib/getStep';
describe('lib/getStep', () => {
test('should return default step when the given range is less than 1 day', () => {
@ -40,7 +40,8 @@ describe('lib/getStep', () => {
const startUnix = start.valueOf();
const endUnix = end.valueOf();
const expectedStepSize = end.diff(start, 'days') * DefaultStepSize;
const expectedStepSize = Math.floor(end.diff(start, 's') / MaxDataPoints);
expect(
getStep({
start: startUnix / 1e3,

View File

@ -1,6 +1,6 @@
import { SettingPeroid } from 'container/GeneralSettings';
import { SettingPeriod } from 'container/GeneralSettings';
const converIntoHr = (value: number, peroid: SettingPeroid): number => {
const converIntoHr = (value: number, peroid: SettingPeriod): number => {
if (peroid === 'day') {
return value * 24;
}

View File

@ -1,4 +1,4 @@
import { SettingPeroid } from 'container/GeneralSettings';
import { SettingPeriod } from 'container/GeneralSettings';
const getSettingsPeroid = (hr: number): PayloadProps => {
if (hr <= 0) {
@ -30,7 +30,7 @@ const getSettingsPeroid = (hr: number): PayloadProps => {
interface PayloadProps {
value: number;
peroid: SettingPeroid;
peroid: SettingPeriod;
}
export default getSettingsPeroid;

View File

@ -30,6 +30,7 @@ const convertToMs = (
};
export const DefaultStepSize = 60;
export const MaxDataPoints = 200;
/**
* Returns relevant step size based on given start and end date.
@ -37,13 +38,9 @@ export const DefaultStepSize = 60;
const getStep = ({ start, end, inputFormat = 'ms' }: GetStepInput): number => {
const startDate = dayjs(convertToMs(Number(start), inputFormat));
const endDate = dayjs(convertToMs(Number(end), inputFormat));
const diffDays = Math.abs(endDate.diff(startDate, 'days'));
const diffSec = Math.abs(endDate.diff(startDate, 's'));
if (diffDays > 1) {
return DefaultStepSize * diffDays;
}
return DefaultStepSize;
return Math.max(Math.floor(diffSec / MaxDataPoints), DefaultStepSize);
};
export default getStep;

View File

@ -1,12 +1,13 @@
/* eslint-disable */
//@ts-nocheck
import { Card } from 'antd';
import Spinner from 'components/Spinner';
import React, { useEffect, useRef } from 'react';
import { ForceGraph2D } from 'react-force-graph';
import { connect } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { getDetailedServiceMapItems, getServiceMapItems } from 'store/actions';
import { getDetailedServiceMapItems, ServiceMapStore } from 'store/actions';
import { AppState } from 'store/reducers';
import styled from 'styled-components';
import { GlobalTime } from 'types/actions/globalTime';
@ -31,9 +32,8 @@ const Container = styled.div`
`;
interface ServiceMapProps extends RouteComponentProps<any> {
serviceMap: serviceMapStore;
serviceMap: ServiceMapStore;
globalTime: GlobalTime;
getServiceMapItems: (time: GlobalTime) => void;
getDetailedServiceMapItems: (time: GlobalTime) => void;
}
interface graphNode {
@ -53,29 +53,32 @@ export interface graphDataType {
function ServiceMap(props: ServiceMapProps): JSX.Element {
const fgRef = useRef();
const {
getDetailedServiceMapItems,
getServiceMapItems,
globalTime,
serviceMap,
} = props;
const { getDetailedServiceMapItems, globalTime, serviceMap } = props;
useEffect(() => {
/*
Call the apis only when the route is loaded.
Check this issue: https://github.com/SigNoz/signoz/issues/110
*/
getServiceMapItems(globalTime);
getDetailedServiceMapItems(globalTime);
}, [globalTime, getServiceMapItems, getDetailedServiceMapItems]);
}, [globalTime, getDetailedServiceMapItems]);
useEffect(() => {
fgRef.current && fgRef.current.d3Force('charge').strength(-400);
});
if (!serviceMap.items.length || !serviceMap.services.length) {
if (serviceMap.loading) {
return <Spinner size="large" tip="Loading..." />;
}
if (!serviceMap.loading && serviceMap.items.length === 0) {
return (
<Container>
<Card>No Service Found</Card>
</Container>
);
}
const zoomToService = (value: string): void => {
fgRef &&
fgRef.current &&
@ -149,7 +152,6 @@ const mapStateToProps = (
export default withRouter(
connect(mapStateToProps, {
getServiceMapItems,
getDetailedServiceMapItems,
})(ServiceMap),
);

View File

@ -84,7 +84,7 @@ function _UsageExplorer(props: UsageExplorerProps): JSX.Element {
if (selectedTime && selectedInterval) {
const maxTime = new Date().getTime() * 1000000;
const minTime = maxTime - selectedTime.value * 24 * 3600000 * 1000000;
getUsageData(minTime, maxTime, selectedInterval.value, selectedService);
}
}, [selectedTime, selectedInterval, selectedService, getUsageData]);

View File

@ -5,29 +5,32 @@ import CreateAlertChannels from 'container/CreateAlertChannels';
import GeneralSettings from 'container/GeneralSettings';
import history from 'lib/history';
import React from 'react';
import { useTranslation } from 'react-i18next';
function SettingsPage(): JSX.Element {
const pathName = history.location.pathname;
const { t } = useTranslation();
return (
<RouteTab
{...{
routes: [
{
Component: GeneralSettings,
name: 'General Settings',
name: t('routes.general'),
route: ROUTES.SETTINGS,
},
{
Component: (): JSX.Element => {
return <CreateAlertChannels preType="slack" />;
},
name: 'Alert Channels',
name: t('routes.alert_channels'),
route: ROUTES.ALL_CHANNELS,
},
],
activeKey:
pathName === ROUTES.SETTINGS ? 'General Settings' : 'Alert Channels',
pathName === ROUTES.SETTINGS
? t('routes.general')
: t('routes.alert_channels'),
}}
/>
);

View File

@ -4,27 +4,30 @@ import AlertChannels from 'container/AllAlertChannels';
import GeneralSettings from 'container/GeneralSettings';
import history from 'lib/history';
import React from 'react';
import { useTranslation } from 'react-i18next';
function AllAlertChannels(): JSX.Element {
const pathName = history.location.pathname;
const { t } = useTranslation();
return (
<RouteTab
{...{
routes: [
{
Component: GeneralSettings,
name: 'General Settings',
name: t('routes.general'),
route: ROUTES.SETTINGS,
},
{
Component: AlertChannels,
name: 'Alert Channels',
name: t('routes.alert_channels'),
route: ROUTES.ALL_CHANNELS,
},
],
activeKey:
pathName === ROUTES.SETTINGS ? 'General Settings' : 'Alert Channels',
pathName === ROUTES.SETTINGS
? t('routes.general')
: t('routes.alert_channels'),
}}
/>
);

View File

@ -14,7 +14,7 @@ function SettingsPage(): JSX.Element {
routes: [
{
Component: GeneralSettings,
name: 'General Settings',
name: 'General',
route: ROUTES.SETTINGS,
},
{
@ -23,8 +23,7 @@ function SettingsPage(): JSX.Element {
route: ROUTES.ALL_CHANNELS,
},
],
activeKey:
pathName === ROUTES.ALL_CHANNELS ? 'Alert Channels' : 'General Settings',
activeKey: pathName === ROUTES.ALL_CHANNELS ? 'Alert Channels' : 'General',
}}
/>
);

View File

@ -0,0 +1,8 @@
import Version from 'container/Version';
import React from 'react';
function Status(): JSX.Element {
return <Version />;
}
export default Status;

View File

@ -5,6 +5,7 @@ import TraceGraph from 'container/Trace/Graph';
import Search from 'container/Trace/Search';
import TraceGraphFilter from 'container/Trace/TraceGraphFilter';
import TraceTable from 'container/Trace/TraceTable';
import getStep from 'lib/getStep';
import history from 'lib/history';
import React, { useCallback, useEffect, useState } from 'react';
import { connect, useDispatch, useSelector } from 'react-redux';
@ -63,7 +64,7 @@ function Trace({
current: spansAggregate.currentPage,
pageSize: spansAggregate.pageSize,
selectedTags,
order: 'ascending',
order: spansAggregate.order === 'ascend' ? 'ascending' : 'descending',
});
}, [
selectedTags,
@ -73,6 +74,7 @@ function Trace({
getSpansAggregate,
spansAggregate.currentPage,
spansAggregate.pageSize,
spansAggregate.order,
]);
useEffect(() => {
@ -83,7 +85,7 @@ function Trace({
selectedFilter,
selectedTags,
start: minTime,
step: 60,
step: getStep({ start: minTime, end: maxTime, inputFormat: 'ns' }),
isFilterExclude,
});
}, [
@ -93,8 +95,8 @@ function Trace({
selectedTags,
maxTime,
minTime,
isFilterExclude,
getSpans,
isFilterExclude,
]);
useEffect(() => {

View File

@ -2,27 +2,30 @@ import { Typography } from 'antd';
import getTraceItem from 'api/trace/getTraceItem';
import Spinner from 'components/Spinner';
import TraceDetailContainer from 'container/TraceDetail';
import useFetch from 'hooks/useFetch';
import React from 'react';
import { useQuery } from 'react-query';
import { useParams } from 'react-router-dom';
import { Props as TraceDetailProps } from 'types/api/trace/getTraceItem';
function TraceDetail(): JSX.Element {
const { id } = useParams<TraceDetailProps>();
const { data: traceDetailResponse, error, isLoading, isError } = useQuery(
`getTraceItem/${id}`,
() => getTraceItem({ id }),
{
cacheTime: 3000,
},
);
const traceDetailResponse = useFetch(getTraceItem, {
id,
});
if (traceDetailResponse.error) {
if (traceDetailResponse?.error || error || isError) {
return (
<Typography>
{traceDetailResponse.errorMessage || 'Something went wrong'}
{traceDetailResponse?.error || 'Something went wrong'}
</Typography>
);
}
if (traceDetailResponse.loading || traceDetailResponse.payload === undefined) {
if (isLoading || !(traceDetailResponse && traceDetailResponse.payload)) {
return <Spinner tip="Loading.." />;
}

View File

@ -5,6 +5,7 @@ import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems
import GetMaxMinTime from 'lib/getMaxMinTime';
import GetMinMax from 'lib/getMinMax';
import GetStartAndEndTime from 'lib/getStartAndEndTime';
import getStep from 'lib/getStep';
import { Dispatch } from 'redux';
import store from 'store';
import AppActions from 'types/actions';
@ -45,7 +46,7 @@ export const GetQueryResults = (
end,
query: encodeURIComponent(query.query),
start,
step: '60',
step: `${getStep({ start, end, inputFormat: 'ms' })}`,
});
return {
query: query.query,

View File

@ -7,6 +7,7 @@ import { ActionTypes } from './types';
export interface ServiceMapStore {
items: ServicesMapItem[];
services: ServicesItem[];
loading: boolean;
}
export interface ServicesItem {
@ -37,38 +38,39 @@ export interface ServicesAction {
payload: ServicesItem[];
}
export const getServiceMapItems = (globalTime: GlobalTime) => {
return async (dispatch: Dispatch): Promise<void> => {
dispatch<ServiceMapItemAction>({
type: ActionTypes.getServiceMapItems,
payload: [],
});
const requestString = `/serviceMapDependencies?start=${globalTime.minTime}&end=${globalTime.maxTime}`;
const response = await api.get<ServicesMapItem[]>(requestString);
dispatch<ServiceMapItemAction>({
type: ActionTypes.getServiceMapItems,
payload: response.data,
});
export interface ServiceMapLoading {
type: ActionTypes.serviceMapLoading;
payload: {
loading: ServiceMapStore['loading'];
};
};
}
export const getDetailedServiceMapItems = (globalTime: GlobalTime) => {
return async (dispatch: Dispatch): Promise<void> => {
dispatch<ServicesAction>({
type: ActionTypes.getServices,
payload: [],
});
const requestString = `/services?start=${globalTime.minTime}&end=${globalTime.maxTime}`;
const response = await api.get<ServicesItem[]>(requestString);
const serviceMapDependencies = `/serviceMapDependencies?start=${globalTime.minTime}&end=${globalTime.maxTime}`;
const [serviceMapDependenciesResponse, response] = await Promise.all([
api.get<ServicesMapItem[]>(serviceMapDependencies),
api.get<ServicesItem[]>(requestString),
]);
dispatch<ServicesAction>({
type: ActionTypes.getServices,
payload: response.data,
});
dispatch<ServiceMapItemAction>({
type: ActionTypes.getServiceMapItems,
payload: serviceMapDependenciesResponse.data,
});
dispatch<ServiceMapLoading>({
type: ActionTypes.serviceMapLoading,
payload: {
loading: false,
},
});
};
};

View File

@ -18,6 +18,7 @@ import {
parseIsSkippedSelection,
parseQueryIntoCurrent,
parseQueryIntoFilter,
parseQueryIntoOrder,
parseQueryIntoSelectedTags,
parseSelectedFilter,
} from './util';
@ -66,6 +67,11 @@ export const GetInitialTraceFilter = (
traces.spansAggregate.currentPage,
);
const parsedQueryOrder = parseQueryIntoOrder(
query,
traces.spansAggregate.order,
);
const isSelectionSkipped = parseIsSkippedSelection(query);
const parsedSelectedTags = parseQueryIntoSelectedTags(
@ -148,6 +154,7 @@ export const GetInitialTraceFilter = (
selectedTags: parsedSelectedTags.currentValue,
userSelected: getUserSelected.currentValue,
isFilterExclude: getIsFilterExcluded.currentValue,
order: parsedQueryOrder.currentValue,
},
});
} else {

View File

@ -3,11 +3,13 @@ import getSpansAggregate from 'api/trace/getSpansAggregate';
import { Dispatch, Store } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { UPDATE_SPANS_AGGREEGATE } from 'types/actions/trace';
import { UPDATE_SPANS_AGGREGATE } from 'types/actions/trace';
import { Props as GetSpanAggregateProps } from 'types/api/trace/getSpanAggregate';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace';
import { updateURL } from './util';
export const GetSpansAggregate = (
props: GetSpansAggregateProps,
): ((
@ -29,10 +31,12 @@ export const GetSpansAggregate = (
return;
}
const order = props.order === 'ascending' ? 'ascend' : 'descend';
try {
// triggering loading
dispatch({
type: UPDATE_SPANS_AGGREEGATE,
type: UPDATE_SPANS_AGGREGATE,
payload: {
spansAggregate: {
currentPage: props.current,
@ -41,6 +45,7 @@ export const GetSpansAggregate = (
error: false,
total: spansAggregate.total,
pageSize: props.pageSize,
order,
},
},
});
@ -53,12 +58,12 @@ export const GetSpansAggregate = (
offset: props.current * props.pageSize - props.pageSize,
selectedTags: props.selectedTags,
isFilterExclude: traces.isFilterExclude,
order: props.order,
order,
});
if (response.statusCode === 200) {
dispatch({
type: UPDATE_SPANS_AGGREEGATE,
type: UPDATE_SPANS_AGGREGATE,
payload: {
spansAggregate: {
currentPage: props.current,
@ -67,16 +72,27 @@ export const GetSpansAggregate = (
error: false,
total: response.payload.totalSpans,
pageSize: props.pageSize,
order,
},
},
});
updateURL(
traces.selectedFilter,
traces.filterToFetchData,
props.current,
traces.selectedTags,
traces.isFilterExclude,
traces.userSelectedFilter,
order,
);
} else {
notification.error({
message: response.error || 'Something went wrong',
});
dispatch({
type: UPDATE_SPANS_AGGREEGATE,
type: UPDATE_SPANS_AGGREGATE,
payload: {
spansAggregate: {
currentPage: props.current,
@ -85,13 +101,14 @@ export const GetSpansAggregate = (
error: true,
total: spansAggregate.total,
pageSize: props.pageSize,
order,
},
},
});
}
} catch (error) {
dispatch({
type: UPDATE_SPANS_AGGREEGATE,
type: UPDATE_SPANS_AGGREGATE,
payload: {
spansAggregate: {
currentPage: props.current,
@ -100,6 +117,7 @@ export const GetSpansAggregate = (
error: true,
total: spansAggregate.total,
pageSize: props.pageSize,
order,
},
},
});

View File

@ -1,4 +1,3 @@
export * from './current';
export * from './filter';
export * from './filterToFetchData';
export * from './isFilterExclude';
@ -6,3 +5,5 @@ export * from './minMaxTime';
export * from './selectedFilter';
export * from './selectedTags';
export * from './skippedSelected';
export * from './spanAggregateCurrentPage';
export * from './spanAggregateOrder';

View File

@ -10,7 +10,7 @@ export const parseQueryIntoCurrent = (
let current = 1;
const selected = url.get('current');
const selected = url.get('spanAggregateCurrentPage');
if (selected) {
try {

View File

@ -0,0 +1,39 @@
import { TraceReducer } from 'types/reducer/trace';
import { ParsedUrl } from '../util';
export const parseQueryIntoOrder = (
query: string,
stateCurrent: TraceReducer['spansAggregate']['order'],
): ParsedUrl<TraceReducer['spansAggregate']['order']> => {
const url = new URLSearchParams(query);
let current = 'ascend';
const selected = url.get('spanAggregateOrder');
if (selected) {
try {
const parsedValue = selected;
if (parsedValue && typeof parsedValue === 'string') {
current = parsedValue;
}
} catch (error) {
console.log(error);
console.log('error while parsing json');
}
}
if (selected) {
return {
currentValue: current,
urlValue: current,
};
}
return {
currentValue: stateCurrent,
urlValue: current,
};
};

View File

@ -44,9 +44,9 @@ export const SelectedTraceFilter = (props: {
traces.filterToFetchData,
traces.spansAggregate.currentPage,
traces.selectedTags,
traces.filter,
traces.isFilterExclude,
traces.userSelectedFilter,
traces.spansAggregate.order,
);
};
};

View File

@ -1,14 +1,14 @@
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import { UPDATE_TAG_MODAL_VISIBLITY } from 'types/actions/trace';
import { UPDATE_TAG_MODAL_VISIBILITY } from 'types/actions/trace';
import { TraceReducer } from 'types/reducer/trace';
export const UpdateTagVisiblity = (
export const UpdateTagVisibility = (
isTagModalOpen: TraceReducer['isTagModalOpen'],
): ((dispatch: Dispatch<AppActions>) => void) => {
return (dispatch): void => {
dispatch({
type: UPDATE_TAG_MODAL_VISIBLITY,
type: UPDATE_TAG_MODAL_VISIBILITY,
payload: {
isTagModalOpen,
},

View File

@ -18,11 +18,11 @@ export function isTraceFilterEnum(
export const updateURL = (
selectedFilter: TraceReducer['selectedFilter'],
filterToFetchData: TraceReducer['filterToFetchData'],
current: TraceReducer['spansAggregate']['total'],
spanAggregateCurrentPage: TraceReducer['spansAggregate']['currentPage'],
selectedTags: TraceReducer['selectedTags'],
filter: TraceReducer['filter'],
isFilterExclude: TraceReducer['isFilterExclude'],
userSelectedFilter: TraceReducer['userSelectedFilter'],
spanAggregateOrder: TraceReducer['spansAggregate']['order'],
): void => {
const search = new URLSearchParams(window.location.search);
const preResult: { key: string; value: string }[] = [];
@ -30,11 +30,12 @@ export const updateURL = (
const keyToSkip = [
'selected',
'filterToFetchData',
'current',
'selectedTags',
'filter',
'isFilterExclude',
'userSelectedFilter',
'spanAggregateCurrentPage',
'spanAggregateOrder',
];
search.forEach((value, key) => {
@ -51,15 +52,15 @@ export const updateURL = (
Object.fromEntries(selectedFilter),
)}&filterToFetchData=${JSON.stringify(
filterToFetchData,
)}&current=${current}&selectedTags=${JSON.stringify(
)}&spanAggregateCurrentPage=${spanAggregateCurrentPage}&selectedTags=${JSON.stringify(
selectedTags,
)}&filter=${JSON.stringify(Object.fromEntries(filter))}&${preResult
)}&${preResult
.map((e) => `${e.key}=${e.value}`)
.join('&')}&isFilterExclude=${JSON.stringify(
Object.fromEntries(isFilterExclude),
)}&userSelectedFilter=${JSON.stringify(
Object.fromEntries(userSelectedFilter),
)}`,
)}&spanAggregateCurrentPage=${spanAggregateCurrentPage}&spanAggregateOrder=${spanAggregateOrder}`,
);
};

View File

@ -1,14 +1,22 @@
import { ServiceMapItemAction, ServicesAction } from './serviceMap';
import {
ServiceMapItemAction,
ServiceMapLoading,
ServicesAction,
} from './serviceMap';
import { GetUsageDataAction } from './usage';
export enum ActionTypes {
updateTraceFilters = 'UPDATE_TRACES_FILTER',
updateTimeInterval = 'UPDATE_TIME_INTERVAL',
getServiceMapItems = 'GET_SERVICE_MAP_ITEMS',
getServices = 'GET_SERVICES',
getUsageData = 'GET_USAGE_DATE',
fetchTraces = 'FETCH_TRACES',
fetchTraceItem = 'FETCH_TRACE_ITEM',
serviceMapLoading = 'UPDATE_SERVICE_MAP_LOADING',
}
export type Action = GetUsageDataAction | ServicesAction | ServiceMapItemAction;
export type Action =
| GetUsageDataAction
| ServicesAction
| ServiceMapItemAction
| ServiceMapLoading;

View File

@ -7,6 +7,10 @@ import {
LOGGED_IN,
SIDEBAR_COLLAPSE,
SWITCH_DARK_MODE,
UPDATE_CURRENT_ERROR,
UPDATE_CURRENT_VERSION,
UPDATE_LATEST_VERSION,
UPDATE_LATEST_VERSION_ERROR,
} from 'types/actions/app';
import InitialValueTypes from 'types/reducer/app';
@ -14,6 +18,10 @@ const InitialValue: InitialValueTypes = {
isDarkMode: getTheme() === 'darkMode',
isLoggedIn: getLocalStorageKey(IS_LOGGED_IN) === 'yes',
isSideBarCollapsed: getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true',
currentVersion: '',
latestVersion: '',
isCurrentVersionError: false,
isLatestVersionError: false,
};
const appReducer = (
@ -42,6 +50,28 @@ const appReducer = (
};
}
case UPDATE_CURRENT_VERSION: {
return {
...state,
currentVersion: action.payload.currentVersion,
};
}
case UPDATE_LATEST_VERSION: {
return { ...state, latestVersion: action.payload.latestVersion };
}
case UPDATE_CURRENT_ERROR: {
return { ...state, isCurrentVersionError: true };
}
case UPDATE_LATEST_VERSION_ERROR: {
return {
...state,
isLatestVersionError: true,
};
}
default:
return state;
}

View File

@ -3,6 +3,7 @@ import { Action, ActionTypes, ServiceMapStore } from 'store/actions';
const initialState: ServiceMapStore = {
items: [],
services: [],
loading: true,
};
export const ServiceMapReducer = (
@ -20,6 +21,12 @@ export const ServiceMapReducer = (
...state,
services: action.payload,
};
case ActionTypes.serviceMapLoading: {
return {
...state,
loading: action.payload.loading,
};
}
default:
return state;
}

View File

@ -9,8 +9,10 @@ import {
UPDATE_SELECTED_FUNCTION,
UPDATE_SELECTED_GROUP_BY,
UPDATE_SELECTED_TAGS,
UPDATE_SPANS_AGGREEGATE,
UPDATE_TAG_MODAL_VISIBLITY,
UPDATE_SPAN_ORDER,
UPDATE_SPANS_AGGREGATE,
UPDATE_SPANS_AGGREGATE_PAGE_NUMBER,
UPDATE_TAG_MODAL_VISIBILITY,
UPDATE_TRACE_FILTER,
UPDATE_TRACE_FILTER_LOADING,
UPDATE_TRACE_GRAPH_ERROR,
@ -37,6 +39,7 @@ const initialValue: TraceReducer = {
error: false,
total: 0,
pageSize: 10,
order: 'ascend',
},
selectedGroupBy: '',
selectedFunction: 'count',
@ -71,6 +74,7 @@ const traceReducer = (
selectedTags,
userSelected,
isFilterExclude,
order,
} = payload;
return {
@ -84,6 +88,7 @@ const traceReducer = (
spansAggregate: {
...state.spansAggregate,
currentPage: current,
order,
},
};
}
@ -115,14 +120,14 @@ const traceReducer = (
};
}
case UPDATE_SPANS_AGGREEGATE: {
case UPDATE_SPANS_AGGREGATE: {
return {
...state,
spansAggregate: action.payload.spansAggregate,
};
}
case UPDATE_TAG_MODAL_VISIBLITY: {
case UPDATE_TAG_MODAL_VISIBILITY: {
return {
...state,
isTagModalOpen: action.payload.isTagModalOpen,
@ -199,6 +204,26 @@ const traceReducer = (
};
}
case UPDATE_SPAN_ORDER: {
return {
...state,
spansAggregate: {
...state.spansAggregate,
order: action.payload.order,
},
};
}
case UPDATE_SPANS_AGGREGATE_PAGE_NUMBER: {
return {
...state,
spansAggregate: {
...state.spansAggregate,
currentPage: action.payload.currentPage,
},
};
}
default:
return state;
}

View File

@ -1,7 +1,15 @@
import AppReducer from 'types/reducer/app';
export const SWITCH_DARK_MODE = 'SWITCH_DARK_MODE';
export const LOGGED_IN = 'LOGGED_IN';
export const SIDEBAR_COLLAPSE = 'SIDEBAR_COLLAPSE';
export const UPDATE_CURRENT_VERSION = 'UPDATE_CURRENT_VERSION';
export const UPDATE_LATEST_VERSION = 'UPDATE_LATEST_VERSION';
export const UPDATE_CURRENT_ERROR = 'UPDATE_CURRENT_ERROR';
export const UPDATE_LATEST_VERSION_ERROR = 'UPDATE_LATEST_VERSION_ERROR';
export interface SwitchDarkMode {
type: typeof SWITCH_DARK_MODE;
}
@ -15,4 +23,31 @@ export interface SideBarCollapse {
payload: boolean;
}
export type AppAction = SwitchDarkMode | LoggedInUser | SideBarCollapse;
export interface UpdateAppVersion {
type: typeof UPDATE_CURRENT_VERSION;
payload: {
currentVersion: AppReducer['currentVersion'];
};
}
export interface UpdateLatestVersion {
type: typeof UPDATE_LATEST_VERSION;
payload: {
latestVersion: AppReducer['latestVersion'];
};
}
export interface UpdateVersionError {
type: typeof UPDATE_CURRENT_ERROR | typeof UPDATE_LATEST_VERSION_ERROR;
payload: {
isError: boolean;
};
}
export type AppAction =
| SwitchDarkMode
| LoggedInUser
| SideBarCollapse
| UpdateAppVersion
| UpdateLatestVersion
| UpdateVersionError;

View File

@ -7,9 +7,9 @@ export const UPDATE_TRACE_FILTER_LOADING = 'UPDATE_TRACE_FILTER_LOADING';
export const SELECT_TRACE_FILTER = 'SELECT_TRACE_FILTER';
export const UPDATE_ALL_FILTERS = 'UPDATE_ALL_FILTERS';
export const UPDATE_SELECTED_TAGS = 'UPDATE_SELECTED_TAGS';
export const UPDATE_TAG_MODAL_VISIBLITY = 'UPDATE_TAG_MODAL_VISIBLITY';
export const UPDATE_TAG_MODAL_VISIBILITY = 'UPDATE_TAG_MODAL_VISIBILITY';
export const UPDATE_SPANS_AGGREEGATE = 'UPDATE_SPANS_AGGREEGATE';
export const UPDATE_SPANS_AGGREGATE = 'UPDATE_SPANS_AGGREGATE';
export const UPDATE_IS_TAG_ERROR = 'UPDATE_IS_TAG_ERROR';
@ -25,6 +25,10 @@ export const UPDATE_FILTER_RESPONSE_SELECTED =
'UPDATE_FILTER_RESPONSE_SELECTED';
export const UPDATE_FILTER_EXCLUDE = 'UPDATE_FILTER_EXCLUDE';
export const UPDATE_SPAN_ORDER = 'UPDATE_SPAN_ORDER';
export const UPDATE_SPANS_AGGREGATE_PAGE_NUMBER =
'UPDATE_SPANS_AGGREGATE_PAGE_NUMBER';
export interface UpdateFilter {
type: typeof UPDATE_TRACE_FILTER;
payload: {
@ -33,14 +37,14 @@ export interface UpdateFilter {
}
export interface UpdateSpansAggregate {
type: typeof UPDATE_SPANS_AGGREEGATE;
type: typeof UPDATE_SPANS_AGGREGATE;
payload: {
spansAggregate: TraceReducer['spansAggregate'];
};
}
export interface UpdateTagVisiblity {
type: typeof UPDATE_TAG_MODAL_VISIBLITY;
export interface UpdateTagVisibility {
type: typeof UPDATE_TAG_MODAL_VISIBILITY;
payload: {
isTagModalOpen: TraceReducer['isTagModalOpen'];
};
@ -70,6 +74,7 @@ export interface UpdateAllFilters {
selectedTags: TraceReducer['selectedTags'];
userSelected: TraceReducer['userSelectedFilter'];
isFilterExclude: TraceReducer['isFilterExclude'];
order: TraceReducer['spansAggregate']['order'];
};
}
@ -149,6 +154,20 @@ export interface UpdateSpans {
};
}
export interface UpdateSpansAggregatePageNumber {
type: typeof UPDATE_SPANS_AGGREGATE_PAGE_NUMBER;
payload: {
currentPage: TraceReducer['spansAggregate']['currentPage'];
};
}
export interface UpdateSpanOrder {
type: typeof UPDATE_SPAN_ORDER;
payload: {
order: TraceReducer['spansAggregate']['order'];
};
}
export type TraceActions =
| UpdateFilter
| GetTraceFilter
@ -156,7 +175,7 @@ export type TraceActions =
| SelectTraceFilter
| UpdateAllFilters
| UpdateSelectedTags
| UpdateTagVisiblity
| UpdateTagVisibility
| UpdateSpansAggregate
| UpdateIsTagError
| UpdateSelectedGroupBy
@ -166,4 +185,6 @@ export type TraceActions =
| UpdateSpans
| ResetTraceFilter
| UpdateSelected
| UpdateFilterExclude;
| UpdateFilterExclude
| UpdateSpanOrder
| UpdateSpansAggregatePageNumber;

View File

@ -0,0 +1,5 @@
export type PayloadProps = IDiskType[];
export interface IDiskType {
name: string;
type: string;
}

View File

@ -1,4 +1,6 @@
export interface PayloadProps {
metrics_ttl_duration_hrs: number;
metrics_move_ttl_duration_hrs?: number;
traces_ttl_duration_hrs: number;
traces_move_ttl_duration_hrs?: number;
}

View File

@ -1,6 +1,8 @@
export interface Props {
type: 'metrics' | 'traces';
duration: string;
totalDuration: string;
coldStorage?: 's3' | null;
toColdDuration?: string;
}
export interface PayloadProps {

View File

@ -7,7 +7,7 @@ export interface Props {
limit: number;
offset: number;
selectedTags: TraceReducer['selectedTags'];
order?: 'descending' | 'ascending';
order?: TraceReducer['spansAggregate']['order'];
isFilterExclude: TraceReducer['isFilterExclude'];
}

View File

@ -0,0 +1,13 @@
import { GlobalReducer } from 'types/reducer/globalTime';
export interface Props {
start: GlobalReducer['minTime'];
end: GlobalReducer['maxTime'];
tagKey: string;
}
interface Value {
tagValues: string;
}
export type PayloadProps = Value[];

View File

@ -0,0 +1,18 @@
export interface PayloadProps {
body: string;
created_at: string;
draft: boolean;
html_url: string;
id: number;
mentions_count: number;
name: string;
node_id: number;
prerelease: boolean;
published_at: string;
tag_name: number;
tarball_url: string;
target_commitish: string;
upload_url: string;
url: string;
zipball_url: string;
}

View File

@ -2,4 +2,8 @@ export default interface AppReducer {
isDarkMode: boolean;
isLoggedIn: boolean;
isSideBarCollapsed: boolean;
currentVersion: string;
latestVersion: string;
isCurrentVersionError: boolean;
isLatestVersionError: boolean;
}

View File

@ -4,8 +4,10 @@ export interface TraceReducer {
filter: Map<TraceFilterEnum, Record<string, string>>;
filterToFetchData: TraceFilterEnum[];
filterLoading: boolean;
selectedFilter: Map<TraceFilterEnum, string[]>;
userSelectedFilter: Map<TraceFilterEnum, string[]>;
isFilterExclude: Map<TraceFilterEnum, boolean>;
selectedTags: Tags[];
isTagModalOpen: boolean;
@ -18,6 +20,7 @@ export interface TraceReducer {
error: boolean;
total: number;
pageSize: number;
order: string;
};
selectedGroupBy: string;
selectedFunction: string;

View File

@ -25,8 +25,7 @@ export const getSpanTreeMetadata = (
globalEnd = Math.max(globalEnd, endTime);
if (treeNode.hasError) {
treeNode.serviceColour = errorColor;
}
treeNode.serviceColour = spanServiceColours[treeNode.serviceName];
} else treeNode.serviceColour = spanServiceColours[treeNode.serviceName];
treeNode.children.forEach((childNode) => {
traverse(childNode, level + 1);
});

View File

@ -22,5 +22,5 @@
"plugins": [{ "name": "typescript-plugin-css-modules" }]
},
"exclude": ["node_modules"],
"include": ["./src"]
"include": ["./src", "./babel.config.js", "./jest.config.ts"]
}

File diff suppressed because it is too large Load Diff

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