Merge pull request #4045 from SigNoz/release/v0.34.3

Release/v0.34.3
This commit is contained in:
Prashant Shahi 2023-11-23 22:08:49 +05:30 committed by GitHub
commit 7c87310fa6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 523 additions and 104 deletions

1
.github/CODEOWNERS vendored
View File

@ -5,6 +5,7 @@
/frontend/ @palashgdev @YounixM /frontend/ @palashgdev @YounixM
/frontend/src/container/MetricsApplication @srikanthccv /frontend/src/container/MetricsApplication @srikanthccv
/frontend/src/container/NewWidget/RightContainer/types.ts @srikanthccv
/deploy/ @prashant-shahi /deploy/ @prashant-shahi
/sample-apps/ @prashant-shahi /sample-apps/ @prashant-shahi
**/query-service/ @srikanthccv **/query-service/ @srikanthccv

10
.gitignore vendored
View File

@ -37,7 +37,7 @@ frontend/src/constants/env.ts
**/locust-scripts/__pycache__/ **/locust-scripts/__pycache__/
**/__debug_bin **/__debug_bin
frontend/.env .env
pkg/query-service/signoz.db pkg/query-service/signoz.db
pkg/query-service/tests/test-deploy/data/ pkg/query-service/tests/test-deploy/data/
@ -53,3 +53,11 @@ ee/query-service/tests/test-deploy/data/
bin/ bin/
*/query-service/queries.active */query-service/queries.active
# e2e
e2e/node_modules/
e2e/test-results/
e2e/playwright-report/
e2e/blob-report/
e2e/playwright/.cache/

View File

@ -146,7 +146,7 @@ services:
condition: on-failure condition: on-failure
query-service: query-service:
image: signoz/query-service:0.34.2 image: signoz/query-service:0.34.3
command: command:
[ [
"-config=/root/config/prometheus.yml", "-config=/root/config/prometheus.yml",
@ -186,7 +186,7 @@ services:
<<: *db-depend <<: *db-depend
frontend: frontend:
image: signoz/frontend:0.34.2 image: signoz/frontend:0.34.3
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure

View File

@ -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` # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service: query-service:
image: signoz/query-service:${DOCKER_TAG:-0.34.2} image: signoz/query-service:${DOCKER_TAG:-0.34.3}
container_name: signoz-query-service container_name: signoz-query-service
command: command:
[ [
@ -203,7 +203,7 @@ services:
<<: *db-depend <<: *db-depend
frontend: frontend:
image: signoz/frontend:${DOCKER_TAG:-0.34.2} image: signoz/frontend:${DOCKER_TAG:-0.34.3}
container_name: signoz-frontend container_name: signoz-frontend
restart: on-failure restart: on-failure
depends_on: depends_on:

14
e2e/package.json Normal file
View 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
View 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
View 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
View 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==

View File

@ -82,6 +82,7 @@
"react-drag-listview": "2.0.0", "react-drag-listview": "2.0.0",
"react-error-boundary": "4.0.11", "react-error-boundary": "4.0.11",
"react-force-graph": "^1.43.0", "react-force-graph": "^1.43.0",
"react-full-screen": "1.1.1",
"react-grid-layout": "^1.3.4", "react-grid-layout": "^1.3.4",
"react-helmet-async": "1.3.0", "react-helmet-async": "1.3.0",
"react-i18next": "^11.16.1", "react-i18next": "^11.16.1",

View File

@ -17,6 +17,7 @@
"layout_saved_successfully": "Layout saved successfully", "layout_saved_successfully": "Layout saved successfully",
"add_panel": "Add Panel", "add_panel": "Add Panel",
"save_layout": "Save Layout", "save_layout": "Save Layout",
"full_view": "Full Screen View",
"variable_updated_successfully": "Variable updated successfully", "variable_updated_successfully": "Variable updated successfully",
"error_while_updating_variable": "Error while updating variable", "error_while_updating_variable": "Error while updating variable",
"dashboard_has_been_updated": "Dashboard has been updated", "dashboard_has_been_updated": "Dashboard has been updated",

View File

@ -57,6 +57,13 @@ const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
chart.destroy(); chart.destroy();
chartRef.current = null; chartRef.current = null;
} }
// remove chart tooltip on cleanup
const overlay = document.getElementById('overlay');
if (overlay) {
overlay.style.display = 'none';
}
}, []); }, []);
const create = useCallback(() => { const create = useCallback(() => {

View File

@ -1,4 +1,5 @@
export enum Events { export enum Events {
UPDATE_GRAPH_VISIBILITY_STATE = 'UPDATE_GRAPH_VISIBILITY_STATE', UPDATE_GRAPH_VISIBILITY_STATE = 'UPDATE_GRAPH_VISIBILITY_STATE',
UPDATE_GRAPH_MANAGER_TABLE = 'UPDATE_GRAPH_MANAGER_TABLE', UPDATE_GRAPH_MANAGER_TABLE = 'UPDATE_GRAPH_MANAGER_TABLE',
TABLE_COLUMNS_DATA = 'TABLE_COLUMNS_DATA',
} }

View File

@ -0,0 +1,7 @@
.fullscreen-grid-container {
overflow: auto;
.react-grid-layout {
border: none !important;
}
}

View File

@ -1,3 +1,5 @@
import './GridCardLayout.styles.scss';
import { PlusOutlined, SaveFilled } from '@ant-design/icons'; import { PlusOutlined, SaveFilled } from '@ant-design/icons';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
@ -5,7 +7,9 @@ import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import useComponentPermission from 'hooks/useComponentPermission'; import useComponentPermission from 'hooks/useComponentPermission';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { FullscreenIcon } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useDashboard } from 'providers/Dashboard/Dashboard';
import { FullScreen, useFullScreenHandle } from 'react-full-screen';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
@ -34,6 +38,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
isDashboardLocked, isDashboardLocked,
} = useDashboard(); } = useDashboard();
const { data } = selectedDashboard || {}; const { data } = selectedDashboard || {};
const handle = useFullScreenHandle();
const { widgets, variables } = data || {}; const { widgets, variables } = data || {};
@ -106,6 +111,15 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
<> <>
{!isDashboardLocked && ( {!isDashboardLocked && (
<ButtonContainer> <ButtonContainer>
<Button
loading={updateDashboardMutation.isLoading}
onClick={handle.enter}
icon={<FullscreenIcon size={16} />}
disabled={updateDashboardMutation.isLoading}
>
{t('dashboard:full_view')}
</Button>
{saveLayoutPermission && ( {saveLayoutPermission && (
<Button <Button
loading={updateDashboardMutation.isLoading} loading={updateDashboardMutation.isLoading}
@ -129,46 +143,48 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
</ButtonContainer> </ButtonContainer>
)} )}
<ReactGridLayout <FullScreen handle={handle} className="fullscreen-grid-container">
cols={12} <ReactGridLayout
rowHeight={100} cols={12}
autoSize rowHeight={100}
width={100} autoSize
useCSSTransforms width={100}
isDraggable={!isDashboardLocked && addPanelPermission} useCSSTransforms
isDroppable={!isDashboardLocked && addPanelPermission} isDraggable={!isDashboardLocked && addPanelPermission}
isResizable={!isDashboardLocked && addPanelPermission} isDroppable={!isDashboardLocked && addPanelPermission}
allowOverlap={false} isResizable={!isDashboardLocked && addPanelPermission}
onLayoutChange={setLayouts} allowOverlap={false}
draggableHandle=".drag-handle" onLayoutChange={setLayouts}
layout={layouts} draggableHandle=".drag-handle"
> layout={layouts}
{layouts.map((layout) => { >
const { i: id } = layout; {layouts.map((layout) => {
const currentWidget = (widgets || [])?.find((e) => e.id === id); const { i: id } = layout;
const currentWidget = (widgets || [])?.find((e) => e.id === id);
return ( return (
<CardContainer <CardContainer
className={isDashboardLocked ? '' : 'enable-resize'} className={isDashboardLocked ? '' : 'enable-resize'}
isDarkMode={isDarkMode} isDarkMode={isDarkMode}
key={id} key={id}
data-grid={layout} data-grid={layout}
>
<Card
className="grid-item"
$panelType={currentWidget?.panelTypes || PANEL_TYPES.TIME_SERIES}
> >
<GridCard <Card
widget={currentWidget || ({ id, query: {} } as Widgets)} className="grid-item"
name={currentWidget?.id || ''} $panelType={currentWidget?.panelTypes || PANEL_TYPES.TIME_SERIES}
headerMenuList={widgetActions} >
variables={variables} <GridCard
/> widget={currentWidget || ({ id, query: {} } as Widgets)}
</Card> name={currentWidget?.id || ''}
</CardContainer> headerMenuList={widgetActions}
); variables={variables}
})} />
</ReactGridLayout> </Card>
</CardContainer>
);
})}
</ReactGridLayout>
</FullScreen>
</> </>
); );
} }

View File

@ -26,7 +26,12 @@ const GridPanelSwitch = forwardRef<
yAxisUnit, yAxisUnit,
thresholds, 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.LIST]: null,
[PANEL_TYPES.TRACE]: null, [PANEL_TYPES.TRACE]: null,
[PANEL_TYPES.EMPTY_WIDGET]: null, [PANEL_TYPES.EMPTY_WIDGET]: null,

View File

@ -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 { 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 { WrapperStyled } from './styles';
import { GridTableComponentProps } from './types'; import { GridTableComponentProps } from './types';
import { findMatchingThreshold } from './utils';
function GridTableComponent({ function GridTableComponent({
data, data,
query, query,
thresholds,
...props ...props
}: GridTableComponentProps): JSX.Element { }: 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 ( return (
<WrapperStyled> <WrapperStyled>
<QueryTable <QueryTable
query={query} query={query}
queryTableData={data} queryTableData={data}
loading={false} loading={false}
columns={newColumnData}
dataSource={dataSource}
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
{...props} {...props}
/> />

View File

@ -1,10 +1,23 @@
import { TableProps } from 'antd'; import { TableProps } from 'antd';
import { LogsExplorerTableProps } from 'container/LogsExplorerTable/LogsExplorerTable.interfaces'; import { LogsExplorerTableProps } from 'container/LogsExplorerTable/LogsExplorerTable.interfaces';
import {
ThresholdOperators,
ThresholdProps,
} from 'container/NewWidget/RightContainer/Threshold/types';
import { RowData } from 'lib/query/createTableColumnsFromQuery'; import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { Query } from 'types/api/queryBuilder/queryBuilderData';
export type GridTableComponentProps = { query: Query } & Pick< export type GridTableComponentProps = {
LogsExplorerTableProps, query: Query;
'data' thresholds?: ThresholdProps[];
> & } & Pick<LogsExplorerTableProps, 'data'> &
Omit<TableProps<RowData>, 'columns' | 'dataSource'>; Omit<TableProps<RowData>, 'columns' | 'dataSource'>;
export type RequiredThresholdProps = Omit<
ThresholdProps,
'thresholdTableOptions' | 'thresholdOperator' | 'thresholdValue'
> & {
thresholdTableOptions: string;
thresholdOperator: ThresholdOperators;
thresholdValue: number;
};

View 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,
};
}

View File

@ -1,6 +1,7 @@
.show-case-container { .show-case-container {
padding: 5px 15px; padding: 5px 15px;
border-radius: 5px; border-radius: 5px;
display: inline-block;
} }
.show-case-dark { .show-case-dark {

View File

@ -1,3 +1,4 @@
/* eslint-disable sonarjs/cognitive-complexity */
import './Threshold.styles.scss'; import './Threshold.styles.scss';
import { CheckOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'; import { CheckOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons';
@ -40,6 +41,8 @@ function Threshold({
moveThreshold, moveThreshold,
selectedGraph, selectedGraph,
thresholdLabel = '', thresholdLabel = '',
tableOptions,
thresholdTableOptions = '',
}: ThresholdProps): JSX.Element { }: ThresholdProps): JSX.Element {
const [isEditMode, setIsEditMode] = useState<boolean>(isEditEnabled); const [isEditMode, setIsEditMode] = useState<boolean>(isEditEnabled);
const [operator, setOperator] = useState<string | number>( const [operator, setOperator] = useState<string | number>(
@ -52,6 +55,9 @@ function Threshold({
thresholdFormat, thresholdFormat,
); );
const [label, setLabel] = useState<string>(thresholdLabel); const [label, setLabel] = useState<string>(thresholdLabel);
const [tableSelectedOption, setTableSelectedOption] = useState<string>(
thresholdTableOptions,
);
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
@ -72,6 +78,7 @@ function Threshold({
thresholdUnit: unit, thresholdUnit: unit,
thresholdValue: value, thresholdValue: value,
thresholdLabel: label, thresholdLabel: label,
thresholdTableOptions: tableSelectedOption,
}; };
} }
return threshold; return threshold;
@ -104,6 +111,10 @@ function Threshold({
setFormat(value); setFormat(value);
}; };
const handleTableOptionsChange = (value: string): void => {
setTableSelectedOption(value);
};
const deleteHandler = (): void => { const deleteHandler = (): void => {
if (thresholdDeleteHandler) { if (thresholdDeleteHandler) {
thresholdDeleteHandler(index); thresholdDeleteHandler(index);
@ -203,7 +214,11 @@ function Threshold({
/> />
</div> </div>
<div> <div>
<Space> <Space
direction={
selectedGraph === PANEL_TYPES.TABLE ? 'vertical' : 'horizontal'
}
>
{selectedGraph === PANEL_TYPES.TIME_SERIES && ( {selectedGraph === PANEL_TYPES.TIME_SERIES && (
<> <>
<Typography.Text>Label</Typography.Text> <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 ? ( {isEditMode ? (
<Select <>
style={{ minWidth: '73px', backgroundColor }} {selectedGraph === PANEL_TYPES.TABLE && (
defaultValue={operator} <Space>
options={operatorOptions} <Select
onChange={handleOperatorChange} style={{
bordered={!isDarkMode} 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} /> <ShowCaseValue width="100px" value={format} />
</> </>
)} )}

View File

@ -1,9 +1,13 @@
import './ThresholdSelector.styles.scss'; import './ThresholdSelector.styles.scss';
import { Button, Typography } from 'antd'; 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 { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import { eventEmitter } from 'utils/getEventEmitter';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import Threshold from './Threshold'; import Threshold from './Threshold';
@ -15,6 +19,22 @@ function ThresholdSelector({
yAxisUnit, yAxisUnit,
selectedGraph, selectedGraph,
}: ThresholdSelectorProps): JSX.Element { }: 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( const moveThreshold = useCallback(
(dragIndex: number, hoverIndex: number) => { (dragIndex: number, hoverIndex: number) => {
setThresholds((prevCards) => { setThresholds((prevCards) => {
@ -44,6 +64,7 @@ function ThresholdSelector({
moveThreshold, moveThreshold,
keyIndex: thresholds.length, keyIndex: thresholds.length,
selectedGraph, selectedGraph,
thresholdTableOptions: tableOptions[0]?.value || '',
}, },
]); ]);
}; };
@ -75,6 +96,8 @@ function ThresholdSelector({
moveThreshold={moveThreshold} moveThreshold={moveThreshold}
selectedGraph={selectedGraph} selectedGraph={selectedGraph}
thresholdLabel={threshold.thresholdLabel} thresholdLabel={threshold.thresholdLabel}
tableOptions={tableOptions}
thresholdTableOptions={threshold.thresholdTableOptions}
/> />
))} ))}
<Button className="threshold-selector-button" onClick={addThresholdHandler}> <Button className="threshold-selector-button" onClick={addThresholdHandler}>

View File

@ -1,7 +1,7 @@
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import { Dispatch, ReactNode, SetStateAction } from 'react'; import { Dispatch, ReactNode, SetStateAction } from 'react';
type ThresholdOperators = '>' | '<' | '>=' | '<=' | '='; export type ThresholdOperators = '>' | '<' | '>=' | '<=' | '=';
export type ThresholdProps = { export type ThresholdProps = {
index: string; index: string;
@ -14,9 +14,11 @@ export type ThresholdProps = {
thresholdFormat?: 'Text' | 'Background'; thresholdFormat?: 'Text' | 'Background';
isEditEnabled?: boolean; isEditEnabled?: boolean;
thresholdLabel?: string; thresholdLabel?: string;
thresholdTableOptions?: string;
setThresholds?: Dispatch<SetStateAction<ThresholdProps[]>>; setThresholds?: Dispatch<SetStateAction<ThresholdProps[]>>;
moveThreshold: (dragIndex: number, hoverIndex: number) => void; moveThreshold: (dragIndex: number, hoverIndex: number) => void;
selectedGraph: PANEL_TYPES; selectedGraph: PANEL_TYPES;
tableOptions?: Array<{ value: string; label: string }>;
}; };
export type ShowCaseValueProps = { export type ShowCaseValueProps = {

View File

@ -24,7 +24,7 @@ export const showAsOptions: DefaultOptionType[] = [
export const panelTypeVsThreshold: { [key in PANEL_TYPES]: boolean } = { export const panelTypeVsThreshold: { [key in PANEL_TYPES]: boolean } = {
[PANEL_TYPES.TIME_SERIES]: true, [PANEL_TYPES.TIME_SERIES]: true,
[PANEL_TYPES.VALUE]: true, [PANEL_TYPES.VALUE]: true,
[PANEL_TYPES.TABLE]: false, [PANEL_TYPES.TABLE]: true,
[PANEL_TYPES.LIST]: false, [PANEL_TYPES.LIST]: false,
[PANEL_TYPES.TRACE]: false, [PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false, [PANEL_TYPES.EMPTY_WIDGET]: false,

View File

@ -16,4 +16,6 @@ export type QueryTableProps = Omit<
modifyColumns?: (columns: ColumnsType<RowData>) => ColumnsType<RowData>; modifyColumns?: (columns: ColumnsType<RowData>) => ColumnsType<RowData>;
renderColumnCell?: Record<string, (record: RowData) => ReactNode>; renderColumnCell?: Record<string, (record: RowData) => ReactNode>;
downloadOption?: DownloadOptions; downloadOption?: DownloadOptions;
columns?: ColumnsType<RowData>;
dataSource?: RowData[];
}; };

View File

@ -17,25 +17,35 @@ export function QueryTable({
modifyColumns, modifyColumns,
renderColumnCell, renderColumnCell,
downloadOption, downloadOption,
columns,
dataSource,
...props ...props
}: QueryTableProps): JSX.Element { }: QueryTableProps): JSX.Element {
const { isDownloadEnabled = false, fileName = '' } = downloadOption || {}; const { isDownloadEnabled = false, fileName = '' } = downloadOption || {};
const { servicename } = useParams<IServiceName>(); const { servicename } = useParams<IServiceName>();
const { loading } = props; const { loading } = props;
const { columns, dataSource } = useMemo( const { columns: newColumns, dataSource: newDataSource } = useMemo(() => {
() => if (columns && dataSource) {
createTableColumnsFromQuery({ return { columns, dataSource };
query, }
queryTableData, return createTableColumnsFromQuery({
renderActionCell, query,
renderColumnCell, queryTableData,
}), renderActionCell,
[query, queryTableData, renderActionCell, renderColumnCell], 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 ( return (
<div className="query-table"> <div className="query-table">

View File

@ -7803,6 +7803,11 @@ fs.realpath@^1.0.0:
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== 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: fsevents@2.3.2, fsevents@^2.3.2, fsevents@~2.3.2:
version "2.3.2" version "2.3.2"
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz"
@ -12850,6 +12855,13 @@ react-force-graph@^1.43.0:
prop-types "15" prop-types "15"
react-kapsule "2" 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: react-grid-layout@^1.3.4:
version "1.3.4" version "1.3.4"
resolved "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.3.4.tgz" resolved "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.3.4.tgz"

View File

@ -43,9 +43,9 @@ var (
// FromUnit returns a converter for the given unit // FromUnit returns a converter for the given unit
func FromUnit(u Unit) Converter { func FromUnit(u Unit) Converter {
switch u { switch u {
case "ns", "us", "ms", "s", "m", "h", "d": case "ns", "us", "µs", "ms", "s", "m", "h", "d":
return DurationConverter 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 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": 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 return DataRateConverter
@ -64,7 +64,7 @@ func UnitToName(u string) string {
switch u { switch u {
case "ns": case "ns":
return " ns" return " ns"
case "us": case "us", "µs":
return " us" return " us"
case "ms": case "ms":
return " ms" return " ms"
@ -86,23 +86,23 @@ func UnitToName(u string) string {
return " bits" return " bits"
case "kbytes": case "kbytes":
return " KiB" return " KiB"
case "decKbytes": case "decKbytes", "deckbytes":
return " kB" return " kB"
case "mbytes": case "mbytes":
return " MiB" return " MiB"
case "decMbytes": case "decMbytes", "decmbytes":
return " MB" return " MB"
case "gbytes": case "gbytes":
return " GiB" return " GiB"
case "decGbytes": case "decGbytes", "decgbytes":
return " GB" return " GB"
case "tbytes": case "tbytes":
return " TiB" return " TiB"
case "decTbytes": case "decTbytes", "decybytes":
return " TB" return " TB"
case "pbytes": case "pbytes":
return " PiB" return " PiB"
case "decPbytes": case "decPbytes", "decpbytes":
return " PB" return " PB"
case "binBps": case "binBps":
return " bytes/sec(IEC)" return " bytes/sec(IEC)"

View File

@ -70,23 +70,23 @@ func FromDataUnit(u Unit) float64 {
return Bit return Bit
case "kbytes": // base 2 case "kbytes": // base 2
return Kibibyte return Kibibyte
case "deckbytes": // base 10 case "decKbytes", "deckbytes": // base 10
return Kilobyte return Kilobyte
case "mbytes": // base 2 case "mbytes": // base 2
return Mebibyte return Mebibyte
case "decmbytes": // base 10 case "decMbytes", "decmbytes": // base 10
return Megabyte return Megabyte
case "gbytes": // base 2 case "gbytes": // base 2
return Gibibyte return Gibibyte
case "decgbytes": // base 10 case "decGbytes", "decgbytes": // base 10
return Gigabyte return Gigabyte
case "tbytes": // base 2 case "tbytes": // base 2
return Tebibyte return Tebibyte
case "dectbytes": // base 10 case "decTbytes", "dectbytes": // base 10
return Terabyte return Terabyte
case "pbytes": // base 2 case "pbytes": // base 2
return Pebibyte return Pebibyte
case "decpbytes": // base 10 case "decPbytes", "decpbytes": // base 10
return Petabyte return Petabyte
default: default:
return 1 return 1

View File

@ -31,7 +31,7 @@ func FromTimeUnit(u Unit) Duration {
switch u { switch u {
case "ns": case "ns":
return Nanosecond return Nanosecond
case "us": case "us", "µs":
return Microsecond return Microsecond
case "ms": case "ms":
return Millisecond return Millisecond

View File

@ -30,23 +30,23 @@ func (f *dataFormatter) Format(value float64, unit string) string {
return humanize.Bytes(uint64(value * converter.Bit)) return humanize.Bytes(uint64(value * converter.Bit))
case "kbytes": case "kbytes":
return humanize.IBytes(uint64(value * converter.Kibibit)) return humanize.IBytes(uint64(value * converter.Kibibit))
case "deckbytes": case "decKbytes", "deckbytes":
return humanize.IBytes(uint64(value * converter.Kilobit)) return humanize.IBytes(uint64(value * converter.Kilobit))
case "mbytes": case "mbytes":
return humanize.IBytes(uint64(value * converter.Mebibit)) return humanize.IBytes(uint64(value * converter.Mebibit))
case "decmbytes": case "decMbytes", "decmbytes":
return humanize.Bytes(uint64(value * converter.Megabit)) return humanize.Bytes(uint64(value * converter.Megabit))
case "gbytes": case "gbytes":
return humanize.IBytes(uint64(value * converter.Gibibit)) return humanize.IBytes(uint64(value * converter.Gibibit))
case "decgbytes": case "decGbytes", "decgbytes":
return humanize.Bytes(uint64(value * converter.Gigabit)) return humanize.Bytes(uint64(value * converter.Gigabit))
case "tbytes": case "tbytes":
return humanize.IBytes(uint64(value * converter.Tebibit)) return humanize.IBytes(uint64(value * converter.Tebibit))
case "dectbytes": case "decTbytes", "dectbytes":
return humanize.Bytes(uint64(value * converter.Terabit)) return humanize.Bytes(uint64(value * converter.Terabit))
case "pbytes": case "pbytes":
return humanize.IBytes(uint64(value * converter.Pebibit)) return humanize.IBytes(uint64(value * converter.Pebibit))
case "decpbytes": case "decPbytes", "decpbytes":
return humanize.Bytes(uint64(value * converter.Petabit)) return humanize.Bytes(uint64(value * converter.Petabit))
} }
// When unit is not matched, return the value as it is. // When unit is not matched, return the value as it is.

View File

@ -18,9 +18,9 @@ var (
func FromUnit(u string) Formatter { func FromUnit(u string) Formatter {
switch u { switch u {
case "ns", "us", "ms", "s", "m", "h", "d": case "ns", "us", "µs", "ms", "s", "m", "h", "d":
return DurationFormatter 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 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": 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 return DataRateFormatter

View File

@ -81,10 +81,9 @@ func toFixed(value float64, decimals DecimalCount) string {
precision := 0 precision := 0
if decimalPos != -1 { if decimalPos != -1 {
precision = len(formatted) - 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 return formatted

View 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))
}

View File

@ -20,7 +20,7 @@ func (f *durationFormatter) Format(value float64, unit string) string {
switch unit { switch unit {
case "ns": case "ns":
return toNanoSeconds(value) return toNanoSeconds(value)
case "µs": case "µs", "us":
return toMicroSeconds(value) return toMicroSeconds(value)
case "ms": case "ms":
return toMilliSeconds(value) return toMilliSeconds(value)

View File

@ -7,7 +7,6 @@ import (
"math" "math"
"reflect" "reflect"
"sort" "sort"
"strconv"
"sync" "sync"
"text/template" "text/template"
"time" "time"
@ -780,7 +779,8 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time, queriers *Querie
} }
value := valueFormatter.Format(smpl.V, r.Unit()) 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) 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) tmplData := AlertTemplateData(l, value, threshold)