Merge branch 'develop' into 412-trace-detail

This commit is contained in:
palash-signoz 2022-06-08 16:25:31 +05:30 committed by GitHub
commit b3dfd567e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 814 additions and 643 deletions

View File

@ -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

View File

@ -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"]

View File

@ -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`

View File

@ -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",

View File

@ -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);

View File

@ -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%;
`;

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

View File

@ -0,0 +1,8 @@
import styled from 'styled-components';
export const Container = styled.div`
height: 100%;
display: flex;
justify-content: center;
align-items: center;
`;

View File

@ -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));

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

View File

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

View File

@ -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);

View File

@ -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;

View File

@ -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;
} }

View File

@ -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) {

View File

@ -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

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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.." />;
} }

View File

@ -340,7 +340,8 @@ function SignUp({ version }: SignUpProps): JSX.Element {
!organizationName || !organizationName ||
!password || !password ||
!confirmPassword || !confirmPassword ||
confirmPasswordError confirmPasswordError ||
isPasswordPolicyError
} }
> >
Get Started Get Started

View File

@ -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[]>>;
} }

View File

@ -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: [

View File

@ -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: {

View File

@ -26,7 +26,7 @@ export const parseQueryIntoFilter = (
}); });
} }
} catch (error) { } catch (error) {
console.log(error); console.error(error);
} }
} }

View File

@ -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');
} }
} }

View File

@ -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;
} }

View File

@ -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[];
}; };
} }

View File

@ -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==

View File

@ -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)

View File

@ -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
} }

View File

@ -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 {

View File

@ -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

View File

@ -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:

View File

@ -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`