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 { 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<AppState, DashboardReducer>(
(state) => state.dashboards,
);
const [saveLayoutState, setSaveLayoutState] = useState<State>({
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 => (
<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 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 => (
<Graph isDeleted={isDeleted} widget={widgets[index]} />
),
};
});
};
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<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) {
return <Spinner height="40vh" size="large" tip="Loading..." />;
}
return (
<ReactGridLayout
isResizable
isDraggable
cols={12}
rowHeight={100}
autoSize
width={100}
isDroppable
useCSSTransforms
onDrop={onDropHandler}
>
{layouts.map(({ Component, ...rest }, index) => {
const widget = (widgets || [])[index] || {};
<>
<ButtonContainer>
<Button
loading={saveLayoutState.loading}
onClick={onLayoutSaveHanlder}
icon={<SaveFilled />}
danger={saveLayoutState.error}
>
Save Layout
</Button>
</ButtonContainer>
const type = widget.panelTypes;
<ReactGridLayout
isResizable
isDraggable
cols={12}
rowHeight={100}
autoSize
width={100}
isDroppable
useCSSTransforms
onDrop={onDropHandler}
onLayoutChange={onLayoutChangeHandler}
>
{layouts.map(({ Component, ...rest }, index) => {
const widget = (widgets || [])[index] || {};
const isQueryType = type === 'VALUE';
const type = widget.panelTypes;
return (
<CardContainer key={rest.i} data-grid={rest}>
<Card isQueryType={isQueryType}>
<Component />
</Card>
</CardContainer>
);
})}
</ReactGridLayout>
const isQueryType = type === 'VALUE';
return (
<CardContainer key={rest.i} data-grid={rest}>
<Card isQueryType={isQueryType}>
<Component />
</Card>
</CardContainer>
);
})}
</ReactGridLayout>
</>
);
};
@ -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);

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

View File

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

View File

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

View File

@ -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 (
<Container>
<InputContainer>
<Input
onChangeHandler={(event): void =>
onChangeHandler(setPromqlQuery, event.target.value)
}
size="middle"
value={promqlQuery}
addonBefore={'PromQL Query'}
onBlur={(): void => onBlurHandler()}
/>
</InputContainer>
const onDeleteQueryHandler = (): void => {
deleteQuery({
widgetId: widgetId,
currentIndex,
});
};
return (
<>
<Container>
<QueryWrapper>
<InputContainer>
<Input
onChangeHandler={(event): void =>
onChangeHandler(setPromqlQuery, event.target.value)
}
size="middle"
value={promqlQuery}
addonBefore={'PromQL Query'}
onBlur={(): void => onBlurHandler()}
/>
</InputContainer>
<InputContainer>
<Input
onChangeHandler={(event): void =>
onChangeHandler(setLegendFormat, event.target.value)
}
size="middle"
value={legendFormat}
addonBefore={'Legend Format'}
onBlur={(): void => onBlurHandler()}
/>
</InputContainer>
</QueryWrapper>
<Button onClick={onDeleteQueryHandler}>Delete</Button>
</Container>
<InputContainer>
<Input
onChangeHandler={(event): void =>
onChangeHandler(setLegendFormat, event.target.value)
}
size="middle"
value={legendFormat}
addonBefore={'Legend Format'}
onBlur={(): void => onBlurHandler()}
/>
</InputContainer>
<Divider />
</Container>
</>
);
};
@ -77,12 +94,16 @@ interface DispatchProps {
updateQuery: (
props: UpdateQueryProps,
) => (dispatch: Dispatch<AppActions>) => void;
deleteQuery: (
props: DeleteQueryProps,
) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateQuery: bindActionCreators(UpdateQuery, dispatch),
deleteQuery: bindActionCreators(DeleteQuery, dispatch),
});
interface QueryProps extends DispatchProps {

View File

@ -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 <Spinner size="small" height="30vh" tip="Loading..." />;
}
return (
<div>
<>
{query.map((e, index) => (
<Query
currentIndex={index}
@ -66,7 +61,7 @@ const QuerySection = ({
<QueryButton onClick={queryOnClickHandler} icon={<PlusOutlined />}>
Query
</QueryButton>
</div>
</>
);
};

View File

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

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 './createQuery';
export * from './deleteDashboard';
export * from './deleteQuery';
export * from './getAllDashboard';
export * from './getDashboard';
export * from './toggleEditMode';

View File

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

View File

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

View File

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