From 1c90e621898e6a0294ce59d5e04e52e4a65afcac Mon Sep 17 00:00:00 2001 From: palash-signoz <88981777+palash-signoz@users.noreply.github.com> Date: Tue, 7 Jun 2022 16:14:49 +0530 Subject: [PATCH] feat: dashboard layout is updated (#1221) * feat: dashboard layout is updated * feat: onClick is made fixed * feat: layout is updated * feat: layout is updated * feat: layout is updated * fix: memo is removed and grid layout component is refactored to use use query * fix: saveDashboard is updated * feat: layout is fixed * fix: tsc error are fixed * fix: delete widgets is updated * fix: useMount once is added * fix: useMount once is removed * chore: removed the commented code Co-authored-by: Ankit Nayan --- frontend/package.json | 2 +- .../GridGraphLayout/AddWidget/index.tsx | 53 -- .../GridGraphLayout/AddWidget/styles.ts | 18 - .../GridGraphLayout/EmptyWidget/index.tsx | 16 + .../GridGraphLayout/EmptyWidget/styles.ts | 8 + .../container/GridGraphLayout/Graph/index.tsx | 222 ++++---- .../container/GridGraphLayout/GraphLayout.tsx | 101 ++++ .../GridGraphLayout/WidgetHeader/index.tsx | 2 +- .../src/container/GridGraphLayout/index.tsx | 479 +++++++++--------- .../src/container/GridGraphLayout/styles.ts | 22 +- .../src/container/GridGraphLayout/utils.ts | 33 +- .../NewDashboard/ComponentsSlider/index.tsx | 82 +-- .../ComponentsSlider/menuItems.ts | 2 +- .../LeftContainer/WidgetGraph/index.tsx | 8 +- frontend/src/pages/NewDashboard/index.tsx | 14 +- .../store/actions/dashboard/deleteWidget.ts | 25 +- .../store/actions/dashboard/saveDashboard.ts | 33 +- .../store/actions/trace/getInitialFilter.ts | 2 +- .../store/actions/trace/parseFilter/filter.ts | 2 +- .../spanAggregateCurrentPageSize.ts | 2 +- frontend/src/store/reducers/dashboard.ts | 8 +- frontend/src/types/actions/dashboard.ts | 2 + frontend/yarn.lock | 2 +- 23 files changed, 631 insertions(+), 507 deletions(-) delete mode 100644 frontend/src/container/GridGraphLayout/AddWidget/index.tsx delete mode 100644 frontend/src/container/GridGraphLayout/AddWidget/styles.ts create mode 100644 frontend/src/container/GridGraphLayout/EmptyWidget/index.tsx create mode 100644 frontend/src/container/GridGraphLayout/EmptyWidget/styles.ts create mode 100644 frontend/src/container/GridGraphLayout/GraphLayout.tsx diff --git a/frontend/package.json b/frontend/package.json index c15ce3e693..ebaffb5fae 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/container/GridGraphLayout/AddWidget/index.tsx b/frontend/src/container/GridGraphLayout/AddWidget/index.tsx deleted file mode 100644 index cbe6116aaf..0000000000 --- a/frontend/src/container/GridGraphLayout/AddWidget/index.tsx +++ /dev/null @@ -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( - (state) => state.dashboards, - ); - - const onToggleHandler = useCallback(() => { - toggleAddWidget(true); - }, [toggleAddWidget]); - - return ( - - {!isAddWidget ? ( - - ) : ( - Click a widget icon to add it here - )} - - ); -} - -interface DispatchProps { - toggleAddWidget: ( - props: ToggleAddWidgetProps, - ) => (dispatch: Dispatch) => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - toggleAddWidget: bindActionCreators(ToggleAddWidget, dispatch), -}); - -type Props = DispatchProps; - -export default connect(null, mapDispatchToProps)(AddWidget); diff --git a/frontend/src/container/GridGraphLayout/AddWidget/styles.ts b/frontend/src/container/GridGraphLayout/AddWidget/styles.ts deleted file mode 100644 index 9a7d6b58f3..0000000000 --- a/frontend/src/container/GridGraphLayout/AddWidget/styles.ts +++ /dev/null @@ -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%; -`; diff --git a/frontend/src/container/GridGraphLayout/EmptyWidget/index.tsx b/frontend/src/container/GridGraphLayout/EmptyWidget/index.tsx new file mode 100644 index 0000000000..df9b7662fe --- /dev/null +++ b/frontend/src/container/GridGraphLayout/EmptyWidget/index.tsx @@ -0,0 +1,16 @@ +import { Typography } from 'antd'; +import React from 'react'; + +import { Container } from './styles'; + +function EmptyWidget(): JSX.Element { + return ( + + + Click one of the widget types above (Time Series / Value) to add here + + + ); +} + +export default EmptyWidget; diff --git a/frontend/src/container/GridGraphLayout/EmptyWidget/styles.ts b/frontend/src/container/GridGraphLayout/EmptyWidget/styles.ts new file mode 100644 index 0000000000..914b47cb9b --- /dev/null +++ b/frontend/src/container/GridGraphLayout/EmptyWidget/styles.ts @@ -0,0 +1,8 @@ +import styled from 'styled-components'; + +export const Container = styled.div` + height: 100%; + display: flex; + justify-content: center; + align-items: center; +`; diff --git a/frontend/src/container/GridGraphLayout/Graph/index.tsx b/frontend/src/container/GridGraphLayout/Graph/index.tsx index 42d1d3fb49..a57a396fd0 100644 --- a/frontend/src/container/GridGraphLayout/Graph/index.tsx +++ b/frontend/src/container/GridGraphLayout/Graph/index.tsx @@ -1,13 +1,14 @@ 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({ - loading: true, - errorMessage: '', - error: false, - payload: undefined, - }); const [hovered, setHovered] = useState(false); const [modal, setModal] = useState(false); const { minTime, maxTime } = useSelector( (state) => state.globalTime, ); - const [deleteModal, setDeletModal] = useState(false); + const [deleteModal, setDeleteModal] = useState(false); - useEffect(() => { - (async (): Promise => { - try { - const getMaxMinTime = GetMaxMinTime({ - graphType: widget?.panelTypes, - maxTime, - minTime, - }); + const getMaxMinTime = GetMaxMinTime({ + graphType: widget?.panelTypes, + maxTime, + minTime, + }); - const { start, end } = GetStartAndEndTime({ - type: widget.timePreferance, - maxTime: getMaxMinTime.maxTime, - minTime: getMaxMinTime.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', - }); + const queryLength = widget?.query?.filter((e) => e.query.length !== 0) || []; - 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 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, + }; + }), + ); - 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 isError = + response.find((e) => e?.data?.statusCode !== 200) !== undefined || + response.some((e) => e.isError === true); + + 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>) => { @@ -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 ( <> 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 ; + } + + 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)} /> - {state.errorMessage} + {errorMessage} ); } - if (state.loading === true || state.payload === undefined) { - return ; - } + 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 ( - onToggleModal(setModal)} - onDelete={(): void => onToggleModal(setDeletModal)} - /> + {!isEmptyLayout && ( + onToggleModal(setModal)} + onDelete={(): void => onToggleModal(setDeleteModal)} + /> + )} - {getModals()} + {!isEmptyLayout && getModals()} - + {!isEmpty(widget) && ( + + )} + + {isEmptyLayout && } ); } -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; 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>; } 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)); diff --git a/frontend/src/container/GridGraphLayout/GraphLayout.tsx b/frontend/src/container/GridGraphLayout/GraphLayout.tsx new file mode 100644 index 0000000000..32b87ee09d --- /dev/null +++ b/frontend/src/container/GridGraphLayout/GraphLayout.tsx @@ -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((state) => state.app); + const { isDarkMode } = useSelector((state) => state.app); + + const [saveLayout] = useComponentPermission(['save_layout'], role); + + return ( + <> + + {saveLayout && ( + + )} + + + + + + {layouts.map(({ Component, ...rest }) => { + const currentWidget = (widgets || [])?.find((e) => e.id === rest.i); + + return ( + + + + + + ); + })} + + + ); +} + +interface GraphLayoutProps { + layouts: LayoutProps[]; + saveLayoutState: State; + onLayoutSaveHandler: (layout: Layout[]) => Promise; + addPanelLoading: boolean; + onAddPanelHandler: VoidFunction; + onLayoutChangeHandler: (layout: Layout[]) => Promise; + widgets: Widgets[] | undefined; + setLayout: React.Dispatch>; +} + +export default GraphLayout; diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx b/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx index a627bac3e1..d560aaab82 100644 --- a/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx +++ b/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx @@ -104,7 +104,7 @@ function WidgetHeader({ overlay={menu} trigger={['click']} overlayStyle={{ minWidth: 100 }} - placement="bottomCenter" + placement="bottom" > setLocalHover(true)} diff --git a/frontend/src/container/GridGraphLayout/index.tsx b/frontend/src/container/GridGraphLayout/index.tsx index 80abd97593..73fb17f759 100644 --- a/frontend/src/container/GridGraphLayout/index.tsx +++ b/frontend/src/container/GridGraphLayout/index.tsx @@ -1,285 +1,288 @@ /* 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 { AppState } from 'store/reducers'; -import AppReducer from 'types/reducer/app'; -import DashboardReducer from 'types/reducer/dashboards'; -import { v4 } from 'uuid'; - -import AddWidget from './AddWidget'; -import Graph from './Graph'; +import { useTranslation } from 'react-i18next'; +import { connect, useDispatch, useSelector } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; import { - Button, - ButtonContainer, - Card, - CardContainer, - ReactGridLayout, -} from './styles'; -import { updateDashboard } from './utils'; + ToggleAddWidget, + ToggleAddWidgetProps, +} from 'store/actions/dashboard/toggleAddWidget'; +import { AppState } from 'store/reducers'; +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'; -function GridGraph(): JSX.Element { - const { dashboards, loading } = useSelector( +import Graph from './Graph'; +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 ( + + ); + }, + })); + +function GridGraph(props: Props): JSX.Element { + const { toggleAddWidget } = props; + const [addPanelLoading, setAddPanelLoading] = useState(false); + const { t } = useTranslation(['common']); + const { dashboards, isAddWidget } = useSelector( (state) => state.dashboards, ); - const [saveLayoutState, setSaveLayoutState] = useState({ loading: false, error: false, errorMessage: '', payload: [], }); - const [selectedDashboard] = dashboards; const { data } = selectedDashboard; const { widgets } = data; - const [layouts, setLayout] = useState([]); + const dispatch = useDispatch>(); - const AddWidgetWrapper = useCallback(() => , []); - - const isMounted = useRef(true); - const isDeleted = useRef(false); - const { role } = useSelector((state) => state.app); - - const { isDarkMode } = useSelector((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 => ( - - ), - }; - }); - } - return data.layout - .filter((_, index) => widgets[index]) - .map((e, index) => ({ - ...e, - Component: (): JSX.Element => { - if (widgets[index]) { - return ( - - ); - } - return
; - }, - })); - }, [widgets, data?.layout]); + const [layouts, setLayout] = useState( + 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 => { + 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, - }, - ]; - }); - } - - 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), + await updateDashboardApi({ + data: updatedDashboard.data, + uuid: updatedDashboard.uuid, }); - } catch (error) { - notification.error({ - message: - error instanceof Error ? error.toString() : 'Something went wrong', + + dispatch({ + type: UPDATE_DASHBOARD, + 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 => { - 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 => ( + + ), + }; + }), + ); + }, + [widgets], + ); + + const onEmptyWidgetHandler = useCallback(async () => { + try { + const id = 'empty'; + + const layout = [ + { + 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 => { + setLayoutFunction(layout); + + await onLayoutSaveHandler(layout); }; - const onLayoutChangeHandler = (layout: Layout[]): void => { - setSaveLayoutState({ - loading: false, - error: false, - errorMessage: '', - payload: layout, - }); - }; + const onAddPanelHandler = useCallback(() => { + try { + setAddPanelLoading(true); + const isEmptyLayoutPresent = + layouts.find((e) => e.i === 'empty') !== undefined; - if (layouts.length === 0) { - return ; - } + 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 ( - <> - {saveLayout && ( - - - - )} - - - {layouts.map(({ Component, ...rest }, index) => { - const widget = (widgets || [])[index] || {}; - - const type = widget?.panelTypes || 'TIME_SERIES'; - - const isQueryType = type === 'VALUE'; - - return ( - - - - - - ); - })} - - + ); } -interface LayoutProps extends Layout { - Component: () => JSX.Element; +interface ComponentProps { + setLayout: React.Dispatch>; } -interface State { +export interface LayoutProps extends Layout { + Component: (props: ComponentProps) => JSX.Element; +} + +export interface State { loading: boolean; error: boolean; payload: Layout[]; errorMessage: string; } -export default memo(GridGraph); +interface DispatchProps { + toggleAddWidget: ( + props: ToggleAddWidgetProps, + ) => (dispatch: Dispatch) => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + toggleAddWidget: bindActionCreators(ToggleAddWidget, dispatch), +}); + +type Props = DispatchProps; + +export default connect(null, mapDispatchToProps)(GridGraph); diff --git a/frontend/src/container/GridGraphLayout/styles.ts b/frontend/src/container/GridGraphLayout/styles.ts index 880697fbd1..9bb4b219bb 100644 --- a/frontend/src/container/GridGraphLayout/styles.ts +++ b/frontend/src/container/GridGraphLayout/styles.ts @@ -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 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)` +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; diff --git a/frontend/src/container/GridGraphLayout/utils.ts b/frontend/src/container/GridGraphLayout/utils.ts index 2bfcb4e61f..67b854368c 100644 --- a/frontend/src/container/GridGraphLayout/utils.ts +++ b/frontend/src/container/GridGraphLayout/utils.ts @@ -1,18 +1,20 @@ 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 => { - const response = await updateDashboardApi({ + isRedirected, +}: UpdateDashboardProps): Promise => { + 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; } diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx index 6647f54b3a..a67deef062 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx +++ b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx @@ -1,17 +1,23 @@ /* 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( (state) => state.dashboards, ); @@ -19,47 +25,30 @@ function DashboardGraphSlider(): JSX.Element { const [selectedDashboard] = dashboards; const { data } = selectedDashboard; - const onDragStartHandler: React.DragEventHandler = useCallback( - (event: React.DragEvent) => { - 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((state) => state.app); const fillColor: React.CSSProperties['color'] = isDarkMode ? 'white' : 'black'; @@ -68,11 +57,12 @@ function DashboardGraphSlider(): JSX.Element { {menuItems.map(({ name, Icon, display }) => ( => onClickHandler(name)} + onClick={(event): void => { + event.preventDefault(); + onClickHandler(name); + }} id={name} - onDragStart={onDragStartHandler} key={name} - draggable > {display} @@ -84,4 +74,18 @@ function DashboardGraphSlider(): JSX.Element { export type GRAPH_TYPES = ITEMS; -export default DashboardGraphSlider; +interface DispatchProps { + toggleAddWidget: ( + props: ToggleAddWidgetProps, + ) => (dispatch: Dispatch) => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + toggleAddWidget: bindActionCreators(ToggleAddWidget, dispatch), +}); + +type Props = DispatchProps; + +export default connect(null, mapDispatchToProps)(DashboardGraphSlider); diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts b/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts index d820142baa..12eeab0751 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts +++ b/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts @@ -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; diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx index dc0945be84..2fa28b7e1b 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx @@ -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( (state) => state.dashboards, ); - const { isDarkMode } = useSelector((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 ( - - Invalid widget - - ); + return Invalid widget; } const { queryData } = selectedWidget; diff --git a/frontend/src/pages/NewDashboard/index.tsx b/frontend/src/pages/NewDashboard/index.tsx index fbe9198568..6d35ffede4 100644 --- a/frontend/src/pages/NewDashboard/index.tsx +++ b/frontend/src/pages/NewDashboard/index.tsx @@ -19,16 +19,20 @@ function NewDashboardPage({ getDashboard }: NewDashboardProps): JSX.Element { const { dashboardId } = useParams(); useEffect(() => { - getDashboard({ - uuid: dashboardId, - }); - }, [getDashboard, dashboardId]); + if (dashboards.length !== 1) { + getDashboard({ + uuid: dashboardId, + }); + } + }, [getDashboard, dashboardId, dashboards.length]); if (error && !loading && dashboards.length === 0) { return
{errorMessage}
; } - 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 ; } diff --git a/frontend/src/store/actions/dashboard/deleteWidget.ts b/frontend/src/store/actions/dashboard/deleteWidget.ts index 6e2a34cf93..3aea4902b8 100644 --- a/frontend/src/store/actions/dashboard/deleteWidget.ts +++ b/frontend/src/store/actions/dashboard/deleteWidget.ts @@ -1,12 +1,15 @@ 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) => void) => { return async (dispatch: Dispatch): Promise => { 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>; } diff --git a/frontend/src/store/actions/dashboard/saveDashboard.ts b/frontend/src/store/actions/dashboard/saveDashboard.ts index 32ca92ab8b..a1e28d3efe 100644 --- a/frontend/src/store/actions/dashboard/saveDashboard.ts +++ b/frontend/src/store/actions/dashboard/saveDashboard.ts @@ -2,11 +2,13 @@ import updateDashboardApi from 'api/dashboard/update'; import { AxiosError } from 'axios'; import ROUTES from 'constants/routes'; import history from 'lib/history'; +import { Layout } from 'react-grid-layout'; import { generatePath } from 'react-router-dom'; import { Dispatch } from 'redux'; import store from 'store'; import AppActions from 'types/actions'; import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; +import { v4 } from 'uuid'; export const SaveDashboard = ({ uuid, @@ -20,9 +22,11 @@ export const SaveDashboard = ({ dashboardId, yAxisUnit, }: SaveDashboardProps): ((dispatch: Dispatch) => void) => { + // eslint-disable-next-line sonarjs/cognitive-complexity return async (dispatch: Dispatch): Promise => { try { const dashboard = store.getState(); + const search = new URLSearchParams(history.location.search); const selectedDashboard = dashboard.dashboards.dashboards.find( (e) => e.uuid === uuid, @@ -46,16 +50,41 @@ export const SaveDashboard = ({ (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 afterWidget = data.widgets?.slice( (selectedWidgetIndex || 0) + 1, // this is never undefined data.widgets?.length, ) || []; + const selectedWidget = (selectedDashboard.data.widgets || [])[ 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({ data: { ...selectedDashboard.data, @@ -64,19 +93,21 @@ export const SaveDashboard = ({ description: selectedDashboard.data.description, tags: selectedDashboard.data.tags, name: selectedDashboard.data.name, + layout: allLayout, // as we are updated the widget only widgets: [ ...preWidget, { ...selectedWidget, description: updatedDescription, - id: widgetId, + id: isEmptyWidget ? newWidgetId : widgetId, isStacked: updatedisStacked, nullZeroValues: updatednullZeroValues, opacity: updatedopacity, title: updatedTitle, timePreferance: updatedtimePreferance, yAxisUnit: updatedYAxisUnit, + panelTypes: search.get('graphType') as Widgets['panelTypes'], queryData: { ...selectedWidget.queryData, data: [ diff --git a/frontend/src/store/actions/trace/getInitialFilter.ts b/frontend/src/store/actions/trace/getInitialFilter.ts index ea6487d78e..6d5247bcf4 100644 --- a/frontend/src/store/actions/trace/getInitialFilter.ts +++ b/frontend/src/store/actions/trace/getInitialFilter.ts @@ -191,7 +191,7 @@ export const GetInitialTraceFilter = ( }, }); } catch (error) { - console.log(error); + console.error(error); dispatch({ type: UPDATE_TRACE_FILTER_LOADING, payload: { diff --git a/frontend/src/store/actions/trace/parseFilter/filter.ts b/frontend/src/store/actions/trace/parseFilter/filter.ts index 934d370864..3f7a594843 100644 --- a/frontend/src/store/actions/trace/parseFilter/filter.ts +++ b/frontend/src/store/actions/trace/parseFilter/filter.ts @@ -26,7 +26,7 @@ export const parseQueryIntoFilter = ( }); } } catch (error) { - console.log(error); + console.error(error); } } diff --git a/frontend/src/store/actions/trace/parseFilter/spanAggregateCurrentPageSize.ts b/frontend/src/store/actions/trace/parseFilter/spanAggregateCurrentPageSize.ts index 6bdabe7870..76b39d05af 100644 --- a/frontend/src/store/actions/trace/parseFilter/spanAggregateCurrentPageSize.ts +++ b/frontend/src/store/actions/trace/parseFilter/spanAggregateCurrentPageSize.ts @@ -19,7 +19,7 @@ export const parseQueryIntoPageSize = ( current = parseInt(parsedValue, 10); } } catch (error) { - console.log('error while parsing json'); + console.error('error while parsing json'); } } diff --git a/frontend/src/store/reducers/dashboard.ts b/frontend/src/store/reducers/dashboard.ts index 7e46a21d3c..d205341e44 100644 --- a/frontend/src/store/reducers/dashboard.ts +++ b/frontend/src/store/reducers/dashboard.ts @@ -17,6 +17,7 @@ import { QUERY_SUCCESS, SAVE_SETTING_TO_PANEL_SUCCESS, TOGGLE_EDIT_MODE, + UPDATE_DASHBOARD, UPDATE_QUERY, UPDATE_TITLE_DESCRIPTION_TAGS_SUCCESS, } 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; return { @@ -369,7 +371,7 @@ const dashboard = ( } case DELETE_WIDGET_SUCCESS: { - const { widgetId } = action.payload; + const { widgetId, layout } = action.payload; const { dashboards } = state; const [selectedDashboard] = dashboards; @@ -384,6 +386,7 @@ const dashboard = ( data: { ...data, widgets: widgets.filter((e) => e.id !== widgetId), + layout, }, }, ], @@ -480,6 +483,7 @@ const dashboard = ( ], }; } + default: return state; } diff --git a/frontend/src/types/actions/dashboard.ts b/frontend/src/types/actions/dashboard.ts index b0d02d1b67..5f60ce747d 100644 --- a/frontend/src/types/actions/dashboard.ts +++ b/frontend/src/types/actions/dashboard.ts @@ -1,3 +1,4 @@ +import { Layout } from 'react-grid-layout'; import { ApplySettingsToPanelProps } from 'store/actions/dashboard/applySettingsToPanel'; import { Dashboard, Query, Widgets } from 'types/api/dashboard/getAll'; import { QueryData } from 'types/api/widgets/getQuery'; @@ -159,6 +160,7 @@ interface WidgetDeleteSuccess { type: typeof DELETE_WIDGET_SUCCESS; payload: { widgetId: Widgets['id']; + layout: Layout[]; }; } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 13ce6c7403..2227658f6b 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -10794,7 +10794,7 @@ react-graph-vis@^1.0.5: vis-data "^7.1.2" vis-network "^9.0.0" -react-grid-layout@^1.2.5: +react-grid-layout@^1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-1.3.4.tgz#4fa819be24a1ba9268aa11b82d63afc4762a32ff" integrity sha512-sB3rNhorW77HUdOjB4JkelZTdJGQKuXLl3gNg+BI8gJkTScspL1myfZzW/EM0dLEn+1eH+xW+wNqk0oIM9o7cw==