diff --git a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml index 6024201cb8..49b944af3d 100644 --- a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml @@ -28,7 +28,7 @@ services: volumes: - ./data/alertmanager:/data command: - - --queryService.url=http://query-service:8080 + - --queryService.url=http://query-service:8085 - --storage.path=/data depends_on: - query-service diff --git a/deploy/docker/clickhouse-setup/docker-compose.arm.yaml b/deploy/docker/clickhouse-setup/docker-compose.arm.yaml index cc29115bf1..0cd172ceea 100644 --- a/deploy/docker/clickhouse-setup/docker-compose.arm.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose.arm.yaml @@ -30,7 +30,7 @@ services: condition: service_healthy restart: on-failure command: - - --queryService.url=http://query-service:8080 + - --queryService.url=http://query-service:8085 - --storage.path=/data # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` @@ -53,7 +53,6 @@ services: - GODEBUG=netdns=go - TELEMETRY_ENABLED=true - DEPLOYMENT_TYPE=docker-standalone-arm - restart: on-failure healthcheck: test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"] diff --git a/deploy/docker/clickhouse-setup/docker-compose.yaml b/deploy/docker/clickhouse-setup/docker-compose.yaml index e18d584e1d..bfcace3144 100644 --- a/deploy/docker/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose.yaml @@ -30,7 +30,7 @@ services: condition: service_healthy restart: on-failure command: - - --queryService.url=http://query-service:8080 + - --queryService.url=http://query-service:8085 - --storage.path=/data # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` 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/ListOfDashboard/index.tsx b/frontend/src/container/ListOfDashboard/index.tsx index 64f3f573c1..cb375b9742 100644 --- a/frontend/src/container/ListOfDashboard/index.tsx +++ b/frontend/src/container/ListOfDashboard/index.tsx @@ -15,11 +15,19 @@ import ROUTES from 'constants/routes'; import SearchFilter from 'container/ListOfDashboard/SearchFilter'; import useComponentPermission from 'hooks/useComponentPermission'; import history from 'lib/history'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { + Dispatch, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; import { useTranslation } from 'react-i18next'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { generatePath } from 'react-router-dom'; import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import { GET_ALL_DASHBOARD_SUCCESS } from 'types/actions/dashboard'; import { Dashboard } from 'types/api/dashboard/getAll'; import AppReducer from 'types/reducer/app'; import DashboardReducer from 'types/reducer/dashboards'; @@ -36,6 +44,7 @@ function ListOfAllDashboard(): JSX.Element { const { dashboards, loading } = useSelector( (state) => state.dashboards, ); + const dispatch = useDispatch>(); const { role } = useSelector((state) => state.app); const [action, createNewDashboard, newDashboard] = useComponentPermission( @@ -131,6 +140,10 @@ function ListOfAllDashboard(): JSX.Element { }); if (response.statusCode === 200) { + dispatch({ + type: GET_ALL_DASHBOARD_SUCCESS, + payload: [], + }); history.push( generatePath(ROUTES.DASHBOARD, { dashboardId: response.payload.uuid, @@ -151,7 +164,7 @@ function ListOfAllDashboard(): JSX.Element { errorMessage: (error as AxiosError).toString() || 'Something went Wrong', }); } - }, [newDashboardState, t]); + }, [newDashboardState, t, dispatch]); const getText = useCallback(() => { if (!newDashboardState.error && !newDashboardState.loading) { diff --git a/frontend/src/container/MetricsTable/SkipOnBoardModal/index.tsx b/frontend/src/container/MetricsTable/SkipOnBoardModal/index.tsx index b50c0ac9fb..2018d49a9d 100644 --- a/frontend/src/container/MetricsTable/SkipOnBoardModal/index.tsx +++ b/frontend/src/container/MetricsTable/SkipOnBoardModal/index.tsx @@ -18,7 +18,7 @@ function SkipOnBoardingModal({ onContinueClick }: Props): JSX.Element {