Feat(FE): Delete Query, Save Layout (#306)

* feat: Delete Query functionality is added

* feat: save layout is updated
This commit is contained in:
Palash 2021-09-28 18:38:34 +05:30 committed by GitHub
parent cc91242e9a
commit ea5b40c7ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 294 additions and 86 deletions

View File

@ -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 Spinner from 'components/Spinner';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import React, { memo, useCallback, useEffect, useRef, useState } from 'react'; import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
@ -10,7 +13,13 @@ import { v4 } from 'uuid';
import AddWidget from './AddWidget'; import AddWidget from './AddWidget';
import Graph from './Graph'; import Graph from './Graph';
import { Card, CardContainer, ReactGridLayout } from './styles'; import {
Button,
ButtonContainer,
Card,
CardContainer,
ReactGridLayout,
} from './styles';
const GridGraph = (): JSX.Element => { const GridGraph = (): JSX.Element => {
const { push } = useHistory(); const { push } = useHistory();
@ -19,6 +28,12 @@ const GridGraph = (): JSX.Element => {
const { dashboards, loading } = useSelector<AppState, DashboardReducer>( const { dashboards, loading } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards, (state) => state.dashboards,
); );
const [saveLayoutState, setSaveLayoutState] = useState<State>({
loading: false,
error: false,
errorMessage: '',
payload: [],
});
const [selectedDashboard] = dashboards; const [selectedDashboard] = dashboards;
const { data } = selectedDashboard; const { data } = selectedDashboard;
@ -31,16 +46,12 @@ const GridGraph = (): JSX.Element => {
const isMounted = useRef(true); const isMounted = useRef(true);
const isDeleted = useRef(false); const isDeleted = useRef(false);
useEffect(() => { const getPreLayouts: () => LayoutProps[] = useCallback(() => {
if (
loading === false &&
(isMounted.current === true || isDeleted.current === true)
) {
const getPreLayouts = (): LayoutProps[] => {
if (widgets === undefined) { if (widgets === undefined) {
return []; return [];
} }
if (data.layout === undefined) {
return widgets.map((e, index) => { return widgets.map((e, index) => {
return { return {
h: 2, h: 2,
@ -48,16 +59,28 @@ const GridGraph = (): JSX.Element => {
y: Infinity, y: Infinity,
i: (index + 1).toString(), i: (index + 1).toString(),
x: (index % 2) * 6, x: (index % 2) * 6,
// eslint-disable-next-line react/display-name
Component: (): JSX.Element => ( Component: (): JSX.Element => (
<Graph isDeleted={isDeleted} widget={widgets[index]} /> <Graph isDeleted={isDeleted} widget={widgets[index]} />
), ),
}; };
}); });
}; } else {
return data.layout.map((e, index) => ({
...e,
y: 0,
Component: (): JSX.Element => (
<Graph isDeleted={isDeleted} widget={widgets[index]} />
),
}));
}
}, [widgets, data.layout]);
useEffect(() => {
if (
loading === false &&
(isMounted.current === true || isDeleted.current === true)
) {
const preLayouts = getPreLayouts(); const preLayouts = getPreLayouts();
setLayout(() => [ setLayout(() => [
...preLayouts, ...preLayouts,
{ {
@ -67,6 +90,10 @@ const GridGraph = (): JSX.Element => {
w: 6, w: 6,
h: 2, h: 2,
Component: AddWidgetWrapper, Component: AddWidgetWrapper,
maxW: 6,
isDraggable: false,
isResizable: false,
isBounded: true,
}, },
]); ]);
} }
@ -74,7 +101,7 @@ const GridGraph = (): JSX.Element => {
return (): void => { return (): void => {
isMounted.current = false; isMounted.current = false;
}; };
}, [widgets, layouts.length, AddWidgetWrapper, loading]); }, [widgets, layouts.length, AddWidgetWrapper, loading, getPreLayouts]);
const onDropHandler = useCallback( const onDropHandler = useCallback(
(allLayouts: Layout[], currectLayout: Layout, event: DragEvent) => { (allLayouts: Layout[], currectLayout: Layout, event: DragEvent) => {
@ -88,11 +115,66 @@ const GridGraph = (): JSX.Element => {
[pathname, push], [pathname, push],
); );
const onLayoutSaveHanlder = async (): Promise<void> => {
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) { if (layouts.length === 0) {
return <Spinner height="40vh" size="large" tip="Loading..." />; return <Spinner height="40vh" size="large" tip="Loading..." />;
} }
return ( return (
<>
<ButtonContainer>
<Button
loading={saveLayoutState.loading}
onClick={onLayoutSaveHanlder}
icon={<SaveFilled />}
danger={saveLayoutState.error}
>
Save Layout
</Button>
</ButtonContainer>
<ReactGridLayout <ReactGridLayout
isResizable isResizable
isDraggable isDraggable
@ -103,6 +185,7 @@ const GridGraph = (): JSX.Element => {
isDroppable isDroppable
useCSSTransforms useCSSTransforms
onDrop={onDropHandler} onDrop={onDropHandler}
onLayoutChange={onLayoutChangeHandler}
> >
{layouts.map(({ Component, ...rest }, index) => { {layouts.map(({ Component, ...rest }, index) => {
const widget = (widgets || [])[index] || {}; const widget = (widgets || [])[index] || {};
@ -120,6 +203,7 @@ const GridGraph = (): JSX.Element => {
); );
})} })}
</ReactGridLayout> </ReactGridLayout>
</>
); );
}; };
@ -127,4 +211,11 @@ interface LayoutProps extends Layout {
Component: () => JSX.Element; Component: () => JSX.Element;
} }
interface State {
loading: boolean;
error: boolean;
payload: Layout[];
errorMessage: string;
}
export default memo(GridGraph); export default memo(GridGraph);

View File

@ -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 RGL, { WidthProvider } from 'react-grid-layout';
import styled from 'styled-components'; import styled from 'styled-components';
@ -40,3 +40,17 @@ export const ReactGridLayout = styled(ReactGridLayoutComponent)`
margin-top: 1rem; margin-top: 1rem;
position: relative; 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;
}
`;

View File

@ -1,6 +1,6 @@
import GridGraphLayout from 'container/GridGraphLayout'; import GridGraphLayout from 'container/GridGraphLayout';
import ComponentsSlider from 'container/NewDashboard/ComponentsSlider'; import ComponentsSlider from 'container/NewDashboard/ComponentsSlider';
import React, { useCallback, useState } from 'react'; import React from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import DashboardReducer from 'types/reducer/dashboards'; import DashboardReducer from 'types/reducer/dashboards';

View File

@ -5,10 +5,10 @@ import GridGraphs from './GridGraphs';
const NewDashboard = (): JSX.Element => { const NewDashboard = (): JSX.Element => {
return ( return (
<div> <>
<Description /> <Description />
<GridGraphs /> <GridGraphs />
</div> </>
); );
}; };

View File

@ -1,4 +1,4 @@
import { Divider } from 'antd'; import { Button, Divider } from 'antd';
import Input from 'components/Input'; import Input from 'components/Input';
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems'; import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
@ -6,19 +6,22 @@ import { connect } from 'react-redux';
import { useLocation } from 'react-router'; import { useLocation } from 'react-router';
import { bindActionCreators, Dispatch } from 'redux'; import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk'; import { ThunkDispatch } from 'redux-thunk';
import { DeleteQuery } from 'store/actions';
import { import {
UpdateQuery, UpdateQuery,
UpdateQueryProps, UpdateQueryProps,
} from 'store/actions/dashboard/updateQuery'; } from 'store/actions/dashboard/updateQuery';
import AppActions from 'types/actions'; import AppActions from 'types/actions';
import { DeleteQueryProps } from 'types/actions/dashboard';
import { Container, InputContainer } from './styles'; import { Container, InputContainer, QueryWrapper } from './styles';
const Query = ({ const Query = ({
currentIndex, currentIndex,
preLegend, preLegend,
preQuery, preQuery,
updateQuery, updateQuery,
deleteQuery,
}: QueryProps): JSX.Element => { }: QueryProps): JSX.Element => {
const [promqlQuery, setPromqlQuery] = useState(preQuery); const [promqlQuery, setPromqlQuery] = useState(preQuery);
const [legendFormat, setLegendFormat] = useState(preLegend); const [legendFormat, setLegendFormat] = useState(preLegend);
@ -43,8 +46,17 @@ const Query = ({
}); });
}; };
const onDeleteQueryHandler = (): void => {
deleteQuery({
widgetId: widgetId,
currentIndex,
});
};
return ( return (
<>
<Container> <Container>
<QueryWrapper>
<InputContainer> <InputContainer>
<Input <Input
onChangeHandler={(event): void => onChangeHandler={(event): void =>
@ -68,8 +80,13 @@ const Query = ({
onBlur={(): void => onBlurHandler()} onBlur={(): void => onBlurHandler()}
/> />
</InputContainer> </InputContainer>
<Divider /> </QueryWrapper>
<Button onClick={onDeleteQueryHandler}>Delete</Button>
</Container> </Container>
<Divider />
</>
); );
}; };
@ -77,12 +94,16 @@ interface DispatchProps {
updateQuery: ( updateQuery: (
props: UpdateQueryProps, props: UpdateQueryProps,
) => (dispatch: Dispatch<AppActions>) => void; ) => (dispatch: Dispatch<AppActions>) => void;
deleteQuery: (
props: DeleteQueryProps,
) => (dispatch: Dispatch<AppActions>) => void;
} }
const mapDispatchToProps = ( const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>, dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({ ): DispatchProps => ({
updateQuery: bindActionCreators(UpdateQuery, dispatch), updateQuery: bindActionCreators(UpdateQuery, dispatch),
deleteQuery: bindActionCreators(DeleteQuery, dispatch),
}); });
interface QueryProps extends DispatchProps { interface QueryProps extends DispatchProps {

View File

@ -1,5 +1,4 @@
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import Spinner from 'components/Spinner';
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems'; import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { connect, useSelector } from 'react-redux'; import { connect, useSelector } from 'react-redux';
@ -47,12 +46,8 @@ const QuerySection = ({
}); });
}, [createQuery, urlQuery]); }, [createQuery, urlQuery]);
if (query.length === 0) {
return <Spinner size="small" height="30vh" tip="Loading..." />;
}
return ( return (
<div> <>
{query.map((e, index) => ( {query.map((e, index) => (
<Query <Query
currentIndex={index} currentIndex={index}
@ -66,7 +61,7 @@ const QuerySection = ({
<QueryButton onClick={queryOnClickHandler} icon={<PlusOutlined />}> <QueryButton onClick={queryOnClickHandler} icon={<PlusOutlined />}>
Query Query
</QueryButton> </QueryButton>
</div> </>
); );
}; };

View File

@ -7,6 +7,7 @@ export const InputContainer = styled.div`
export const Container = styled.div` export const Container = styled.div`
margin-top: 1rem; margin-top: 1rem;
display: flex;
`; `;
export const QueryButton = styled(Button)` export const QueryButton = styled(Button)`
@ -15,3 +16,11 @@ export const QueryButton = styled(Button)`
align-items: center; 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
}
`;

View File

@ -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<AppActions>) => void) => {
return (dispatch: Dispatch<AppActions>): void => {
dispatch({
type: 'DELETE_QUERY',
payload: {
currentIndex: props.currentIndex,
widgetId: props.widgetId,
},
});
};
};

View File

@ -1,6 +1,7 @@
export * from './applySettingsToPanel'; export * from './applySettingsToPanel';
export * from './createQuery'; export * from './createQuery';
export * from './deleteDashboard'; export * from './deleteDashboard';
export * from './deleteQuery';
export * from './getAllDashboard'; export * from './getAllDashboard';
export * from './getDashboard'; export * from './getDashboard';
export * from './toggleEditMode'; export * from './toggleEditMode';

View File

@ -4,6 +4,7 @@ import {
CREATE_NEW_QUERY, CREATE_NEW_QUERY,
DashboardActions, DashboardActions,
DELETE_DASHBOARD_SUCCESS, DELETE_DASHBOARD_SUCCESS,
DELETE_QUERY,
DELETE_WIDGET_SUCCESS, DELETE_WIDGET_SUCCESS,
GET_ALL_DASHBOARD_ERROR, GET_ALL_DASHBOARD_ERROR,
GET_ALL_DASHBOARD_LOADING_START, 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: default:
return state; return state;
} }

View File

@ -40,6 +40,8 @@ export const DELETE_WIDGET_ERROR = 'DELETE_WIDGET_ERROR';
export const IS_ADD_WIDGET = 'IS_ADD_WIDGET'; export const IS_ADD_WIDGET = 'IS_ADD_WIDGET';
export const DELETE_QUERY = 'DELETE_QUERY';
interface GetDashboard { interface GetDashboard {
type: typeof GET_DASHBOARD; type: typeof GET_DASHBOARD;
payload: 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 = export type DashboardActions =
| GetDashboard | GetDashboard
| UpdateDashboard | UpdateDashboard
@ -177,4 +189,5 @@ export type DashboardActions =
| SaveDashboardSuccess | SaveDashboardSuccess
| WidgetDeleteSuccess | WidgetDeleteSuccess
| IsAddWidget | IsAddWidget
| UpdateQuery; | UpdateQuery
| DeleteQuery;

View File

@ -1,5 +1,6 @@
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems'; import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { Layout } from 'react-grid-layout';
import { QueryData } from '../widgets/getQuery'; import { QueryData } from '../widgets/getQuery';
@ -19,6 +20,7 @@ export interface DashboardData {
name?: string; name?: string;
widgets?: Widgets[]; widgets?: Widgets[];
title: string; title: string;
layout?: Layout[];
} }
export interface Widgets { export interface Widgets {