Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions web/client/actions/widgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const TOGGLE_TRAY = "WIDGET:TOGGLE_TRAY";

export const REPLACE_LAYOUT_VIEW = "WIDGET:REPLACE_LAYOUT_VIEW";
export const SET_SELECTED_LAYOUT_VIEW_ID = "WIDGET:SET_SELECTED_LAYOUT_VIEW_ID";
export const SET_LINKED_DASHBOARD_DATA = "WIDGET:SET_LINKED_DASHBOARD_DATA";

/**
* Intent to create a new Widgets
Expand Down Expand Up @@ -348,3 +349,15 @@ export const setSelectedLayoutViewId = (viewId, target = DEFAULT_TARGET) => ({
target,
viewId
});

/**
* Set the layouts, widgets of the linked dashboard
* @param {object} data The layouts and widgets
* @param {string} [target=floating] the target container of the layouts
* @return {object} action with type `WIDGETS:SET_LINKED_DASHBOARD_DATA`, the data and the target
*/
export const setLinkedDashboardData = (data, target = DEFAULT_TARGET) => ({
type: SET_LINKED_DASHBOARD_DATA,
data,
target
});
72 changes: 65 additions & 7 deletions web/client/components/dashboard/ConfigureView.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,40 @@
import React, { useEffect, useState } from 'react';
import Dialog from '../misc/Dialog';
import { Button, ControlLabel, FormControl, FormGroup, Glyphicon } from 'react-bootstrap';
import { Button, ControlLabel, FormControl, FormGroup, Glyphicon, InputGroup } from 'react-bootstrap';
import Message from '../I18N/Message';
import ColorSelector from '../style/ColorSelector';
import Select from 'react-select';
import Portal from '../misc/Portal';
import { getCatalogResources } from '../../api/persistence';
import withTooltip from '../misc/enhancers/tooltip';
import FlexBox from '../layout/FlexBox';

const ConfigureView = ({ active, onToggle, name, color, onSave }) => {
const GlyphiconIndicator = withTooltip(Glyphicon);

const ConfigureView = ({ active, onToggle, data, onSave, user, monitoredState }) => {
const [setting, setSetting] = useState({ name: null, color: null });
const [dashboardOptions, setDashboardOptions] = useState([]);

useEffect(() => {
setSetting(data);
}, [data]);

useEffect(() => {
setSetting({ name, color });
}, [name, color]);
if (!active || !user || !monitoredState) return;
const args = [{ params: { pageSize: 9999999 }, monitoredState }, { user }, ["DASHBOARD"]];
const catalogResources = getCatalogResources(...args).toPromise();
catalogResources.then(res => {
const options = res.resources.map(d => ({
value: d.id || d.pk,
label: d.name
}));
setDashboardOptions(options);
});
}, [active, user, monitoredState]);

const canAddLayout = !data.dashboard
? (data.layoutsData?.md?.length === 0 && data.layoutsData?.xxs?.length === 0)
: true;

return active && (
<Portal>
Expand All @@ -36,19 +61,52 @@ const ConfigureView = ({ active, onToggle, name, color, onSave }) => {
}}
/>
</FormGroup>
<FormGroup className="ms-flex-box _flex _flex-gap-sm _flex-center-v">
<FormGroup className="ms-flex-box _flex _flex-gap-sm _flex-center-v form-group-inline-content _relative">
<ControlLabel><Message msgId="dashboard.view.color" /></ControlLabel>
<div className="dashboard-color-picker">
<InputGroup className="dashboard-color-picker _relative">
<ColorSelector
format="rgb"
color={setting.color}
onChangeColor={(colorVal) => colorVal && setSetting(prev =>({
...prev,
color: colorVal
}))}
placement="right"
/>
</InputGroup>
</FormGroup>
<FormGroup className="ms-flex-box _flex _flex-gap-sm _flex-center-v form-group-inline-content">
<ControlLabel>Link existing dashboard</ControlLabel>
<FlexBox centerChildrenVertically gap="sm">
<FormControl
key={setting.linkExistingDashboard}
type="checkbox"
disabled={!canAddLayout}
checked={setting.linkExistingDashboard}
onChange={event => {
const { checked } = event.target || {};
setSetting(prev => ({ ...prev, linkExistingDashboard: checked }));
}}
/>
</div>
{!canAddLayout && (
<GlyphiconIndicator
glyph="info-sign"
tooltipId="dashboard.view.cannotAddExistingDashboard"
/>
)}
</FlexBox>
</FormGroup>
{setting.linkExistingDashboard && (
<FormGroup>
<ControlLabel>Select dashboard</ControlLabel>
<Select
value={setting.dashboard || ''}
options={dashboardOptions}
name="dashboard"
onChange={selected => setSetting(prev => ({...prev, dashboard: selected?.value }))}
/>
</FormGroup>
)}
</div>
<div role="footer">
<Button
Expand Down
43 changes: 43 additions & 0 deletions web/client/components/dashboard/Layouts.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react';
import LoadingSpinner from '../misc/LoadingSpinner';
import Message from '../I18N/Message';
import FlexBox from '../layout/FlexBox';
import WidgetsView from '../widgets/view/WidgetsView';
import useLoadDashboardData from './hooks/useLoadDashboardData';
import EmptyView from '../misc/EmptyView';

export default function Layouts({ selectedLayout, layoutsData, ...props }) {
const [isLoading, error] = useLoadDashboardData({ props, dashboard: selectedLayout.dashboard });

const filteredProps = {...props};
if (props.widgets) {
filteredProps.widgets = props.widgets.filter(widget => widget.layoutId === props.selectedLayoutId);
}

if (error) {
return (
<div style={{ margin: "auto", fontWeight: 'bold' }} >
<EmptyView title={<Message msgId={error.messageId} />} />
</div>
);
}

return isLoading
? (
<div style={{ margin: "auto", fontWeight: 'bold' }} >
<LoadingSpinner style={{ display: "inline-block", verticalAlign: "middle" }}/>
<Message msgId="loading" />
</div>
)
: (
<FlexBox.Fill classNames={["_relative", "_overflow-auto"]}>
<WidgetsView
{...filteredProps}
layouts={layoutsData}
canEdit={selectedLayout.dashboard ? false : props.canEdit}
/>
</FlexBox.Fill>
);


}
49 changes: 25 additions & 24 deletions web/client/components/dashboard/WidgetViewWrapper.jsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import React from 'react';
import WidgetsView from '../widgets/view/WidgetsView';
import ViewSwitcher from './ViewSwitcher';
import uuidv1 from 'uuid/v1';
import { getNextAvailableName } from '../../utils/WidgetsUtils';
import ConfigureView from './ConfigureView';
import FlexBox from '../layout/FlexBox';
import Layouts from './Layouts';

const WidgetViewWrapper = props => {
const {
layouts = [],
onLayoutViewReplace,
selectedLayoutId,
onLayoutViewSelected,
viewConfigurationActive,
setViewConfigurationActive,
widgets = [],
onWidgetsReplace,
user,
monitoredState,
viewConfigurationActive,
setViewConfigurationActive,
canEdit
} = props;

Expand All @@ -33,12 +35,7 @@ const WidgetViewWrapper = props => {

// strip out "properties" before passing
const selectedLayout = getSelectedLayout();
const { id, name, color, order, ...layoutForWidgets } = selectedLayout;

const filteredProps = {...props};
if (props.widgets) {
filteredProps.widgets = props.widgets.filter(widget => widget.layoutId === selectedLayoutId);
}
const { id, name, color, linkExistingDashboard, dashboard, ...layoutsData } = selectedLayout;

const handleAddLayout = () => {
const newLayout = {
Expand Down Expand Up @@ -84,7 +81,13 @@ const WidgetViewWrapper = props => {

const handleSave = (data) => {
const updatedLayouts = layouts.map(layout => layout.id === id
? { ...layout, name: data.name, color: data.color }
? {
...layout,
name: data.name,
color: data.color,
linkExistingDashboard: data.linkExistingDashboard,
dashboard: data.dashboard
}
: layout
);
onLayoutViewReplace(updatedLayouts);
Expand All @@ -95,15 +98,10 @@ const WidgetViewWrapper = props => {

return (
<FlexBox column classNames={["_relative", "_fill"]}>
<FlexBox.Fill classNames={["_relative", "_overflow-auto"]}>
<WidgetsView
{...filteredProps}
layouts={layoutForWidgets} // only selected layout without properties
/>
</FlexBox.Fill>
<Layouts selectedLayout={selectedLayout} layoutsData={layoutsData} {...props} />
{(canEdit || layoutViews.length > 1) && (
<ViewSwitcher
layouts={layoutViews}
layouts={Array.isArray(layouts) ? layouts : [layouts]}
selectedLayoutId={selectedLayoutId}
onSelect={handleSelectLayout}
onAdd={handleAddLayout}
Expand All @@ -113,13 +111,16 @@ const WidgetViewWrapper = props => {
canEdit={canEdit}
/>
)}
<ConfigureView
active={viewConfigurationActive}
onToggle={handleToggle}
onSave={handleSave}
name={name}
color={color}
/>
{viewConfigurationActive && (
<ConfigureView
active={viewConfigurationActive}
onToggle={handleToggle}
onSave={handleSave}
data={{ name, color, linkExistingDashboard, dashboard, layoutsData }}
user={user}
monitoredState={monitoredState}
/>
)}
</FlexBox>
);
};
Expand Down
8 changes: 4 additions & 4 deletions web/client/components/dashboard/__tests__/Dashboard-test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,29 +43,29 @@ describe('WidgetsView component', () => {
setTimeout(done);
});
it('DashBoard empty', () => {
ReactDOM.render(<Dashboard widgets={[]} layouts={layouts}/>, document.getElementById("container"));
ReactDOM.render(<Dashboard widgets={[]} layouts={layouts} />, document.getElementById("container"));
const container = document.getElementById('container');
const el = container.querySelector('.widget-card-on-map');
expect(el).toNotExist();
});
it('DashBoard empty', () => {
ReactDOM.render(<Dashboard widgets={[testWidget]} layouts={layouts}/>, document.getElementById("container"));
ReactDOM.render(<Dashboard widgets={[testWidget]} layouts={layouts} />, document.getElementById("container"));
const container = document.getElementById('container');
const el = container.querySelector('.mapstore-widget-card');
expect(el).toExist();
});
it('DashBoard with width=460', () => {
const WIDGET_MOBILE_RIGHT_SPACE = 18;
const width = 460;
let cmp = ReactDOM.render(<Dashboard width={width} widgets={[testWidget]} layouts={layouts}/>, document.getElementById("container"));
let cmp = ReactDOM.render(<Dashboard width={width} widgets={[testWidget]} layouts={layouts} />, document.getElementById("container"));
expect(cmp).toBeTruthy();
const innerLayout = ReactTestUtils.findRenderedComponentWithType(cmp, Responsive);
expect(innerLayout).toExist();
expect(innerLayout.props.width).toEqual(width - WIDGET_MOBILE_RIGHT_SPACE);
});
it('DashBoard with width=640', () => {
const width = 640;
const cmp = ReactDOM.render(<Dashboard width={width} widgets={[testWidget]} layouts={layouts}/>, document.getElementById("container"));
const cmp = ReactDOM.render(<Dashboard width={width} widgets={[testWidget]} layouts={layouts} />, document.getElementById("container"));
expect(cmp).toExist();
const innerLayout = ReactTestUtils.findRenderedComponentWithType(cmp, Responsive);
expect(innerLayout).toExist();
Expand Down
83 changes: 83 additions & 0 deletions web/client/components/dashboard/hooks/useLoadDashboardData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { getErrorMessageId, updateDependenciesMap } from '../../../utils/WidgetsUtils';
import { getResource } from '../../../api/persistence';

export default function useLoadDashboardData({ props, dashboard }) {
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState({});

// Track previous dashboard per layoutId
const prevDashboardsRef = useRef({});
const isFetchingRef = useRef(false);

const { layouts = [], selectedLayoutId, onLinkedDashboardDataLoad, widgets = [] } = props || {};

const layout = useMemo(() => {
if (!layouts || !selectedLayoutId) return null;
return layouts.find(l => l.id === selectedLayoutId);
}, [selectedLayoutId, layouts]);

useEffect(() => {
if (!dashboard || !layout) return;

const prevDashboard = prevDashboardsRef.current[selectedLayoutId];
setErrors(prev => ({ ...prev, [selectedLayoutId]: null }));

const hasLayoutData = layout?.md?.length > 0 || layout?.xxs?.length > 0;
const isSameDashboard = prevDashboard === dashboard;

// If layout already has data AND dashboard hasn't changed, skip API call
if ((hasLayoutData && isSameDashboard) || isFetchingRef.current) return;

isFetchingRef.current = true;
setLoading(true);
getResource(dashboard, 'dashboard')
.toPromise()
.then(res => {
const layoutsData = Array.isArray(res.data.layouts) ? res.data.layouts : [res.data.layouts];
// Handle multiple dashboard error
if (layoutsData?.length > 1) {
const messageId = "dashboard.errors.multipleDashboardError";
setErrors(prev => ({ ...prev, [selectedLayoutId]: { messageId } }));
return;
}

const layoutData = layoutsData?.[0];
// Prefix widget IDs and dependencies
const widgetsData = res.data.widgets.map(w => ({
...w,
id: `${selectedLayoutId}-${w.id}`,
layoutId: selectedLayoutId,
...(w.dependenciesMap && {
dependenciesMap: updateDependenciesMap(w.dependenciesMap, selectedLayoutId)
})
}));

// Prefix layout "i" fields
const prefixLayoutItems = (items = []) =>
items.map(item => ({ ...item, i: `${selectedLayoutId}-${item.i}` }));

const updatedLayouts = layouts.map(l => l.id === selectedLayoutId
? { ...l, md: prefixLayoutItems(layoutData.md), xxs: prefixLayoutItems(layoutData.xxs) }
: l
);
onLinkedDashboardDataLoad({
// filter out the existing widgets when the dashboard is changed
widgets: [...widgets.filter(w => w.layoutId !== selectedLayoutId), ...widgetsData],
layouts: updatedLayouts
});
// Update the dashboard for this layout
prevDashboardsRef.current[selectedLayoutId] = dashboard;
})
.catch(err => {
const messageId = getErrorMessageId(err);
setErrors(prev => ({ ...prev, [selectedLayoutId]: { messageId } }));
})
.finally(() => {
isFetchingRef.current = false;
setLoading(false);
});
}, [dashboard, layout, layouts, onLinkedDashboardDataLoad, selectedLayoutId, widgets]);

return [loading, errors[selectedLayoutId]];
}
Loading