mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-07-25 09:44:25 +08:00
commit
7c87310fa6
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -5,6 +5,7 @@
|
||||
|
||||
/frontend/ @palashgdev @YounixM
|
||||
/frontend/src/container/MetricsApplication @srikanthccv
|
||||
/frontend/src/container/NewWidget/RightContainer/types.ts @srikanthccv
|
||||
/deploy/ @prashant-shahi
|
||||
/sample-apps/ @prashant-shahi
|
||||
**/query-service/ @srikanthccv
|
||||
|
10
.gitignore
vendored
10
.gitignore
vendored
@ -37,7 +37,7 @@ frontend/src/constants/env.ts
|
||||
**/locust-scripts/__pycache__/
|
||||
**/__debug_bin
|
||||
|
||||
frontend/.env
|
||||
.env
|
||||
pkg/query-service/signoz.db
|
||||
|
||||
pkg/query-service/tests/test-deploy/data/
|
||||
@ -53,3 +53,11 @@ ee/query-service/tests/test-deploy/data/
|
||||
bin/
|
||||
|
||||
*/query-service/queries.active
|
||||
|
||||
# e2e
|
||||
|
||||
e2e/node_modules/
|
||||
e2e/test-results/
|
||||
e2e/playwright-report/
|
||||
e2e/blob-report/
|
||||
e2e/playwright/.cache/
|
@ -146,7 +146,7 @@ services:
|
||||
condition: on-failure
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.34.2
|
||||
image: signoz/query-service:0.34.3
|
||||
command:
|
||||
[
|
||||
"-config=/root/config/prometheus.yml",
|
||||
@ -186,7 +186,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.34.2
|
||||
image: signoz/frontend:0.34.3
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
@ -164,7 +164,7 @@ services:
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.34.2}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.34.3}
|
||||
container_name: signoz-query-service
|
||||
command:
|
||||
[
|
||||
@ -203,7 +203,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.34.2}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.34.3}
|
||||
container_name: signoz-frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
|
14
e2e/package.json
Normal file
14
e2e/package.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "e2e",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.22.0",
|
||||
"@types/node": "^20.9.2"
|
||||
},
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"dotenv": "8.2.0"
|
||||
}
|
||||
}
|
33
e2e/playwright.config.ts
Normal file
33
e2e/playwright.config.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
|
||||
fullyParallel: true,
|
||||
|
||||
forbidOnly: !!process.env.CI,
|
||||
|
||||
name: "Signoz E2E",
|
||||
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
|
||||
reporter: process.env.CI ? "github" : "list",
|
||||
|
||||
preserveOutput: "always",
|
||||
|
||||
updateSnapshots: "all",
|
||||
|
||||
quiet: false,
|
||||
|
||||
testMatch: ["**/*.spec.ts"],
|
||||
|
||||
use: {
|
||||
trace: "on-first-retry",
|
||||
|
||||
baseURL:
|
||||
process.env.PLAYWRIGHT_TEST_BASE_URL || "https://stagingapp.signoz.io/",
|
||||
},
|
||||
});
|
33
e2e/tests/login.spec.ts
Normal file
33
e2e/tests/login.spec.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import ROUTES from "../../frontend/src/constants/routes";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
test("E2E Login Test", async ({ page }) => {
|
||||
await Promise.all([page.goto("/"), page.waitForRequest("**/version")]);
|
||||
|
||||
const signup = "Monitor your applications. Find what is causing issues.";
|
||||
|
||||
const el = await page.locator(`text=${signup}`);
|
||||
|
||||
expect(el).toBeVisible();
|
||||
|
||||
await page
|
||||
.locator("id=loginEmail")
|
||||
.type(
|
||||
process.env.PLAYWRIGHT_USERNAME ? process.env.PLAYWRIGHT_USERNAME : ""
|
||||
);
|
||||
|
||||
await page.getByText("Next").click();
|
||||
|
||||
await page
|
||||
.locator('input[id="currentPassword"]')
|
||||
.fill(
|
||||
process.env.PLAYWRIGHT_PASSWORD ? process.env.PLAYWRIGHT_PASSWORD : ""
|
||||
);
|
||||
|
||||
await page.locator('button[data-attr="signup"]').click();
|
||||
|
||||
await expect(page).toHaveURL(ROUTES.APPLICATION);
|
||||
});
|
46
e2e/yarn.lock
Normal file
46
e2e/yarn.lock
Normal file
@ -0,0 +1,46 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@playwright/test@^1.22.0":
|
||||
version "1.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.40.0.tgz#d06c506977dd7863aa16e07f2136351ecc1be6ed"
|
||||
integrity sha512-PdW+kn4eV99iP5gxWNSDQCbhMaDVej+RXL5xr6t04nbKLCBwYtA046t7ofoczHOm8u6c+45hpDKQVZqtqwkeQg==
|
||||
dependencies:
|
||||
playwright "1.40.0"
|
||||
|
||||
"@types/node@^20.9.2":
|
||||
version "20.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.9.2.tgz#002815c8e87fe0c9369121c78b52e800fadc0ac6"
|
||||
integrity sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
dotenv@8.2.0:
|
||||
version "8.2.0"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
|
||||
integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
|
||||
|
||||
fsevents@2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
||||
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
||||
|
||||
playwright-core@1.40.0:
|
||||
version "1.40.0"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.40.0.tgz#82f61e5504cb3097803b6f8bbd98190dd34bdf14"
|
||||
integrity sha512-fvKewVJpGeca8t0ipM56jkVSU6Eo0RmFvQ/MaCQNDYm+sdvKkMBBWTE1FdeMqIdumRaXXjZChWHvIzCGM/tA/Q==
|
||||
|
||||
playwright@1.40.0:
|
||||
version "1.40.0"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.40.0.tgz#2a1824b9fe5c4fe52ed53db9ea68003543a99df0"
|
||||
integrity sha512-gyHAgQjiDf1m34Xpwzaqb76KgfzYrhK7iih+2IzcOCoZWr/8ZqmdBw+t0RU85ZmfJMgtgAiNtBQ/KS2325INXw==
|
||||
dependencies:
|
||||
playwright-core "1.40.0"
|
||||
optionalDependencies:
|
||||
fsevents "2.3.2"
|
||||
|
||||
undici-types@~5.26.4:
|
||||
version "5.26.5"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
@ -82,6 +82,7 @@
|
||||
"react-drag-listview": "2.0.0",
|
||||
"react-error-boundary": "4.0.11",
|
||||
"react-force-graph": "^1.43.0",
|
||||
"react-full-screen": "1.1.1",
|
||||
"react-grid-layout": "^1.3.4",
|
||||
"react-helmet-async": "1.3.0",
|
||||
"react-i18next": "^11.16.1",
|
||||
|
@ -17,6 +17,7 @@
|
||||
"layout_saved_successfully": "Layout saved successfully",
|
||||
"add_panel": "Add Panel",
|
||||
"save_layout": "Save Layout",
|
||||
"full_view": "Full Screen View",
|
||||
"variable_updated_successfully": "Variable updated successfully",
|
||||
"error_while_updating_variable": "Error while updating variable",
|
||||
"dashboard_has_been_updated": "Dashboard has been updated",
|
||||
|
@ -57,6 +57,13 @@ const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
|
||||
chart.destroy();
|
||||
chartRef.current = null;
|
||||
}
|
||||
|
||||
// remove chart tooltip on cleanup
|
||||
const overlay = document.getElementById('overlay');
|
||||
|
||||
if (overlay) {
|
||||
overlay.style.display = 'none';
|
||||
}
|
||||
}, []);
|
||||
|
||||
const create = useCallback(() => {
|
||||
|
@ -1,4 +1,5 @@
|
||||
export enum Events {
|
||||
UPDATE_GRAPH_VISIBILITY_STATE = 'UPDATE_GRAPH_VISIBILITY_STATE',
|
||||
UPDATE_GRAPH_MANAGER_TABLE = 'UPDATE_GRAPH_MANAGER_TABLE',
|
||||
TABLE_COLUMNS_DATA = 'TABLE_COLUMNS_DATA',
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
.fullscreen-grid-container {
|
||||
overflow: auto;
|
||||
|
||||
.react-grid-layout {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import './GridCardLayout.styles.scss';
|
||||
|
||||
import { PlusOutlined, SaveFilled } from '@ant-design/icons';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@ -5,7 +7,9 @@ import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { FullscreenIcon } from 'lucide-react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { FullScreen, useFullScreenHandle } from 'react-full-screen';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
@ -34,6 +38,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
isDashboardLocked,
|
||||
} = useDashboard();
|
||||
const { data } = selectedDashboard || {};
|
||||
const handle = useFullScreenHandle();
|
||||
|
||||
const { widgets, variables } = data || {};
|
||||
|
||||
@ -106,6 +111,15 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
<>
|
||||
{!isDashboardLocked && (
|
||||
<ButtonContainer>
|
||||
<Button
|
||||
loading={updateDashboardMutation.isLoading}
|
||||
onClick={handle.enter}
|
||||
icon={<FullscreenIcon size={16} />}
|
||||
disabled={updateDashboardMutation.isLoading}
|
||||
>
|
||||
{t('dashboard:full_view')}
|
||||
</Button>
|
||||
|
||||
{saveLayoutPermission && (
|
||||
<Button
|
||||
loading={updateDashboardMutation.isLoading}
|
||||
@ -129,46 +143,48 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
</ButtonContainer>
|
||||
)}
|
||||
|
||||
<ReactGridLayout
|
||||
cols={12}
|
||||
rowHeight={100}
|
||||
autoSize
|
||||
width={100}
|
||||
useCSSTransforms
|
||||
isDraggable={!isDashboardLocked && addPanelPermission}
|
||||
isDroppable={!isDashboardLocked && addPanelPermission}
|
||||
isResizable={!isDashboardLocked && addPanelPermission}
|
||||
allowOverlap={false}
|
||||
onLayoutChange={setLayouts}
|
||||
draggableHandle=".drag-handle"
|
||||
layout={layouts}
|
||||
>
|
||||
{layouts.map((layout) => {
|
||||
const { i: id } = layout;
|
||||
const currentWidget = (widgets || [])?.find((e) => e.id === id);
|
||||
<FullScreen handle={handle} className="fullscreen-grid-container">
|
||||
<ReactGridLayout
|
||||
cols={12}
|
||||
rowHeight={100}
|
||||
autoSize
|
||||
width={100}
|
||||
useCSSTransforms
|
||||
isDraggable={!isDashboardLocked && addPanelPermission}
|
||||
isDroppable={!isDashboardLocked && addPanelPermission}
|
||||
isResizable={!isDashboardLocked && addPanelPermission}
|
||||
allowOverlap={false}
|
||||
onLayoutChange={setLayouts}
|
||||
draggableHandle=".drag-handle"
|
||||
layout={layouts}
|
||||
>
|
||||
{layouts.map((layout) => {
|
||||
const { i: id } = layout;
|
||||
const currentWidget = (widgets || [])?.find((e) => e.id === id);
|
||||
|
||||
return (
|
||||
<CardContainer
|
||||
className={isDashboardLocked ? '' : 'enable-resize'}
|
||||
isDarkMode={isDarkMode}
|
||||
key={id}
|
||||
data-grid={layout}
|
||||
>
|
||||
<Card
|
||||
className="grid-item"
|
||||
$panelType={currentWidget?.panelTypes || PANEL_TYPES.TIME_SERIES}
|
||||
return (
|
||||
<CardContainer
|
||||
className={isDashboardLocked ? '' : 'enable-resize'}
|
||||
isDarkMode={isDarkMode}
|
||||
key={id}
|
||||
data-grid={layout}
|
||||
>
|
||||
<GridCard
|
||||
widget={currentWidget || ({ id, query: {} } as Widgets)}
|
||||
name={currentWidget?.id || ''}
|
||||
headerMenuList={widgetActions}
|
||||
variables={variables}
|
||||
/>
|
||||
</Card>
|
||||
</CardContainer>
|
||||
);
|
||||
})}
|
||||
</ReactGridLayout>
|
||||
<Card
|
||||
className="grid-item"
|
||||
$panelType={currentWidget?.panelTypes || PANEL_TYPES.TIME_SERIES}
|
||||
>
|
||||
<GridCard
|
||||
widget={currentWidget || ({ id, query: {} } as Widgets)}
|
||||
name={currentWidget?.id || ''}
|
||||
headerMenuList={widgetActions}
|
||||
variables={variables}
|
||||
/>
|
||||
</Card>
|
||||
</CardContainer>
|
||||
);
|
||||
})}
|
||||
</ReactGridLayout>
|
||||
</FullScreen>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -26,7 +26,12 @@ const GridPanelSwitch = forwardRef<
|
||||
yAxisUnit,
|
||||
thresholds,
|
||||
},
|
||||
[PANEL_TYPES.TABLE]: { ...GRID_TABLE_CONFIG, data: panelData, query },
|
||||
[PANEL_TYPES.TABLE]: {
|
||||
...GRID_TABLE_CONFIG,
|
||||
data: panelData,
|
||||
query,
|
||||
thresholds,
|
||||
},
|
||||
[PANEL_TYPES.LIST]: null,
|
||||
[PANEL_TYPES.TRACE]: null,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: null,
|
||||
|
@ -1,20 +1,86 @@
|
||||
import { ExclamationCircleFilled } from '@ant-design/icons';
|
||||
import { Space, Tooltip } from 'antd';
|
||||
import { Events } from 'constants/events';
|
||||
import { QueryTable } from 'container/QueryTable';
|
||||
import { memo } from 'react';
|
||||
import { createTableColumnsFromQuery } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { memo, ReactNode, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { eventEmitter } from 'utils/getEventEmitter';
|
||||
|
||||
import { WrapperStyled } from './styles';
|
||||
import { GridTableComponentProps } from './types';
|
||||
import { findMatchingThreshold } from './utils';
|
||||
|
||||
function GridTableComponent({
|
||||
data,
|
||||
query,
|
||||
thresholds,
|
||||
...props
|
||||
}: GridTableComponentProps): JSX.Element {
|
||||
const { t } = useTranslation(['valueGraph']);
|
||||
const { columns, dataSource } = useMemo(
|
||||
() =>
|
||||
createTableColumnsFromQuery({
|
||||
query,
|
||||
queryTableData: data,
|
||||
}),
|
||||
[data, query],
|
||||
);
|
||||
|
||||
const newColumnData = columns.map((e) => ({
|
||||
...e,
|
||||
render: (text: string): ReactNode => {
|
||||
const isNumber = !Number.isNaN(Number(text));
|
||||
if (thresholds && isNumber) {
|
||||
const { hasMultipleMatches, threshold } = findMatchingThreshold(
|
||||
thresholds,
|
||||
e.title as string,
|
||||
Number(text),
|
||||
);
|
||||
|
||||
const idx = thresholds.findIndex(
|
||||
(t) => t.thresholdTableOptions === e.title,
|
||||
);
|
||||
if (idx !== -1) {
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
threshold.thresholdFormat === 'Background'
|
||||
? { backgroundColor: threshold.thresholdColor }
|
||||
: { color: threshold.thresholdColor }
|
||||
}
|
||||
>
|
||||
<Space>
|
||||
{text}
|
||||
{hasMultipleMatches && (
|
||||
<Tooltip title={t('this_value_satisfies_multiple_thresholds')}>
|
||||
<ExclamationCircleFilled className="value-graph-icon" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return <div>{text}</div>;
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
eventEmitter.emit(Events.TABLE_COLUMNS_DATA, {
|
||||
columns: newColumnData,
|
||||
dataSource,
|
||||
});
|
||||
}, [dataSource, newColumnData]);
|
||||
|
||||
return (
|
||||
<WrapperStyled>
|
||||
<QueryTable
|
||||
query={query}
|
||||
queryTableData={data}
|
||||
loading={false}
|
||||
columns={newColumnData}
|
||||
dataSource={dataSource}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
/>
|
||||
|
@ -1,10 +1,23 @@
|
||||
import { TableProps } from 'antd';
|
||||
import { LogsExplorerTableProps } from 'container/LogsExplorerTable/LogsExplorerTable.interfaces';
|
||||
import {
|
||||
ThresholdOperators,
|
||||
ThresholdProps,
|
||||
} from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export type GridTableComponentProps = { query: Query } & Pick<
|
||||
LogsExplorerTableProps,
|
||||
'data'
|
||||
> &
|
||||
export type GridTableComponentProps = {
|
||||
query: Query;
|
||||
thresholds?: ThresholdProps[];
|
||||
} & Pick<LogsExplorerTableProps, 'data'> &
|
||||
Omit<TableProps<RowData>, 'columns' | 'dataSource'>;
|
||||
|
||||
export type RequiredThresholdProps = Omit<
|
||||
ThresholdProps,
|
||||
'thresholdTableOptions' | 'thresholdOperator' | 'thresholdValue'
|
||||
> & {
|
||||
thresholdTableOptions: string;
|
||||
thresholdOperator: ThresholdOperators;
|
||||
thresholdValue: number;
|
||||
};
|
||||
|
58
frontend/src/container/GridTableComponent/utils.ts
Normal file
58
frontend/src/container/GridTableComponent/utils.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
|
||||
// Helper function to evaluate the condition based on the operator
|
||||
function evaluateCondition(
|
||||
operator: string | undefined,
|
||||
value: number,
|
||||
thresholdValue: number,
|
||||
): boolean {
|
||||
switch (operator) {
|
||||
case '>':
|
||||
return value > thresholdValue;
|
||||
case '<':
|
||||
return value < thresholdValue;
|
||||
case '>=':
|
||||
return value >= thresholdValue;
|
||||
case '<=':
|
||||
return value <= thresholdValue;
|
||||
case '==':
|
||||
return value === thresholdValue;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function findMatchingThreshold(
|
||||
thresholds: ThresholdProps[],
|
||||
label: string,
|
||||
value: number,
|
||||
): {
|
||||
threshold: ThresholdProps;
|
||||
hasMultipleMatches: boolean;
|
||||
} {
|
||||
const matchingThresholds: ThresholdProps[] = [];
|
||||
let hasMultipleMatches = false;
|
||||
|
||||
thresholds.forEach((threshold) => {
|
||||
if (
|
||||
threshold.thresholdValue !== undefined &&
|
||||
threshold.thresholdTableOptions === label &&
|
||||
evaluateCondition(
|
||||
threshold.thresholdOperator,
|
||||
value,
|
||||
threshold.thresholdValue,
|
||||
)
|
||||
) {
|
||||
matchingThresholds.push(threshold);
|
||||
}
|
||||
});
|
||||
|
||||
if (matchingThresholds.length > 1) {
|
||||
hasMultipleMatches = true;
|
||||
}
|
||||
|
||||
return {
|
||||
threshold: matchingThresholds[0],
|
||||
hasMultipleMatches,
|
||||
};
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
.show-case-container {
|
||||
padding: 5px 15px;
|
||||
border-radius: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.show-case-dark {
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import './Threshold.styles.scss';
|
||||
|
||||
import { CheckOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons';
|
||||
@ -40,6 +41,8 @@ function Threshold({
|
||||
moveThreshold,
|
||||
selectedGraph,
|
||||
thresholdLabel = '',
|
||||
tableOptions,
|
||||
thresholdTableOptions = '',
|
||||
}: ThresholdProps): JSX.Element {
|
||||
const [isEditMode, setIsEditMode] = useState<boolean>(isEditEnabled);
|
||||
const [operator, setOperator] = useState<string | number>(
|
||||
@ -52,6 +55,9 @@ function Threshold({
|
||||
thresholdFormat,
|
||||
);
|
||||
const [label, setLabel] = useState<string>(thresholdLabel);
|
||||
const [tableSelectedOption, setTableSelectedOption] = useState<string>(
|
||||
thresholdTableOptions,
|
||||
);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
@ -72,6 +78,7 @@ function Threshold({
|
||||
thresholdUnit: unit,
|
||||
thresholdValue: value,
|
||||
thresholdLabel: label,
|
||||
thresholdTableOptions: tableSelectedOption,
|
||||
};
|
||||
}
|
||||
return threshold;
|
||||
@ -104,6 +111,10 @@ function Threshold({
|
||||
setFormat(value);
|
||||
};
|
||||
|
||||
const handleTableOptionsChange = (value: string): void => {
|
||||
setTableSelectedOption(value);
|
||||
};
|
||||
|
||||
const deleteHandler = (): void => {
|
||||
if (thresholdDeleteHandler) {
|
||||
thresholdDeleteHandler(index);
|
||||
@ -203,7 +214,11 @@ function Threshold({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Space>
|
||||
<Space
|
||||
direction={
|
||||
selectedGraph === PANEL_TYPES.TABLE ? 'vertical' : 'horizontal'
|
||||
}
|
||||
>
|
||||
{selectedGraph === PANEL_TYPES.TIME_SERIES && (
|
||||
<>
|
||||
<Typography.Text>Label</Typography.Text>
|
||||
@ -219,19 +234,49 @@ function Threshold({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{selectedGraph === PANEL_TYPES.VALUE && (
|
||||
{(selectedGraph === PANEL_TYPES.VALUE ||
|
||||
selectedGraph === PANEL_TYPES.TABLE) && (
|
||||
<>
|
||||
<Typography.Text>If value is</Typography.Text>
|
||||
<Typography.Text>
|
||||
If value {selectedGraph === PANEL_TYPES.TABLE ? 'in' : 'is'}
|
||||
</Typography.Text>
|
||||
{isEditMode ? (
|
||||
<Select
|
||||
style={{ minWidth: '73px', backgroundColor }}
|
||||
defaultValue={operator}
|
||||
options={operatorOptions}
|
||||
onChange={handleOperatorChange}
|
||||
bordered={!isDarkMode}
|
||||
/>
|
||||
<>
|
||||
{selectedGraph === PANEL_TYPES.TABLE && (
|
||||
<Space>
|
||||
<Select
|
||||
style={{
|
||||
minWidth: '150px',
|
||||
backgroundColor,
|
||||
borderRadius: '5px',
|
||||
}}
|
||||
defaultValue={tableSelectedOption}
|
||||
options={tableOptions}
|
||||
bordered={!isDarkMode}
|
||||
showSearch
|
||||
onChange={handleTableOptionsChange}
|
||||
/>
|
||||
<Typography.Text>is</Typography.Text>
|
||||
</Space>
|
||||
)}
|
||||
<Select
|
||||
style={{ minWidth: '73px', backgroundColor }}
|
||||
defaultValue={operator}
|
||||
options={operatorOptions}
|
||||
onChange={handleOperatorChange}
|
||||
bordered={!isDarkMode}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<ShowCaseValue width="49px" value={operator} />
|
||||
<>
|
||||
{selectedGraph === PANEL_TYPES.TABLE && (
|
||||
<Space>
|
||||
<ShowCaseValue width="150px" value={tableSelectedOption} />
|
||||
<Typography.Text>is</Typography.Text>
|
||||
</Space>
|
||||
)}
|
||||
<ShowCaseValue width="49px" value={operator} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@ -280,7 +325,7 @@ function Threshold({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ShowCaseValue width="100px" value={<CustomColor color={color} />} />
|
||||
<ShowCaseValue width="120px" value={<CustomColor color={color} />} />
|
||||
<ShowCaseValue width="100px" value={format} />
|
||||
</>
|
||||
)}
|
||||
|
@ -1,9 +1,13 @@
|
||||
import './ThresholdSelector.styles.scss';
|
||||
|
||||
import { Button, Typography } from 'antd';
|
||||
import { useCallback } from 'react';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import { Events } from 'constants/events';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { eventEmitter } from 'utils/getEventEmitter';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import Threshold from './Threshold';
|
||||
@ -15,6 +19,22 @@ function ThresholdSelector({
|
||||
yAxisUnit,
|
||||
selectedGraph,
|
||||
}: ThresholdSelectorProps): JSX.Element {
|
||||
const [tableOptions, setTableOptions] = useState<
|
||||
Array<{ value: string; label: string }>
|
||||
>([]);
|
||||
useEffect(() => {
|
||||
eventEmitter.on(
|
||||
Events.TABLE_COLUMNS_DATA,
|
||||
(data: { columns: ColumnsType<RowData>; dataSource: RowData[] }) => {
|
||||
const newTableOptions = data.columns.map((e) => ({
|
||||
value: e.title as string,
|
||||
label: e.title as string,
|
||||
}));
|
||||
setTableOptions([...newTableOptions]);
|
||||
},
|
||||
);
|
||||
}, []);
|
||||
|
||||
const moveThreshold = useCallback(
|
||||
(dragIndex: number, hoverIndex: number) => {
|
||||
setThresholds((prevCards) => {
|
||||
@ -44,6 +64,7 @@ function ThresholdSelector({
|
||||
moveThreshold,
|
||||
keyIndex: thresholds.length,
|
||||
selectedGraph,
|
||||
thresholdTableOptions: tableOptions[0]?.value || '',
|
||||
},
|
||||
]);
|
||||
};
|
||||
@ -75,6 +96,8 @@ function ThresholdSelector({
|
||||
moveThreshold={moveThreshold}
|
||||
selectedGraph={selectedGraph}
|
||||
thresholdLabel={threshold.thresholdLabel}
|
||||
tableOptions={tableOptions}
|
||||
thresholdTableOptions={threshold.thresholdTableOptions}
|
||||
/>
|
||||
))}
|
||||
<Button className="threshold-selector-button" onClick={addThresholdHandler}>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { Dispatch, ReactNode, SetStateAction } from 'react';
|
||||
|
||||
type ThresholdOperators = '>' | '<' | '>=' | '<=' | '=';
|
||||
export type ThresholdOperators = '>' | '<' | '>=' | '<=' | '=';
|
||||
|
||||
export type ThresholdProps = {
|
||||
index: string;
|
||||
@ -14,9 +14,11 @@ export type ThresholdProps = {
|
||||
thresholdFormat?: 'Text' | 'Background';
|
||||
isEditEnabled?: boolean;
|
||||
thresholdLabel?: string;
|
||||
thresholdTableOptions?: string;
|
||||
setThresholds?: Dispatch<SetStateAction<ThresholdProps[]>>;
|
||||
moveThreshold: (dragIndex: number, hoverIndex: number) => void;
|
||||
selectedGraph: PANEL_TYPES;
|
||||
tableOptions?: Array<{ value: string; label: string }>;
|
||||
};
|
||||
|
||||
export type ShowCaseValueProps = {
|
||||
|
@ -24,7 +24,7 @@ export const showAsOptions: DefaultOptionType[] = [
|
||||
export const panelTypeVsThreshold: { [key in PANEL_TYPES]: boolean } = {
|
||||
[PANEL_TYPES.TIME_SERIES]: true,
|
||||
[PANEL_TYPES.VALUE]: true,
|
||||
[PANEL_TYPES.TABLE]: false,
|
||||
[PANEL_TYPES.TABLE]: true,
|
||||
[PANEL_TYPES.LIST]: false,
|
||||
[PANEL_TYPES.TRACE]: false,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||
|
@ -16,4 +16,6 @@ export type QueryTableProps = Omit<
|
||||
modifyColumns?: (columns: ColumnsType<RowData>) => ColumnsType<RowData>;
|
||||
renderColumnCell?: Record<string, (record: RowData) => ReactNode>;
|
||||
downloadOption?: DownloadOptions;
|
||||
columns?: ColumnsType<RowData>;
|
||||
dataSource?: RowData[];
|
||||
};
|
||||
|
@ -17,25 +17,35 @@ export function QueryTable({
|
||||
modifyColumns,
|
||||
renderColumnCell,
|
||||
downloadOption,
|
||||
columns,
|
||||
dataSource,
|
||||
...props
|
||||
}: QueryTableProps): JSX.Element {
|
||||
const { isDownloadEnabled = false, fileName = '' } = downloadOption || {};
|
||||
const { servicename } = useParams<IServiceName>();
|
||||
const { loading } = props;
|
||||
const { columns, dataSource } = useMemo(
|
||||
() =>
|
||||
createTableColumnsFromQuery({
|
||||
query,
|
||||
queryTableData,
|
||||
renderActionCell,
|
||||
renderColumnCell,
|
||||
}),
|
||||
[query, queryTableData, renderActionCell, renderColumnCell],
|
||||
);
|
||||
const { columns: newColumns, dataSource: newDataSource } = useMemo(() => {
|
||||
if (columns && dataSource) {
|
||||
return { columns, dataSource };
|
||||
}
|
||||
return createTableColumnsFromQuery({
|
||||
query,
|
||||
queryTableData,
|
||||
renderActionCell,
|
||||
renderColumnCell,
|
||||
});
|
||||
}, [
|
||||
columns,
|
||||
dataSource,
|
||||
query,
|
||||
queryTableData,
|
||||
renderActionCell,
|
||||
renderColumnCell,
|
||||
]);
|
||||
|
||||
const downloadableData = createDownloadableData(dataSource);
|
||||
const downloadableData = createDownloadableData(newDataSource);
|
||||
|
||||
const tableColumns = modifyColumns ? modifyColumns(columns) : columns;
|
||||
const tableColumns = modifyColumns ? modifyColumns(newColumns) : newColumns;
|
||||
|
||||
return (
|
||||
<div className="query-table">
|
||||
|
@ -7803,6 +7803,11 @@ fs.realpath@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
|
||||
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
|
||||
|
||||
fscreen@^1.0.2:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/fscreen/-/fscreen-1.2.0.tgz#1a8c88e06bc16a07b473ad96196fb06d6657f59e"
|
||||
integrity sha512-hlq4+BU0hlPmwsFjwGGzZ+OZ9N/wq9Ljg/sq3pX+2CD7hrJsX9tJgWWK/wiNTFM212CLHWhicOoqwXyZGGetJg==
|
||||
|
||||
fsevents@2.3.2, fsevents@^2.3.2, fsevents@~2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz"
|
||||
@ -12850,6 +12855,13 @@ react-force-graph@^1.43.0:
|
||||
prop-types "15"
|
||||
react-kapsule "2"
|
||||
|
||||
react-full-screen@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react-full-screen/-/react-full-screen-1.1.1.tgz#b707d56891015a71c503a65dbab3086d75be97d7"
|
||||
integrity sha512-xoEgkoTiN0dw9cjYYGViiMCBYbkS97BYb4bHPhQVWXj1UnOs8PZ1rPzpX+2HMhuvQV1jA5AF9GaRbO3fA5aZtg==
|
||||
dependencies:
|
||||
fscreen "^1.0.2"
|
||||
|
||||
react-grid-layout@^1.3.4:
|
||||
version "1.3.4"
|
||||
resolved "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.3.4.tgz"
|
||||
|
@ -43,9 +43,9 @@ var (
|
||||
// FromUnit returns a converter for the given unit
|
||||
func FromUnit(u Unit) Converter {
|
||||
switch u {
|
||||
case "ns", "us", "ms", "s", "m", "h", "d":
|
||||
case "ns", "us", "µs", "ms", "s", "m", "h", "d":
|
||||
return DurationConverter
|
||||
case "bytes", "decbytes", "bits", "decbits", "kbytes", "decKbytes", "mbytes", "decMbytes", "gbytes", "decGbytes", "tbytes", "decTbytes", "pbytes", "decPbytes":
|
||||
case "bytes", "decbytes", "bits", "decbits", "kbytes", "decKbytes", "deckbytes", "mbytes", "decMbytes", "decmbytes", "gbytes", "decGbytes", "decgbytes", "tbytes", "decTbytes", "dectbytes", "pbytes", "decPbytes", "decpbytes":
|
||||
return DataConverter
|
||||
case "binBps", "Bps", "binbps", "bps", "KiBs", "Kibits", "KBs", "Kbits", "MiBs", "Mibits", "MBs", "Mbits", "GiBs", "Gibits", "GBs", "Gbits", "TiBs", "Tibits", "TBs", "Tbits", "PiBs", "Pibits", "PBs", "Pbits":
|
||||
return DataRateConverter
|
||||
@ -64,7 +64,7 @@ func UnitToName(u string) string {
|
||||
switch u {
|
||||
case "ns":
|
||||
return " ns"
|
||||
case "us":
|
||||
case "us", "µs":
|
||||
return " us"
|
||||
case "ms":
|
||||
return " ms"
|
||||
@ -86,23 +86,23 @@ func UnitToName(u string) string {
|
||||
return " bits"
|
||||
case "kbytes":
|
||||
return " KiB"
|
||||
case "decKbytes":
|
||||
case "decKbytes", "deckbytes":
|
||||
return " kB"
|
||||
case "mbytes":
|
||||
return " MiB"
|
||||
case "decMbytes":
|
||||
case "decMbytes", "decmbytes":
|
||||
return " MB"
|
||||
case "gbytes":
|
||||
return " GiB"
|
||||
case "decGbytes":
|
||||
case "decGbytes", "decgbytes":
|
||||
return " GB"
|
||||
case "tbytes":
|
||||
return " TiB"
|
||||
case "decTbytes":
|
||||
case "decTbytes", "decybytes":
|
||||
return " TB"
|
||||
case "pbytes":
|
||||
return " PiB"
|
||||
case "decPbytes":
|
||||
case "decPbytes", "decpbytes":
|
||||
return " PB"
|
||||
case "binBps":
|
||||
return " bytes/sec(IEC)"
|
||||
|
@ -70,23 +70,23 @@ func FromDataUnit(u Unit) float64 {
|
||||
return Bit
|
||||
case "kbytes": // base 2
|
||||
return Kibibyte
|
||||
case "deckbytes": // base 10
|
||||
case "decKbytes", "deckbytes": // base 10
|
||||
return Kilobyte
|
||||
case "mbytes": // base 2
|
||||
return Mebibyte
|
||||
case "decmbytes": // base 10
|
||||
case "decMbytes", "decmbytes": // base 10
|
||||
return Megabyte
|
||||
case "gbytes": // base 2
|
||||
return Gibibyte
|
||||
case "decgbytes": // base 10
|
||||
case "decGbytes", "decgbytes": // base 10
|
||||
return Gigabyte
|
||||
case "tbytes": // base 2
|
||||
return Tebibyte
|
||||
case "dectbytes": // base 10
|
||||
case "decTbytes", "dectbytes": // base 10
|
||||
return Terabyte
|
||||
case "pbytes": // base 2
|
||||
return Pebibyte
|
||||
case "decpbytes": // base 10
|
||||
case "decPbytes", "decpbytes": // base 10
|
||||
return Petabyte
|
||||
default:
|
||||
return 1
|
||||
|
@ -31,7 +31,7 @@ func FromTimeUnit(u Unit) Duration {
|
||||
switch u {
|
||||
case "ns":
|
||||
return Nanosecond
|
||||
case "us":
|
||||
case "us", "µs":
|
||||
return Microsecond
|
||||
case "ms":
|
||||
return Millisecond
|
||||
|
@ -30,23 +30,23 @@ func (f *dataFormatter) Format(value float64, unit string) string {
|
||||
return humanize.Bytes(uint64(value * converter.Bit))
|
||||
case "kbytes":
|
||||
return humanize.IBytes(uint64(value * converter.Kibibit))
|
||||
case "deckbytes":
|
||||
case "decKbytes", "deckbytes":
|
||||
return humanize.IBytes(uint64(value * converter.Kilobit))
|
||||
case "mbytes":
|
||||
return humanize.IBytes(uint64(value * converter.Mebibit))
|
||||
case "decmbytes":
|
||||
case "decMbytes", "decmbytes":
|
||||
return humanize.Bytes(uint64(value * converter.Megabit))
|
||||
case "gbytes":
|
||||
return humanize.IBytes(uint64(value * converter.Gibibit))
|
||||
case "decgbytes":
|
||||
case "decGbytes", "decgbytes":
|
||||
return humanize.Bytes(uint64(value * converter.Gigabit))
|
||||
case "tbytes":
|
||||
return humanize.IBytes(uint64(value * converter.Tebibit))
|
||||
case "dectbytes":
|
||||
case "decTbytes", "dectbytes":
|
||||
return humanize.Bytes(uint64(value * converter.Terabit))
|
||||
case "pbytes":
|
||||
return humanize.IBytes(uint64(value * converter.Pebibit))
|
||||
case "decpbytes":
|
||||
case "decPbytes", "decpbytes":
|
||||
return humanize.Bytes(uint64(value * converter.Petabit))
|
||||
}
|
||||
// When unit is not matched, return the value as it is.
|
||||
|
@ -18,9 +18,9 @@ var (
|
||||
|
||||
func FromUnit(u string) Formatter {
|
||||
switch u {
|
||||
case "ns", "us", "ms", "s", "m", "h", "d":
|
||||
case "ns", "us", "µs", "ms", "s", "m", "h", "d":
|
||||
return DurationFormatter
|
||||
case "bytes", "decbytes", "bits", "decbits", "kbytes", "decKbytes", "mbytes", "decMbytes", "gbytes", "decGbytes", "tbytes", "decTbytes", "pbytes", "decPbytes":
|
||||
case "bytes", "decbytes", "bits", "decbits", "kbytes", "decKbytes", "deckbytes", "mbytes", "decMbytes", "decmbytes", "gbytes", "decGbytes", "decgbytes", "tbytes", "decTbytes", "dectbytes", "pbytes", "decPbytes", "decpbytes":
|
||||
return DataFormatter
|
||||
case "binBps", "Bps", "binbps", "bps", "KiBs", "Kibits", "KBs", "Kbits", "MiBs", "Mibits", "MBs", "Mbits", "GiBs", "Gibits", "GBs", "Gbits", "TiBs", "Tibits", "TBs", "Tbits", "PiBs", "Pibits", "PBs", "Pbits":
|
||||
return DataRateFormatter
|
||||
|
@ -81,10 +81,9 @@ func toFixed(value float64, decimals DecimalCount) string {
|
||||
precision := 0
|
||||
if decimalPos != -1 {
|
||||
precision = len(formatted) - decimalPos - 1
|
||||
}
|
||||
|
||||
if precision < *decimals {
|
||||
return formatted + strings.Repeat("0", *decimals-precision)
|
||||
if precision < *decimals {
|
||||
return formatted + strings.Repeat("0", *decimals-precision)
|
||||
}
|
||||
}
|
||||
|
||||
return formatted
|
||||
|
15
pkg/query-service/formatter/scale_test.go
Normal file
15
pkg/query-service/formatter/scale_test.go
Normal file
@ -0,0 +1,15 @@
|
||||
package formatter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestToFixed(t *testing.T) {
|
||||
twoDecimals := 2
|
||||
require.Equal(t, "0", toFixed(0, nil))
|
||||
require.Equal(t, "61", toFixed(60.99, nil))
|
||||
require.Equal(t, "51.4", toFixed(51.42, nil))
|
||||
require.Equal(t, "51.42", toFixed(51.42, &twoDecimals))
|
||||
}
|
@ -20,7 +20,7 @@ func (f *durationFormatter) Format(value float64, unit string) string {
|
||||
switch unit {
|
||||
case "ns":
|
||||
return toNanoSeconds(value)
|
||||
case "µs":
|
||||
case "µs", "us":
|
||||
return toMicroSeconds(value)
|
||||
case "ms":
|
||||
return toMilliSeconds(value)
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"math"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"text/template"
|
||||
"time"
|
||||
@ -780,7 +779,8 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time, queriers *Querie
|
||||
}
|
||||
|
||||
value := valueFormatter.Format(smpl.V, r.Unit())
|
||||
threshold := strconv.FormatFloat(r.targetVal(), 'f', 2, 64) + converter.UnitToName(r.ruleCondition.TargetUnit)
|
||||
thresholdFormatter := formatter.FromUnit(r.ruleCondition.TargetUnit)
|
||||
threshold := thresholdFormatter.Format(r.targetVal(), r.ruleCondition.TargetUnit)
|
||||
zap.S().Debugf("Alert template data for rule %s: Formatter=%s, Value=%s, Threshold=%s", r.Name(), valueFormatter.Name(), value, threshold)
|
||||
|
||||
tmplData := AlertTemplateData(l, value, threshold)
|
||||
|
Loading…
x
Reference in New Issue
Block a user