mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-07-31 19:41:58 +08:00
Merge branch 'develop' into 412-trace-detail
This commit is contained in:
commit
b3dfd567e0
@ -28,7 +28,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./data/alertmanager:/data
|
- ./data/alertmanager:/data
|
||||||
command:
|
command:
|
||||||
- --queryService.url=http://query-service:8080
|
- --queryService.url=http://query-service:8085
|
||||||
- --storage.path=/data
|
- --storage.path=/data
|
||||||
depends_on:
|
depends_on:
|
||||||
- query-service
|
- query-service
|
||||||
|
@ -30,7 +30,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
command:
|
command:
|
||||||
- --queryService.url=http://query-service:8080
|
- --queryService.url=http://query-service:8085
|
||||||
- --storage.path=/data
|
- --storage.path=/data
|
||||||
|
|
||||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||||
@ -53,7 +53,6 @@ services:
|
|||||||
- GODEBUG=netdns=go
|
- GODEBUG=netdns=go
|
||||||
- TELEMETRY_ENABLED=true
|
- TELEMETRY_ENABLED=true
|
||||||
- DEPLOYMENT_TYPE=docker-standalone-arm
|
- DEPLOYMENT_TYPE=docker-standalone-arm
|
||||||
|
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
|
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
|
||||||
|
@ -30,7 +30,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
command:
|
command:
|
||||||
- --queryService.url=http://query-service:8080
|
- --queryService.url=http://query-service:8085
|
||||||
- --storage.path=/data
|
- --storage.path=/data
|
||||||
|
|
||||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||||
|
@ -68,7 +68,7 @@
|
|||||||
"react-dom": "17.0.0",
|
"react-dom": "17.0.0",
|
||||||
"react-force-graph": "^1.41.0",
|
"react-force-graph": "^1.41.0",
|
||||||
"react-graph-vis": "^1.0.5",
|
"react-graph-vis": "^1.0.5",
|
||||||
"react-grid-layout": "^1.2.5",
|
"react-grid-layout": "^1.3.4",
|
||||||
"react-i18next": "^11.16.1",
|
"react-i18next": "^11.16.1",
|
||||||
"react-query": "^3.34.19",
|
"react-query": "^3.34.19",
|
||||||
"react-redux": "^7.2.2",
|
"react-redux": "^7.2.2",
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
import { PlusOutlined } from '@ant-design/icons';
|
|
||||||
import { Typography } from 'antd';
|
|
||||||
import React, { useCallback } from 'react';
|
|
||||||
import { connect, useSelector } from 'react-redux';
|
|
||||||
import { bindActionCreators, Dispatch } from 'redux';
|
|
||||||
import { ThunkDispatch } from 'redux-thunk';
|
|
||||||
import {
|
|
||||||
ToggleAddWidget,
|
|
||||||
ToggleAddWidgetProps,
|
|
||||||
} from 'store/actions/dashboard/toggleAddWidget';
|
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import AppActions from 'types/actions';
|
|
||||||
import DashboardReducer from 'types/reducer/dashboards';
|
|
||||||
|
|
||||||
import { Button, Container } from './styles';
|
|
||||||
|
|
||||||
function AddWidget({ toggleAddWidget }: Props): JSX.Element {
|
|
||||||
const { isAddWidget } = useSelector<AppState, DashboardReducer>(
|
|
||||||
(state) => state.dashboards,
|
|
||||||
);
|
|
||||||
|
|
||||||
const onToggleHandler = useCallback(() => {
|
|
||||||
toggleAddWidget(true);
|
|
||||||
}, [toggleAddWidget]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
{!isAddWidget ? (
|
|
||||||
<Button onClick={onToggleHandler} icon={<PlusOutlined />}>
|
|
||||||
Add Widgets
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Typography>Click a widget icon to add it here</Typography>
|
|
||||||
)}
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DispatchProps {
|
|
||||||
toggleAddWidget: (
|
|
||||||
props: ToggleAddWidgetProps,
|
|
||||||
) => (dispatch: Dispatch<AppActions>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = (
|
|
||||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
|
||||||
): DispatchProps => ({
|
|
||||||
toggleAddWidget: bindActionCreators(ToggleAddWidget, dispatch),
|
|
||||||
});
|
|
||||||
|
|
||||||
type Props = DispatchProps;
|
|
||||||
|
|
||||||
export default connect(null, mapDispatchToProps)(AddWidget);
|
|
@ -1,18 +0,0 @@
|
|||||||
import { Button as ButtonComponent } from 'antd';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export const Button = styled(ButtonComponent)`
|
|
||||||
&&& {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Container = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
`;
|
|
16
frontend/src/container/GridGraphLayout/EmptyWidget/index.tsx
Normal file
16
frontend/src/container/GridGraphLayout/EmptyWidget/index.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Typography } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Container } from './styles';
|
||||||
|
|
||||||
|
function EmptyWidget(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Typography.Paragraph>
|
||||||
|
Click one of the widget types above (Time Series / Value) to add here
|
||||||
|
</Typography.Paragraph>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EmptyWidget;
|
@ -0,0 +1,8 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const Container = styled.div`
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
@ -1,13 +1,14 @@
|
|||||||
import { Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
import getQueryResult from 'api/widgets/getQuery';
|
import getQueryResult from 'api/widgets/getQuery';
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ChartData } from 'chart.js';
|
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import GridGraphComponent from 'container/GridGraphComponent';
|
import GridGraphComponent from 'container/GridGraphComponent';
|
||||||
import getChartData from 'lib/getChartData';
|
import getChartData from 'lib/getChartData';
|
||||||
import GetMaxMinTime from 'lib/getMaxMinTime';
|
import GetMaxMinTime from 'lib/getMaxMinTime';
|
||||||
import GetStartAndEndTime from 'lib/getStartAndEndTime';
|
import GetStartAndEndTime from 'lib/getStartAndEndTime';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import isEmpty from 'lodash-es/isEmpty';
|
||||||
|
import React, { memo, useCallback, useState } from 'react';
|
||||||
|
import { Layout } from 'react-grid-layout';
|
||||||
|
import { useQueries } from 'react-query';
|
||||||
import { connect, useSelector } from 'react-redux';
|
import { connect, useSelector } from 'react-redux';
|
||||||
import { bindActionCreators, Dispatch } from 'redux';
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
import { ThunkDispatch } from 'redux-thunk';
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
@ -20,6 +21,8 @@ import AppActions from 'types/actions';
|
|||||||
import { GlobalTime } from 'types/actions/globalTime';
|
import { GlobalTime } from 'types/actions/globalTime';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
|
import { LayoutProps } from '..';
|
||||||
|
import EmptyWidget from '../EmptyWidget';
|
||||||
import WidgetHeader from '../WidgetHeader';
|
import WidgetHeader from '../WidgetHeader';
|
||||||
import FullView from './FullView';
|
import FullView from './FullView';
|
||||||
import { ErrorContainer, FullViewContainer, Modal } from './styles';
|
import { ErrorContainer, FullViewContainer, Modal } from './styles';
|
||||||
@ -27,91 +30,65 @@ import { ErrorContainer, FullViewContainer, Modal } from './styles';
|
|||||||
function GridCardGraph({
|
function GridCardGraph({
|
||||||
widget,
|
widget,
|
||||||
deleteWidget,
|
deleteWidget,
|
||||||
isDeleted,
|
|
||||||
name,
|
name,
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
|
layout = [],
|
||||||
|
setLayout,
|
||||||
}: GridCardGraphProps): JSX.Element {
|
}: GridCardGraphProps): JSX.Element {
|
||||||
const [state, setState] = useState<GridCardGraphState>({
|
|
||||||
loading: true,
|
|
||||||
errorMessage: '',
|
|
||||||
error: false,
|
|
||||||
payload: undefined,
|
|
||||||
});
|
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const [modal, setModal] = useState(false);
|
const [modal, setModal] = useState(false);
|
||||||
const { minTime, maxTime } = useSelector<AppState, GlobalTime>(
|
const { minTime, maxTime } = useSelector<AppState, GlobalTime>(
|
||||||
(state) => state.globalTime,
|
(state) => state.globalTime,
|
||||||
);
|
);
|
||||||
const [deleteModal, setDeletModal] = useState(false);
|
const [deleteModal, setDeleteModal] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
const getMaxMinTime = GetMaxMinTime({
|
||||||
(async (): Promise<void> => {
|
graphType: widget?.panelTypes,
|
||||||
try {
|
maxTime,
|
||||||
const getMaxMinTime = GetMaxMinTime({
|
minTime,
|
||||||
graphType: widget?.panelTypes,
|
});
|
||||||
maxTime,
|
|
||||||
minTime,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { start, end } = GetStartAndEndTime({
|
const { start, end } = GetStartAndEndTime({
|
||||||
type: widget.timePreferance,
|
type: widget?.timePreferance,
|
||||||
maxTime: getMaxMinTime.maxTime,
|
maxTime: getMaxMinTime.maxTime,
|
||||||
minTime: getMaxMinTime.minTime,
|
minTime: getMaxMinTime.minTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await Promise.all(
|
const queryLength = widget?.query?.filter((e) => e.query.length !== 0) || [];
|
||||||
widget.query
|
|
||||||
.filter((e) => e.query.length !== 0)
|
|
||||||
.map(async (query) => {
|
|
||||||
const result = await getQueryResult({
|
|
||||||
end,
|
|
||||||
query: encodeURIComponent(query.query),
|
|
||||||
start,
|
|
||||||
step: '60',
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
const response = useQueries(
|
||||||
query: query.query,
|
queryLength?.map((query) => {
|
||||||
queryData: result,
|
return {
|
||||||
legend: query.legend,
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
};
|
queryFn: () => {
|
||||||
}),
|
return getQueryResult({
|
||||||
);
|
end,
|
||||||
|
query: query?.query,
|
||||||
const isError = response.find((e) => e.queryData.statusCode !== 200);
|
start,
|
||||||
|
step: '60',
|
||||||
if (isError !== undefined) {
|
|
||||||
setState((state) => ({
|
|
||||||
...state,
|
|
||||||
error: true,
|
|
||||||
errorMessage: isError.queryData.error || 'Something went wrong',
|
|
||||||
loading: false,
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
const chartDataSet = getChartData({
|
|
||||||
queryData: response.map((e) => ({
|
|
||||||
query: e.query,
|
|
||||||
legend: e.legend,
|
|
||||||
queryData: e.queryData.payload?.result || [],
|
|
||||||
})),
|
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
queryHash: `${query?.query}-${query?.legend}-${start}-${end}`,
|
||||||
|
retryOnMount: false,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
setState((state) => ({
|
const isError =
|
||||||
...state,
|
response.find((e) => e?.data?.statusCode !== 200) !== undefined ||
|
||||||
loading: false,
|
response.some((e) => e.isError === true);
|
||||||
payload: chartDataSet,
|
|
||||||
}));
|
const isLoading = response.some((e) => e.isLoading === true);
|
||||||
}
|
|
||||||
} catch (error) {
|
const errorMessage = response.find((e) => e.data?.error !== null)?.data?.error;
|
||||||
setState((state) => ({
|
|
||||||
...state,
|
const data = response.map((responseOfQuery) =>
|
||||||
error: true,
|
responseOfQuery?.data?.payload?.result.map((e, index) => ({
|
||||||
errorMessage: (error as AxiosError).toString(),
|
query: queryLength[index]?.query,
|
||||||
loading: false,
|
queryData: e,
|
||||||
}));
|
legend: queryLength[index]?.legend,
|
||||||
}
|
})),
|
||||||
})();
|
);
|
||||||
}, [widget, maxTime, minTime]);
|
|
||||||
|
|
||||||
const onToggleModal = useCallback(
|
const onToggleModal = useCallback(
|
||||||
(func: React.Dispatch<React.SetStateAction<boolean>>) => {
|
(func: React.Dispatch<React.SetStateAction<boolean>>) => {
|
||||||
@ -121,18 +98,20 @@ function GridCardGraph({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const onDeleteHandler = useCallback(() => {
|
const onDeleteHandler = useCallback(() => {
|
||||||
deleteWidget({ widgetId: widget.id });
|
const isEmptyWidget = widget?.id === 'empty' || isEmpty(widget);
|
||||||
onToggleModal(setDeletModal);
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
const widgetId = isEmptyWidget ? layout[0].i : widget?.id;
|
||||||
isDeleted.current = true;
|
|
||||||
}, [deleteWidget, widget, onToggleModal, isDeleted]);
|
deleteWidget({ widgetId, setLayout });
|
||||||
|
onToggleModal(setDeleteModal);
|
||||||
|
}, [deleteWidget, layout, onToggleModal, setLayout, widget]);
|
||||||
|
|
||||||
const getModals = (): JSX.Element => {
|
const getModals = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
onCancel={(): void => onToggleModal(setDeletModal)}
|
onCancel={(): void => onToggleModal(setDeleteModal)}
|
||||||
visible={deleteModal}
|
visible={deleteModal}
|
||||||
title="Delete"
|
title="Delete"
|
||||||
height="10vh"
|
height="10vh"
|
||||||
@ -163,7 +142,16 @@ function GridCardGraph({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (state.error) {
|
const isEmptyLayout = widget?.id === 'empty' || isEmpty(widget);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Spinner height="20vh" tip="Loading..." />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(isError || data === undefined || data[0] === undefined) &&
|
||||||
|
!isEmptyLayout
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{getModals()}
|
{getModals()}
|
||||||
@ -172,17 +160,21 @@ function GridCardGraph({
|
|||||||
title={widget?.title}
|
title={widget?.title}
|
||||||
widget={widget}
|
widget={widget}
|
||||||
onView={(): void => onToggleModal(setModal)}
|
onView={(): void => onToggleModal(setModal)}
|
||||||
onDelete={(): void => onToggleModal(setDeletModal)}
|
onDelete={(): void => onToggleModal(setDeleteModal)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ErrorContainer>{state.errorMessage}</ErrorContainer>
|
<ErrorContainer>{errorMessage}</ErrorContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.loading === true || state.payload === undefined) {
|
const chartData = getChartData({
|
||||||
return <Spinner height="20vh" tip="Loading..." />;
|
queryData: data.map((e) => ({
|
||||||
}
|
query: e?.map((e) => e.query).join(' ') || '',
|
||||||
|
queryData: e?.map((e) => e.queryData) || [],
|
||||||
|
legend: e?.map((e) => e.legend).join('') || '',
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
@ -199,38 +191,37 @@ function GridCardGraph({
|
|||||||
setHovered(false);
|
setHovered(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<WidgetHeader
|
{!isEmptyLayout && (
|
||||||
parentHover={hovered}
|
<WidgetHeader
|
||||||
title={widget.title}
|
parentHover={hovered}
|
||||||
widget={widget}
|
title={widget?.title}
|
||||||
onView={(): void => onToggleModal(setModal)}
|
widget={widget}
|
||||||
onDelete={(): void => onToggleModal(setDeletModal)}
|
onView={(): void => onToggleModal(setModal)}
|
||||||
/>
|
onDelete={(): void => onToggleModal(setDeleteModal)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{getModals()}
|
{!isEmptyLayout && getModals()}
|
||||||
|
|
||||||
<GridGraphComponent
|
{!isEmpty(widget) && (
|
||||||
{...{
|
<GridGraphComponent
|
||||||
GRAPH_TYPES: widget.panelTypes,
|
{...{
|
||||||
data: state.payload,
|
GRAPH_TYPES: widget.panelTypes,
|
||||||
isStacked: widget.isStacked,
|
data: chartData,
|
||||||
opacity: widget.opacity,
|
isStacked: widget.isStacked,
|
||||||
title: ' ', // empty title to accommodate absolutely positioned widget header
|
opacity: widget.opacity,
|
||||||
name,
|
title: ' ', // empty title to accommodate absolutely positioned widget header
|
||||||
yAxisUnit,
|
name,
|
||||||
}}
|
yAxisUnit,
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isEmptyLayout && <EmptyWidget />}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GridCardGraphState {
|
|
||||||
loading: boolean;
|
|
||||||
error: boolean;
|
|
||||||
errorMessage: string;
|
|
||||||
payload: ChartData | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DispatchProps {
|
interface DispatchProps {
|
||||||
deleteWidget: ({
|
deleteWidget: ({
|
||||||
widgetId,
|
widgetId,
|
||||||
@ -239,9 +230,12 @@ interface DispatchProps {
|
|||||||
|
|
||||||
interface GridCardGraphProps extends DispatchProps {
|
interface GridCardGraphProps extends DispatchProps {
|
||||||
widget: Widgets;
|
widget: Widgets;
|
||||||
isDeleted: React.MutableRefObject<boolean>;
|
|
||||||
name: string;
|
name: string;
|
||||||
yAxisUnit: string | undefined;
|
yAxisUnit: string | undefined;
|
||||||
|
// eslint-disable-next-line react/require-default-props
|
||||||
|
layout?: Layout[];
|
||||||
|
// eslint-disable-next-line react/require-default-props
|
||||||
|
setLayout?: React.Dispatch<React.SetStateAction<LayoutProps[]>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = (
|
const mapDispatchToProps = (
|
||||||
@ -250,4 +244,4 @@ const mapDispatchToProps = (
|
|||||||
deleteWidget: bindActionCreators(DeleteWidget, dispatch),
|
deleteWidget: bindActionCreators(DeleteWidget, dispatch),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(null, mapDispatchToProps)(GridCardGraph);
|
export default connect(null, mapDispatchToProps)(memo(GridCardGraph));
|
||||||
|
101
frontend/src/container/GridGraphLayout/GraphLayout.tsx
Normal file
101
frontend/src/container/GridGraphLayout/GraphLayout.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { PlusOutlined, SaveFilled } from '@ant-design/icons';
|
||||||
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
|
import React from 'react';
|
||||||
|
import { Layout } from 'react-grid-layout';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
|
import { LayoutProps, State } from '.';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonContainer,
|
||||||
|
Card,
|
||||||
|
CardContainer,
|
||||||
|
ReactGridLayout,
|
||||||
|
} from './styles';
|
||||||
|
|
||||||
|
function GraphLayout({
|
||||||
|
layouts,
|
||||||
|
saveLayoutState,
|
||||||
|
onLayoutSaveHandler,
|
||||||
|
addPanelLoading,
|
||||||
|
onAddPanelHandler,
|
||||||
|
onLayoutChangeHandler,
|
||||||
|
widgets,
|
||||||
|
setLayout,
|
||||||
|
}: GraphLayoutProps): JSX.Element {
|
||||||
|
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
|
const [saveLayout] = useComponentPermission(['save_layout'], role);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ButtonContainer>
|
||||||
|
{saveLayout && (
|
||||||
|
<Button
|
||||||
|
loading={saveLayoutState.loading}
|
||||||
|
onClick={(): Promise<void> => onLayoutSaveHandler(layouts)}
|
||||||
|
icon={<SaveFilled />}
|
||||||
|
danger={saveLayoutState.error}
|
||||||
|
>
|
||||||
|
Save Layout
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
loading={addPanelLoading}
|
||||||
|
disabled={addPanelLoading}
|
||||||
|
onClick={onAddPanelHandler}
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
>
|
||||||
|
Add Panel
|
||||||
|
</Button>
|
||||||
|
</ButtonContainer>
|
||||||
|
|
||||||
|
<ReactGridLayout
|
||||||
|
isResizable
|
||||||
|
cols={12}
|
||||||
|
rowHeight={100}
|
||||||
|
autoSize
|
||||||
|
width={100}
|
||||||
|
isDraggable
|
||||||
|
isDroppable
|
||||||
|
useCSSTransforms
|
||||||
|
allowOverlap={false}
|
||||||
|
onLayoutChange={onLayoutChangeHandler}
|
||||||
|
>
|
||||||
|
{layouts.map(({ Component, ...rest }) => {
|
||||||
|
const currentWidget = (widgets || [])?.find((e) => e.id === rest.i);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardContainer
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
key={currentWidget?.id || 'empty'} // don't change this key
|
||||||
|
data-grid={rest}
|
||||||
|
>
|
||||||
|
<Card>
|
||||||
|
<Component setLayout={setLayout} />
|
||||||
|
</Card>
|
||||||
|
</CardContainer>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ReactGridLayout>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GraphLayoutProps {
|
||||||
|
layouts: LayoutProps[];
|
||||||
|
saveLayoutState: State;
|
||||||
|
onLayoutSaveHandler: (layout: Layout[]) => Promise<void>;
|
||||||
|
addPanelLoading: boolean;
|
||||||
|
onAddPanelHandler: VoidFunction;
|
||||||
|
onLayoutChangeHandler: (layout: Layout[]) => Promise<void>;
|
||||||
|
widgets: Widgets[] | undefined;
|
||||||
|
setLayout: React.Dispatch<React.SetStateAction<LayoutProps[]>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GraphLayout;
|
@ -104,7 +104,7 @@ function WidgetHeader({
|
|||||||
overlay={menu}
|
overlay={menu}
|
||||||
trigger={['click']}
|
trigger={['click']}
|
||||||
overlayStyle={{ minWidth: 100 }}
|
overlayStyle={{ minWidth: 100 }}
|
||||||
placement="bottomCenter"
|
placement="bottom"
|
||||||
>
|
>
|
||||||
<HeaderContainer
|
<HeaderContainer
|
||||||
onMouseOver={(): void => setLocalHover(true)}
|
onMouseOver={(): void => setLocalHover(true)}
|
||||||
|
@ -1,285 +1,288 @@
|
|||||||
/* eslint-disable react/no-unstable-nested-components */
|
/* eslint-disable react/no-unstable-nested-components */
|
||||||
import { SaveFilled } from '@ant-design/icons';
|
|
||||||
import { notification } from 'antd';
|
import { notification } from 'antd';
|
||||||
import updateDashboardApi from 'api/dashboard/update';
|
import updateDashboardApi from 'api/dashboard/update';
|
||||||
import Spinner from 'components/Spinner';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
|
||||||
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
|
|
||||||
import { Layout } from 'react-grid-layout';
|
import { Layout } from 'react-grid-layout';
|
||||||
import { useSelector } from 'react-redux';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { AppState } from 'store/reducers';
|
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||||
import AppReducer from 'types/reducer/app';
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
import DashboardReducer from 'types/reducer/dashboards';
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
import { v4 } from 'uuid';
|
|
||||||
|
|
||||||
import AddWidget from './AddWidget';
|
|
||||||
import Graph from './Graph';
|
|
||||||
import {
|
import {
|
||||||
Button,
|
ToggleAddWidget,
|
||||||
ButtonContainer,
|
ToggleAddWidgetProps,
|
||||||
Card,
|
} from 'store/actions/dashboard/toggleAddWidget';
|
||||||
CardContainer,
|
import { AppState } from 'store/reducers';
|
||||||
ReactGridLayout,
|
import AppActions from 'types/actions';
|
||||||
} from './styles';
|
import { UPDATE_DASHBOARD } from 'types/actions/dashboard';
|
||||||
import { updateDashboard } from './utils';
|
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||||
|
import DashboardReducer from 'types/reducer/dashboards';
|
||||||
|
|
||||||
function GridGraph(): JSX.Element {
|
import Graph from './Graph';
|
||||||
const { dashboards, loading } = useSelector<AppState, DashboardReducer>(
|
import GraphLayoutContainer from './GraphLayout';
|
||||||
|
import { UpdateDashboard } from './utils';
|
||||||
|
|
||||||
|
export const getPreLayouts = (
|
||||||
|
widgets: Widgets[] | undefined,
|
||||||
|
layout: Layout[],
|
||||||
|
): LayoutProps[] =>
|
||||||
|
layout.map((e, index) => ({
|
||||||
|
...e,
|
||||||
|
Component: ({ setLayout }: ComponentProps): JSX.Element => {
|
||||||
|
const widget = widgets?.find((widget) => widget.id === e.i);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Graph
|
||||||
|
name={e.i + index}
|
||||||
|
widget={widget as Widgets}
|
||||||
|
yAxisUnit={widget?.yAxisUnit}
|
||||||
|
layout={layout}
|
||||||
|
setLayout={setLayout}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
function GridGraph(props: Props): JSX.Element {
|
||||||
|
const { toggleAddWidget } = props;
|
||||||
|
const [addPanelLoading, setAddPanelLoading] = useState(false);
|
||||||
|
const { t } = useTranslation(['common']);
|
||||||
|
const { dashboards, isAddWidget } = useSelector<AppState, DashboardReducer>(
|
||||||
(state) => state.dashboards,
|
(state) => state.dashboards,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [saveLayoutState, setSaveLayoutState] = useState<State>({
|
const [saveLayoutState, setSaveLayoutState] = useState<State>({
|
||||||
loading: false,
|
loading: false,
|
||||||
error: false,
|
error: false,
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
payload: [],
|
payload: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const [selectedDashboard] = dashboards;
|
const [selectedDashboard] = dashboards;
|
||||||
const { data } = selectedDashboard;
|
const { data } = selectedDashboard;
|
||||||
const { widgets } = data;
|
const { widgets } = data;
|
||||||
const [layouts, setLayout] = useState<LayoutProps[]>([]);
|
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||||
|
|
||||||
const AddWidgetWrapper = useCallback(() => <AddWidget />, []);
|
const [layouts, setLayout] = useState<LayoutProps[]>(
|
||||||
|
getPreLayouts(widgets, selectedDashboard.data.layout || []),
|
||||||
const isMounted = useRef(true);
|
);
|
||||||
const isDeleted = useRef(false);
|
|
||||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
|
||||||
|
|
||||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
|
||||||
|
|
||||||
const [saveLayout] = useComponentPermission(['save_layout'], role);
|
|
||||||
|
|
||||||
const getPreLayouts: () => LayoutProps[] = useCallback(() => {
|
|
||||||
if (widgets === undefined) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// when the layout is not present
|
|
||||||
if (data.layout === undefined) {
|
|
||||||
return widgets.map((e, index) => {
|
|
||||||
return {
|
|
||||||
h: 2,
|
|
||||||
w: 6,
|
|
||||||
y: Infinity,
|
|
||||||
i: (index + 1).toString(),
|
|
||||||
x: (index % 2) * 6,
|
|
||||||
Component: (): JSX.Element => (
|
|
||||||
<Graph
|
|
||||||
name={`${e.id + index}non-expanded`}
|
|
||||||
isDeleted={isDeleted}
|
|
||||||
widget={widgets[index]}
|
|
||||||
yAxisUnit={e.yAxisUnit}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return data.layout
|
|
||||||
.filter((_, index) => widgets[index])
|
|
||||||
.map((e, index) => ({
|
|
||||||
...e,
|
|
||||||
Component: (): JSX.Element => {
|
|
||||||
if (widgets[index]) {
|
|
||||||
return (
|
|
||||||
<Graph
|
|
||||||
name={e.i + index}
|
|
||||||
isDeleted={isDeleted}
|
|
||||||
widget={widgets[index]}
|
|
||||||
yAxisUnit={widgets[index].yAxisUnit}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <div />;
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}, [widgets, data?.layout]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
(async (): Promise<void> => {
|
||||||
loading === false &&
|
if (!isAddWidget) {
|
||||||
(isMounted.current === true || isDeleted.current === true)
|
const isEmptyLayoutPresent = layouts.find((e) => e.i === 'empty');
|
||||||
) {
|
if (isEmptyLayoutPresent) {
|
||||||
const preLayouts = getPreLayouts();
|
// non empty layout
|
||||||
setLayout(() => {
|
const updatedLayout = layouts.filter((e) => e.i !== 'empty');
|
||||||
const getX = (): number => {
|
// non widget
|
||||||
if (preLayouts && preLayouts?.length > 0) {
|
const updatedWidget = widgets?.filter((e) => e.id !== 'empty');
|
||||||
const last = preLayouts[(preLayouts?.length || 0) - 1];
|
setLayout(updatedLayout);
|
||||||
|
|
||||||
return (last.w + last.x) % 12;
|
const updatedDashboard: Dashboard = {
|
||||||
}
|
...selectedDashboard,
|
||||||
return 0;
|
data: {
|
||||||
};
|
...selectedDashboard.data,
|
||||||
|
layout: updatedLayout,
|
||||||
|
widgets: updatedWidget,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return [
|
await updateDashboardApi({
|
||||||
...preLayouts,
|
data: updatedDashboard.data,
|
||||||
{
|
uuid: updatedDashboard.uuid,
|
||||||
i: (preLayouts.length + 1).toString(),
|
|
||||||
x: getX(),
|
|
||||||
y: Infinity,
|
|
||||||
w: 6,
|
|
||||||
h: 2,
|
|
||||||
Component: AddWidgetWrapper,
|
|
||||||
maxW: 6,
|
|
||||||
isDraggable: false,
|
|
||||||
isResizable: false,
|
|
||||||
isBounded: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (): void => {
|
|
||||||
isMounted.current = false;
|
|
||||||
};
|
|
||||||
}, [widgets, layouts.length, AddWidgetWrapper, loading, getPreLayouts]);
|
|
||||||
|
|
||||||
const onDropHandler = useCallback(
|
|
||||||
async (allLayouts: Layout[], currentLayout: Layout, event: DragEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
if (event.dataTransfer) {
|
|
||||||
try {
|
|
||||||
const graphType = event.dataTransfer.getData('text') as GRAPH_TYPES;
|
|
||||||
const generateWidgetId = v4();
|
|
||||||
|
|
||||||
await updateDashboard({
|
|
||||||
data,
|
|
||||||
generateWidgetId,
|
|
||||||
graphType,
|
|
||||||
selectedDashboard,
|
|
||||||
layout: allLayouts
|
|
||||||
.map((e, index) => ({
|
|
||||||
...e,
|
|
||||||
i: index.toString(),
|
|
||||||
// when a new element drops
|
|
||||||
w: e.i === '__dropping-elem__' ? 6 : e.w,
|
|
||||||
h: e.i === '__dropping-elem__' ? 2 : e.h,
|
|
||||||
}))
|
|
||||||
// removing add widgets layout config
|
|
||||||
.filter((e) => e.maxW === undefined),
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
notification.error({
|
dispatch({
|
||||||
message:
|
type: UPDATE_DASHBOARD,
|
||||||
error instanceof Error ? error.toString() : 'Something went wrong',
|
payload: updatedDashboard,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onLayoutSaveHandler = useCallback(
|
||||||
|
async (layout: Layout[]) => {
|
||||||
|
try {
|
||||||
|
setSaveLayoutState((state) => ({
|
||||||
|
...state,
|
||||||
|
error: false,
|
||||||
|
errorMessage: '',
|
||||||
|
loading: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const response = await updateDashboardApi({
|
||||||
|
data: {
|
||||||
|
title: data.title,
|
||||||
|
description: data.description,
|
||||||
|
name: data.name,
|
||||||
|
tags: data.tags,
|
||||||
|
widgets: data.widgets,
|
||||||
|
layout,
|
||||||
|
},
|
||||||
|
uuid: selectedDashboard.uuid,
|
||||||
|
});
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
setSaveLayoutState((state) => ({
|
||||||
|
...state,
|
||||||
|
error: false,
|
||||||
|
errorMessage: '',
|
||||||
|
loading: false,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
setSaveLayoutState((state) => ({
|
||||||
|
...state,
|
||||||
|
error: true,
|
||||||
|
errorMessage: response.error || 'Something went wrong',
|
||||||
|
loading: false,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[data, selectedDashboard],
|
[
|
||||||
|
data.description,
|
||||||
|
data.name,
|
||||||
|
data.tags,
|
||||||
|
data.title,
|
||||||
|
data.widgets,
|
||||||
|
selectedDashboard.uuid,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLayoutSaveHandler = async (): Promise<void> => {
|
const setLayoutFunction = useCallback(
|
||||||
setSaveLayoutState((state) => ({
|
(layout: Layout[]) => {
|
||||||
...state,
|
setLayout(
|
||||||
error: false,
|
layout.map((e) => {
|
||||||
errorMessage: '',
|
const currentWidget =
|
||||||
loading: true,
|
widgets?.find((widget) => widget.id === e.i) || ({} as Widgets);
|
||||||
}));
|
|
||||||
|
|
||||||
const response = await updateDashboardApi({
|
return {
|
||||||
data: {
|
...e,
|
||||||
title: data.title,
|
Component: (): JSX.Element => (
|
||||||
description: data.description,
|
<Graph
|
||||||
name: data.name,
|
name={currentWidget.id}
|
||||||
tags: data.tags,
|
widget={currentWidget}
|
||||||
widgets: data.widgets,
|
yAxisUnit={currentWidget?.yAxisUnit}
|
||||||
layout: saveLayoutState.payload.filter((e) => e.maxW === undefined),
|
layout={layout}
|
||||||
},
|
setLayout={setLayout}
|
||||||
uuid: selectedDashboard.uuid,
|
/>
|
||||||
});
|
),
|
||||||
if (response.statusCode === 200) {
|
};
|
||||||
setSaveLayoutState((state) => ({
|
}),
|
||||||
...state,
|
);
|
||||||
error: false,
|
},
|
||||||
errorMessage: '',
|
[widgets],
|
||||||
loading: false,
|
);
|
||||||
}));
|
|
||||||
} else {
|
const onEmptyWidgetHandler = useCallback(async () => {
|
||||||
setSaveLayoutState((state) => ({
|
try {
|
||||||
...state,
|
const id = 'empty';
|
||||||
error: true,
|
|
||||||
errorMessage: response.error || 'Something went wrong',
|
const layout = [
|
||||||
loading: false,
|
{
|
||||||
}));
|
i: id,
|
||||||
|
w: 6,
|
||||||
|
x: 0,
|
||||||
|
h: 2,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
...(data.layout || []),
|
||||||
|
];
|
||||||
|
|
||||||
|
await UpdateDashboard({
|
||||||
|
data,
|
||||||
|
generateWidgetId: id,
|
||||||
|
graphType: 'EMPTY_WIDGET',
|
||||||
|
selectedDashboard,
|
||||||
|
layout,
|
||||||
|
isRedirected: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
setLayoutFunction(layout);
|
||||||
|
} catch (error) {
|
||||||
|
notification.error({
|
||||||
|
message: error instanceof Error ? error.toString() : 'Something went wrong',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
}, [data, selectedDashboard, setLayoutFunction]);
|
||||||
|
|
||||||
|
const onLayoutChangeHandler = async (layout: Layout[]): Promise<void> => {
|
||||||
|
setLayoutFunction(layout);
|
||||||
|
|
||||||
|
await onLayoutSaveHandler(layout);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onLayoutChangeHandler = (layout: Layout[]): void => {
|
const onAddPanelHandler = useCallback(() => {
|
||||||
setSaveLayoutState({
|
try {
|
||||||
loading: false,
|
setAddPanelLoading(true);
|
||||||
error: false,
|
const isEmptyLayoutPresent =
|
||||||
errorMessage: '',
|
layouts.find((e) => e.i === 'empty') !== undefined;
|
||||||
payload: layout,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (layouts.length === 0) {
|
if (!isEmptyLayoutPresent) {
|
||||||
return <Spinner height="40vh" size="large" tip="Loading..." />;
|
onEmptyWidgetHandler()
|
||||||
}
|
.then(() => {
|
||||||
|
setAddPanelLoading(false);
|
||||||
|
toggleAddWidget(true);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
notification.error(t('something_went_wrong'));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toggleAddWidget(true);
|
||||||
|
setAddPanelLoading(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (typeof error === 'string') {
|
||||||
|
notification.error({
|
||||||
|
message: error || t('something_went_wrong'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [layouts, onEmptyWidgetHandler, t, toggleAddWidget]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<GraphLayoutContainer
|
||||||
{saveLayout && (
|
{...{
|
||||||
<ButtonContainer>
|
addPanelLoading,
|
||||||
<Button
|
layouts,
|
||||||
loading={saveLayoutState.loading}
|
onAddPanelHandler,
|
||||||
onClick={onLayoutSaveHandler}
|
onLayoutChangeHandler,
|
||||||
icon={<SaveFilled />}
|
onLayoutSaveHandler,
|
||||||
danger={saveLayoutState.error}
|
saveLayoutState,
|
||||||
>
|
widgets,
|
||||||
Save Layout
|
setLayout,
|
||||||
</Button>
|
}}
|
||||||
</ButtonContainer>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
<ReactGridLayout
|
|
||||||
isResizable
|
|
||||||
isDraggable
|
|
||||||
cols={12}
|
|
||||||
rowHeight={100}
|
|
||||||
autoSize
|
|
||||||
width={100}
|
|
||||||
isDroppable
|
|
||||||
useCSSTransforms
|
|
||||||
onDrop={onDropHandler}
|
|
||||||
onLayoutChange={onLayoutChangeHandler}
|
|
||||||
>
|
|
||||||
{layouts.map(({ Component, ...rest }, index) => {
|
|
||||||
const widget = (widgets || [])[index] || {};
|
|
||||||
|
|
||||||
const type = widget?.panelTypes || 'TIME_SERIES';
|
|
||||||
|
|
||||||
const isQueryType = type === 'VALUE';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CardContainer
|
|
||||||
isQueryType={isQueryType}
|
|
||||||
isDarkMode={isDarkMode}
|
|
||||||
key={rest.i + JSON.stringify(widget)}
|
|
||||||
data-grid={rest}
|
|
||||||
>
|
|
||||||
<Card isDarkMode={isDarkMode} isQueryType={isQueryType}>
|
|
||||||
<Component />
|
|
||||||
</Card>
|
|
||||||
</CardContainer>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ReactGridLayout>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LayoutProps extends Layout {
|
interface ComponentProps {
|
||||||
Component: () => JSX.Element;
|
setLayout: React.Dispatch<React.SetStateAction<LayoutProps[]>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
export interface LayoutProps extends Layout {
|
||||||
|
Component: (props: ComponentProps) => JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface State {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: boolean;
|
error: boolean;
|
||||||
payload: Layout[];
|
payload: Layout[];
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(GridGraph);
|
interface DispatchProps {
|
||||||
|
toggleAddWidget: (
|
||||||
|
props: ToggleAddWidgetProps,
|
||||||
|
) => (dispatch: Dispatch<AppActions>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (
|
||||||
|
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||||
|
): DispatchProps => ({
|
||||||
|
toggleAddWidget: bindActionCreators(ToggleAddWidget, dispatch),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Props = DispatchProps;
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(GridGraph);
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
import { Button as ButtonComponent, Card as CardComponent } from 'antd';
|
import { Button as ButtonComponent, Card as CardComponent, Space } from 'antd';
|
||||||
import { StyledCSS } from 'container/GantChart/Trace/styles';
|
import { StyledCSS } from 'container/GantChart/Trace/styles';
|
||||||
import RGL, { WidthProvider } from 'react-grid-layout';
|
import RGL, { WidthProvider } from 'react-grid-layout';
|
||||||
import styled, { css } from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
|
|
||||||
const ReactGridLayoutComponent = WidthProvider(RGL);
|
const ReactGridLayoutComponent = WidthProvider(RGL);
|
||||||
|
|
||||||
interface Props {
|
export const Card = styled(CardComponent)`
|
||||||
isQueryType: boolean;
|
|
||||||
}
|
|
||||||
export const Card = styled(CardComponent)<Props>`
|
|
||||||
&&& {
|
&&& {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
@ -54,9 +51,22 @@ export const ReactGridLayout = styled(ReactGridLayoutComponent)`
|
|||||||
border: 1px solid #434343;
|
border: 1px solid #434343;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
min-height: 40vh;
|
||||||
|
|
||||||
|
.react-grid-item.react-grid-placeholder {
|
||||||
|
background: grey;
|
||||||
|
opacity: 0.2;
|
||||||
|
transition-duration: 100ms;
|
||||||
|
z-index: 2;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
-o-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ButtonContainer = styled.div`
|
export const ButtonContainer = styled(Space)`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
import { notification } from 'antd';
|
import { notification } from 'antd';
|
||||||
import updateDashboardApi from 'api/dashboard/update';
|
import updateDashboardApi from 'api/dashboard/update';
|
||||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||||
import history from 'lib/history';
|
|
||||||
import { Layout } from 'react-grid-layout';
|
import { Layout } from 'react-grid-layout';
|
||||||
|
import store from 'store';
|
||||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
export const updateDashboard = async ({
|
export const UpdateDashboard = async ({
|
||||||
data,
|
data,
|
||||||
graphType,
|
graphType,
|
||||||
generateWidgetId,
|
generateWidgetId,
|
||||||
layout,
|
layout,
|
||||||
selectedDashboard,
|
selectedDashboard,
|
||||||
}: UpdateDashboardProps): Promise<void> => {
|
isRedirected,
|
||||||
const response = await updateDashboardApi({
|
}: UpdateDashboardProps): Promise<Dashboard | undefined> => {
|
||||||
|
const updatedSelectedDashboard: Dashboard = {
|
||||||
|
...selectedDashboard,
|
||||||
data: {
|
data: {
|
||||||
title: data.title,
|
title: data.title,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
@ -46,17 +48,27 @@ export const updateDashboard = async ({
|
|||||||
layout,
|
layout,
|
||||||
},
|
},
|
||||||
uuid: selectedDashboard.uuid,
|
uuid: selectedDashboard.uuid,
|
||||||
});
|
};
|
||||||
|
|
||||||
if (response.statusCode === 200) {
|
const response = await updateDashboardApi(updatedSelectedDashboard);
|
||||||
history.push(
|
|
||||||
`${history.location.pathname}/new?graphType=${graphType}&widgetId=${generateWidgetId}`,
|
if (response.payload) {
|
||||||
);
|
store.dispatch({
|
||||||
} else {
|
type: 'UPDATE_DASHBOARD',
|
||||||
|
payload: response.payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRedirected) {
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
return response.payload;
|
||||||
|
}
|
||||||
notification.error({
|
notification.error({
|
||||||
message: response.error || 'Something went wrong',
|
message: response.error || 'Something went wrong',
|
||||||
});
|
});
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface UpdateDashboardProps {
|
interface UpdateDashboardProps {
|
||||||
@ -65,4 +77,5 @@ interface UpdateDashboardProps {
|
|||||||
generateWidgetId: string;
|
generateWidgetId: string;
|
||||||
layout: Layout[];
|
layout: Layout[];
|
||||||
selectedDashboard: Dashboard;
|
selectedDashboard: Dashboard;
|
||||||
|
isRedirected: boolean;
|
||||||
}
|
}
|
||||||
|
@ -15,11 +15,19 @@ import ROUTES from 'constants/routes';
|
|||||||
import SearchFilter from 'container/ListOfDashboard/SearchFilter';
|
import SearchFilter from 'container/ListOfDashboard/SearchFilter';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, {
|
||||||
|
Dispatch,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { generatePath } from 'react-router-dom';
|
import { generatePath } from 'react-router-dom';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import { GET_ALL_DASHBOARD_SUCCESS } from 'types/actions/dashboard';
|
||||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
import DashboardReducer from 'types/reducer/dashboards';
|
import DashboardReducer from 'types/reducer/dashboards';
|
||||||
@ -36,6 +44,7 @@ function ListOfAllDashboard(): JSX.Element {
|
|||||||
const { dashboards, loading } = useSelector<AppState, DashboardReducer>(
|
const { dashboards, loading } = useSelector<AppState, DashboardReducer>(
|
||||||
(state) => state.dashboards,
|
(state) => state.dashboards,
|
||||||
);
|
);
|
||||||
|
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
const [action, createNewDashboard, newDashboard] = useComponentPermission(
|
const [action, createNewDashboard, newDashboard] = useComponentPermission(
|
||||||
@ -131,6 +140,10 @@ function ListOfAllDashboard(): JSX.Element {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.statusCode === 200) {
|
if (response.statusCode === 200) {
|
||||||
|
dispatch({
|
||||||
|
type: GET_ALL_DASHBOARD_SUCCESS,
|
||||||
|
payload: [],
|
||||||
|
});
|
||||||
history.push(
|
history.push(
|
||||||
generatePath(ROUTES.DASHBOARD, {
|
generatePath(ROUTES.DASHBOARD, {
|
||||||
dashboardId: response.payload.uuid,
|
dashboardId: response.payload.uuid,
|
||||||
@ -151,7 +164,7 @@ function ListOfAllDashboard(): JSX.Element {
|
|||||||
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
|
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [newDashboardState, t]);
|
}, [newDashboardState, t, dispatch]);
|
||||||
|
|
||||||
const getText = useCallback(() => {
|
const getText = useCallback(() => {
|
||||||
if (!newDashboardState.error && !newDashboardState.loading) {
|
if (!newDashboardState.error && !newDashboardState.loading) {
|
||||||
|
@ -18,7 +18,7 @@ function SkipOnBoardingModal({ onContinueClick }: Props): JSX.Element {
|
|||||||
<iframe
|
<iframe
|
||||||
width="100%"
|
width="100%"
|
||||||
height="265"
|
height="265"
|
||||||
src="https://www.youtube.com/embed/Ly34WBQ2640"
|
src="https://www.youtube.com/embed/J1Bof55DOb4"
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
|
@ -1,17 +1,23 @@
|
|||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
import { notification } from 'antd';
|
import { notification } from 'antd';
|
||||||
import { updateDashboard } from 'container/GridGraphLayout/utils';
|
import history from 'lib/history';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { connect, useSelector } from 'react-redux';
|
||||||
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
|
import {
|
||||||
|
ToggleAddWidget,
|
||||||
|
ToggleAddWidgetProps,
|
||||||
|
} from 'store/actions/dashboard/toggleAddWidget';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
import DashboardReducer from 'types/reducer/dashboards';
|
import DashboardReducer from 'types/reducer/dashboards';
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
|
|
||||||
import menuItems, { ITEMS } from './menuItems';
|
import menuItems, { ITEMS } from './menuItems';
|
||||||
import { Card, Container, Text } from './styles';
|
import { Card, Container, Text } from './styles';
|
||||||
|
|
||||||
function DashboardGraphSlider(): JSX.Element {
|
function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element {
|
||||||
const { dashboards } = useSelector<AppState, DashboardReducer>(
|
const { dashboards } = useSelector<AppState, DashboardReducer>(
|
||||||
(state) => state.dashboards,
|
(state) => state.dashboards,
|
||||||
);
|
);
|
||||||
@ -19,47 +25,30 @@ function DashboardGraphSlider(): JSX.Element {
|
|||||||
const [selectedDashboard] = dashboards;
|
const [selectedDashboard] = dashboards;
|
||||||
const { data } = selectedDashboard;
|
const { data } = selectedDashboard;
|
||||||
|
|
||||||
const onDragStartHandler: React.DragEventHandler<HTMLDivElement> = useCallback(
|
|
||||||
(event: React.DragEvent<HTMLDivElement>) => {
|
|
||||||
event.dataTransfer.setData('text/plain', event.currentTarget.id);
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onClickHandler = useCallback(
|
const onClickHandler = useCallback(
|
||||||
async (name: ITEMS) => {
|
async (name: ITEMS) => {
|
||||||
try {
|
try {
|
||||||
const getX = (): number => {
|
const emptyLayout = data.layout?.find((e) => e.i === 'empty');
|
||||||
if (data.layout && data.layout?.length > 0) {
|
|
||||||
const lastIndexX = data.layout[(data.layout?.length || 0) - 1];
|
|
||||||
return (lastIndexX.w + lastIndexX.x) % 12;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
await updateDashboard({
|
if (emptyLayout === undefined) {
|
||||||
data,
|
notification.error({
|
||||||
generateWidgetId: uuid(),
|
message: 'Please click on Add Panel Button',
|
||||||
graphType: name,
|
});
|
||||||
layout: [
|
return;
|
||||||
...(data.layout || []),
|
}
|
||||||
{
|
|
||||||
h: 2,
|
toggleAddWidget(false);
|
||||||
i: (((data.layout || [])?.length || 0) + 1).toString(),
|
|
||||||
w: 6,
|
history.push(
|
||||||
x: getX(),
|
`${history.location.pathname}/new?graphType=${name}&widgetId=${emptyLayout.i}`,
|
||||||
y: 0,
|
);
|
||||||
},
|
|
||||||
],
|
|
||||||
selectedDashboard,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notification.error({
|
notification.error({
|
||||||
message: 'Something went wrong',
|
message: 'Something went wrong',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[data, selectedDashboard],
|
[data, toggleAddWidget],
|
||||||
);
|
);
|
||||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
const fillColor: React.CSSProperties['color'] = isDarkMode ? 'white' : 'black';
|
const fillColor: React.CSSProperties['color'] = isDarkMode ? 'white' : 'black';
|
||||||
@ -68,11 +57,12 @@ function DashboardGraphSlider(): JSX.Element {
|
|||||||
<Container>
|
<Container>
|
||||||
{menuItems.map(({ name, Icon, display }) => (
|
{menuItems.map(({ name, Icon, display }) => (
|
||||||
<Card
|
<Card
|
||||||
onClick={(): Promise<void> => onClickHandler(name)}
|
onClick={(event): void => {
|
||||||
|
event.preventDefault();
|
||||||
|
onClickHandler(name);
|
||||||
|
}}
|
||||||
id={name}
|
id={name}
|
||||||
onDragStart={onDragStartHandler}
|
|
||||||
key={name}
|
key={name}
|
||||||
draggable
|
|
||||||
>
|
>
|
||||||
<Icon fillColor={fillColor} />
|
<Icon fillColor={fillColor} />
|
||||||
<Text>{display}</Text>
|
<Text>{display}</Text>
|
||||||
@ -84,4 +74,18 @@ function DashboardGraphSlider(): JSX.Element {
|
|||||||
|
|
||||||
export type GRAPH_TYPES = ITEMS;
|
export type GRAPH_TYPES = ITEMS;
|
||||||
|
|
||||||
export default DashboardGraphSlider;
|
interface DispatchProps {
|
||||||
|
toggleAddWidget: (
|
||||||
|
props: ToggleAddWidgetProps,
|
||||||
|
) => (dispatch: Dispatch<AppActions>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (
|
||||||
|
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||||
|
): DispatchProps => ({
|
||||||
|
toggleAddWidget: bindActionCreators(ToggleAddWidget, dispatch),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Props = DispatchProps;
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(DashboardGraphSlider);
|
||||||
|
@ -14,7 +14,7 @@ const Items: ItemsProps[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export type ITEMS = 'TIME_SERIES' | 'VALUE';
|
export type ITEMS = 'TIME_SERIES' | 'VALUE' | 'EMPTY_WIDGET';
|
||||||
|
|
||||||
interface ItemsProps {
|
interface ItemsProps {
|
||||||
name: ITEMS;
|
name: ITEMS;
|
||||||
|
@ -5,7 +5,6 @@ import React, { memo } from 'react';
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import AppReducer from 'types/reducer/app';
|
|
||||||
import DashboardReducer from 'types/reducer/dashboards';
|
import DashboardReducer from 'types/reducer/dashboards';
|
||||||
|
|
||||||
import { NewWidgetProps } from '../../index';
|
import { NewWidgetProps } from '../../index';
|
||||||
@ -19,7 +18,6 @@ function WidgetGraph({
|
|||||||
const { dashboards, isQueryFired } = useSelector<AppState, DashboardReducer>(
|
const { dashboards, isQueryFired } = useSelector<AppState, DashboardReducer>(
|
||||||
(state) => state.dashboards,
|
(state) => state.dashboards,
|
||||||
);
|
);
|
||||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
|
||||||
const [selectedDashboard] = dashboards;
|
const [selectedDashboard] = dashboards;
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
|
|
||||||
@ -33,11 +31,7 @@ function WidgetGraph({
|
|||||||
const selectedWidget = widgets.find((e) => e.id === widgetId);
|
const selectedWidget = widgets.find((e) => e.id === widgetId);
|
||||||
|
|
||||||
if (selectedWidget === undefined) {
|
if (selectedWidget === undefined) {
|
||||||
return (
|
return <Card>Invalid widget</Card>;
|
||||||
<Card isDarkMode={isDarkMode} isQueryType={false}>
|
|
||||||
Invalid widget
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { queryData } = selectedWidget;
|
const { queryData } = selectedWidget;
|
||||||
|
@ -19,16 +19,20 @@ function NewDashboardPage({ getDashboard }: NewDashboardProps): JSX.Element {
|
|||||||
const { dashboardId } = useParams<Params>();
|
const { dashboardId } = useParams<Params>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getDashboard({
|
if (dashboards.length !== 1) {
|
||||||
uuid: dashboardId,
|
getDashboard({
|
||||||
});
|
uuid: dashboardId,
|
||||||
}, [getDashboard, dashboardId]);
|
});
|
||||||
|
}
|
||||||
|
}, [getDashboard, dashboardId, dashboards.length]);
|
||||||
|
|
||||||
if (error && !loading && dashboards.length === 0) {
|
if (error && !loading && dashboards.length === 0) {
|
||||||
return <div>{errorMessage}</div>;
|
return <div>{errorMessage}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading || dashboards.length === 0) {
|
// when user comes from dashboard page. dashboard array is populated with some dashboard as dashboard is populated
|
||||||
|
// so to avoid any unmount call dashboard must have length zero
|
||||||
|
if (loading || dashboards.length === 0 || dashboards.length !== 1) {
|
||||||
return <Spinner tip="Loading.." />;
|
return <Spinner tip="Loading.." />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,7 +340,8 @@ function SignUp({ version }: SignUpProps): JSX.Element {
|
|||||||
!organizationName ||
|
!organizationName ||
|
||||||
!password ||
|
!password ||
|
||||||
!confirmPassword ||
|
!confirmPassword ||
|
||||||
confirmPasswordError
|
confirmPasswordError ||
|
||||||
|
isPasswordPolicyError
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Get Started
|
Get Started
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import updateDashboardApi from 'api/dashboard/update';
|
import updateDashboardApi from 'api/dashboard/update';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
|
import { getPreLayouts, LayoutProps } from 'container/GridGraphLayout';
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
import AppActions from 'types/actions';
|
import AppActions from 'types/actions';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { UPDATE_DASHBOARD } from 'types/actions/dashboard';
|
||||||
|
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
export const DeleteWidget = ({
|
export const DeleteWidget = ({
|
||||||
widgetId,
|
widgetId,
|
||||||
|
setLayout,
|
||||||
}: DeleteWidgetProps): ((dispatch: Dispatch<AppActions>) => void) => {
|
}: DeleteWidgetProps): ((dispatch: Dispatch<AppActions>) => void) => {
|
||||||
return async (dispatch: Dispatch<AppActions>): Promise<void> => {
|
return async (dispatch: Dispatch<AppActions>): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
@ -15,25 +18,32 @@ export const DeleteWidget = ({
|
|||||||
|
|
||||||
const { widgets = [] } = selectedDashboard.data;
|
const { widgets = [] } = selectedDashboard.data;
|
||||||
const updatedWidgets = widgets.filter((e) => e.id !== widgetId);
|
const updatedWidgets = widgets.filter((e) => e.id !== widgetId);
|
||||||
|
const updatedLayout =
|
||||||
|
selectedDashboard.data.layout?.filter((e) => e.i !== widgetId) || [];
|
||||||
|
|
||||||
const response = await updateDashboardApi({
|
const updatedSelectedDashboard: Dashboard = {
|
||||||
|
...selectedDashboard,
|
||||||
data: {
|
data: {
|
||||||
title: selectedDashboard.data.title,
|
title: selectedDashboard.data.title,
|
||||||
description: selectedDashboard.data.description,
|
description: selectedDashboard.data.description,
|
||||||
name: selectedDashboard.data.name,
|
name: selectedDashboard.data.name,
|
||||||
tags: selectedDashboard.data.tags,
|
tags: selectedDashboard.data.tags,
|
||||||
widgets: updatedWidgets,
|
widgets: updatedWidgets,
|
||||||
|
layout: updatedLayout,
|
||||||
},
|
},
|
||||||
uuid: selectedDashboard.uuid,
|
uuid: selectedDashboard.uuid,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const response = await updateDashboardApi(updatedSelectedDashboard);
|
||||||
|
|
||||||
if (response.statusCode === 200) {
|
if (response.statusCode === 200) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'DELETE_WIDGET_SUCCESS',
|
type: UPDATE_DASHBOARD,
|
||||||
payload: {
|
payload: updatedSelectedDashboard,
|
||||||
widgetId,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
if (setLayout) {
|
||||||
|
setLayout(getPreLayouts(updatedWidgets, updatedLayout));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'DELETE_WIDGET_ERROR',
|
type: 'DELETE_WIDGET_ERROR',
|
||||||
@ -55,4 +65,5 @@ export const DeleteWidget = ({
|
|||||||
|
|
||||||
export interface DeleteWidgetProps {
|
export interface DeleteWidgetProps {
|
||||||
widgetId: Widgets['id'];
|
widgetId: Widgets['id'];
|
||||||
|
setLayout?: React.Dispatch<React.SetStateAction<LayoutProps[]>>;
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,13 @@ import updateDashboardApi from 'api/dashboard/update';
|
|||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
|
import { Layout } from 'react-grid-layout';
|
||||||
import { generatePath } from 'react-router-dom';
|
import { generatePath } from 'react-router-dom';
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
import AppActions from 'types/actions';
|
import AppActions from 'types/actions';
|
||||||
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
export const SaveDashboard = ({
|
export const SaveDashboard = ({
|
||||||
uuid,
|
uuid,
|
||||||
@ -20,9 +22,11 @@ export const SaveDashboard = ({
|
|||||||
dashboardId,
|
dashboardId,
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
}: SaveDashboardProps): ((dispatch: Dispatch<AppActions>) => void) => {
|
}: SaveDashboardProps): ((dispatch: Dispatch<AppActions>) => void) => {
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
return async (dispatch: Dispatch<AppActions>): Promise<void> => {
|
return async (dispatch: Dispatch<AppActions>): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const dashboard = store.getState();
|
const dashboard = store.getState();
|
||||||
|
const search = new URLSearchParams(history.location.search);
|
||||||
|
|
||||||
const selectedDashboard = dashboard.dashboards.dashboards.find(
|
const selectedDashboard = dashboard.dashboards.dashboards.find(
|
||||||
(e) => e.uuid === uuid,
|
(e) => e.uuid === uuid,
|
||||||
@ -46,16 +50,41 @@ export const SaveDashboard = ({
|
|||||||
(e) => e.id === widgetId,
|
(e) => e.id === widgetId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isEmptyWidget = widgetId === 'empty';
|
||||||
|
|
||||||
|
const emptyLayoutIndex = data.layout?.findIndex((e) => e.i === 'empty');
|
||||||
|
|
||||||
|
const newWidgetId = v4();
|
||||||
|
|
||||||
const preWidget = data.widgets?.slice(0, selectedWidgetIndex) || [];
|
const preWidget = data.widgets?.slice(0, selectedWidgetIndex) || [];
|
||||||
|
|
||||||
const afterWidget =
|
const afterWidget =
|
||||||
data.widgets?.slice(
|
data.widgets?.slice(
|
||||||
(selectedWidgetIndex || 0) + 1, // this is never undefined
|
(selectedWidgetIndex || 0) + 1, // this is never undefined
|
||||||
data.widgets?.length,
|
data.widgets?.length,
|
||||||
) || [];
|
) || [];
|
||||||
|
|
||||||
const selectedWidget = (selectedDashboard.data.widgets || [])[
|
const selectedWidget = (selectedDashboard.data.widgets || [])[
|
||||||
selectedWidgetIndex || 0
|
selectedWidgetIndex || 0
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const getAllLayout = (): Layout[] => {
|
||||||
|
const allLayout = data.layout || [];
|
||||||
|
|
||||||
|
// empty layout is not present
|
||||||
|
if (emptyLayoutIndex === -1 || emptyLayoutIndex === undefined) {
|
||||||
|
return allLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
...allLayout.slice(0, emptyLayoutIndex),
|
||||||
|
{ ...allLayout[emptyLayoutIndex], i: newWidgetId },
|
||||||
|
...allLayout.slice(emptyLayoutIndex + 1, allLayout.length),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const allLayout = getAllLayout();
|
||||||
|
|
||||||
const response = await updateDashboardApi({
|
const response = await updateDashboardApi({
|
||||||
data: {
|
data: {
|
||||||
...selectedDashboard.data,
|
...selectedDashboard.data,
|
||||||
@ -64,19 +93,21 @@ export const SaveDashboard = ({
|
|||||||
description: selectedDashboard.data.description,
|
description: selectedDashboard.data.description,
|
||||||
tags: selectedDashboard.data.tags,
|
tags: selectedDashboard.data.tags,
|
||||||
name: selectedDashboard.data.name,
|
name: selectedDashboard.data.name,
|
||||||
|
layout: allLayout,
|
||||||
// as we are updated the widget only
|
// as we are updated the widget only
|
||||||
widgets: [
|
widgets: [
|
||||||
...preWidget,
|
...preWidget,
|
||||||
{
|
{
|
||||||
...selectedWidget,
|
...selectedWidget,
|
||||||
description: updatedDescription,
|
description: updatedDescription,
|
||||||
id: widgetId,
|
id: isEmptyWidget ? newWidgetId : widgetId,
|
||||||
isStacked: updatedisStacked,
|
isStacked: updatedisStacked,
|
||||||
nullZeroValues: updatednullZeroValues,
|
nullZeroValues: updatednullZeroValues,
|
||||||
opacity: updatedopacity,
|
opacity: updatedopacity,
|
||||||
title: updatedTitle,
|
title: updatedTitle,
|
||||||
timePreferance: updatedtimePreferance,
|
timePreferance: updatedtimePreferance,
|
||||||
yAxisUnit: updatedYAxisUnit,
|
yAxisUnit: updatedYAxisUnit,
|
||||||
|
panelTypes: search.get('graphType') as Widgets['panelTypes'],
|
||||||
queryData: {
|
queryData: {
|
||||||
...selectedWidget.queryData,
|
...selectedWidget.queryData,
|
||||||
data: [
|
data: [
|
||||||
|
@ -191,7 +191,7 @@ export const GetInitialTraceFilter = (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.error(error);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: UPDATE_TRACE_FILTER_LOADING,
|
type: UPDATE_TRACE_FILTER_LOADING,
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -26,7 +26,7 @@ export const parseQueryIntoFilter = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ export const parseQueryIntoPageSize = (
|
|||||||
current = parseInt(parsedValue, 10);
|
current = parseInt(parsedValue, 10);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('error while parsing json');
|
console.error('error while parsing json');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ import {
|
|||||||
QUERY_SUCCESS,
|
QUERY_SUCCESS,
|
||||||
SAVE_SETTING_TO_PANEL_SUCCESS,
|
SAVE_SETTING_TO_PANEL_SUCCESS,
|
||||||
TOGGLE_EDIT_MODE,
|
TOGGLE_EDIT_MODE,
|
||||||
|
UPDATE_DASHBOARD,
|
||||||
UPDATE_QUERY,
|
UPDATE_QUERY,
|
||||||
UPDATE_TITLE_DESCRIPTION_TAGS_SUCCESS,
|
UPDATE_TITLE_DESCRIPTION_TAGS_SUCCESS,
|
||||||
} from 'types/actions/dashboard';
|
} from 'types/actions/dashboard';
|
||||||
@ -355,7 +356,8 @@ const dashboard = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case SAVE_SETTING_TO_PANEL_SUCCESS: {
|
case SAVE_SETTING_TO_PANEL_SUCCESS:
|
||||||
|
case UPDATE_DASHBOARD: {
|
||||||
const selectedDashboard = action.payload;
|
const selectedDashboard = action.payload;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -369,7 +371,7 @@ const dashboard = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
case DELETE_WIDGET_SUCCESS: {
|
case DELETE_WIDGET_SUCCESS: {
|
||||||
const { widgetId } = action.payload;
|
const { widgetId, layout } = action.payload;
|
||||||
|
|
||||||
const { dashboards } = state;
|
const { dashboards } = state;
|
||||||
const [selectedDashboard] = dashboards;
|
const [selectedDashboard] = dashboards;
|
||||||
@ -384,6 +386,7 @@ const dashboard = (
|
|||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
widgets: widgets.filter((e) => e.id !== widgetId),
|
widgets: widgets.filter((e) => e.id !== widgetId),
|
||||||
|
layout,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -480,6 +483,7 @@ const dashboard = (
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { Layout } from 'react-grid-layout';
|
||||||
import { ApplySettingsToPanelProps } from 'store/actions/dashboard/applySettingsToPanel';
|
import { ApplySettingsToPanelProps } from 'store/actions/dashboard/applySettingsToPanel';
|
||||||
import { Dashboard, Query, Widgets } from 'types/api/dashboard/getAll';
|
import { Dashboard, Query, Widgets } from 'types/api/dashboard/getAll';
|
||||||
import { QueryData } from 'types/api/widgets/getQuery';
|
import { QueryData } from 'types/api/widgets/getQuery';
|
||||||
@ -159,6 +160,7 @@ interface WidgetDeleteSuccess {
|
|||||||
type: typeof DELETE_WIDGET_SUCCESS;
|
type: typeof DELETE_WIDGET_SUCCESS;
|
||||||
payload: {
|
payload: {
|
||||||
widgetId: Widgets['id'];
|
widgetId: Widgets['id'];
|
||||||
|
layout: Layout[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10794,7 +10794,7 @@ react-graph-vis@^1.0.5:
|
|||||||
vis-data "^7.1.2"
|
vis-data "^7.1.2"
|
||||||
vis-network "^9.0.0"
|
vis-network "^9.0.0"
|
||||||
|
|
||||||
react-grid-layout@^1.2.5:
|
react-grid-layout@^1.3.4:
|
||||||
version "1.3.4"
|
version "1.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-1.3.4.tgz#4fa819be24a1ba9268aa11b82d63afc4762a32ff"
|
resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-1.3.4.tgz#4fa819be24a1ba9268aa11b82d63afc4762a32ff"
|
||||||
integrity sha512-sB3rNhorW77HUdOjB4JkelZTdJGQKuXLl3gNg+BI8gJkTScspL1myfZzW/EM0dLEn+1eH+xW+wNqk0oIM9o7cw==
|
integrity sha512-sB3rNhorW77HUdOjB4JkelZTdJGQKuXLl3gNg+BI8gJkTScspL1myfZzW/EM0dLEn+1eH+xW+wNqk0oIM9o7cw==
|
||||||
|
@ -277,6 +277,11 @@ func AdminAccess(f func(http.ResponseWriter, *http.Request)) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterPrivateRoutes registers routes for this handler on the given router
|
||||||
|
func (aH *APIHandler) RegisterPrivateRoutes(router *mux.Router) {
|
||||||
|
router.HandleFunc("/api/v1/channels", aH.listChannels).Methods(http.MethodGet)
|
||||||
|
}
|
||||||
|
|
||||||
// RegisterRoutes registers routes for this handler on the given router
|
// RegisterRoutes registers routes for this handler on the given router
|
||||||
func (aH *APIHandler) RegisterRoutes(router *mux.Router) {
|
func (aH *APIHandler) RegisterRoutes(router *mux.Router) {
|
||||||
router.HandleFunc("/api/v1/query_range", ViewAccess(aH.queryRangeMetrics)).Methods(http.MethodGet)
|
router.HandleFunc("/api/v1/query_range", ViewAccess(aH.queryRangeMetrics)).Methods(http.MethodGet)
|
||||||
|
@ -25,23 +25,24 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ServerOptions struct {
|
type ServerOptions struct {
|
||||||
HTTPHostPort string
|
HTTPHostPort string
|
||||||
|
PrivateHostPort string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server runs HTTP, Mux and a grpc server
|
// Server runs HTTP, Mux and a grpc server
|
||||||
type Server struct {
|
type Server struct {
|
||||||
// logger *zap.Logger
|
// logger *zap.Logger
|
||||||
// querySvc *querysvc.QueryService
|
|
||||||
// queryOptions *QueryOptions
|
|
||||||
|
|
||||||
// tracer opentracing.Tracer // TODO make part of flags.Service
|
// tracer opentracing.Tracer // TODO make part of flags.Service
|
||||||
serverOptions *ServerOptions
|
serverOptions *ServerOptions
|
||||||
conn net.Listener
|
|
||||||
// grpcConn net.Listener
|
// public http router
|
||||||
httpConn net.Listener
|
httpConn net.Listener
|
||||||
// grpcServer *grpc.Server
|
httpServer *http.Server
|
||||||
httpServer *http.Server
|
|
||||||
separatePorts bool
|
// private http
|
||||||
|
privateConn net.Listener
|
||||||
|
privateHTTP *http.Server
|
||||||
|
|
||||||
unavailableChannel chan healthcheck.Status
|
unavailableChannel chan healthcheck.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,59 +52,20 @@ func (s Server) HealthCheckStatus() chan healthcheck.Status {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewServer creates and initializes Server
|
// NewServer creates and initializes Server
|
||||||
// func NewServer(logger *zap.Logger, querySvc *querysvc.QueryService, options *QueryOptions, tracer opentracing.Tracer) (*Server, error) {
|
|
||||||
func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||||
|
|
||||||
// _, httpPort, err := net.SplitHostPort(serverOptions.HTTPHostPort)
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// _, grpcPort, err := net.SplitHostPort(options.GRPCHostPort)
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// grpcServer, err := createGRPCServer(querySvc, options, logger, tracer)
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
|
|
||||||
if err := dao.InitDao("sqlite", constants.RELATIONAL_DATASOURCE_PATH); err != nil {
|
if err := dao.InitDao("sqlite", constants.RELATIONAL_DATASOURCE_PATH); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &Server{
|
|
||||||
// logger: logger,
|
|
||||||
// querySvc: querySvc,
|
|
||||||
// queryOptions: options,
|
|
||||||
// tracer: tracer,
|
|
||||||
// grpcServer: grpcServer,
|
|
||||||
serverOptions: serverOptions,
|
|
||||||
separatePorts: true,
|
|
||||||
// separatePorts: grpcPort != httpPort,
|
|
||||||
unavailableChannel: make(chan healthcheck.Status),
|
|
||||||
}
|
|
||||||
httpServer, err := s.createHTTPServer()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s.httpServer = httpServer
|
|
||||||
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) createHTTPServer() (*http.Server, error) {
|
|
||||||
|
|
||||||
localDB, err := dashboards.InitDB(constants.RELATIONAL_DATASOURCE_PATH)
|
localDB, err := dashboards.InitDB(constants.RELATIONAL_DATASOURCE_PATH)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
localDB.SetMaxOpenConns(10)
|
localDB.SetMaxOpenConns(10)
|
||||||
|
|
||||||
var reader Reader
|
var reader Reader
|
||||||
|
|
||||||
storage := os.Getenv("STORAGE")
|
storage := os.Getenv("STORAGE")
|
||||||
if storage == "clickhouse" {
|
if storage == "clickhouse" {
|
||||||
zap.S().Info("Using ClickHouse as datastore ...")
|
zap.S().Info("Using ClickHouse as datastore ...")
|
||||||
@ -119,24 +81,75 @@ func (s *Server) createHTTPServer() (*http.Server, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s := &Server{
|
||||||
|
// logger: logger,
|
||||||
|
// tracer: tracer,
|
||||||
|
serverOptions: serverOptions,
|
||||||
|
unavailableChannel: make(chan healthcheck.Status),
|
||||||
|
}
|
||||||
|
|
||||||
|
httpServer, err := s.createPublicServer(apiHandler)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.httpServer = httpServer
|
||||||
|
|
||||||
|
privateServer, err := s.createPrivateServer(apiHandler)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.privateHTTP = privateServer
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) createPrivateServer(api *APIHandler) (*http.Server, error) {
|
||||||
|
|
||||||
|
r := NewRouter()
|
||||||
|
|
||||||
|
r.Use(setTimeoutMiddleware)
|
||||||
|
r.Use(s.analyticsMiddleware)
|
||||||
|
r.Use(loggingMiddlewarePrivate)
|
||||||
|
|
||||||
|
api.RegisterPrivateRoutes(r)
|
||||||
|
|
||||||
|
c := cors.New(cors.Options{
|
||||||
|
//todo(amol): find out a way to add exact domain or
|
||||||
|
// ip here for alert manager
|
||||||
|
AllowedOrigins: []string{"*"},
|
||||||
|
AllowedMethods: []string{"GET", "DELETE", "POST", "PUT"},
|
||||||
|
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
|
||||||
|
})
|
||||||
|
|
||||||
|
handler := c.Handler(r)
|
||||||
|
handler = handlers.CompressHandler(handler)
|
||||||
|
|
||||||
|
return &http.Server{
|
||||||
|
Handler: handler,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) createPublicServer(api *APIHandler) (*http.Server, error) {
|
||||||
|
|
||||||
r := NewRouter()
|
r := NewRouter()
|
||||||
|
|
||||||
r.Use(setTimeoutMiddleware)
|
r.Use(setTimeoutMiddleware)
|
||||||
r.Use(s.analyticsMiddleware)
|
r.Use(s.analyticsMiddleware)
|
||||||
r.Use(loggingMiddleware)
|
r.Use(loggingMiddleware)
|
||||||
|
|
||||||
apiHandler.RegisterRoutes(r)
|
api.RegisterRoutes(r)
|
||||||
apiHandler.RegisterMetricsRoutes(r)
|
api.RegisterMetricsRoutes(r)
|
||||||
|
|
||||||
c := cors.New(cors.Options{
|
c := cors.New(cors.Options{
|
||||||
AllowedOrigins: []string{"*"},
|
AllowedOrigins: []string{"*"},
|
||||||
// AllowCredentials: true,
|
|
||||||
AllowedMethods: []string{"GET", "DELETE", "POST", "PUT"},
|
AllowedMethods: []string{"GET", "DELETE", "POST", "PUT"},
|
||||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
|
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
|
||||||
})
|
})
|
||||||
|
|
||||||
handler := c.Handler(r)
|
handler := c.Handler(r)
|
||||||
// var handler http.Handler = r
|
|
||||||
|
|
||||||
handler = handlers.CompressHandler(handler)
|
handler = handlers.CompressHandler(handler)
|
||||||
|
|
||||||
@ -145,6 +158,7 @@ func (s *Server) createHTTPServer() (*http.Server, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loggingMiddleware is used for logging public api calls
|
||||||
func loggingMiddleware(next http.Handler) http.Handler {
|
func loggingMiddleware(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
route := mux.CurrentRoute(r)
|
route := mux.CurrentRoute(r)
|
||||||
@ -155,6 +169,18 @@ func loggingMiddleware(next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loggingMiddlewarePrivate is used for logging private api calls
|
||||||
|
// from internal services like alert manager
|
||||||
|
func loggingMiddlewarePrivate(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
route := mux.CurrentRoute(r)
|
||||||
|
path, _ := route.GetPathTemplate()
|
||||||
|
startTime := time.Now()
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
zap.S().Info(path, "\tprivatePort: true", "\ttimeTaken: ", time.Now().Sub(startTime))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
type loggingResponseWriter struct {
|
type loggingResponseWriter struct {
|
||||||
http.ResponseWriter
|
http.ResponseWriter
|
||||||
statusCode int
|
statusCode int
|
||||||
@ -198,61 +224,42 @@ func setTimeoutMiddleware(next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// initListener initialises listeners of the server
|
// initListeners initialises listeners of the server
|
||||||
func (s *Server) initListener() (cmux.CMux, error) {
|
func (s *Server) initListeners() error {
|
||||||
if s.separatePorts { // use separate ports and listeners each for gRPC and HTTP requests
|
// listen on public port
|
||||||
var err error
|
var err error
|
||||||
// s.grpcConn, err = net.Listen("tcp", s.queryOptions.GRPCHostPort)
|
publicHostPort := s.serverOptions.HTTPHostPort
|
||||||
// if err != nil {
|
if publicHostPort == "" {
|
||||||
// return nil, err
|
return fmt.Errorf("constants.HTTPHostPort is required")
|
||||||
// }
|
|
||||||
|
|
||||||
s.httpConn, err = net.Listen("tcp", s.serverOptions.HTTPHostPort)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
zap.S().Info("Query server started ...")
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// // old behavior using cmux
|
s.httpConn, err = net.Listen("tcp", publicHostPort)
|
||||||
// conn, err := net.Listen("tcp", s.queryOptions.HostPort)
|
if err != nil {
|
||||||
// if err != nil {
|
return err
|
||||||
// return nil, err
|
}
|
||||||
// }
|
|
||||||
// s.conn = conn
|
|
||||||
|
|
||||||
// var tcpPort int
|
zap.S().Info(fmt.Sprintf("Query server started listening on %s...", s.serverOptions.HTTPHostPort))
|
||||||
// if port, err := netutils
|
|
||||||
|
|
||||||
// utils.GetPort(s.conn.Addr()); err == nil {
|
// listen on private port to support internal services
|
||||||
// tcpPort = port
|
privateHostPort := s.serverOptions.PrivateHostPort
|
||||||
// }
|
|
||||||
|
|
||||||
// zap.S().Info(
|
if privateHostPort == "" {
|
||||||
// "Query server started",
|
return fmt.Errorf("constants.PrivateHostPort is required")
|
||||||
// zap.Int("port", tcpPort),
|
}
|
||||||
// zap.String("addr", s.queryOptions.HostPort))
|
|
||||||
|
|
||||||
// // cmux server acts as a reverse-proxy between HTTP and GRPC backends.
|
s.privateConn, err = net.Listen("tcp", privateHostPort)
|
||||||
// cmuxServer := cmux.New(s.conn)
|
if err != nil {
|
||||||
|
return err
|
||||||
// s.grpcConn = cmuxServer.MatchWithWriters(
|
}
|
||||||
// cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc"),
|
zap.S().Info(fmt.Sprintf("Query server started listening on private port %s...", s.serverOptions.PrivateHostPort))
|
||||||
// cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc+proto"),
|
|
||||||
// )
|
|
||||||
// s.httpConn = cmuxServer.Match(cmux.Any())
|
|
||||||
// s.queryOptions.HTTPHostPort = s.queryOptions.HostPort
|
|
||||||
// s.queryOptions.GRPCHostPort = s.queryOptions.HostPort
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start http, GRPC and cmux servers concurrently
|
// Start listening on http and private http port concurrently
|
||||||
func (s *Server) Start() error {
|
func (s *Server) Start() error {
|
||||||
|
|
||||||
_, err := s.initListener()
|
err := s.initListeners()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -283,5 +290,25 @@ func (s *Server) Start() error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
var privatePort int
|
||||||
|
if port, err := utils.GetPort(s.privateConn.Addr()); err == nil {
|
||||||
|
privatePort = port
|
||||||
|
}
|
||||||
|
fmt.Println("starting private http")
|
||||||
|
go func() {
|
||||||
|
zap.S().Info("Starting Private HTTP server", zap.Int("port", privatePort), zap.String("addr", s.serverOptions.PrivateHostPort))
|
||||||
|
|
||||||
|
switch err := s.privateHTTP.Serve(s.privateConn); err {
|
||||||
|
case nil, http.ErrServerClosed, cmux.ErrListenerClosed:
|
||||||
|
// normal exit, nothing to do
|
||||||
|
zap.S().Info("private http server closed")
|
||||||
|
default:
|
||||||
|
zap.S().Error("Could not start private HTTP server", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
s.unavailableChannel <- healthcheck.Unavailable
|
||||||
|
|
||||||
|
}()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
HTTPHostPort = "0.0.0.0:8080" // Address to serve http (query service)
|
HTTPHostPort = "0.0.0.0:8080" // Address to serve http (query service)
|
||||||
DebugHttpPort = "0.0.0.0:6060" // Address to serve http (pprof)
|
PrivateHostPort = "0.0.0.0:8085" // Address to server internal services like alert manager
|
||||||
|
DebugHttpPort = "0.0.0.0:6060" // Address to serve http (pprof)
|
||||||
)
|
)
|
||||||
|
|
||||||
var DEFAULT_TELEMETRY_ANONYMOUS = false
|
var DEFAULT_TELEMETRY_ANONYMOUS = false
|
||||||
@ -37,29 +38,29 @@ var AmChannelApiPath = GetOrDefaultEnv("ALERTMANAGER_API_CHANNEL_PATH", "v1/rout
|
|||||||
var RELATIONAL_DATASOURCE_PATH = GetOrDefaultEnv("SIGNOZ_LOCAL_DB_PATH", "/var/lib/signoz/signoz.db")
|
var RELATIONAL_DATASOURCE_PATH = GetOrDefaultEnv("SIGNOZ_LOCAL_DB_PATH", "/var/lib/signoz/signoz.db")
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ServiceName = "serviceName"
|
ServiceName = "serviceName"
|
||||||
HttpRoute = "httpRoute"
|
HttpRoute = "httpRoute"
|
||||||
HttpCode = "httpCode"
|
HttpCode = "httpCode"
|
||||||
HttpHost = "httpHost"
|
HttpHost = "httpHost"
|
||||||
HttpUrl = "httpUrl"
|
HttpUrl = "httpUrl"
|
||||||
HttpMethod = "httpMethod"
|
HttpMethod = "httpMethod"
|
||||||
Component = "component"
|
Component = "component"
|
||||||
OperationDB = "name"
|
OperationDB = "name"
|
||||||
OperationRequest = "operation"
|
OperationRequest = "operation"
|
||||||
Status = "status"
|
Status = "status"
|
||||||
Duration = "duration"
|
Duration = "duration"
|
||||||
DBName = "dbName"
|
DBName = "dbName"
|
||||||
DBOperation = "dbOperation"
|
DBOperation = "dbOperation"
|
||||||
DBSystem = "dbSystem"
|
DBSystem = "dbSystem"
|
||||||
MsgSystem = "msgSystem"
|
MsgSystem = "msgSystem"
|
||||||
MsgOperation = "msgOperation"
|
MsgOperation = "msgOperation"
|
||||||
Timestamp = "timestamp"
|
Timestamp = "timestamp"
|
||||||
Descending = "descending"
|
Descending = "descending"
|
||||||
Ascending = "ascending"
|
Ascending = "ascending"
|
||||||
ContextTimeout = 60 // seconds
|
ContextTimeout = 60 // seconds
|
||||||
StatusPending = "pending"
|
StatusPending = "pending"
|
||||||
StatusFailed = "failed"
|
StatusFailed = "failed"
|
||||||
StatusSuccess = "success"
|
StatusSuccess = "success"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetOrDefaultEnv(key string, fallback string) string {
|
func GetOrDefaultEnv(key string, fallback string) string {
|
||||||
|
@ -34,7 +34,8 @@ func main() {
|
|||||||
version.PrintVersion()
|
version.PrintVersion()
|
||||||
|
|
||||||
serverOptions := &app.ServerOptions{
|
serverOptions := &app.ServerOptions{
|
||||||
HTTPHostPort: constants.HTTPHostPort,
|
HTTPHostPort: constants.HTTPHostPort,
|
||||||
|
PrivateHostPort: constants.PrivateHostPort,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the jwt secret key
|
// Read the jwt secret key
|
||||||
|
@ -23,7 +23,7 @@ services:
|
|||||||
- query-service
|
- query-service
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
command:
|
command:
|
||||||
- --queryService.url=http://query-service:8080
|
- --queryService.url=http://query-service:8085
|
||||||
- --storage.path=/data
|
- --storage.path=/data
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
|
@ -26,7 +26,7 @@ services:
|
|||||||
- query-service
|
- query-service
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
command:
|
command:
|
||||||
- --queryService.url=http://query-service:8080
|
- --queryService.url=http://query-service:8085
|
||||||
- --storage.path=/data
|
- --storage.path=/data
|
||||||
|
|
||||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||||
|
Loading…
x
Reference in New Issue
Block a user