Commit 90289c83 authored by Srikanth Chekuri's avatar Srikanth Chekuri
Browse files

Merge branch 'chore/improve-metrics-perf' of github.com:SigNoz/signoz into...

Merge branch 'chore/improve-metrics-perf' of github.com:SigNoz/signoz into chore/improve-metrics-perf
parents de320581 738b8fe4
Showing with 609 additions and 491 deletions
+609 -491
......@@ -28,7 +28,7 @@ services:
volumes:
- ./data/alertmanager:/data
command:
- --queryService.url=http://query-service:8080
- --queryService.url=http://query-service:8085
- --storage.path=/data
depends_on:
- query-service
......
......@@ -30,7 +30,7 @@ services:
condition: service_healthy
restart: on-failure
command:
- --queryService.url=http://query-service:8080
- --queryService.url=http://query-service:8085
- --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`
......@@ -53,7 +53,6 @@ services:
- GODEBUG=netdns=go
- TELEMETRY_ENABLED=true
- DEPLOYMENT_TYPE=docker-standalone-arm
restart: on-failure
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
......
......@@ -30,7 +30,7 @@ services:
condition: service_healthy
restart: on-failure
command:
- --queryService.url=http://query-service:8080
- --queryService.url=http://query-service:8085
- --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`
......
......@@ -68,7 +68,7 @@
"react-dom": "17.0.0",
"react-force-graph": "^1.41.0",
"react-graph-vis": "^1.0.5",
"react-grid-layout": "^1.2.5",
"react-grid-layout": "^1.3.4",
"react-i18next": "^11.16.1",
"react-query": "^3.34.19",
"react-redux": "^7.2.2",
......
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);
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;
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`
height: 100%;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
`;
import { Typography } from 'antd';
import getQueryResult from 'api/widgets/getQuery';
import { AxiosError } from 'axios';
import { ChartData } from 'chart.js';
import Spinner from 'components/Spinner';
import GridGraphComponent from 'container/GridGraphComponent';
import getChartData from 'lib/getChartData';
import GetMaxMinTime from 'lib/getMaxMinTime';
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 { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
......@@ -20,6 +21,8 @@ import AppActions from 'types/actions';
import { GlobalTime } from 'types/actions/globalTime';
import { Widgets } from 'types/api/dashboard/getAll';
import { LayoutProps } from '..';
import EmptyWidget from '../EmptyWidget';
import WidgetHeader from '../WidgetHeader';
import FullView from './FullView';
import { ErrorContainer, FullViewContainer, Modal } from './styles';
......@@ -27,91 +30,65 @@ import { ErrorContainer, FullViewContainer, Modal } from './styles';
function GridCardGraph({
widget,
deleteWidget,
isDeleted,
name,
yAxisUnit,
layout = [],
setLayout,
}: GridCardGraphProps): JSX.Element {
const [state, setState] = useState<GridCardGraphState>({
loading: true,
errorMessage: '',
error: false,
payload: undefined,
});
const [hovered, setHovered] = useState(false);
const [modal, setModal] = useState(false);
const { minTime, maxTime } = useSelector<AppState, GlobalTime>(
(state) => state.globalTime,
);
const [deleteModal, setDeletModal] = useState(false);
useEffect(() => {
(async (): Promise<void> => {
try {
const getMaxMinTime = GetMaxMinTime({
graphType: widget?.panelTypes,
maxTime,
minTime,
});
const { start, end } = GetStartAndEndTime({
type: widget.timePreferance,
maxTime: getMaxMinTime.maxTime,
minTime: getMaxMinTime.minTime,
});
const response = await Promise.all(
widget.query
.filter((e) => e.query.length !== 0)
.map(async (query) => {
const result = await getQueryResult({
end,
query: encodeURIComponent(query.query),
start,
step: '60',
});
return {
query: query.query,
queryData: result,
legend: query.legend,
};
}),
);
const isError = response.find((e) => e.queryData.statusCode !== 200);
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 || [],
})),
const [deleteModal, setDeleteModal] = useState(false);
const getMaxMinTime = GetMaxMinTime({
graphType: widget?.panelTypes,
maxTime,
minTime,
});
const { start, end } = GetStartAndEndTime({
type: widget?.timePreferance,
maxTime: getMaxMinTime.maxTime,
minTime: getMaxMinTime.minTime,
});
const queryLength = widget?.query?.filter((e) => e.query.length !== 0) || [];
const response = useQueries(
queryLength?.map((query) => {
return {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
queryFn: () => {
return getQueryResult({
end,
query: query?.query,
start,
step: '60',
});
},
queryHash: `${query?.query}-${query?.legend}-${start}-${end}`,
retryOnMount: false,
};
}),
);
const isError =
response.find((e) => e?.data?.statusCode !== 200) !== undefined ||
response.some((e) => e.isError === true);
setState((state) => ({
...state,
loading: false,
payload: chartDataSet,
}));
}
} catch (error) {
setState((state) => ({
...state,
error: true,
errorMessage: (error as AxiosError).toString(),
loading: false,
}));
}
})();
}, [widget, maxTime, minTime]);
const isLoading = response.some((e) => e.isLoading === true);
const errorMessage = response.find((e) => e.data?.error !== null)?.data?.error;
const data = response.map((responseOfQuery) =>
responseOfQuery?.data?.payload?.result.map((e, index) => ({
query: queryLength[index]?.query,
queryData: e,
legend: queryLength[index]?.legend,
})),
);
const onToggleModal = useCallback(
(func: React.Dispatch<React.SetStateAction<boolean>>) => {
......@@ -121,18 +98,20 @@ function GridCardGraph({
);
const onDeleteHandler = useCallback(() => {
deleteWidget({ widgetId: widget.id });
onToggleModal(setDeletModal);
// eslint-disable-next-line no-param-reassign
isDeleted.current = true;
}, [deleteWidget, widget, onToggleModal, isDeleted]);
const isEmptyWidget = widget?.id === 'empty' || isEmpty(widget);
const widgetId = isEmptyWidget ? layout[0].i : widget?.id;
deleteWidget({ widgetId, setLayout });
onToggleModal(setDeleteModal);
}, [deleteWidget, layout, onToggleModal, setLayout, widget]);
const getModals = (): JSX.Element => {
return (
<>
<Modal
destroyOnClose
onCancel={(): void => onToggleModal(setDeletModal)}
onCancel={(): void => onToggleModal(setDeleteModal)}
visible={deleteModal}
title="Delete"
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 (
<>
{getModals()}
......@@ -172,17 +160,21 @@ function GridCardGraph({
title={widget?.title}
widget={widget}
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) {
return <Spinner height="20vh" tip="Loading..." />;
}
const chartData = getChartData({
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 (
<span
......@@ -199,38 +191,37 @@ function GridCardGraph({
setHovered(false);
}}
>
<WidgetHeader
parentHover={hovered}
title={widget.title}
widget={widget}
onView={(): void => onToggleModal(setModal)}
onDelete={(): void => onToggleModal(setDeletModal)}
/>
{getModals()}
<GridGraphComponent
{...{
GRAPH_TYPES: widget.panelTypes,
data: state.payload,
isStacked: widget.isStacked,
opacity: widget.opacity,
title: ' ', // empty title to accommodate absolutely positioned widget header
name,
yAxisUnit,
}}
/>
{!isEmptyLayout && (
<WidgetHeader
parentHover={hovered}
title={widget?.title}
widget={widget}
onView={(): void => onToggleModal(setModal)}
onDelete={(): void => onToggleModal(setDeleteModal)}
/>
)}
{!isEmptyLayout && getModals()}
{!isEmpty(widget) && (
<GridGraphComponent
{...{
GRAPH_TYPES: widget.panelTypes,
data: chartData,
isStacked: widget.isStacked,
opacity: widget.opacity,
title: ' ', // empty title to accommodate absolutely positioned widget header
name,
yAxisUnit,
}}
/>
)}
{isEmptyLayout && <EmptyWidget />}
</span>
);
}
interface GridCardGraphState {
loading: boolean;
error: boolean;
errorMessage: string;
payload: ChartData | undefined;
}
interface DispatchProps {
deleteWidget: ({
widgetId,
......@@ -239,9 +230,12 @@ interface DispatchProps {
interface GridCardGraphProps extends DispatchProps {
widget: Widgets;
isDeleted: React.MutableRefObject<boolean>;
name: string;
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 = (
......@@ -250,4 +244,4 @@ const mapDispatchToProps = (
deleteWidget: bindActionCreators(DeleteWidget, dispatch),
});
export default connect(null, mapDispatchToProps)(GridCardGraph);
export default connect(null, mapDispatchToProps)(memo(GridCardGraph));
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}
trigger={['click']}
overlayStyle={{ minWidth: 100 }}
placement="bottomCenter"
placement="bottom"
>
<HeaderContainer
onMouseOver={(): void => setLocalHover(true)}
......
/* eslint-disable react/no-unstable-nested-components */
import { SaveFilled } from '@ant-design/icons';
import { notification } from 'antd';
import updateDashboardApi from 'api/dashboard/update';
import Spinner from 'components/Spinner';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import useComponentPermission from 'hooks/useComponentPermission';
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { Layout } from 'react-grid-layout';
import { useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { connect, useDispatch, 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 AppReducer from 'types/reducer/app';
import AppActions from 'types/actions';
import { UPDATE_DASHBOARD } from 'types/actions/dashboard';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import DashboardReducer from 'types/reducer/dashboards';
import { v4 } from 'uuid';
import AddWidget from './AddWidget';
import Graph from './Graph';
import {
Button,
ButtonContainer,
Card,
CardContainer,
ReactGridLayout,
} from './styles';
import { updateDashboard } from './utils';
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(): JSX.Element {
const { dashboards, loading } = useSelector<AppState, DashboardReducer>(
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,
);
const [saveLayoutState, setSaveLayoutState] = useState<State>({
loading: false,
error: false,
errorMessage: '',
payload: [],
});
const [selectedDashboard] = dashboards;
const { data } = selectedDashboard;
const { widgets } = data;
const [layouts, setLayout] = useState<LayoutProps[]>([]);
const AddWidgetWrapper = useCallback(() => <AddWidget />, []);
const dispatch = useDispatch<Dispatch<AppActions>>();
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]);
const [layouts, setLayout] = useState<LayoutProps[]>(
getPreLayouts(widgets, selectedDashboard.data.layout || []),
);
useEffect(() => {
if (
loading === false &&
(isMounted.current === true || isDeleted.current === true)
) {
const preLayouts = getPreLayouts();
setLayout(() => {
const getX = (): number => {
if (preLayouts && preLayouts?.length > 0) {
const last = preLayouts[(preLayouts?.length || 0) - 1];
(async (): Promise<void> => {
if (!isAddWidget) {
const isEmptyLayoutPresent = layouts.find((e) => e.i === 'empty');
if (isEmptyLayoutPresent) {
// non empty layout
const updatedLayout = layouts.filter((e) => e.i !== 'empty');
// non widget
const updatedWidget = widgets?.filter((e) => e.id !== 'empty');
setLayout(updatedLayout);
return (last.w + last.x) % 12;
}
return 0;
};
const updatedDashboard: Dashboard = {
...selectedDashboard,
data: {
...selectedDashboard.data,
layout: updatedLayout,
widgets: updatedWidget,
},
};
return [
...preLayouts,
{
i: (preLayouts.length + 1).toString(),
x: getX(),
y: Infinity,
w: 6,
h: 2,
Component: AddWidgetWrapper,
maxW: 6,
isDraggable: false,
isResizable: false,
isBounded: true,
},
];
});
}
await updateDashboardApi({
data: updatedDashboard.data,
uuid: updatedDashboard.uuid,
});
return (): void => {
isMounted.current = false;
};
}, [widgets, layouts.length, AddWidgetWrapper, loading, getPreLayouts]);
dispatch({
type: UPDATE_DASHBOARD,
payload: updatedDashboard,
});
}
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
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();
const onLayoutSaveHandler = useCallback(
async (layout: Layout[]) => {
try {
setSaveLayoutState((state) => ({
...state,
error: false,
errorMessage: '',
loading: true,
}));
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({
message:
error instanceof Error ? error.toString() : 'Something went wrong',
});
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> => {
setSaveLayoutState((state) => ({
...state,
error: false,
errorMessage: '',
loading: true,
}));
const setLayoutFunction = useCallback(
(layout: Layout[]) => {
setLayout(
layout.map((e) => {
const currentWidget =
widgets?.find((widget) => widget.id === e.i) || ({} as Widgets);
const response = await updateDashboardApi({
data: {
title: data.title,
description: data.description,
name: data.name,
tags: data.tags,
widgets: data.widgets,
layout: saveLayoutState.payload.filter((e) => e.maxW === undefined),
},
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,
}));
}
};
return {
...e,
Component: (): JSX.Element => (
<Graph
name={currentWidget.id}
widget={currentWidget}
yAxisUnit={currentWidget?.yAxisUnit}
layout={layout}
setLayout={setLayout}
/>
),
};
}),
);
},
[widgets],
);
const onLayoutChangeHandler = (layout: Layout[]): void => {
setSaveLayoutState({
loading: false,
error: false,
errorMessage: '',
payload: layout,
});
};
const onEmptyWidgetHandler = useCallback(async () => {
try {
const id = 'empty';
if (layouts.length === 0) {
return <Spinner height="40vh" size="large" tip="Loading..." />;
}
const layout = [
{
i: id,
w: 6,
x: 0,
h: 2,
y: 0,
},
...(data.layout || []),
];
return (
<>
{saveLayout && (
<ButtonContainer>
<Button
loading={saveLayoutState.loading}
onClick={onLayoutSaveHandler}
icon={<SaveFilled />}
danger={saveLayoutState.error}
>
Save Layout
</Button>
</ButtonContainer>
)}
await UpdateDashboard({
data,
generateWidgetId: id,
graphType: 'EMPTY_WIDGET',
selectedDashboard,
layout,
isRedirected: false,
});
<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] || {};
setLayoutFunction(layout);
} catch (error) {
notification.error({
message: error instanceof Error ? error.toString() : 'Something went wrong',
});
}
}, [data, selectedDashboard, setLayoutFunction]);
const type = widget?.panelTypes || 'TIME_SERIES';
const onLayoutChangeHandler = async (layout: Layout[]): Promise<void> => {
setLayoutFunction(layout);
const isQueryType = type === 'VALUE';
await onLayoutSaveHandler(layout);
};
const onAddPanelHandler = useCallback(() => {
try {
setAddPanelLoading(true);
const isEmptyLayoutPresent =
layouts.find((e) => e.i === 'empty') !== undefined;
if (!isEmptyLayoutPresent) {
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 (
<CardContainer
isQueryType={isQueryType}
isDarkMode={isDarkMode}
key={rest.i + JSON.stringify(widget)}
data-grid={rest}
>
<Card isDarkMode={isDarkMode} isQueryType={isQueryType}>
<Component />
</Card>
</CardContainer>
);
})}
</ReactGridLayout>
</>
return (
<GraphLayoutContainer
{...{
addPanelLoading,
layouts,
onAddPanelHandler,
onLayoutChangeHandler,
onLayoutSaveHandler,
saveLayoutState,
widgets,
setLayout,
}}
/>
);
}
interface LayoutProps extends Layout {
Component: () => JSX.Element;
interface ComponentProps {
setLayout: React.Dispatch<React.SetStateAction<LayoutProps[]>>;
}
export interface LayoutProps extends Layout {
Component: (props: ComponentProps) => JSX.Element;
}
interface State {
export interface State {
loading: boolean;
error: boolean;
payload: Layout[];
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);
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 RGL, { WidthProvider } from 'react-grid-layout';
import styled, { css } from 'styled-components';
const ReactGridLayoutComponent = WidthProvider(RGL);
interface Props {
isQueryType: boolean;
}
export const Card = styled(CardComponent)<Props>`
export const Card = styled(CardComponent)`
&&& {
height: 100%;
}
......@@ -54,9 +51,22 @@ export const ReactGridLayout = styled(ReactGridLayoutComponent)`
border: 1px solid #434343;
margin-top: 1rem;
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;
justify-content: end;
margin-top: 1rem;
......
import { notification } from 'antd';
import updateDashboardApi from 'api/dashboard/update';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import history from 'lib/history';
import { Layout } from 'react-grid-layout';
import store from 'store';
import { Dashboard } from 'types/api/dashboard/getAll';
export const updateDashboard = async ({
export const UpdateDashboard = async ({
data,
graphType,
generateWidgetId,
layout,
selectedDashboard,
}: UpdateDashboardProps): Promise<void> => {
const response = await updateDashboardApi({
isRedirected,
}: UpdateDashboardProps): Promise<Dashboard | undefined> => {
const updatedSelectedDashboard: Dashboard = {
...selectedDashboard,
data: {
title: data.title,
description: data.description,
......@@ -46,17 +48,27 @@ export const updateDashboard = async ({
layout,
},
uuid: selectedDashboard.uuid,
});
};
if (response.statusCode === 200) {
history.push(
`${history.location.pathname}/new?graphType=${graphType}&widgetId=${generateWidgetId}`,
);
} else {
const response = await updateDashboardApi(updatedSelectedDashboard);
if (response.payload) {
store.dispatch({
type: 'UPDATE_DASHBOARD',
payload: response.payload,
});
}
if (isRedirected) {
if (response.statusCode === 200) {
return response.payload;
}
notification.error({
message: response.error || 'Something went wrong',
});
return undefined;
}
return undefined;
};
interface UpdateDashboardProps {
......@@ -65,4 +77,5 @@ interface UpdateDashboardProps {
generateWidgetId: string;
layout: Layout[];
selectedDashboard: Dashboard;
isRedirected: boolean;
}
......@@ -15,11 +15,19 @@ import ROUTES from 'constants/routes';
import SearchFilter from 'container/ListOfDashboard/SearchFilter';
import useComponentPermission from 'hooks/useComponentPermission';
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 { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { generatePath } from 'react-router-dom';
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 AppReducer from 'types/reducer/app';
import DashboardReducer from 'types/reducer/dashboards';
......@@ -36,6 +44,7 @@ function ListOfAllDashboard(): JSX.Element {
const { dashboards, loading } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
const dispatch = useDispatch<Dispatch<AppActions>>();
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const [action, createNewDashboard, newDashboard] = useComponentPermission(
......@@ -131,6 +140,10 @@ function ListOfAllDashboard(): JSX.Element {
});
if (response.statusCode === 200) {
dispatch({
type: GET_ALL_DASHBOARD_SUCCESS,
payload: [],
});
history.push(
generatePath(ROUTES.DASHBOARD, {
dashboardId: response.payload.uuid,
......@@ -151,7 +164,7 @@ function ListOfAllDashboard(): JSX.Element {
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
});
}
}, [newDashboardState, t]);
}, [newDashboardState, t, dispatch]);
const getText = useCallback(() => {
if (!newDashboardState.error && !newDashboardState.loading) {
......
/* eslint-disable @typescript-eslint/naming-convention */
import { notification } from 'antd';
import { updateDashboard } from 'container/GridGraphLayout/utils';
import history from 'lib/history';
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 AppActions from 'types/actions';
import AppReducer from 'types/reducer/app';
import DashboardReducer from 'types/reducer/dashboards';
import { v4 as uuid } from 'uuid';
import menuItems, { ITEMS } from './menuItems';
import { Card, Container, Text } from './styles';
function DashboardGraphSlider(): JSX.Element {
function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element {
const { dashboards } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
......@@ -19,47 +25,30 @@ function DashboardGraphSlider(): JSX.Element {
const [selectedDashboard] = dashboards;
const { data } = selectedDashboard;
const onDragStartHandler: React.DragEventHandler<HTMLDivElement> = useCallback(
(event: React.DragEvent<HTMLDivElement>) => {
event.dataTransfer.setData('text/plain', event.currentTarget.id);
},
[],
);
const onClickHandler = useCallback(
async (name: ITEMS) => {
try {
const getX = (): number => {
if (data.layout && data.layout?.length > 0) {
const lastIndexX = data.layout[(data.layout?.length || 0) - 1];
return (lastIndexX.w + lastIndexX.x) % 12;
}
return 0;
};
const emptyLayout = data.layout?.find((e) => e.i === 'empty');
await updateDashboard({
data,
generateWidgetId: uuid(),
graphType: name,
layout: [
...(data.layout || []),
{
h: 2,
i: (((data.layout || [])?.length || 0) + 1).toString(),
w: 6,
x: getX(),
y: 0,
},
],
selectedDashboard,
});
if (emptyLayout === undefined) {
notification.error({
message: 'Please click on Add Panel Button',
});
return;
}
toggleAddWidget(false);
history.push(
`${history.location.pathname}/new?graphType=${name}&widgetId=${emptyLayout.i}`,
);
} catch (error) {
notification.error({
message: 'Something went wrong',
});
}
},
[data, selectedDashboard],
[data, toggleAddWidget],
);
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const fillColor: React.CSSProperties['color'] = isDarkMode ? 'white' : 'black';
......@@ -68,11 +57,12 @@ function DashboardGraphSlider(): JSX.Element {
<Container>
{menuItems.map(({ name, Icon, display }) => (
<Card
onClick={(): Promise<void> => onClickHandler(name)}
onClick={(event): void => {
event.preventDefault();
onClickHandler(name);
}}
id={name}
onDragStart={onDragStartHandler}
key={name}
draggable
>
<Icon fillColor={fillColor} />
<Text>{display}</Text>
......@@ -84,4 +74,18 @@ function DashboardGraphSlider(): JSX.Element {
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 {
name: ITEMS;
......
......@@ -5,7 +5,6 @@ import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import DashboardReducer from 'types/reducer/dashboards';
import { NewWidgetProps } from '../../index';
......@@ -19,7 +18,6 @@ function WidgetGraph({
const { dashboards, isQueryFired } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const [selectedDashboard] = dashboards;
const { search } = useLocation();
......@@ -33,11 +31,7 @@ function WidgetGraph({
const selectedWidget = widgets.find((e) => e.id === widgetId);
if (selectedWidget === undefined) {
return (
<Card isDarkMode={isDarkMode} isQueryType={false}>
Invalid widget
</Card>
);
return <Card>Invalid widget</Card>;
}
const { queryData } = selectedWidget;
......
......@@ -19,16 +19,20 @@ function NewDashboardPage({ getDashboard }: NewDashboardProps): JSX.Element {
const { dashboardId } = useParams<Params>();
useEffect(() => {
getDashboard({
uuid: dashboardId,
});
}, [getDashboard, dashboardId]);
if (dashboards.length !== 1) {
getDashboard({
uuid: dashboardId,
});
}
}, [getDashboard, dashboardId, dashboards.length]);
if (error && !loading && dashboards.length === 0) {
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.." />;
}
......
......@@ -340,7 +340,8 @@ function SignUp({ version }: SignUpProps): JSX.Element {
!organizationName ||
!password ||
!confirmPassword ||
confirmPasswordError
confirmPasswordError ||
isPasswordPolicyError
}
>
Get Started
......
import updateDashboardApi from 'api/dashboard/update';
import { AxiosError } from 'axios';
import { getPreLayouts, LayoutProps } from 'container/GridGraphLayout';
import { Dispatch } from 'redux';
import store from 'store';
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 = ({
widgetId,
setLayout,
}: DeleteWidgetProps): ((dispatch: Dispatch<AppActions>) => void) => {
return async (dispatch: Dispatch<AppActions>): Promise<void> => {
try {
......@@ -15,25 +18,32 @@ export const DeleteWidget = ({
const { widgets = [] } = selectedDashboard.data;
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: {
title: selectedDashboard.data.title,
description: selectedDashboard.data.description,
name: selectedDashboard.data.name,
tags: selectedDashboard.data.tags,
widgets: updatedWidgets,
layout: updatedLayout,
},
uuid: selectedDashboard.uuid,
});
};
const response = await updateDashboardApi(updatedSelectedDashboard);
if (response.statusCode === 200) {
dispatch({
type: 'DELETE_WIDGET_SUCCESS',
payload: {
widgetId,
},
type: UPDATE_DASHBOARD,
payload: updatedSelectedDashboard,
});
if (setLayout) {
setLayout(getPreLayouts(updatedWidgets, updatedLayout));
}
} else {
dispatch({
type: 'DELETE_WIDGET_ERROR',
......@@ -55,4 +65,5 @@ export const DeleteWidget = ({
export interface DeleteWidgetProps {
widgetId: Widgets['id'];
setLayout?: React.Dispatch<React.SetStateAction<LayoutProps[]>>;
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment