feat(ui) - dashboards - widget drap and other changes

This commit is contained in:
Shekar Siri 2022-03-29 12:10:47 +02:00
parent 3efe0aed5b
commit 6607d3cb74
17 changed files with 188 additions and 60 deletions

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { DNDSource, DNDTarget } from 'Components/hocs/dnd';
// import { DNDSource, DNDTarget } from 'Components/hocs/dnd';
import Event, { TYPES } from 'Types/filter/event';
import { operatorOptions } from 'Types/filter';
import { editEvent, removeEvent, clearEvents, applyFilter } from 'Duck/filters';
@ -25,8 +25,8 @@ const getLabel = ({ type }) => {
return getPlaceholder({ type });
};
@DNDTarget('event')
@DNDSource('event')
// @DNDTarget('event')
// @DNDSource('event')
@connect(state => ({
isLastEvent: state.getIn([ 'filters', 'appliedFilter', 'events' ]).size === 1,
}), { editEvent, removeEvent, clearEvents, applyFilter })

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import { Input } from 'semantic-ui-react';
import { DNDContext } from 'Components/hocs/dnd';
// import { DNDContext } from 'Components/hocs/dnd';
import {
addEvent, applyFilter, moveEvent, clearEvents, edit,
addCustomFilter, addAttribute, setSearchQuery, setActiveFlow, setFilterOption
@ -45,7 +45,7 @@ import SaveFilterButton from 'Shared/SaveFilterButton';
setBlink,
edit,
})
@DNDContext
// @DNDContext
export default class EventFilter extends React.PureComponent {
state = { search: '', showFilterModal: false, showPlacehoder: true }
fetchEventList = debounce(this.props.fetchEventList, 500)

View file

@ -26,7 +26,10 @@ function NewDashboard(props) {
}, []);
useEffect(() => {
if (!dashboard || !dashboard.dashboardId) {
if (dashboardId) {
store.selectDashboardById(dashboardId);
}
if (!dashboardId) {
if (dashboardId) {
store.selectDashboardById(dashboardId);
} else {
@ -35,19 +38,9 @@ function NewDashboard(props) {
});
}
}
}, []);
// if (dashboard) {
// if (dashboard.dashboardId !== dashboardId) {
// history.push(withSiteId(dashboardSelected(dashboard.dashboardId), parseInt(siteId)));
// }
// history.replace(withSiteId(dashboardSelected(dashboard.dashboardId), parseInt(siteId)));
// }
// console.log('dashboard', dashboard)
}, [dashboard]);
console.log('rendering dashboard', props.match.params);
// console.log('rendering dashboard', props.match.params);
return (
<>

View file

@ -1,18 +1,61 @@
import React from 'react';
import React, { useRef } from 'react';
import { useDashboardStore } from '../store/store';
import cn from 'classnames';
import { ItemMenu } from 'UI';
import { useDrag, useDrop } from 'react-dnd';
function WidgetWrapper(props) {
const { widget } = props;
// const store: any = useDashboardStore();
// const dashboard = store.selectedDashboard;
// const siteId = store.siteId;
const { widget, index, moveListItem } = props;
// useDrag - the list item is draggable
const [{ opacity, isDragging }, dragRef] = useDrag({
type: 'item',
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
opacity: monitor.isDragging() ? 0.5 : 1,
}),
}, [index]);
// useDrop - the list item is also a drop area
const [spec, dropRef] = useDrop({
accept: 'item',
drop: (item: any) => {
if (item.index === index) return;
moveListItem(item.index, index);
},
// hover: (item: any, monitor: any) => {
// const dragIndex = item.index
// const hoverIndex = index
// const hoverBoundingRect = ref.current?.getBoundingClientRect()
// const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2
// const hoverActualY = monitor.getClientOffset().y - hoverBoundingRect.top
// // if dragging down, continue only when hover is smaller than middle Y
// if (dragIndex < hoverIndex && hoverActualY < hoverMiddleY) return
// // if dragging up, continue only when hover is bigger than middle Y
// if (dragIndex > hoverIndex && hoverActualY > hoverMiddleY) return
// moveListItem(dragIndex, hoverIndex)
// item.index = hoverIndex
// },
}, [])
console.log('spec', spec)
const ref: any = useRef(null)
const dragDropRef: any = dragRef(dropRef(ref))
return (
<div className={cn("border rounded bg-white", 'col-span-' + widget.colSpan)} style={{ userSelect: 'none'}}>
<div
className={cn("border rounded bg-white", 'col-span-' + widget.colSpan)}
style={{ userSelect: 'none', opacity }}
ref={dragDropRef}
>
{/* <Link to={withSiteId(dashboardMetricDetails(dashboard.dashboardId, widget.widgetId), siteId)}> */}
<div className="p-3 cursor-pointe border-b flex items-center justify-between">
<div
className="p-3 cursor-move border-b flex items-center justify-between"
>
{widget.name} - {widget.position}
<div>
<ItemMenu

View file

@ -6,6 +6,7 @@ import { Button, PageTitle, Link } from 'UI';
import { withSiteId, dashboardMetricCreate } from 'App/routes';
import withModal from 'App/components/Modal/withModal';
import DashboardModal from '../DashboardModal'
import DashboardWidgetGrid from '../DashboardWidgetGrid';
function DashboardView(props) {
// let { handleModal } = React.useContext(ModalContext);
@ -17,13 +18,16 @@ function DashboardView(props) {
}, [])
return (
<div>
<div className="flex items-center mb-4">
<PageTitle title={dashboard.name} className="mr-3" />
<Link to={withSiteId(dashboardMetricCreate(dashboard.dashboardId), store.siteId)}><Button primary size="small">Add Metric</Button></Link>
</div>
<div className="grid grid-cols-2 gap-4">
{list && list.map(item => <WidgetWrapper widget={item} key={item.widgetId} />)}
<div className="flex items-center mb-4 justify-between">
<div className="flex items-center">
<PageTitle title={dashboard.name} className="mr-3" />
<Link to={withSiteId(dashboardMetricCreate(dashboard.dashboardId), store.siteId)}><Button primary size="small">Add Metric</Button></Link>
</div>
<div>
Right
</div>
</div>
<DashboardWidgetGrid />
</div>
)
}

View file

@ -0,0 +1,41 @@
import React from 'react';
import { useDashboardStore } from '../../store/store';
import WidgetWrapper from '../../WidgetWrapper';
import { NoContent, Button, Loader } from 'UI';
import { useObserver } from 'mobx-react-lite';
// import { divider } from '../../Filters/filters.css';
function DashboardWidgetGrid(props) {
const store: any = useDashboardStore();
const loading = store.isLoading;
const dashbaord = store.selectedDashboard;
const list = dashbaord.widgets;
return useObserver(() => (
<Loader loading={loading}>
<NoContent
show={list.length === 0}
icon="exclamation-circle"
title="No metrics added to this dashboard"
subtext={
<div>
<p>Metrics helps you visualize trends from sessions captured by OpenReplay</p>
<Button size="small" primary>Add Metric</Button>
</div>
}
>
<div className="grid grid-cols-2 gap-4">
{list && list.map((item, index) => (
<WidgetWrapper
index={index}
widget={item}
key={item.widgetId}
moveListItem={(dragIndex, hoverIndex) => dashbaord.swapWidgetPosition(dragIndex, hoverIndex)}
/>
))}
</div>
</NoContent>
</Loader>
));
}
export default DashboardWidgetGrid;

View file

@ -0,0 +1 @@
export { default } from './DashboardWidgetGrid';

View file

@ -1,20 +1,24 @@
import { useObserver } from 'mobx-react-lite';
import React from 'react';
import { Icon, NoContent, Label, Link } from 'UI';
import { Icon, NoContent, Label, Link, Pagination } from 'UI';
import { useDashboardStore } from '../../store/store';
import { getRE } from 'App/utils';
interface Props { }
function MetricsList(props: Props) {
const store: any = useDashboardStore();
const widgets = store.widgets;
const lenth = widgets.length;
const currentPage = store.metricsPage;
const totalPages = widgets.length;
const currentPage = useObserver(() => store.metricsPage);
const metricsSearch = useObserver(() => store.metricsSearch);
const filterRE = getRE(metricsSearch, 'i');
const list = widgets.filter(w => filterRE.test(w.name))
const totalPages = list.length;
const pageSize = store.metricsPageSize;
const start = (currentPage - 1) * pageSize;
const end = currentPage * pageSize;
const list = widgets.slice(start, end);
return useObserver(() => (
<NoContent show={lenth === 0} icon="exclamation-circle">
@ -28,7 +32,7 @@ function MetricsList(props: Props) {
<div>Last Modified</div>
</div>
{list.map((metric: any) => (
{list.slice(start, end).map((metric: any) => (
<div className="grid grid-cols-7 p-3 border-t select-none">
<div className="col-span-2">
<Link to="/dashboard/metrics/create" className="link">
@ -55,6 +59,16 @@ function MetricsList(props: Props) {
</div>
))}
</div>
<div className="w-full flex items-center justify-center py-6">
<Pagination
page={currentPage}
totalPages={Math.ceil(totalPages / pageSize)}
onPageChange={(page) => store.updateKey('metricsPage', page)}
limit={pageSize}
debounceRequest={100}
/>
</div>
</NoContent>
));
}

View file

@ -0,0 +1,25 @@
import { useObserver } from 'mobx-react-lite';
import React from 'react';
import { useDashboardStore } from '../../store/store';
import { Icon } from 'UI';
function MetricsSearch(props) {
const store: any = useDashboardStore();
const metricsSearch = useObserver(() => store.metricsSearch);
return useObserver(() => (
<div className="relative">
<Icon name="search" className="absolute top-0 bottom-0 ml-2 m-auto" size="18" />
<input
value={metricsSearch}
name="metricsSearch"
className="bg-white p-2 border rounded w-full pl-10"
placeholder="Filter by title, type, dashboard and owner"
onChange={({ target: { name, value } }) => store.updateKey(name, value)}
/>
</div>
));
}
export default MetricsSearch;

View file

@ -0,0 +1 @@
export { default } from './MetricsSearch';

View file

@ -2,6 +2,7 @@ import React from 'react';
import { Button, PageTitle, Icon, Link } from 'UI';
import { withSiteId, dashboardMetricCreate } from 'App/routes';
import MetricsList from '../MetricsList';
import MetricsSearch from '../MetricsSearch';
function MetricsView(props) {
return (
@ -9,11 +10,8 @@ function MetricsView(props) {
<div className="flex items-center mb-4 justify-between">
<PageTitle title="Metrics" className="mr-3" />
{/* <Link to={withSiteId(dashboardMetricCreate(dashboard.dashboardId), store.siteId)}><Button primary size="small">Add Metric</Button></Link> */}
<div className="ml-auto w-1/4">
<input
className="bg-white p-2 border rounded w-full"
placeholder="Filter by title, type, dashboard and owner"
/>
<div className="ml-auto w-1/3">
<MetricsSearch />
</div>
</div>
<MetricsList />

View file

@ -104,6 +104,7 @@ export default class Dashboard {
}
swapWidgetPosition(positionA, positionB) {
console.log('swapWidgetPosition', positionA, positionB)
const widgetA = this.widgets[positionA]
const widgetB = this.widgets[positionB]
this.widgets[positionA] = widgetB

View file

@ -14,15 +14,12 @@ export default class DashboardStore {
widgets: Widget[] = []
metricsPage: number = 1
metricsPageSize: number = 10
metricsSearch: string = ''
private client = new APIClient()
constructor() {
makeAutoObservable(this, {
dashboards: observable,
selectedDashboard: observable,
isLoading: observable,
resetCurrentWidget: action,
addDashboard: action,
removeDashboard: action,
@ -38,6 +35,7 @@ export default class DashboardStore {
fromJson: action,
setSiteId: action,
editWidget: action,
updateKey: action,
})
@ -54,7 +52,7 @@ export default class DashboardStore {
// this.selectedDashboard?.swapWidgetPosition(2, 0)
// }, 3000)
for (let i = 0; i < 20; i++) {
for (let i = 0; i < 15; i++) {
const widget: any= {};
widget.widgetId = `${i}`
widget.name = `Widget ${i}`;
@ -84,6 +82,10 @@ export default class DashboardStore {
}
updateKey(key: any, value: any) {
this[key] = value
}
resetCurrentWidget() {
this.currentWidget = new Widget()
}

View file

@ -1,6 +1,6 @@
import HTML5Backend from 'react-dnd-html5-backend';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { findDOMNode } from 'react-dom';
import { DragSource, DropTarget, DragDropContext } from 'react-dnd';
import { DragSource, DropTarget, DndContext } from 'react-dnd';
const cardSource = {
beginDrag(props) {
@ -50,7 +50,7 @@ const cardTarget = {
},
};
export const DNDContext = DragDropContext(HTML5Backend);
export const DNDContext = DndContext(HTML5Backend);
export const DNDSource = name => DragSource(name, cardSource, (connect, monitor) => ({
connectDragSource: connect.dragSource(),

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { DNDSource, DNDTarget } from 'Components/hocs/dnd';
// import { DNDSource, DNDTarget } from 'Components/hocs/dnd';
import Event, { TYPES } from 'Types/filter/event';
import { operatorOptions } from 'Types/filter';
import { editEvent, removeEvent, clearEvents, applyFilter } from 'Duck/funnelFilters';
@ -24,8 +24,8 @@ const getLabel = ({ type }) => {
return getPlaceholder({ type });
};
@DNDTarget('event')
@DNDSource('event')
// @DNDTarget('event')
// @DNDSource('event')
@connect(state => ({
isLastEvent: state.getIn([ 'filters', 'appliedFilter', 'events' ]).size === 1,
funnel: state.getIn(['funnels', 'instance']),

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { DNDContext } from 'Components/hocs/dnd';
// import { DNDContext } from 'Components/hocs/dnd';
import {
addEvent, applyFilter, moveEvent, clearEvents,
addCustomFilter, addAttribute, setSearchQuery, setActiveFlow, setFilterOption
@ -39,7 +39,7 @@ import CustomFilters from './CustomFilters';
updateFunnelFilters,
refreshFunnel
})
@DNDContext
// @DNDContext
export default class EventFilter extends React.PureComponent {
state = { search: '', showFilterModal: false, showPlacehoder: true, showSaveModal: false }
fetchEventList = debounce(this.props.fetchEventList, 500)

View file

@ -9,6 +9,9 @@ import DashboardStore from './components/Dashboard/store';
import { DashboardStoreProvider } from './components/Dashboard/store/store';
import { ModalProvider } from './components/Modal/ModalContext';
import ModalRoot from './components/Modal/ModalRoot';
import { HTML5Backend } from 'react-dnd-html5-backend'
import { DndProvider } from 'react-dnd'
document.addEventListener('DOMContentLoaded', () => {
@ -16,12 +19,14 @@ document.addEventListener('DOMContentLoaded', () => {
render(
(
<Provider store={ store }>
<DashboardStoreProvider store={ dashboardStore }>
<ModalProvider>
<ModalRoot />
<Router />
</ModalProvider>
</DashboardStoreProvider>
<DndProvider backend={HTML5Backend}>
<DashboardStoreProvider store={ dashboardStore }>
<ModalProvider>
<ModalRoot />
<Router />
</ModalProvider>
</DashboardStoreProvider>
</DndProvider>
</Provider>
),
document.getElementById('app'),