mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-11 15:59:04 +08:00
Feat(FE): Delete Query, Save Layout (#306)
* feat: Delete Query functionality is added * feat: save layout is updated
This commit is contained in:
parent
cc91242e9a
commit
ea5b40c7ea
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
`;
|
||||
|
@ -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';
|
||||
|
@ -5,10 +5,10 @@ import GridGraphs from './GridGraphs';
|
||||
|
||||
const NewDashboard = (): JSX.Element => {
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<Description />
|
||||
<GridGraphs />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
`;
|
||||
|
17
frontend/src/store/actions/dashboard/deleteQuery.ts
Normal file
17
frontend/src/store/actions/dashboard/deleteQuery.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
@ -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';
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user