mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-08 18:59:56 +00:00
Compare commits
25 Commits
v0.35.1
...
jest-githu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ced74603c0 | ||
|
|
f59fb81109 | ||
|
|
507e68a0c1 | ||
|
|
4ad8a1f3ad | ||
|
|
19faf6a584 | ||
|
|
3978ada811 | ||
|
|
0a04fc04a5 | ||
|
|
7c9e333b84 | ||
|
|
dd78afb20f | ||
|
|
237d765376 | ||
|
|
85e865fb1b | ||
|
|
975e5daf03 | ||
|
|
8a532cca17 | ||
|
|
b9c908719f | ||
|
|
63c7b5e9e1 | ||
|
|
32eeb3d106 | ||
|
|
1a4ec2bf00 | ||
|
|
1d014ab4f7 | ||
|
|
418ab67d50 | ||
|
|
7efe907757 | ||
|
|
1d1154aa8c | ||
|
|
a16fca6376 | ||
|
|
9c1ea0cde9 | ||
|
|
ec500831ef | ||
|
|
fcbf82c2f3 |
32
.github/workflows/jest-code-coverage.yml
vendored
Normal file
32
.github/workflows/jest-code-coverage.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: Code Coverage
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- main
|
||||
- release/v*
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- main
|
||||
- release/v*
|
||||
jobs:
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v2
|
||||
- uses: jwalton/gh-find-current-pr@v1
|
||||
id: findPr
|
||||
- uses: ArtiomTr/jest-coverage-report-action@v2
|
||||
with:
|
||||
package-manager: yarn
|
||||
working-directory: frontend
|
||||
test-script: yarn jest:coverage
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
output: comment
|
||||
prnumber: ${{ steps.findPr.outputs.number }}
|
||||
@@ -1,7 +1,7 @@
|
||||
version: "3.9"
|
||||
|
||||
x-clickhouse-defaults: &clickhouse-defaults
|
||||
image: clickhouse/clickhouse-server:23.7.3-alpine
|
||||
image: clickhouse/clickhouse-server:23.11.1-alpine
|
||||
tty: true
|
||||
deploy:
|
||||
restart_policy:
|
||||
|
||||
@@ -3,7 +3,7 @@ version: "2.4"
|
||||
x-clickhouse-defaults: &clickhouse-defaults
|
||||
restart: on-failure
|
||||
# addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab
|
||||
image: clickhouse/clickhouse-server:23.7.3-alpine
|
||||
image: clickhouse/clickhouse-server:23.11.1-alpine
|
||||
tty: true
|
||||
depends_on:
|
||||
- zookeeper-1
|
||||
|
||||
@@ -86,6 +86,7 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
|
||||
'no-plusplus': 'off',
|
||||
'jsx-a11y/label-has-associated-control': [
|
||||
'error',
|
||||
{
|
||||
@@ -109,7 +110,6 @@ module.exports = {
|
||||
// eslint rules need to remove
|
||||
'@typescript-eslint/no-shadow': 'off',
|
||||
'import/no-cycle': 'off',
|
||||
|
||||
'prettier/prettier': [
|
||||
'error',
|
||||
{},
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "6.0.0",
|
||||
"@ant-design/icons": "4.8.0",
|
||||
"@dnd-kit/core": "6.1.0",
|
||||
"@dnd-kit/modifiers": "7.0.0",
|
||||
"@dnd-kit/sortable": "8.0.0",
|
||||
"@grafana/data": "^9.5.2",
|
||||
"@mdx-js/loader": "2.3.0",
|
||||
"@mdx-js/react": "2.3.0",
|
||||
|
||||
@@ -21,5 +21,9 @@
|
||||
"error_while_updating_variable": "Error while updating variable",
|
||||
"dashboard_has_been_updated": "Dashboard has been updated",
|
||||
"do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?",
|
||||
"delete_dashboard_success": "{{name}} dashboard deleted successfully"
|
||||
"delete_dashboard_success": "{{name}} dashboard deleted successfully",
|
||||
"dashboard_unsave_changes": "There are unsaved changes in the Query builder, please stage and run the query or the changes will be lost. Press OK to discard.",
|
||||
"dashboard_save_changes": "Your graph built with {{queryTag}} query will be saved. Press OK to confirm.",
|
||||
"your_graph_build_with": "Your graph built with",
|
||||
"dashboar_ok_confirm": "query will be saved. Press OK to confirm."
|
||||
}
|
||||
|
||||
@@ -24,5 +24,9 @@
|
||||
"do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?",
|
||||
"locked_dashboard_delete_tooltip_admin_author": "Dashboard is locked. Please unlock the dashboard to enable delete.",
|
||||
"locked_dashboard_delete_tooltip_editor": "Dashboard is locked. Please contact admin to delete the dashboard.",
|
||||
"delete_dashboard_success": "{{name}} dashboard deleted successfully"
|
||||
"delete_dashboard_success": "{{name}} dashboard deleted successfully",
|
||||
"dashboard_unsave_changes": "There are unsaved changes in the Query builder, please stage and run the query or the changes will be lost. Press OK to discard.",
|
||||
"dashboard_save_changes": "Your graph built with {{queryTag}} query will be saved. Press OK to confirm.",
|
||||
"your_graph_build_with": "Your graph built with",
|
||||
"dashboar_ok_confirm": "query will be saved. Press OK to confirm."
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import cacheBursting from 'i18n-translations-hash.json';
|
||||
import i18n from 'i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import Backend from 'i18next-http-backend';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import cacheBursting from '../../i18n-translations-hash.json';
|
||||
|
||||
i18n
|
||||
// load translation using http -> see /public/locales
|
||||
.use(Backend)
|
||||
|
||||
@@ -5,6 +5,7 @@ import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import {
|
||||
DeleteViewHandlerProps,
|
||||
@@ -35,6 +36,45 @@ export const getViewDetailsUsingViewKey: GetViewDetailsUsingViewKey = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const omitIdFromQuery = (query: Query | null): any => ({
|
||||
...query,
|
||||
builder: {
|
||||
...query?.builder,
|
||||
queryData: query?.builder.queryData.map((queryData) => {
|
||||
const { id, ...rest } = queryData.aggregateAttribute;
|
||||
const newAggregateAttribute = rest;
|
||||
const newGroupByAttributes = queryData.groupBy.map((groupByAttribute) => {
|
||||
const { id, ...rest } = groupByAttribute;
|
||||
return rest;
|
||||
});
|
||||
const newItems = queryData.filters.items.map((item) => {
|
||||
const { id, ...newItem } = item;
|
||||
if (item.key) {
|
||||
const { id, ...rest } = item.key;
|
||||
return {
|
||||
...newItem,
|
||||
key: rest,
|
||||
};
|
||||
}
|
||||
return newItem;
|
||||
});
|
||||
return {
|
||||
...queryData,
|
||||
aggregateAttribute: newAggregateAttribute,
|
||||
groupBy: newGroupByAttributes,
|
||||
filters: {
|
||||
...queryData.filters,
|
||||
items: newItems,
|
||||
},
|
||||
limit: queryData.limit ? queryData.limit : 0,
|
||||
offset: queryData.offset ? queryData.offset : 0,
|
||||
pageSize: queryData.pageSize ? queryData.pageSize : 0,
|
||||
};
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export const isQueryUpdatedInView = ({
|
||||
viewKey,
|
||||
data,
|
||||
@@ -48,43 +88,7 @@ export const isQueryUpdatedInView = ({
|
||||
const { query, panelType } = currentViewDetails;
|
||||
|
||||
// Omitting id from aggregateAttribute and groupBy
|
||||
const updatedCurrentQuery = {
|
||||
...stagedQuery,
|
||||
builder: {
|
||||
...stagedQuery?.builder,
|
||||
queryData: stagedQuery?.builder.queryData.map((queryData) => {
|
||||
const { id, ...rest } = queryData.aggregateAttribute;
|
||||
const newAggregateAttribute = rest;
|
||||
const newGroupByAttributes = queryData.groupBy.map((groupByAttribute) => {
|
||||
const { id, ...rest } = groupByAttribute;
|
||||
return rest;
|
||||
});
|
||||
const newItems = queryData.filters.items.map((item) => {
|
||||
const { id, ...newItem } = item;
|
||||
if (item.key) {
|
||||
const { id, ...rest } = item.key;
|
||||
return {
|
||||
...newItem,
|
||||
key: rest,
|
||||
};
|
||||
}
|
||||
return newItem;
|
||||
});
|
||||
return {
|
||||
...queryData,
|
||||
aggregateAttribute: newAggregateAttribute,
|
||||
groupBy: newGroupByAttributes,
|
||||
filters: {
|
||||
...queryData.filters,
|
||||
items: newItems,
|
||||
},
|
||||
limit: queryData.limit ? queryData.limit : 0,
|
||||
offset: queryData.offset ? queryData.offset : 0,
|
||||
pageSize: queryData.pageSize ? queryData.pageSize : 0,
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
const updatedCurrentQuery = omitIdFromQuery(stagedQuery);
|
||||
|
||||
return (
|
||||
panelType !== currentPanelType ||
|
||||
|
||||
@@ -13,3 +13,11 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.uplot-no-data {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import './uplot.scss';
|
||||
import './Uplot.styles.scss';
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import { LineChart } from 'lucide-react';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import {
|
||||
forwardRef,
|
||||
@@ -127,6 +128,16 @@ const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
|
||||
}
|
||||
}, [data, resetScales, create]);
|
||||
|
||||
if (data && data[0] && data[0]?.length === 0) {
|
||||
return (
|
||||
<div className="uplot-no-data not-found">
|
||||
<LineChart size={48} strokeWidth={0.5} />
|
||||
|
||||
<Typography>No Data</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
||||
<div className="uplot-graph-container" ref={targetRef}>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -18,6 +18,7 @@ import { AlertDef } from 'types/api/alerts/def';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
import { ChartContainer, FailedMessageContainer } from './styles';
|
||||
import { getThresholdLabel } from './utils';
|
||||
@@ -49,9 +50,13 @@ function ChartPreview({
|
||||
}: ChartPreviewProps): JSX.Element | null {
|
||||
const { t } = useTranslation('alerts');
|
||||
const threshold = alertDef?.condition.target || 0;
|
||||
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||
|
||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const canQuery = useMemo((): boolean => {
|
||||
if (!query || query == null) {
|
||||
@@ -101,6 +106,13 @@ function ChartPreview({
|
||||
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect((): void => {
|
||||
const { startTime, endTime } = getTimeRange(queryResponse);
|
||||
|
||||
setMinTimeScale(startTime);
|
||||
setMaxTimeScale(endTime);
|
||||
}, [maxTime, minTime, globalSelectedInterval, queryResponse]);
|
||||
|
||||
const chartData = getUPlotChartData(queryResponse?.data?.payload);
|
||||
|
||||
const containerDimensions = useResizeObserver(graphRef);
|
||||
@@ -117,6 +129,8 @@ function ChartPreview({
|
||||
yAxisUnit,
|
||||
apiResponse: queryResponse?.data?.payload,
|
||||
dimensions: containerDimensions,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
isDarkMode,
|
||||
thresholds: [
|
||||
{
|
||||
@@ -141,6 +155,8 @@ function ChartPreview({
|
||||
yAxisUnit,
|
||||
queryResponse?.data?.payload,
|
||||
containerDimensions,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
isDarkMode,
|
||||
threshold,
|
||||
t,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Tooltip } from 'antd';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
import { LabelContainer } from '../styles';
|
||||
@@ -23,7 +24,9 @@ function Label({
|
||||
disabled={disabled}
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
{getAbbreviatedLabel(label)}
|
||||
<Tooltip title={label} placement="topLeft">
|
||||
{getAbbreviatedLabel(label)}
|
||||
</Tooltip>
|
||||
</LabelContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import uPlot from 'uplot';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
|
||||
import GraphManager from './GraphManager';
|
||||
@@ -92,6 +93,21 @@ function FullView({
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||
|
||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
useEffect((): void => {
|
||||
const { startTime, endTime } = getTimeRange(response);
|
||||
|
||||
setMinTimeScale(startTime);
|
||||
setMaxTimeScale(endTime);
|
||||
}, [maxTime, minTime, globalSelectedInterval, response]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!response.isFetching && fullViewRef.current) {
|
||||
const width = fullViewRef.current?.clientWidth
|
||||
@@ -114,6 +130,8 @@ function FullView({
|
||||
graphsVisibilityStates,
|
||||
setGraphsVisibilityStates,
|
||||
thresholds: widget.thresholds,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
});
|
||||
|
||||
setChartOptions(newChartOptions);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Skeleton, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@@ -298,7 +299,10 @@ function WidgetGraphComponent({
|
||||
</div>
|
||||
{queryResponse.isLoading && <Skeleton />}
|
||||
{queryResponse.isSuccess && (
|
||||
<div style={{ height: '90%' }} ref={graphRef}>
|
||||
<div
|
||||
className={cx('widget-graph-container', widget.panelTypes)}
|
||||
ref={graphRef}
|
||||
>
|
||||
<GridPanelSwitch
|
||||
panelType={widget.panelTypes}
|
||||
data={data}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
import EmptyWidget from '../EmptyWidget';
|
||||
import { MenuItemKeys } from '../WidgetHeader/contants';
|
||||
@@ -34,6 +35,8 @@ function GridCardGraph({
|
||||
const dispatch = useDispatch();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
|
||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||
|
||||
const onDragSelect = useCallback(
|
||||
(start: number, end: number): void => {
|
||||
@@ -62,16 +65,16 @@ function GridCardGraph({
|
||||
}
|
||||
}, [toScrollWidgetId, setToScrollWidgetId, widget.id]);
|
||||
|
||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const updatedQuery = useStepInterval(widget?.query);
|
||||
|
||||
const isEmptyWidget =
|
||||
widget?.id === PANEL_TYPES.EMPTY_WIDGET || isEmpty(widget);
|
||||
|
||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const queryResponse = useGetQueryRange(
|
||||
{
|
||||
selectedTime: widget?.timePreferance,
|
||||
@@ -103,6 +106,13 @@ function GridCardGraph({
|
||||
|
||||
const containerDimensions = useResizeObserver(graphRef);
|
||||
|
||||
useEffect((): void => {
|
||||
const { startTime, endTime } = getTimeRange(queryResponse);
|
||||
|
||||
setMinTimeScale(startTime);
|
||||
setMaxTimeScale(endTime);
|
||||
}, [maxTime, minTime, globalSelectedInterval, queryResponse]);
|
||||
|
||||
const chartData = getUPlotChartData(queryResponse?.data?.payload, fillSpans);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
@@ -123,6 +133,8 @@ function GridCardGraph({
|
||||
yAxisUnit: widget?.yAxisUnit,
|
||||
onClickHandler,
|
||||
thresholds: widget.thresholds,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
}),
|
||||
[
|
||||
widget?.id,
|
||||
@@ -133,6 +145,8 @@ function GridCardGraph({
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
onClickHandler,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -5,3 +5,11 @@
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.widget-graph-container {
|
||||
height: 100%;
|
||||
|
||||
&.graph {
|
||||
height: calc(100% - 30px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.widget-header-title {
|
||||
@@ -19,6 +22,10 @@
|
||||
visibility: hidden;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
font: 14px;
|
||||
font-weight: 600;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.widget-header-hover {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
MoreOutlined,
|
||||
WarningOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Dropdown, MenuProps, Tooltip, Typography } from 'antd';
|
||||
import { Dropdown, MenuProps, Tooltip, Typography } from 'antd';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -199,9 +199,7 @@ function WidgetHeader({
|
||||
</Tooltip>
|
||||
)}
|
||||
<Dropdown menu={menu} trigger={['hover']} placement="bottomRight">
|
||||
<Button
|
||||
type="default"
|
||||
icon={<MoreOutlined />}
|
||||
<MoreOutlined
|
||||
className={`widget-header-more-options ${
|
||||
parentHover ? 'widget-header-hover' : ''
|
||||
}`}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Button as ButtonComponent, Card as CardComponent, Space } from 'antd';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { StyledCSS } from 'container/GantChart/Trace/styles';
|
||||
import RGL, { WidthProvider } from 'react-grid-layout';
|
||||
import styled, { css, FlattenSimpleInterpolation } from 'styled-components';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
const ReactGridLayoutComponent = WidthProvider(RGL);
|
||||
|
||||
@@ -17,14 +17,8 @@ export const Card = styled(CardComponent)<CardProps>`
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
height: 90%;
|
||||
height: calc(100% - 40px);
|
||||
padding: 0;
|
||||
${({ $panelType }): FlattenSimpleInterpolation =>
|
||||
$panelType === PANEL_TYPES.TABLE
|
||||
? css`
|
||||
padding-top: 1.8rem;
|
||||
`
|
||||
: css``}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ function SettingsDrawer({ drawerTitle }: { drawerTitle: string }): JSX.Element {
|
||||
<DrawerContainer
|
||||
title={drawerTitle}
|
||||
placement="right"
|
||||
width="50%"
|
||||
width="60%"
|
||||
onClose={onClose}
|
||||
open={visible}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.delete-variable-name {
|
||||
font-weight: 700;
|
||||
color: rgb(207, 19, 34);
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -18,10 +18,10 @@ import {
|
||||
VariableQueryTypeArr,
|
||||
VariableSortTypeArr,
|
||||
} from 'types/api/dashboard/getAll';
|
||||
import { v4 } from 'uuid';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
import { variablePropsToPayloadVariables } from '../../../utils';
|
||||
import { TVariableViewMode } from '../types';
|
||||
import { TVariableMode } from '../types';
|
||||
import { LabelContainer, VariableItemRow } from './styles';
|
||||
|
||||
const { Option } = Select;
|
||||
@@ -30,9 +30,9 @@ interface VariableItemProps {
|
||||
variableData: IDashboardVariable;
|
||||
existingVariables: Record<string, IDashboardVariable>;
|
||||
onCancel: () => void;
|
||||
onSave: (name: string, arg0: IDashboardVariable, arg1: string) => void;
|
||||
onSave: (mode: TVariableMode, variableData: IDashboardVariable) => void;
|
||||
validateName: (arg0: string) => boolean;
|
||||
variableViewMode: TVariableViewMode;
|
||||
mode: TVariableMode;
|
||||
}
|
||||
function VariableItem({
|
||||
variableData,
|
||||
@@ -40,7 +40,7 @@ function VariableItem({
|
||||
onCancel,
|
||||
onSave,
|
||||
validateName,
|
||||
variableViewMode,
|
||||
mode,
|
||||
}: VariableItemProps): JSX.Element {
|
||||
const [variableName, setVariableName] = useState<string>(
|
||||
variableData.name || '',
|
||||
@@ -97,7 +97,7 @@ function VariableItem({
|
||||
]);
|
||||
|
||||
const handleSave = (): void => {
|
||||
const newVariableData: IDashboardVariable = {
|
||||
const variable: IDashboardVariable = {
|
||||
name: variableName,
|
||||
description: variableDescription,
|
||||
type: queryType,
|
||||
@@ -111,16 +111,12 @@ function VariableItem({
|
||||
selectedValue: (variableData.selectedValue ||
|
||||
variableTextboxValue) as never,
|
||||
}),
|
||||
modificationUUID: v4(),
|
||||
modificationUUID: generateUUID(),
|
||||
id: variableData.id || generateUUID(),
|
||||
order: variableData.order,
|
||||
};
|
||||
onSave(
|
||||
variableName,
|
||||
newVariableData,
|
||||
(variableViewMode === 'EDIT' && variableName !== variableData.name
|
||||
? variableData.name
|
||||
: '') as string,
|
||||
);
|
||||
onCancel();
|
||||
|
||||
onSave(mode, variable);
|
||||
};
|
||||
|
||||
// Fetches the preview values for the SQL variable query
|
||||
@@ -175,7 +171,6 @@ function VariableItem({
|
||||
return (
|
||||
<div className="variable-item-container">
|
||||
<div className="variable-item-content">
|
||||
{/* <Typography.Title level={3}>Add Variable</Typography.Title> */}
|
||||
<VariableItemRow>
|
||||
<LabelContainer>
|
||||
<Typography>Name</Typography>
|
||||
|
||||
@@ -1,20 +1,78 @@
|
||||
import '../DashboardSettings.styles.scss';
|
||||
|
||||
import { blue, red } from '@ant-design/colors';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Modal, Row, Space, Tag } from 'antd';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { MenuOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core';
|
||||
import {
|
||||
DndContext,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
|
||||
import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { Button, Modal, Row, Space, Table, Typography } from 'antd';
|
||||
import { RowProps } from 'antd/lib';
|
||||
import { convertVariablesToDbFormat } from 'container/NewDashboard/DashboardVariablesSelection/util';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { PencilIcon, TrashIcon } from 'lucide-react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
import { TVariableViewMode } from './types';
|
||||
import { TVariableMode } from './types';
|
||||
import VariableItem from './VariableItem/VariableItem';
|
||||
|
||||
function TableRow({ children, ...props }: RowProps): JSX.Element {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
setActivatorNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
id: props['data-row-key'],
|
||||
});
|
||||
|
||||
const style: React.CSSProperties = {
|
||||
...props.style,
|
||||
transform: CSS.Transform.toString(transform && { ...transform, scaleY: 1 }),
|
||||
transition,
|
||||
...(isDragging ? { position: 'relative', zIndex: 9999 } : {}),
|
||||
};
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<tr {...props} ref={setNodeRef} style={style} {...attributes}>
|
||||
{React.Children.map(children, (child) => {
|
||||
if ((child as React.ReactElement).key === 'sort') {
|
||||
return React.cloneElement(child as React.ReactElement, {
|
||||
children: (
|
||||
<MenuOutlined
|
||||
ref={setActivatorNodeRef}
|
||||
style={{ touchAction: 'none', cursor: 'move' }}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...listeners}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
return child;
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function VariablesSetting(): JSX.Element {
|
||||
const variableToDelete = useRef<string | null>(null);
|
||||
const variableToDelete = useRef<IDashboardVariable | null>(null);
|
||||
const [deleteVariableModal, setDeleteVariableModal] = useState(false);
|
||||
|
||||
const { t } = useTranslation(['dashboard']);
|
||||
@@ -25,16 +83,15 @@ function VariablesSetting(): JSX.Element {
|
||||
|
||||
const { variables = {} } = selectedDashboard?.data || {};
|
||||
|
||||
const variablesTableData = Object.keys(variables).map((variableName) => ({
|
||||
key: variableName,
|
||||
name: variableName,
|
||||
...variables[variableName],
|
||||
}));
|
||||
const [variablesTableData, setVariablesTableData] = useState<any>([]);
|
||||
const [variblesOrderArr, setVariablesOrderArr] = useState<number[]>([]);
|
||||
const [existingVariableNamesMap, setExistingVariableNamesMap] = useState<
|
||||
Record<string, string>
|
||||
>({});
|
||||
|
||||
const [
|
||||
variableViewMode,
|
||||
setVariableViewMode,
|
||||
] = useState<null | TVariableViewMode>(null);
|
||||
const [variableViewMode, setVariableViewMode] = useState<null | TVariableMode>(
|
||||
null,
|
||||
);
|
||||
|
||||
const [
|
||||
variableEditData,
|
||||
@@ -47,7 +104,7 @@ function VariablesSetting(): JSX.Element {
|
||||
};
|
||||
|
||||
const onVariableViewModeEnter = (
|
||||
viewType: TVariableViewMode,
|
||||
viewType: TVariableMode,
|
||||
varData: IDashboardVariable,
|
||||
): void => {
|
||||
setVariableEditData(varData);
|
||||
@@ -56,6 +113,41 @@ function VariablesSetting(): JSX.Element {
|
||||
|
||||
const updateMutation = useUpdateDashboard();
|
||||
|
||||
useEffect(() => {
|
||||
const tableRowData = [];
|
||||
const variableOrderArr = [];
|
||||
const variableNamesMap = {};
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const [key, value] of Object.entries(variables)) {
|
||||
const { order, id, name } = value;
|
||||
|
||||
tableRowData.push({
|
||||
key,
|
||||
name: key,
|
||||
...variables[key],
|
||||
id,
|
||||
});
|
||||
|
||||
if (name) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
variableNamesMap[name] = name;
|
||||
}
|
||||
|
||||
if (order) {
|
||||
variableOrderArr.push(order);
|
||||
}
|
||||
}
|
||||
|
||||
tableRowData.sort((a, b) => a.order - b.order);
|
||||
variableOrderArr.sort((a, b) => a - b);
|
||||
|
||||
setVariablesTableData(tableRowData);
|
||||
setVariablesOrderArr(variableOrderArr);
|
||||
setExistingVariableNamesMap(variableNamesMap);
|
||||
}, [variables]);
|
||||
|
||||
const updateVariables = (
|
||||
updatedVariablesData: Dashboard['data']['variables'],
|
||||
): void => {
|
||||
@@ -89,34 +181,58 @@ function VariablesSetting(): JSX.Element {
|
||||
);
|
||||
};
|
||||
|
||||
const getVariableOrder = (): number => {
|
||||
if (variblesOrderArr && variblesOrderArr.length > 0) {
|
||||
return variblesOrderArr[variblesOrderArr.length - 1] + 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
const onVariableSaveHandler = (
|
||||
name: string,
|
||||
mode: TVariableMode,
|
||||
variableData: IDashboardVariable,
|
||||
oldName: string,
|
||||
): void => {
|
||||
if (!variableData.name) {
|
||||
return;
|
||||
const updatedVariableData = {
|
||||
...variableData,
|
||||
order: variableData?.order >= 0 ? variableData.order : getVariableOrder(),
|
||||
};
|
||||
|
||||
const newVariablesArr = variablesTableData.map(
|
||||
(variable: IDashboardVariable) => {
|
||||
if (variable.id === updatedVariableData.id) {
|
||||
return updatedVariableData;
|
||||
}
|
||||
|
||||
return variable;
|
||||
},
|
||||
);
|
||||
|
||||
if (mode === 'ADD') {
|
||||
newVariablesArr.push(updatedVariableData);
|
||||
}
|
||||
|
||||
const newVariables = { ...variables };
|
||||
newVariables[name] = variableData;
|
||||
const variables = convertVariablesToDbFormat(newVariablesArr);
|
||||
|
||||
if (oldName) {
|
||||
delete newVariables[oldName];
|
||||
}
|
||||
updateVariables(newVariables);
|
||||
setVariablesTableData(newVariablesArr);
|
||||
updateVariables(variables);
|
||||
onDoneVariableViewMode();
|
||||
};
|
||||
|
||||
const onVariableDeleteHandler = (variableName: string): void => {
|
||||
variableToDelete.current = variableName;
|
||||
const onVariableDeleteHandler = (variable: IDashboardVariable): void => {
|
||||
variableToDelete.current = variable;
|
||||
setDeleteVariableModal(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = (): void => {
|
||||
const newVariables = { ...variables };
|
||||
if (variableToDelete?.current) delete newVariables[variableToDelete?.current];
|
||||
updateVariables(newVariables);
|
||||
const newVariablesArr = variablesTableData.filter(
|
||||
(variable: IDashboardVariable) =>
|
||||
variable.id !== variableToDelete?.current?.id,
|
||||
);
|
||||
|
||||
const updatedVariables = convertVariablesToDbFormat(newVariablesArr);
|
||||
|
||||
updateVariables(updatedVariables);
|
||||
variableToDelete.current = null;
|
||||
setDeleteVariableModal(false);
|
||||
};
|
||||
@@ -125,31 +241,36 @@ function VariablesSetting(): JSX.Element {
|
||||
setDeleteVariableModal(false);
|
||||
};
|
||||
|
||||
const validateVariableName = (name: string): boolean => !variables[name];
|
||||
const validateVariableName = (name: string): boolean =>
|
||||
!existingVariableNamesMap[name];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'sort',
|
||||
width: '10%',
|
||||
},
|
||||
{
|
||||
title: 'Variable',
|
||||
dataIndex: 'name',
|
||||
width: 100,
|
||||
width: '40%',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
dataIndex: 'description',
|
||||
width: 100,
|
||||
width: '35%',
|
||||
key: 'description',
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
width: 50,
|
||||
width: '15%',
|
||||
key: 'action',
|
||||
render: (_: IDashboardVariable): JSX.Element => (
|
||||
render: (variable: IDashboardVariable): JSX.Element => (
|
||||
<Space>
|
||||
<Button
|
||||
type="text"
|
||||
style={{ padding: 8, cursor: 'pointer', color: blue[5] }}
|
||||
onClick={(): void => onVariableViewModeEnter('EDIT', _)}
|
||||
onClick={(): void => onVariableViewModeEnter('EDIT', variable)}
|
||||
>
|
||||
<PencilIcon size={14} />
|
||||
</Button>
|
||||
@@ -157,7 +278,9 @@ function VariablesSetting(): JSX.Element {
|
||||
type="text"
|
||||
style={{ padding: 8, color: red[6], cursor: 'pointer' }}
|
||||
onClick={(): void => {
|
||||
if (_.name) onVariableDeleteHandler(_.name);
|
||||
if (variable) {
|
||||
onVariableDeleteHandler(variable);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TrashIcon size={14} />
|
||||
@@ -167,6 +290,51 @@ function VariablesSetting(): JSX.Element {
|
||||
},
|
||||
];
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
// https://docs.dndkit.com/api-documentation/sensors/pointer#activation-constraints
|
||||
distance: 1,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const onDragEnd = ({ active, over }: DragEndEvent): void => {
|
||||
if (active.id !== over?.id) {
|
||||
const activeIndex = variablesTableData.findIndex(
|
||||
(i: { key: UniqueIdentifier }) => i.key === active.id,
|
||||
);
|
||||
const overIndex = variablesTableData.findIndex(
|
||||
(i: { key: UniqueIdentifier | undefined }) => i.key === over?.id,
|
||||
);
|
||||
|
||||
const updatedVariables: IDashboardVariable[] = arrayMove(
|
||||
variablesTableData,
|
||||
activeIndex,
|
||||
overIndex,
|
||||
);
|
||||
|
||||
const reArrangedVariables = {};
|
||||
|
||||
for (let index = 0; index < updatedVariables.length; index += 1) {
|
||||
const variableName = updatedVariables[index].name;
|
||||
|
||||
if (variableName) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
reArrangedVariables[variableName] = {
|
||||
...updatedVariables[index],
|
||||
order: index,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
updateVariables(reArrangedVariables);
|
||||
|
||||
setVariablesTableData(updatedVariables);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{variableViewMode ? (
|
||||
@@ -176,11 +344,17 @@ function VariablesSetting(): JSX.Element {
|
||||
onSave={onVariableSaveHandler}
|
||||
onCancel={onDoneVariableViewMode}
|
||||
validateName={validateVariableName}
|
||||
variableViewMode={variableViewMode}
|
||||
mode={variableViewMode}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Row style={{ flexDirection: 'row-reverse', padding: '0.5rem 0' }}>
|
||||
<Row
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
padding: '0.5rem 0',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
data-testid="add-new-variable"
|
||||
type="primary"
|
||||
@@ -192,7 +366,28 @@ function VariablesSetting(): JSX.Element {
|
||||
</Button>
|
||||
</Row>
|
||||
|
||||
<ResizeTable columns={columns} dataSource={variablesTableData} />
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
modifiers={[restrictToVerticalAxis]}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
// rowKey array
|
||||
items={variablesTableData.map((variable: { key: any }) => variable.key)}
|
||||
>
|
||||
<Table
|
||||
components={{
|
||||
body: {
|
||||
row: TableRow,
|
||||
},
|
||||
}}
|
||||
rowKey="key"
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
dataSource={variablesTableData}
|
||||
/>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</>
|
||||
)}
|
||||
<Modal
|
||||
@@ -202,8 +397,13 @@ function VariablesSetting(): JSX.Element {
|
||||
onOk={handleDeleteConfirm}
|
||||
onCancel={handleDeleteCancel}
|
||||
>
|
||||
Are you sure you want to delete variable{' '}
|
||||
<Tag>{variableToDelete.current}</Tag>?
|
||||
<Typography.Text>
|
||||
Are you sure you want to delete variable{' '}
|
||||
<span className="delete-variable-name">
|
||||
{variableToDelete?.current?.name}
|
||||
</span>
|
||||
?
|
||||
</Typography.Text>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
export type TVariableViewMode = 'EDIT' | 'ADD';
|
||||
export type TVariableMode = 'VIEW' | 'EDIT' | 'ADD';
|
||||
|
||||
export const VariableModes = {
|
||||
VIEW: 'VIEW',
|
||||
EDIT: 'EDIT',
|
||||
ADD: 'ADD',
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Row } from 'antd';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { map, sortBy } from 'lodash-es';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { memo, useState } from 'react';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
import { convertVariablesToDbFormat } from './util';
|
||||
import VariableItem from './VariableItem';
|
||||
|
||||
function DashboardVariableSelection(): JSX.Element | null {
|
||||
@@ -21,8 +21,32 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
const [update, setUpdate] = useState<boolean>(false);
|
||||
const [lastUpdatedVar, setLastUpdatedVar] = useState<string>('');
|
||||
|
||||
const [variablesTableData, setVariablesTableData] = useState<any>([]);
|
||||
|
||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
useEffect(() => {
|
||||
if (variables) {
|
||||
const tableRowData = [];
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const [key, value] of Object.entries(variables)) {
|
||||
const { id } = value;
|
||||
|
||||
tableRowData.push({
|
||||
key,
|
||||
name: key,
|
||||
...variables[key],
|
||||
id,
|
||||
});
|
||||
}
|
||||
|
||||
tableRowData.sort((a, b) => a.order - b.order);
|
||||
|
||||
setVariablesTableData(tableRowData);
|
||||
}
|
||||
}, [variables]);
|
||||
|
||||
const onVarChanged = (name: string): void => {
|
||||
setLastUpdatedVar(name);
|
||||
setUpdate(!update);
|
||||
@@ -64,40 +88,56 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
|
||||
const onValueUpdate = (
|
||||
name: string,
|
||||
id: string,
|
||||
value: IDashboardVariable['selectedValue'],
|
||||
allSelected: boolean,
|
||||
): void => {
|
||||
const updatedVariablesData = { ...variables };
|
||||
updatedVariablesData[name].selectedValue = value;
|
||||
updatedVariablesData[name].allSelected = allSelected;
|
||||
if (id) {
|
||||
const newVariablesArr = variablesTableData.map(
|
||||
(variable: IDashboardVariable) => {
|
||||
const variableCopy = { ...variable };
|
||||
|
||||
console.log('onValue Update', name);
|
||||
if (variableCopy.id === id) {
|
||||
variableCopy.selectedValue = value;
|
||||
variableCopy.allSelected = allSelected;
|
||||
}
|
||||
|
||||
if (role !== 'VIEWER' && selectedDashboard) {
|
||||
updateVariables(name, updatedVariablesData);
|
||||
return variableCopy;
|
||||
},
|
||||
);
|
||||
|
||||
const variables = convertVariablesToDbFormat(newVariablesArr);
|
||||
|
||||
if (role !== 'VIEWER' && selectedDashboard) {
|
||||
updateVariables(name, variables);
|
||||
}
|
||||
onVarChanged(name);
|
||||
|
||||
setUpdate(!update);
|
||||
}
|
||||
onVarChanged(name);
|
||||
|
||||
setUpdate(!update);
|
||||
};
|
||||
|
||||
if (!variables) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const variablesKeys = sortBy(Object.keys(variables));
|
||||
const orderBasedSortedVariables = variablesTableData.sort(
|
||||
(a: { order: number }, b: { order: number }) => a.order - b.order,
|
||||
);
|
||||
|
||||
return (
|
||||
<Row>
|
||||
{variablesKeys &&
|
||||
map(variablesKeys, (variableName) => (
|
||||
{orderBasedSortedVariables &&
|
||||
Array.isArray(orderBasedSortedVariables) &&
|
||||
orderBasedSortedVariables.length > 0 &&
|
||||
orderBasedSortedVariables.map((variable) => (
|
||||
<VariableItem
|
||||
key={`${variableName}${variables[variableName].modificationUUID}`}
|
||||
key={`${variable.name}${variable.id}}${variable.order}`}
|
||||
existingVariables={variables}
|
||||
lastUpdatedVar={lastUpdatedVar}
|
||||
variableData={{
|
||||
name: variableName,
|
||||
...variables[variableName],
|
||||
name: variable.name,
|
||||
...variable,
|
||||
change: update,
|
||||
}}
|
||||
onValueUpdate={onValueUpdate}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import VariableItem from './VariableItem';
|
||||
|
||||
const mockVariableData: IDashboardVariable = {
|
||||
id: 'test_variable',
|
||||
description: 'Test Variable',
|
||||
type: 'TEXTBOX',
|
||||
textboxValue: 'defaultValue',
|
||||
@@ -95,6 +96,7 @@ describe('VariableItem', () => {
|
||||
// expect(mockOnValueUpdate).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnValueUpdate).toHaveBeenCalledWith(
|
||||
'testVariable',
|
||||
'test_variable',
|
||||
'newValue',
|
||||
false,
|
||||
);
|
||||
|
||||
@@ -2,13 +2,14 @@ import './DashboardVariableSelection.styles.scss';
|
||||
|
||||
import { orange } from '@ant-design/colors';
|
||||
import { WarningOutlined } from '@ant-design/icons';
|
||||
import { Input, Popover, Select, Typography } from 'antd';
|
||||
import { Input, Popover, Select, Tooltip, Typography } from 'antd';
|
||||
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
|
||||
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
|
||||
import map from 'lodash-es/map';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
@@ -27,6 +28,7 @@ interface VariableItemProps {
|
||||
existingVariables: Record<string, IDashboardVariable>;
|
||||
onValueUpdate: (
|
||||
name: string,
|
||||
id: string,
|
||||
arg1: IDashboardVariable['selectedValue'],
|
||||
allSelected: boolean,
|
||||
) => void;
|
||||
@@ -48,6 +50,7 @@ function VariableItem({
|
||||
onValueUpdate,
|
||||
lastUpdatedVar,
|
||||
}: VariableItemProps): JSX.Element {
|
||||
const { isDashboardLocked } = useDashboard();
|
||||
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
|
||||
[],
|
||||
);
|
||||
@@ -137,8 +140,9 @@ function VariableItem({
|
||||
} else {
|
||||
[value] = newOptionsData;
|
||||
}
|
||||
if (variableData.name) {
|
||||
onValueUpdate(variableData.name, value, allSelected);
|
||||
|
||||
if (variableData && variableData?.name && variableData?.id) {
|
||||
onValueUpdate(variableData.name, variableData.id, value, allSelected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,14 +153,13 @@ function VariableItem({
|
||||
console.error(e);
|
||||
}
|
||||
} else if (variableData.type === 'CUSTOM') {
|
||||
setOptionsData(
|
||||
sortValues(
|
||||
commaValuesParser(variableData.customValue || ''),
|
||||
variableData.sort,
|
||||
) as never,
|
||||
);
|
||||
const optionsData = sortValues(
|
||||
commaValuesParser(variableData.customValue || ''),
|
||||
variableData.sort,
|
||||
) as never;
|
||||
|
||||
setOptionsData(optionsData);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
};
|
||||
|
||||
const { isLoading } = useQuery(getQueryKey(variableData), {
|
||||
@@ -195,9 +198,9 @@ function VariableItem({
|
||||
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) ||
|
||||
(Array.isArray(value) && value.length === 0)
|
||||
) {
|
||||
onValueUpdate(variableData.name, optionsData, true);
|
||||
onValueUpdate(variableData.name, variableData.id, optionsData, true);
|
||||
} else {
|
||||
onValueUpdate(variableData.name, value, false);
|
||||
onValueUpdate(variableData.name, variableData.id, value, false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -230,72 +233,79 @@ function VariableItem({
|
||||
getOptions(null);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [variableData.type, variableData.customValue]);
|
||||
|
||||
return (
|
||||
<VariableContainer>
|
||||
<Typography.Text className="variable-name" ellipsis>
|
||||
${variableData.name}
|
||||
</Typography.Text>
|
||||
<VariableValue>
|
||||
{variableData.type === 'TEXTBOX' ? (
|
||||
<Input
|
||||
placeholder="Enter value"
|
||||
bordered={false}
|
||||
value={variableValue}
|
||||
onChange={(e): void => {
|
||||
setVaribleValue(e.target.value || '');
|
||||
}}
|
||||
style={{
|
||||
width:
|
||||
50 + ((variableData.selectedValue?.toString()?.length || 0) * 7 || 50),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
!errorMessage &&
|
||||
optionsData && (
|
||||
<Select
|
||||
value={selectValue}
|
||||
onChange={handleChange}
|
||||
<Tooltip
|
||||
placement="top"
|
||||
title={isDashboardLocked ? 'Dashboard is locked' : ''}
|
||||
>
|
||||
<VariableContainer>
|
||||
<Typography.Text className="variable-name" ellipsis>
|
||||
${variableData.name}
|
||||
</Typography.Text>
|
||||
<VariableValue>
|
||||
{variableData.type === 'TEXTBOX' ? (
|
||||
<Input
|
||||
placeholder="Enter value"
|
||||
disabled={isDashboardLocked}
|
||||
bordered={false}
|
||||
placeholder="Select value"
|
||||
mode={mode}
|
||||
dropdownMatchSelectWidth={false}
|
||||
style={SelectItemStyle}
|
||||
loading={isLoading}
|
||||
showArrow
|
||||
showSearch
|
||||
data-testid="variable-select"
|
||||
>
|
||||
{enableSelectAll && (
|
||||
<Select.Option data-testid="option-ALL" value={ALL_SELECT_VALUE}>
|
||||
ALL
|
||||
</Select.Option>
|
||||
)}
|
||||
{map(optionsData, (option) => (
|
||||
<Select.Option
|
||||
data-testid={`option-${option}`}
|
||||
key={option.toString()}
|
||||
value={option}
|
||||
>
|
||||
{option.toString()}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
)
|
||||
)}
|
||||
{errorMessage && (
|
||||
<span style={{ margin: '0 0.5rem' }}>
|
||||
<Popover
|
||||
placement="top"
|
||||
content={<Typography>{errorMessage}</Typography>}
|
||||
>
|
||||
<WarningOutlined style={{ color: orange[5] }} />
|
||||
</Popover>
|
||||
</span>
|
||||
)}
|
||||
</VariableValue>
|
||||
</VariableContainer>
|
||||
value={variableValue}
|
||||
onChange={(e): void => {
|
||||
setVaribleValue(e.target.value || '');
|
||||
}}
|
||||
style={{
|
||||
width:
|
||||
50 + ((variableData.selectedValue?.toString()?.length || 0) * 7 || 50),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
!errorMessage &&
|
||||
optionsData && (
|
||||
<Select
|
||||
value={selectValue}
|
||||
onChange={handleChange}
|
||||
bordered={false}
|
||||
placeholder="Select value"
|
||||
mode={mode}
|
||||
dropdownMatchSelectWidth={false}
|
||||
style={SelectItemStyle}
|
||||
loading={isLoading}
|
||||
showArrow
|
||||
showSearch
|
||||
data-testid="variable-select"
|
||||
disabled={isDashboardLocked}
|
||||
>
|
||||
{enableSelectAll && (
|
||||
<Select.Option data-testid="option-ALL" value={ALL_SELECT_VALUE}>
|
||||
ALL
|
||||
</Select.Option>
|
||||
)}
|
||||
{map(optionsData, (option) => (
|
||||
<Select.Option
|
||||
data-testid={`option-${option}`}
|
||||
key={option.toString()}
|
||||
value={option}
|
||||
>
|
||||
{option.toString()}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
)
|
||||
)}
|
||||
{variableData.type !== 'TEXTBOX' && errorMessage && (
|
||||
<span style={{ margin: '0 0.5rem' }}>
|
||||
<Popover
|
||||
placement="top"
|
||||
content={<Typography>{errorMessage}</Typography>}
|
||||
>
|
||||
<WarningOutlined style={{ color: orange[5] }} />
|
||||
</Popover>
|
||||
</span>
|
||||
)}
|
||||
</VariableValue>
|
||||
</VariableContainer>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
export function areArraysEqual(
|
||||
a: (string | number | boolean)[],
|
||||
b: (string | number | boolean)[],
|
||||
@@ -14,3 +16,16 @@ export function areArraysEqual(
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export const convertVariablesToDbFormat = (
|
||||
variblesArr: IDashboardVariable[],
|
||||
): Dashboard['data']['variables'] =>
|
||||
variblesArr.reduce((result, obj: IDashboardVariable) => {
|
||||
const { id } = obj;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
result[id] = obj;
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
@@ -6,8 +6,10 @@ export function variablePropsToPayloadVariables(
|
||||
): PayloadVariables {
|
||||
const payloadVariables: PayloadVariables = {};
|
||||
|
||||
Object.entries(variables).forEach(([key, value]) => {
|
||||
payloadVariables[key] = value?.selectedValue;
|
||||
Object.entries(variables).forEach(([, value]) => {
|
||||
if (value?.name) {
|
||||
payloadVariables[value.name] = value?.selectedValue;
|
||||
}
|
||||
});
|
||||
|
||||
return payloadVariables;
|
||||
|
||||
@@ -6,13 +6,16 @@ import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
function WidgetGraph({
|
||||
getWidgetQueryRange,
|
||||
@@ -23,6 +26,21 @@ function WidgetGraph({
|
||||
}: WidgetGraphProps): JSX.Element {
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
|
||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||
|
||||
useEffect((): void => {
|
||||
const { startTime, endTime } = getTimeRange(getWidgetQueryRange);
|
||||
|
||||
setMinTimeScale(startTime);
|
||||
setMaxTimeScale(endTime);
|
||||
}, [getWidgetQueryRange, maxTime, minTime, globalSelectedInterval]);
|
||||
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const containerDimensions = useResizeObserver(graphRef);
|
||||
@@ -63,6 +81,8 @@ function WidgetGraph({
|
||||
onDragSelect,
|
||||
thresholds,
|
||||
fillSpans,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
}),
|
||||
[
|
||||
widgetId,
|
||||
@@ -73,6 +93,8 @@ function WidgetGraph({
|
||||
onDragSelect,
|
||||
thresholds,
|
||||
fillSpans,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -12,8 +12,7 @@ export const Container = styled(Card)<Props>`
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
padding: ${({ $panelType }): string =>
|
||||
$panelType === PANEL_TYPES.TABLE ? '0 0' : '1.5rem 0'};
|
||||
padding: 8px;
|
||||
height: 57vh;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { LockFilled } from '@ant-design/icons';
|
||||
import { Button, Modal, Tooltip, Typography } from 'antd';
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { LockFilled, WarningOutlined } from '@ant-design/icons';
|
||||
import { Button, Modal, Space, Tooltip, Typography } from 'antd';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
getSelectedWidgetIndex,
|
||||
} from 'providers/Dashboard/util';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { generatePath, useLocation, useParams } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -39,6 +41,7 @@ import {
|
||||
RightContainerWrapper,
|
||||
} from './styles';
|
||||
import { NewWidgetProps } from './types';
|
||||
import { getIsQueryModified } from './utils';
|
||||
|
||||
function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
const {
|
||||
@@ -47,7 +50,14 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
setToScrollWidgetId,
|
||||
} = useDashboard();
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { t } = useTranslation(['dashboard']);
|
||||
|
||||
const { currentQuery, stagedQuery } = useQueryBuilder();
|
||||
|
||||
const isQueryModified = useMemo(
|
||||
() => getIsQueryModified(currentQuery, stagedQuery),
|
||||
[currentQuery, stagedQuery],
|
||||
);
|
||||
|
||||
const { featureResponse } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
@@ -92,6 +102,12 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
selectedWidget?.fillSpans || false,
|
||||
);
|
||||
const [saveModal, setSaveModal] = useState(false);
|
||||
const [discardModal, setDiscardModal] = useState(false);
|
||||
|
||||
const closeModal = (): void => {
|
||||
setSaveModal(false);
|
||||
setDiscardModal(false);
|
||||
};
|
||||
|
||||
const [graphType, setGraphType] = useState(selectedGraph);
|
||||
|
||||
@@ -206,6 +222,14 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
]);
|
||||
|
||||
const onClickDiscardHandler = useCallback(() => {
|
||||
if (isQueryModified) {
|
||||
setDiscardModal(true);
|
||||
return;
|
||||
}
|
||||
history.push(generatePath(ROUTES.DASHBOARD, { dashboardId }));
|
||||
}, [dashboardId, isQueryModified]);
|
||||
|
||||
const discardChanges = useCallback(() => {
|
||||
history.push(generatePath(ROUTES.DASHBOARD, { dashboardId }));
|
||||
}, [dashboardId]);
|
||||
|
||||
@@ -321,21 +345,54 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
</RightContainerWrapper>
|
||||
</PanelContainer>
|
||||
<Modal
|
||||
title="Save Changes"
|
||||
title={
|
||||
isQueryModified ? (
|
||||
<Space>
|
||||
<WarningOutlined style={{ fontSize: '16px', color: '#fdd600' }} />
|
||||
Unsaved Changes
|
||||
</Space>
|
||||
) : (
|
||||
'Save Widget'
|
||||
)
|
||||
}
|
||||
focusTriggerAfterClose
|
||||
forceRender
|
||||
destroyOnClose
|
||||
closable
|
||||
onCancel={(): void => setSaveModal(false)}
|
||||
onCancel={closeModal}
|
||||
onOk={onClickSaveHandler}
|
||||
centered
|
||||
open={saveModal}
|
||||
width={600}
|
||||
>
|
||||
<Typography>
|
||||
Your graph built with <QueryTypeTag queryType={currentQuery.queryType} />{' '}
|
||||
query will be saved. Press OK to confirm.
|
||||
</Typography>
|
||||
{!isQueryModified ? (
|
||||
<Typography>
|
||||
{t('your_graph_build_with')}{' '}
|
||||
<QueryTypeTag queryType={currentQuery.queryType} />
|
||||
{t('dashboar_ok_confirm')}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography>{t('dashboard_unsave_changes')} </Typography>
|
||||
)}
|
||||
</Modal>
|
||||
<Modal
|
||||
title={
|
||||
<Space>
|
||||
<WarningOutlined style={{ fontSize: '16px', color: '#fdd600' }} />
|
||||
Unsaved Changes
|
||||
</Space>
|
||||
}
|
||||
focusTriggerAfterClose
|
||||
forceRender
|
||||
destroyOnClose
|
||||
closable
|
||||
onCancel={closeModal}
|
||||
onOk={discardChanges}
|
||||
centered
|
||||
open={discardModal}
|
||||
width={600}
|
||||
>
|
||||
<Typography>{t('dashboard_unsave_changes')}</Typography>
|
||||
</Modal>
|
||||
</Container>
|
||||
);
|
||||
|
||||
15
frontend/src/container/NewWidget/utils.ts
Normal file
15
frontend/src/container/NewWidget/utils.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { omitIdFromQuery } from 'components/ExplorerCard/utils';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export const getIsQueryModified = (
|
||||
currentQuery: Query,
|
||||
stagedQuery: Query | null,
|
||||
): boolean => {
|
||||
if (!stagedQuery) {
|
||||
return false;
|
||||
}
|
||||
const omitIdFromStageQuery = omitIdFromQuery(stagedQuery);
|
||||
const omitIdFromCurrentQuery = omitIdFromQuery(currentQuery);
|
||||
return !isEqual(omitIdFromStageQuery, omitIdFromCurrentQuery);
|
||||
};
|
||||
@@ -71,8 +71,8 @@ export default function ModuleStepsContainer({
|
||||
} = useOnboardingContext();
|
||||
|
||||
const [current, setCurrent] = useState(0);
|
||||
const [metaData, setMetaData] = useState<MetaDataProps[]>(defaultMetaData);
|
||||
const { trackEvent } = useAnalytics();
|
||||
const [metaData, setMetaData] = useState<MetaDataProps[]>(defaultMetaData);
|
||||
const lastStepIndex = selectedModuleSteps.length - 1;
|
||||
|
||||
const isValidForm = (): boolean => {
|
||||
|
||||
@@ -3,9 +3,13 @@ import Uplot from 'components/Uplot';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
import { Container, ErrorText } from './styles';
|
||||
|
||||
@@ -31,6 +35,21 @@ function TimeSeriesView({
|
||||
? graphRef.current.clientHeight
|
||||
: 300;
|
||||
|
||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||
|
||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
useEffect((): void => {
|
||||
const { startTime, endTime } = getTimeRange();
|
||||
|
||||
setMinTimeScale(startTime);
|
||||
setMaxTimeScale(endTime);
|
||||
}, [maxTime, minTime, globalSelectedInterval, data]);
|
||||
|
||||
const chartOptions = getUPlotChartOptions({
|
||||
yAxisUnit: yAxisUnit || '',
|
||||
apiResponse: data?.payload,
|
||||
@@ -39,6 +58,8 @@ function TimeSeriesView({
|
||||
height,
|
||||
},
|
||||
isDarkMode,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,12 +3,16 @@ import { Button, Select as DefaultSelect } from 'antd';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import getTimeString from 'lib/getTimeString';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import history from 'lib/history';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
@@ -34,9 +38,9 @@ function DateTimeSelection({
|
||||
}: Props): JSX.Element {
|
||||
const [formSelector] = Form.useForm();
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const searchStartTime = params.get('startTime');
|
||||
const searchEndTime = params.get('endTime');
|
||||
const urlQuery = useUrlQuery();
|
||||
const searchStartTime = urlQuery.get('startTime');
|
||||
const searchEndTime = urlQuery.get('endTime');
|
||||
|
||||
const localstorageStartTime = getLocalStorageKey('startTime');
|
||||
const localstorageEndTime = getLocalStorageKey('endTime');
|
||||
@@ -169,6 +173,11 @@ function DateTimeSelection({
|
||||
return `Last refresh - ${secondsDiff} sec ago`;
|
||||
}, [maxTime, minTime, selectedTime]);
|
||||
|
||||
const isLogsExplorerPage = useMemo(
|
||||
() => location.pathname === ROUTES.LOGS_EXPLORER,
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
const onSelectHandler = (value: Time): void => {
|
||||
if (value !== 'custom') {
|
||||
updateTimeInterval(value);
|
||||
@@ -181,12 +190,18 @@ function DateTimeSelection({
|
||||
setCustomDTPickerVisible(true);
|
||||
}
|
||||
|
||||
const { maxTime, minTime } = GetMinMax(value, getTime());
|
||||
|
||||
if (!isLogsExplorerPage) {
|
||||
urlQuery.set(QueryParams.startTime, minTime.toString());
|
||||
urlQuery.set(QueryParams.endTime, maxTime.toString());
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
history.replace(generatedUrl);
|
||||
}
|
||||
|
||||
if (!stagedQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { maxTime, minTime } = GetMinMax(value, getTime());
|
||||
|
||||
initQueryBuilderData(updateStepInterval(stagedQuery, maxTime, minTime));
|
||||
};
|
||||
|
||||
@@ -207,6 +222,12 @@ function DateTimeSelection({
|
||||
setLocalStorageKey('startTime', startTimeMoment.toString());
|
||||
setLocalStorageKey('endTime', endTimeMoment.toString());
|
||||
updateLocalStorageForRoutes('custom');
|
||||
if (!isLogsExplorerPage) {
|
||||
urlQuery.set(QueryParams.startTime, startTimeMoment.toString());
|
||||
urlQuery.set(QueryParams.endTime, endTimeMoment.toString());
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
history.replace(generatedUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -234,7 +255,6 @@ function DateTimeSelection({
|
||||
if (searchEndTime !== null && searchStartTime !== null) {
|
||||
return 'custom';
|
||||
}
|
||||
|
||||
if (
|
||||
(localstorageEndTime === null || localstorageStartTime === null) &&
|
||||
time === 'custom'
|
||||
@@ -252,16 +272,8 @@ function DateTimeSelection({
|
||||
setRefreshButtonHidden(updatedTime === 'custom');
|
||||
|
||||
updateTimeInterval(updatedTime, [preStartTime, preEndTime]);
|
||||
}, [
|
||||
location.pathname,
|
||||
getTime,
|
||||
localstorageEndTime,
|
||||
localstorageStartTime,
|
||||
searchEndTime,
|
||||
searchStartTime,
|
||||
updateTimeInterval,
|
||||
globalTimeLoading,
|
||||
]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [location.pathname, updateTimeInterval, globalTimeLoading]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -3,6 +3,7 @@ import ROUTES from 'constants/routes';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import history from 'lib/history';
|
||||
import {
|
||||
MouseEventHandler,
|
||||
useCallback,
|
||||
@@ -28,16 +29,27 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const {
|
||||
queryData: timeRange,
|
||||
redirectWithQuery: onTimeRangeChange,
|
||||
} = useUrlQueryData<LogTimeRange | null>(QueryParams.timeRange, null);
|
||||
const { queryData: timeRange } = useUrlQueryData<LogTimeRange | null>(
|
||||
QueryParams.timeRange,
|
||||
null,
|
||||
);
|
||||
|
||||
const { queryData: activeLogId } = useUrlQueryData<string | null>(
|
||||
QueryParams.activeLogId,
|
||||
null,
|
||||
);
|
||||
|
||||
const onTimeRangeChange = useCallback(
|
||||
(newTimeRange: LogTimeRange | null): void => {
|
||||
urlQuery.set(QueryParams.timeRange, JSON.stringify(newTimeRange));
|
||||
urlQuery.set(QueryParams.startTime, newTimeRange?.start.toString() || '');
|
||||
urlQuery.set(QueryParams.endTime, newTimeRange?.end.toString() || '');
|
||||
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
|
||||
history.replace(generatedUrl);
|
||||
},
|
||||
[pathname, urlQuery],
|
||||
);
|
||||
|
||||
const isActiveLog = useMemo(() => activeLogId === logId, [activeLogId, logId]);
|
||||
const [isHighlighted, setIsHighlighted] = useState<boolean>(isActiveLog);
|
||||
|
||||
|
||||
1
frontend/src/i18n-translations-hash.json
Normal file
1
frontend/src/i18n-translations-hash.json
Normal file
@@ -0,0 +1 @@
|
||||
{"/en-GB/alerts":"37ea40b758e14f100b970178809147d7","/en-GB/channels":"b855a58fce92ff62a0ce50cc40d8da0b","/en-GB/common":"d918932fcd1d34b2d84cb463812bd157","/en-GB/dashboard":"9ec66badfc02995263cf108615f6380c","/en-GB/errorDetails":"a1a1ea54ed8adc720e7942c42ce4be0f","/en-GB/explorer":"98106bbc79e701d81f5731dd53a158f0","/en-GB/generalSettings":"65fca62d2f109d73fa4bdc447c353857","/en-GB/licenses":"dc2fea934c67b5b3bf8c940019d820cd","/en-GB/login":"c9d63ef04a9af5ae6aed12b4b725add5","/en-GB/logs":"de363f7feee26d9fc72eccdf69988f09","/en-GB/organizationsettings":"e24624bba7bdd7bf071873940742b1a8","/en-GB/routes":"08585a25257ed898131ba43e4c927d7e","/en-GB/rules":"f134663a0943cdb8cd2a2c169f27ba90","/en-GB/settings":"e2c4003664cc9ba476b658f1e6304fe5","/en-GB/signup":"59c64809b8b4c7b1b8902da5aa2315f0","/en-GB/titles":"9e0515203efab287fdd50afb96bde8c8","/en-GB/trace":"fcf3fda7bee8b609b5a3ab0f749f2594","/en-GB/traceDetails":"f91795f15c286f15bf630a454febb015","/en-GB/translation":"532cce878c691d9ff3c689a73279fd2e","/en/alerts":"37ea40b758e14f100b970178809147d7","/en/channels":"c9bfbd14bb4d3f38e2a669f5fbfadc17","/en/common":"d918932fcd1d34b2d84cb463812bd157","/en/dashboard":"6de8356a6ed53c109746c0f7ef37ffcf","/en/errorDetails":"eb83b35f49830420547f30c08bd88c4e","/en/explorer":"98106bbc79e701d81f5731dd53a158f0","/en/generalSettings":"65fca62d2f109d73fa4bdc447c353857","/en/licenses":"dc2fea934c67b5b3bf8c940019d820cd","/en/login":"c9d63ef04a9af5ae6aed12b4b725add5","/en/logs":"de363f7feee26d9fc72eccdf69988f09","/en/organizationsettings":"e24624bba7bdd7bf071873940742b1a8","/en/pipeline":"9f75c31214b2ae9d362bb6e5985c2e1f","/en/routes":"08585a25257ed898131ba43e4c927d7e","/en/rules":"f134663a0943cdb8cd2a2c169f27ba90","/en/settings":"ffabe7ca89d7992d9639695b4df4d6e9","/en/signup":"59c64809b8b4c7b1b8902da5aa2315f0","/en/titles":"49d542f8f3ca9291777b9042ed8faf1a","/en/trace":"fcf3fda7bee8b609b5a3ab0f749f2594","/en/traceDetails":"f91795f15c286f15bf630a454febb015","/en/translation":"921a0256c8d4d3522754557b41e24362","/en/valueGraph":"cc57d9b83919574016dab2fc9e5adedf"}
|
||||
@@ -20,9 +20,13 @@ export const getDashboardVariables = (
|
||||
SIGNOZ_START_TIME: parseInt(start, 10) * 1e3,
|
||||
SIGNOZ_END_TIME: parseInt(end, 10) * 1e3,
|
||||
};
|
||||
Object.keys(variables).forEach((key) => {
|
||||
variablesTuple[key] = variables[key].selectedValue;
|
||||
|
||||
Object.entries(variables).forEach(([, value]) => {
|
||||
if (value?.name) {
|
||||
variablesTuple[value.name] = value?.selectedValue;
|
||||
}
|
||||
});
|
||||
|
||||
return variablesTuple;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
@@ -15,6 +16,7 @@ import onClickPlugin, { OnClickPluginOpts } from './plugins/onClickPlugin';
|
||||
import tooltipPlugin from './plugins/tooltipPlugin';
|
||||
import getAxes from './utils/getAxes';
|
||||
import getSeries from './utils/getSeriesData';
|
||||
import { getXAxisScale } from './utils/getXAxisScale';
|
||||
import { getYAxisScale } from './utils/getYAxisScale';
|
||||
|
||||
interface GetUPlotChartOptions {
|
||||
@@ -31,6 +33,8 @@ interface GetUPlotChartOptions {
|
||||
thresholdValue?: number;
|
||||
thresholdText?: string;
|
||||
fillSpans?: boolean;
|
||||
minTimeScale?: number;
|
||||
maxTimeScale?: number;
|
||||
}
|
||||
|
||||
export const getUPlotChartOptions = ({
|
||||
@@ -40,18 +44,20 @@ export const getUPlotChartOptions = ({
|
||||
apiResponse,
|
||||
onDragSelect,
|
||||
yAxisUnit,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
onClickHandler = _noop,
|
||||
graphsVisibilityStates,
|
||||
setGraphsVisibilityStates,
|
||||
thresholds,
|
||||
fillSpans,
|
||||
}: GetUPlotChartOptions): uPlot.Options => {
|
||||
// eslint-disable-next-line sonarjs/prefer-immediate-return
|
||||
const chartOptions = {
|
||||
const timeScaleProps = getXAxisScale(minTimeScale, maxTimeScale);
|
||||
|
||||
return {
|
||||
id,
|
||||
width: dimensions.width,
|
||||
height: dimensions.height - 45,
|
||||
// tzDate: (ts) => uPlot.tzDate(new Date(ts * 1e3), ''), // Pass timezone for 2nd param
|
||||
height: dimensions.height - 30,
|
||||
legend: {
|
||||
show: true,
|
||||
live: false,
|
||||
@@ -67,18 +73,18 @@ export const getUPlotChartOptions = ({
|
||||
bias: 1,
|
||||
},
|
||||
points: {
|
||||
size: (u, seriesIdx): number => u.series[seriesIdx].points.size * 2.5,
|
||||
size: (u, seriesIdx): number => u.series[seriesIdx].points.size * 3,
|
||||
width: (u, seriesIdx, size): number => size / 4,
|
||||
stroke: (u, seriesIdx): string =>
|
||||
`${u.series[seriesIdx].points.stroke(u, seriesIdx)}90`,
|
||||
fill: (): string => '#fff',
|
||||
},
|
||||
},
|
||||
padding: [16, 16, 16, 16],
|
||||
padding: [16, 16, 8, 8],
|
||||
scales: {
|
||||
x: {
|
||||
time: true,
|
||||
auto: true, // Automatically adjust scale range
|
||||
spanGaps: true,
|
||||
...timeScaleProps,
|
||||
},
|
||||
y: {
|
||||
...getYAxisScale(
|
||||
@@ -194,6 +200,4 @@ export const getUPlotChartOptions = ({
|
||||
),
|
||||
axes: getAxes(isDarkMode, yAxisUnit),
|
||||
};
|
||||
|
||||
return chartOptions;
|
||||
};
|
||||
|
||||
@@ -54,7 +54,7 @@ const generateTooltipContent = (
|
||||
const value = data[index][idx];
|
||||
const label = getLabelName(metric, queryName || '', legend || '');
|
||||
|
||||
if (value) {
|
||||
if (Number.isFinite(value)) {
|
||||
const tooltipValue = getToolTipValue(value, yAxisUnit);
|
||||
|
||||
const dataObj = {
|
||||
@@ -191,7 +191,8 @@ const tooltipPlugin = (
|
||||
if (overlay) {
|
||||
overlay.textContent = '';
|
||||
const { left, top, idx } = u.cursor;
|
||||
if (idx) {
|
||||
|
||||
if (Number.isInteger(idx)) {
|
||||
const anchor = { left: left + bLeft, top: top + bTop };
|
||||
const content = generateTooltipContent(
|
||||
apiResult,
|
||||
|
||||
@@ -9,8 +9,7 @@ const getAxes = (isDarkMode: boolean, yAxisUnit?: string): any => [
|
||||
stroke: isDarkMode ? 'white' : 'black', // Color of the axis line
|
||||
grid: {
|
||||
stroke: getGridColor(isDarkMode), // Color of the grid lines
|
||||
dash: [10, 10], // Dash pattern for grid lines,
|
||||
width: 0.5, // Width of the grid lines,
|
||||
width: 0.2, // Width of the grid lines,
|
||||
show: true,
|
||||
},
|
||||
ticks: {
|
||||
@@ -24,8 +23,7 @@ const getAxes = (isDarkMode: boolean, yAxisUnit?: string): any => [
|
||||
stroke: isDarkMode ? 'white' : 'black', // Color of the axis line
|
||||
grid: {
|
||||
stroke: getGridColor(isDarkMode), // Color of the grid lines
|
||||
dash: [10, 10], // Dash pattern for grid lines,
|
||||
width: 0.3, // Width of the grid lines
|
||||
width: 0.2, // Width of the grid lines
|
||||
},
|
||||
ticks: {
|
||||
// stroke: isDarkMode ? 'white' : 'black', // Color of the tick lines
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const getGridColor = (isDarkMode: boolean): string => {
|
||||
if (isDarkMode) {
|
||||
return 'rgba(231,233,237,0.2)';
|
||||
return 'rgba(231,233,237,0.3)';
|
||||
}
|
||||
return 'rgba(231,233,237,0.8)';
|
||||
return 'rgba(0,0,0,0.5)';
|
||||
};
|
||||
|
||||
export default getGridColor;
|
||||
|
||||
@@ -46,17 +46,22 @@ const getSeries = (
|
||||
legend || '',
|
||||
);
|
||||
|
||||
const pointSize = seriesList[i].values.length > 1 ? 5 : 10;
|
||||
const showPoints = !(seriesList[i].values.length > 1);
|
||||
|
||||
const seriesObj: any = {
|
||||
width: 1.4,
|
||||
paths,
|
||||
drawStyle: drawStyles.line,
|
||||
lineInterpolation: lineInterpolations.spline,
|
||||
show: newGraphVisibilityStates ? newGraphVisibilityStates[i] : true,
|
||||
label,
|
||||
stroke: color,
|
||||
width: 2,
|
||||
spanGaps: true,
|
||||
points: {
|
||||
show: false,
|
||||
size: pointSize,
|
||||
show: showPoints,
|
||||
stroke: color,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
40
frontend/src/lib/uPlotLib/utils/getXAxisScale.ts
Normal file
40
frontend/src/lib/uPlotLib/utils/getXAxisScale.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
function getFallbackMinMaxTimeStamp(): {
|
||||
fallbackMin: number;
|
||||
fallbackMax: number;
|
||||
} {
|
||||
const currentDate = new Date();
|
||||
// Get the Unix timestamp (milliseconds since January 1, 1970)
|
||||
const currentTime = currentDate.getTime();
|
||||
const currentUnixTimestamp = Math.floor(currentTime / 1000);
|
||||
|
||||
// Calculate the date and time one day ago
|
||||
const oneDayAgoUnixTimestamp = Math.floor(
|
||||
(currentDate.getTime() - 86400000) / 1000,
|
||||
); // 86400000 milliseconds in a day
|
||||
|
||||
return {
|
||||
fallbackMin: oneDayAgoUnixTimestamp,
|
||||
fallbackMax: currentUnixTimestamp,
|
||||
};
|
||||
}
|
||||
|
||||
export const getXAxisScale = (
|
||||
minTimeScale: number,
|
||||
maxTimeScale: number,
|
||||
): {
|
||||
time: boolean;
|
||||
auto: boolean;
|
||||
range?: [number, number];
|
||||
} => {
|
||||
let minTime = minTimeScale;
|
||||
let maxTime = maxTimeScale;
|
||||
|
||||
if (!minTimeScale || !maxTimeScale) {
|
||||
const { fallbackMin, fallbackMax } = getFallbackMinMaxTimeStamp();
|
||||
|
||||
minTime = fallbackMin;
|
||||
maxTime = fallbackMax;
|
||||
}
|
||||
|
||||
return { time: true, auto: false, range: [minTime, maxTime] };
|
||||
};
|
||||
@@ -54,7 +54,12 @@ function getRange(
|
||||
const [minSeriesValue, maxSeriesValue] = findMinMaxValues(series);
|
||||
|
||||
const min = Math.min(minThresholdValue, minSeriesValue);
|
||||
const max = Math.max(maxThresholdValue, maxSeriesValue);
|
||||
let max = Math.max(maxThresholdValue, maxSeriesValue);
|
||||
|
||||
// this is a temp change, we need to figure out a generic way to better handle ranges based on yAxisUnit
|
||||
if (yAxisUnit === 'percentunit' && max < 1) {
|
||||
max = 1;
|
||||
}
|
||||
|
||||
return [min, max];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Modal from 'antd/es/modal';
|
||||
import { Modal } from 'antd';
|
||||
import getDashboard from 'api/dashboard/get';
|
||||
import lockDashboardApi from 'api/dashboard/lockDashboard';
|
||||
import unlockDashboardApi from 'api/dashboard/unlockDashboard';
|
||||
@@ -30,9 +30,10 @@ import { Dispatch } from 'redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
import { IDashboardContext } from './types';
|
||||
|
||||
@@ -102,6 +103,44 @@ export function DashboardProvider({
|
||||
const { t } = useTranslation(['dashboard']);
|
||||
const dashboardRef = useRef<Dashboard>();
|
||||
|
||||
// As we do not have order and ID's in the variables object, we have to process variables to add order and ID if they do not exist in the variables object
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const transformDashboardVariables = (data: Dashboard): Dashboard => {
|
||||
if (data && data.data && data.data.variables) {
|
||||
const clonedDashboardData = JSON.parse(JSON.stringify(data));
|
||||
const { variables } = clonedDashboardData.data;
|
||||
const existingOrders: Set<number> = new Set();
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const key in variables) {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (variables.hasOwnProperty(key)) {
|
||||
const variable: IDashboardVariable = variables[key];
|
||||
|
||||
// Check if 'order' property doesn't exist or is undefined
|
||||
if (variable.order === undefined) {
|
||||
// Find a unique order starting from 0
|
||||
let order = 0;
|
||||
while (existingOrders.has(order)) {
|
||||
order += 1;
|
||||
}
|
||||
|
||||
variable.order = order;
|
||||
existingOrders.add(order);
|
||||
}
|
||||
|
||||
if (variable.id === undefined) {
|
||||
variable.id = generateUUID();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return clonedDashboardData;
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const dashboardResponse = useQuery(
|
||||
[REACT_QUERY_KEY.DASHBOARD_BY_ID, isDashboardPage?.params],
|
||||
{
|
||||
@@ -112,26 +151,27 @@ export function DashboardProvider({
|
||||
}),
|
||||
refetchOnWindowFocus: false,
|
||||
onSuccess: (data) => {
|
||||
const updatedDate = dayjs(data.updated_at);
|
||||
const updatedDashboardData = transformDashboardVariables(data);
|
||||
const updatedDate = dayjs(updatedDashboardData.updated_at);
|
||||
|
||||
setIsDashboardLocked(data?.isLocked || false);
|
||||
setIsDashboardLocked(updatedDashboardData?.isLocked || false);
|
||||
|
||||
// on first render
|
||||
if (updatedTimeRef.current === null) {
|
||||
setSelectedDashboard(data);
|
||||
setSelectedDashboard(updatedDashboardData);
|
||||
|
||||
updatedTimeRef.current = updatedDate;
|
||||
|
||||
dashboardRef.current = data;
|
||||
dashboardRef.current = updatedDashboardData;
|
||||
|
||||
setLayouts(getUpdatedLayout(data.data.layout));
|
||||
setLayouts(getUpdatedLayout(updatedDashboardData.data.layout));
|
||||
}
|
||||
|
||||
if (
|
||||
updatedTimeRef.current !== null &&
|
||||
updatedDate.isAfter(updatedTimeRef.current) &&
|
||||
isVisible &&
|
||||
dashboardRef.current?.id === data.id
|
||||
dashboardRef.current?.id === updatedDashboardData.id
|
||||
) {
|
||||
// show modal when state is out of sync
|
||||
const modal = onModal.confirm({
|
||||
@@ -139,7 +179,7 @@ export function DashboardProvider({
|
||||
title: t('dashboard_has_been_updated'),
|
||||
content: t('do_you_want_to_refresh_the_dashboard'),
|
||||
onOk() {
|
||||
setSelectedDashboard(data);
|
||||
setSelectedDashboard(updatedDashboardData);
|
||||
|
||||
const { maxTime, minTime } = getMinMax(
|
||||
globalTime.selectedTime,
|
||||
@@ -156,32 +196,32 @@ export function DashboardProvider({
|
||||
},
|
||||
});
|
||||
|
||||
dashboardRef.current = data;
|
||||
dashboardRef.current = updatedDashboardData;
|
||||
|
||||
updatedTimeRef.current = dayjs(data.updated_at);
|
||||
updatedTimeRef.current = dayjs(updatedDashboardData.updated_at);
|
||||
|
||||
setLayouts(getUpdatedLayout(data.data.layout));
|
||||
setLayouts(getUpdatedLayout(updatedDashboardData.data.layout));
|
||||
},
|
||||
});
|
||||
|
||||
modalRef.current = modal;
|
||||
} else {
|
||||
// normal flow
|
||||
updatedTimeRef.current = dayjs(data.updated_at);
|
||||
updatedTimeRef.current = dayjs(updatedDashboardData.updated_at);
|
||||
|
||||
dashboardRef.current = data;
|
||||
dashboardRef.current = updatedDashboardData;
|
||||
|
||||
if (!isEqual(selectedDashboard, data)) {
|
||||
setSelectedDashboard(data);
|
||||
if (!isEqual(selectedDashboard, updatedDashboardData)) {
|
||||
setSelectedDashboard(updatedDashboardData);
|
||||
}
|
||||
|
||||
if (
|
||||
!isEqual(
|
||||
[omitBy(layouts, (value): boolean => isUndefined(value))[0]],
|
||||
data.data.layout,
|
||||
updatedDashboardData.data.layout,
|
||||
)
|
||||
) {
|
||||
setLayouts(getUpdatedLayout(data.data.layout));
|
||||
setLayouts(getUpdatedLayout(updatedDashboardData.data.layout));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -14,6 +14,8 @@ export const VariableSortTypeArr = ['DISABLED', 'ASC', 'DESC'] as const;
|
||||
export type TSortVariableValuesType = typeof VariableSortTypeArr[number];
|
||||
|
||||
export interface IDashboardVariable {
|
||||
id: string;
|
||||
order?: any;
|
||||
name?: string; // key will be the source of truth
|
||||
description: string;
|
||||
type: TVariableQueryType;
|
||||
|
||||
36
frontend/src/utils/getTimeRange.ts
Normal file
36
frontend/src/utils/getTimeRange.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import store from 'store';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import {
|
||||
MetricRangePayloadProps,
|
||||
QueryRangePayload,
|
||||
} from 'types/api/metrics/getQueryRange';
|
||||
|
||||
export const getTimeRange = (
|
||||
widgetQueryRange?: UseQueryResult<
|
||||
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||
Error
|
||||
>,
|
||||
): Record<string, number> => {
|
||||
const widgetParams =
|
||||
(widgetQueryRange?.data?.params as QueryRangePayload) || null;
|
||||
|
||||
if (widgetParams && widgetParams?.start && widgetParams?.end) {
|
||||
return {
|
||||
startTime: widgetParams.start / 1000,
|
||||
endTime: widgetParams.end / 1000,
|
||||
};
|
||||
}
|
||||
const { globalTime } = store.getState();
|
||||
|
||||
const { start: globalStartTime, end: globalEndTime } = getStartEndRangeTime({
|
||||
type: 'GLOBAL_TIME',
|
||||
interval: globalTime.selectedTime,
|
||||
});
|
||||
|
||||
return {
|
||||
startTime: (parseInt(globalStartTime, 10) * 1e3) / 1000,
|
||||
endTime: (parseInt(globalEndTime, 10) * 1e3) / 1000,
|
||||
};
|
||||
};
|
||||
@@ -2346,6 +2346,45 @@
|
||||
resolved "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz"
|
||||
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
|
||||
|
||||
"@dnd-kit/accessibility@^3.1.0":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz#1054e19be276b5f1154ced7947fc0cb5d99192e0"
|
||||
integrity sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/core@6.1.0":
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.1.0.tgz#e81a3d10d9eca5d3b01cbf054171273a3fe01def"
|
||||
integrity sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==
|
||||
dependencies:
|
||||
"@dnd-kit/accessibility" "^3.1.0"
|
||||
"@dnd-kit/utilities" "^3.2.2"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/modifiers@7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/modifiers/-/modifiers-7.0.0.tgz#229666dd4e8b9487f348035117f993af755b3db9"
|
||||
integrity sha512-BG/ETy3eBjFap7+zIti53f0PCLGDzNXyTmn6fSdrudORf+OH04MxrW4p5+mPu4mgMk9kM41iYONjc3DOUWTcfg==
|
||||
dependencies:
|
||||
"@dnd-kit/utilities" "^3.2.2"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/sortable@8.0.0":
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/sortable/-/sortable-8.0.0.tgz#086b7ac6723d4618a4ccb6f0227406d8a8862a96"
|
||||
integrity sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==
|
||||
dependencies:
|
||||
"@dnd-kit/utilities" "^3.2.2"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/utilities@^3.2.2":
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.2.2.tgz#5a32b6af356dc5f74d61b37d6f7129a4040ced7b"
|
||||
integrity sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@emotion/hash@^0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz"
|
||||
@@ -14821,6 +14860,11 @@ tslib@^1.8.1, tslib@^1.9.0:
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.0.0:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
||||
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||
|
||||
tsutils@^3.21.0:
|
||||
version "3.21.0"
|
||||
resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz"
|
||||
|
||||
@@ -483,8 +483,10 @@ func isOrderByTs(orderBy []v3.OrderBy) bool {
|
||||
func PrepareLogsQuery(start, end int64, queryType v3.QueryType, panelType v3.PanelType, mq *v3.BuilderQuery, options Options) (string, error) {
|
||||
|
||||
// adjust the start and end time to the step interval
|
||||
start = start - (start % (mq.StepInterval * 1000))
|
||||
end = end - (end % (mq.StepInterval * 1000))
|
||||
if panelType != v3.PanelTypeList {
|
||||
start = start - (start % (mq.StepInterval * 1000))
|
||||
end = end - (end % (mq.StepInterval * 1000))
|
||||
}
|
||||
|
||||
if options.IsLivetailQuery {
|
||||
query, err := buildLogsLiveTailQuery(mq)
|
||||
|
||||
@@ -1353,7 +1353,7 @@ var testPrepLogsQueryLimitOffsetData = []struct {
|
||||
PageSize: 5,
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where (timestamp >= 1680066360000000000 AND timestamp <= 1680066420000000000) order by timestamp desc LIMIT 1",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) order by timestamp desc LIMIT 1",
|
||||
},
|
||||
{
|
||||
Name: "Test limit greater than pageSize - order by ts",
|
||||
@@ -1374,7 +1374,7 @@ var testPrepLogsQueryLimitOffsetData = []struct {
|
||||
PageSize: 10,
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where (timestamp >= 1680066360000000000 AND timestamp <= 1680066420000000000) AND id < '2TNh4vp2TpiWyLt3SzuadLJF2s4' order by timestamp desc LIMIT 10",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND id < '2TNh4vp2TpiWyLt3SzuadLJF2s4' order by timestamp desc LIMIT 10",
|
||||
},
|
||||
{
|
||||
Name: "Test limit less than pageSize - order by custom",
|
||||
@@ -1393,7 +1393,7 @@ var testPrepLogsQueryLimitOffsetData = []struct {
|
||||
PageSize: 5,
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where (timestamp >= 1680066360000000000 AND timestamp <= 1680066420000000000) order by attributes_string_value[indexOf(attributes_string_key, 'method')] desc LIMIT 1 OFFSET 0",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) order by attributes_string_value[indexOf(attributes_string_key, 'method')] desc LIMIT 1 OFFSET 0",
|
||||
},
|
||||
{
|
||||
Name: "Test limit greater than pageSize - order by custom",
|
||||
@@ -1414,7 +1414,7 @@ var testPrepLogsQueryLimitOffsetData = []struct {
|
||||
PageSize: 50,
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where (timestamp >= 1680066360000000000 AND timestamp <= 1680066420000000000) AND id < '2TNh4vp2TpiWyLt3SzuadLJF2s4' order by attributes_string_value[indexOf(attributes_string_key, 'method')] desc LIMIT 50 OFFSET 50",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND id < '2TNh4vp2TpiWyLt3SzuadLJF2s4' order by attributes_string_value[indexOf(attributes_string_key, 'method')] desc LIMIT 50 OFFSET 50",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1021,13 +1021,6 @@ func ParseQueryRangeParams(r *http.Request) (*v3.QueryRangeParamsV3, *model.ApiE
|
||||
queryRangeParams.Start = queryRangeParams.End
|
||||
}
|
||||
|
||||
// round up the end to neaerest multiple
|
||||
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder {
|
||||
end := (queryRangeParams.End) / 1000
|
||||
step := queryRangeParams.Step
|
||||
queryRangeParams.End = (end / step * step) * 1000
|
||||
}
|
||||
|
||||
// replace go template variables in clickhouse query
|
||||
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeClickHouseSQL {
|
||||
for _, chQuery := range queryRangeParams.CompositeQuery.ClickHouseQueries {
|
||||
|
||||
@@ -2,7 +2,7 @@ version: "2.4"
|
||||
|
||||
x-clickhouse-defaults: &clickhouse-defaults
|
||||
restart: on-failure
|
||||
image: clickhouse/clickhouse-server:23.7.3-alpine
|
||||
image: clickhouse/clickhouse-server:23.11.1-alpine
|
||||
tty: true
|
||||
depends_on:
|
||||
- zookeeper-1
|
||||
|
||||
Reference in New Issue
Block a user