From ea5b40c7ea219e39bb050db34bd83f128f6802fb Mon Sep 17 00:00:00 2001 From: Palash <88981777+palash-signoz@users.noreply.github.com> Date: Tue, 28 Sep 2021 18:38:34 +0530 Subject: [PATCH] Feat(FE): Delete Query, Save Layout (#306) * feat: Delete Query functionality is added * feat: save layout is updated --- .../src/container/GridGraphLayout/index.tsx | 185 +++++++++++++----- .../src/container/GridGraphLayout/styles.ts | 16 +- .../NewDashboard/GridGraphs/index.tsx | 2 +- frontend/src/container/NewDashboard/index.tsx | 4 +- .../LeftContainer/QuerySection/Query.tsx | 75 ++++--- .../LeftContainer/QuerySection/index.tsx | 9 +- .../LeftContainer/QuerySection/styles.ts | 9 + .../store/actions/dashboard/deleteQuery.ts | 17 ++ frontend/src/store/actions/dashboard/index.ts | 1 + frontend/src/store/reducers/dashboard.ts | 45 +++++ frontend/src/types/actions/dashboard.ts | 15 +- frontend/src/types/api/dashboard/getAll.ts | 2 + 12 files changed, 294 insertions(+), 86 deletions(-) create mode 100644 frontend/src/store/actions/dashboard/deleteQuery.ts diff --git a/frontend/src/container/GridGraphLayout/index.tsx b/frontend/src/container/GridGraphLayout/index.tsx index 1d069bf835..49fbd0df25 100644 --- a/frontend/src/container/GridGraphLayout/index.tsx +++ b/frontend/src/container/GridGraphLayout/index.tsx @@ -1,3 +1,6 @@ +/* eslint-disable react/display-name */ +import { SaveFilled } from '@ant-design/icons'; +import updateDashboardApi from 'api/dashboard/update'; import Spinner from 'components/Spinner'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import React, { memo, useCallback, useEffect, useRef, useState } from 'react'; @@ -10,7 +13,13 @@ import { v4 } from 'uuid'; import AddWidget from './AddWidget'; import Graph from './Graph'; -import { Card, CardContainer, ReactGridLayout } from './styles'; +import { + Button, + ButtonContainer, + Card, + CardContainer, + ReactGridLayout, +} from './styles'; const GridGraph = (): JSX.Element => { const { push } = useHistory(); @@ -19,6 +28,12 @@ const GridGraph = (): JSX.Element => { const { dashboards, loading } = useSelector( (state) => state.dashboards, ); + const [saveLayoutState, setSaveLayoutState] = useState({ + loading: false, + error: false, + errorMessage: '', + payload: [], + }); const [selectedDashboard] = dashboards; const { data } = selectedDashboard; @@ -31,33 +46,41 @@ const GridGraph = (): JSX.Element => { const isMounted = useRef(true); const isDeleted = useRef(false); + const getPreLayouts: () => LayoutProps[] = useCallback(() => { + if (widgets === undefined) { + return []; + } + + 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 => ( + + ), + }; + }); + } else { + return data.layout.map((e, index) => ({ + ...e, + y: 0, + Component: (): JSX.Element => ( + + ), + })); + } + }, [widgets, data.layout]); + useEffect(() => { if ( loading === false && (isMounted.current === true || isDeleted.current === true) ) { - const getPreLayouts = (): LayoutProps[] => { - if (widgets === undefined) { - return []; - } - - return widgets.map((e, index) => { - return { - h: 2, - w: 6, - y: Infinity, - i: (index + 1).toString(), - x: (index % 2) * 6, - // eslint-disable-next-line react/display-name - Component: (): JSX.Element => ( - - ), - }; - }); - }; - const preLayouts = getPreLayouts(); - setLayout(() => [ ...preLayouts, { @@ -67,6 +90,10 @@ const GridGraph = (): JSX.Element => { w: 6, h: 2, Component: AddWidgetWrapper, + maxW: 6, + isDraggable: false, + isResizable: false, + isBounded: true, }, ]); } @@ -74,7 +101,7 @@ const GridGraph = (): JSX.Element => { return (): void => { isMounted.current = false; }; - }, [widgets, layouts.length, AddWidgetWrapper, loading]); + }, [widgets, layouts.length, AddWidgetWrapper, loading, getPreLayouts]); const onDropHandler = useCallback( (allLayouts: Layout[], currectLayout: Layout, event: DragEvent) => { @@ -88,38 +115,95 @@ const GridGraph = (): JSX.Element => { [pathname, push], ); + const onLayoutSaveHanlder = async (): Promise => { + setSaveLayoutState((state) => ({ + ...state, + error: false, + errorMessage: '', + loading: true, + })); + + const response = await updateDashboardApi({ + title: data.title, + uuid: selectedDashboard.uuid, + description: data.description, + name: data.name, + tags: data.tags, + widgets: data.widgets, + layout: saveLayoutState.payload.filter((e) => e.maxW === undefined), + }); + 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, + })); + } + }; + + const onLayoutChangeHandler = (layout: Layout[]): void => { + setSaveLayoutState({ + loading: false, + error: false, + errorMessage: '', + payload: layout, + }); + }; + if (layouts.length === 0) { return ; } return ( - - {layouts.map(({ Component, ...rest }, index) => { - const widget = (widgets || [])[index] || {}; + <> + + + - const type = widget.panelTypes; + + {layouts.map(({ Component, ...rest }, index) => { + const widget = (widgets || [])[index] || {}; - const isQueryType = type === 'VALUE'; + const type = widget.panelTypes; - return ( - - - - - - ); - })} - + const isQueryType = type === 'VALUE'; + + return ( + + + + + + ); + })} + + ); }; @@ -127,4 +211,11 @@ interface LayoutProps extends Layout { Component: () => JSX.Element; } +interface State { + loading: boolean; + error: boolean; + payload: Layout[]; + errorMessage: string; +} + export default memo(GridGraph); diff --git a/frontend/src/container/GridGraphLayout/styles.ts b/frontend/src/container/GridGraphLayout/styles.ts index eac707db79..d8172e4ebf 100644 --- a/frontend/src/container/GridGraphLayout/styles.ts +++ b/frontend/src/container/GridGraphLayout/styles.ts @@ -1,4 +1,4 @@ -import { Card as CardComponent } from 'antd'; +import { Button as ButtonComponent, Card as CardComponent } from 'antd'; import RGL, { WidthProvider } from 'react-grid-layout'; import styled from 'styled-components'; @@ -40,3 +40,17 @@ export const ReactGridLayout = styled(ReactGridLayoutComponent)` margin-top: 1rem; position: relative; `; + +export const ButtonContainer = styled.div` + display: flex; + justify-content: end; + margin-top: 1rem; +`; + +export const Button = styled(ButtonComponent)` + &&& { + display: flex; + justify-content: center; + align-items: center; + } +`; diff --git a/frontend/src/container/NewDashboard/GridGraphs/index.tsx b/frontend/src/container/NewDashboard/GridGraphs/index.tsx index bc689e0948..569a50968e 100644 --- a/frontend/src/container/NewDashboard/GridGraphs/index.tsx +++ b/frontend/src/container/NewDashboard/GridGraphs/index.tsx @@ -1,6 +1,6 @@ import GridGraphLayout from 'container/GridGraphLayout'; import ComponentsSlider from 'container/NewDashboard/ComponentsSlider'; -import React, { useCallback, useState } from 'react'; +import React from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import DashboardReducer from 'types/reducer/dashboards'; diff --git a/frontend/src/container/NewDashboard/index.tsx b/frontend/src/container/NewDashboard/index.tsx index edf2936414..a4607fbc1d 100644 --- a/frontend/src/container/NewDashboard/index.tsx +++ b/frontend/src/container/NewDashboard/index.tsx @@ -5,10 +5,10 @@ import GridGraphs from './GridGraphs'; const NewDashboard = (): JSX.Element => { return ( -
+ <> -
+ ); }; diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/Query.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/Query.tsx index 02729ab7c3..7d10e56518 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/Query.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/Query.tsx @@ -1,4 +1,4 @@ -import { Divider } from 'antd'; +import { Button, Divider } from 'antd'; import Input from 'components/Input'; import { timePreferance } from 'container/NewWidget/RightContainer/timeItems'; import React, { useCallback, useState } from 'react'; @@ -6,19 +6,22 @@ import { connect } from 'react-redux'; import { useLocation } from 'react-router'; import { bindActionCreators, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; +import { DeleteQuery } from 'store/actions'; import { UpdateQuery, UpdateQueryProps, } from 'store/actions/dashboard/updateQuery'; import AppActions from 'types/actions'; +import { DeleteQueryProps } from 'types/actions/dashboard'; -import { Container, InputContainer } from './styles'; +import { Container, InputContainer, QueryWrapper } from './styles'; const Query = ({ currentIndex, preLegend, preQuery, updateQuery, + deleteQuery, }: QueryProps): JSX.Element => { const [promqlQuery, setPromqlQuery] = useState(preQuery); const [legendFormat, setLegendFormat] = useState(preLegend); @@ -43,33 +46,47 @@ const Query = ({ }); }; - return ( - - - - onChangeHandler(setPromqlQuery, event.target.value) - } - size="middle" - value={promqlQuery} - addonBefore={'PromQL Query'} - onBlur={(): void => onBlurHandler()} - /> - + const onDeleteQueryHandler = (): void => { + deleteQuery({ + widgetId: widgetId, + currentIndex, + }); + }; + + return ( + <> + + + + + onChangeHandler(setPromqlQuery, event.target.value) + } + size="middle" + value={promqlQuery} + addonBefore={'PromQL Query'} + onBlur={(): void => onBlurHandler()} + /> + + + + + onChangeHandler(setLegendFormat, event.target.value) + } + size="middle" + value={legendFormat} + addonBefore={'Legend Format'} + onBlur={(): void => onBlurHandler()} + /> + + + + + - - - onChangeHandler(setLegendFormat, event.target.value) - } - size="middle" - value={legendFormat} - addonBefore={'Legend Format'} - onBlur={(): void => onBlurHandler()} - /> - - + ); }; @@ -77,12 +94,16 @@ interface DispatchProps { updateQuery: ( props: UpdateQueryProps, ) => (dispatch: Dispatch) => void; + deleteQuery: ( + props: DeleteQueryProps, + ) => (dispatch: Dispatch) => void; } const mapDispatchToProps = ( dispatch: ThunkDispatch, ): DispatchProps => ({ updateQuery: bindActionCreators(UpdateQuery, dispatch), + deleteQuery: bindActionCreators(DeleteQuery, dispatch), }); interface QueryProps extends DispatchProps { diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx index 3ca10d230e..6c1c194fa1 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx @@ -1,5 +1,4 @@ import { PlusOutlined } from '@ant-design/icons'; -import Spinner from 'components/Spinner'; import { timePreferance } from 'container/NewWidget/RightContainer/timeItems'; import React, { useCallback, useMemo } from 'react'; import { connect, useSelector } from 'react-redux'; @@ -47,12 +46,8 @@ const QuerySection = ({ }); }, [createQuery, urlQuery]); - if (query.length === 0) { - return ; - } - return ( -
+ <> {query.map((e, index) => ( }> Query -
+ ); }; diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/styles.ts b/frontend/src/container/NewWidget/LeftContainer/QuerySection/styles.ts index 6e63558615..b78ce58e08 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/styles.ts +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/styles.ts @@ -7,6 +7,7 @@ export const InputContainer = styled.div` export const Container = styled.div` margin-top: 1rem; + display: flex; `; export const QueryButton = styled(Button)` @@ -15,3 +16,11 @@ export const QueryButton = styled(Button)` align-items: center; } `; + +export const QueryWrapper = styled.div` + width: 100%; // parent need to 100% + + > div { + width: 95%; // each child is taking 95% of the parent + } +`; diff --git a/frontend/src/store/actions/dashboard/deleteQuery.ts b/frontend/src/store/actions/dashboard/deleteQuery.ts new file mode 100644 index 0000000000..c5940b43a8 --- /dev/null +++ b/frontend/src/store/actions/dashboard/deleteQuery.ts @@ -0,0 +1,17 @@ +import { Dispatch } from 'redux'; +import AppActions from 'types/actions'; +import { DeleteQueryProps } from 'types/actions/dashboard'; + +export const DeleteQuery = ( + props: DeleteQueryProps, +): ((dispatch: Dispatch) => void) => { + return (dispatch: Dispatch): void => { + dispatch({ + type: 'DELETE_QUERY', + payload: { + currentIndex: props.currentIndex, + widgetId: props.widgetId, + }, + }); + }; +}; diff --git a/frontend/src/store/actions/dashboard/index.ts b/frontend/src/store/actions/dashboard/index.ts index 4f0e75fa7d..cc4979e08e 100644 --- a/frontend/src/store/actions/dashboard/index.ts +++ b/frontend/src/store/actions/dashboard/index.ts @@ -1,6 +1,7 @@ export * from './applySettingsToPanel'; export * from './createQuery'; export * from './deleteDashboard'; +export * from './deleteQuery'; export * from './getAllDashboard'; export * from './getDashboard'; export * from './toggleEditMode'; diff --git a/frontend/src/store/reducers/dashboard.ts b/frontend/src/store/reducers/dashboard.ts index a0a39de747..41dbf42007 100644 --- a/frontend/src/store/reducers/dashboard.ts +++ b/frontend/src/store/reducers/dashboard.ts @@ -4,6 +4,7 @@ import { CREATE_NEW_QUERY, DashboardActions, DELETE_DASHBOARD_SUCCESS, + DELETE_QUERY, DELETE_WIDGET_SUCCESS, GET_ALL_DASHBOARD_ERROR, GET_ALL_DASHBOARD_LOADING_START, @@ -438,6 +439,50 @@ const dashboard = ( ], }; } + + case DELETE_QUERY: { + const { currentIndex, widgetId } = action.payload; + const { dashboards } = state; + const [selectedDashboard] = dashboards; + const { data } = selectedDashboard; + const { widgets = [] } = data; + + const selectedWidgetIndex = widgets.findIndex((e) => e.id === widgetId) || 0; + + const preWidget = widgets?.slice(0, selectedWidgetIndex) || []; + const afterWidget = + widgets?.slice( + selectedWidgetIndex + 1, // this is never undefined + widgets.length, + ) || []; + + const selectedWidget = widgets[selectedWidgetIndex]; + + const query = selectedWidget.query; + + const preQuery = query.slice(0, currentIndex); + const postQuery = query.slice(currentIndex + 1, query.length); + + return { + ...state, + dashboards: [ + { + ...selectedDashboard, + data: { + ...data, + widgets: [ + ...preWidget, + { + ...selectedWidget, + query: [...preQuery, ...postQuery], + }, + ...afterWidget, + ], + }, + }, + ], + }; + } default: return state; } diff --git a/frontend/src/types/actions/dashboard.ts b/frontend/src/types/actions/dashboard.ts index 965984b232..4a5022e587 100644 --- a/frontend/src/types/actions/dashboard.ts +++ b/frontend/src/types/actions/dashboard.ts @@ -40,6 +40,8 @@ export const DELETE_WIDGET_ERROR = 'DELETE_WIDGET_ERROR'; export const IS_ADD_WIDGET = 'IS_ADD_WIDGET'; +export const DELETE_QUERY = 'DELETE_QUERY'; + interface GetDashboard { type: typeof GET_DASHBOARD; payload: Dashboard; @@ -159,6 +161,16 @@ interface WidgetDeleteSuccess { }; } +export interface DeleteQueryProps { + widgetId: string; + currentIndex: number; +} + +interface DeleteQuery { + type: typeof DELETE_QUERY; + payload: DeleteQueryProps; +} + export type DashboardActions = | GetDashboard | UpdateDashboard @@ -177,4 +189,5 @@ export type DashboardActions = | SaveDashboardSuccess | WidgetDeleteSuccess | IsAddWidget - | UpdateQuery; + | UpdateQuery + | DeleteQuery; diff --git a/frontend/src/types/api/dashboard/getAll.ts b/frontend/src/types/api/dashboard/getAll.ts index 5bdca3f493..d79930b2b6 100644 --- a/frontend/src/types/api/dashboard/getAll.ts +++ b/frontend/src/types/api/dashboard/getAll.ts @@ -1,5 +1,6 @@ import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems'; +import { Layout } from 'react-grid-layout'; import { QueryData } from '../widgets/getQuery'; @@ -19,6 +20,7 @@ export interface DashboardData { name?: string; widgets?: Widgets[]; title: string; + layout?: Layout[]; } export interface Widgets {