change(ui): dashboard redesign

This commit is contained in:
Shekar Siri 2024-06-25 17:36:02 +02:00
parent 3bfdc0e574
commit 3b2c988e42
3 changed files with 371 additions and 301 deletions

View file

@ -1,6 +1,5 @@
import React, {useState} from 'react';
import FilterList from 'Shared/Filters/FilterList';
import {Icon} from 'UI';
import SeriesName from './SeriesName';
import cn from 'classnames';
import {observer} from 'mobx-react-lite';
@ -9,6 +8,63 @@ import AddStepButton from "Components/Dashboard/components/FilterSeries/AddStepB
import {Button, Space} from "antd";
import {ChevronDown, ChevronUp, Trash} from "lucide-react";
const FilterCountLabels = observer((props: { filters: any, toggleExpand: any }) => {
const events = props.filters.filter((i: any) => i && i.isEvent).length;
const filters = props.filters.filter((i: any) => i && !i.isEvent).length;
return <div className="flex items-center">
<Space>
{events > 0 && (
<Button type="primary" ghost size="small" onClick={props.toggleExpand}>
{`${events} Event${events > 1 ? 's' : ''}`}
</Button>
)}
{filters > 0 && (
<Button type="primary" ghost size="small" onClick={props.toggleExpand}>
{`${filters} Filter${filters > 1 ? 's' : ''}`}
</Button>
)}
</Space>
</div>;
});
const FilterSeriesHeader = observer((props: {
expanded: boolean,
hidden: boolean,
seriesIndex: number,
series: any,
onRemove: (seriesIndex: any) => void,
canDelete: boolean | undefined,
toggleExpand: () => void
}) => {
const onUpdate = (name: any) => {
props.series.update('name', name)
}
return <div className={cn("border-b px-5 h-12 flex items-center relative", {hidden: props.hidden})}>
<Space className="mr-auto" size={30}>
<SeriesName
seriesIndex={props.seriesIndex}
name={props.series.name}
onUpdate={onUpdate}
/>
{!props.expanded &&
<FilterCountLabels filters={props.series.filter.filters} toggleExpand={props.toggleExpand}/>}
</Space>
<Space>
<Button onClick={props.onRemove}
size="small"
disabled={!props.canDelete}
icon={<Trash size={14}/>}/>
<Button onClick={props.toggleExpand}
size="small"
icon={props.expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
</Space>
</div>;
})
interface Props {
seriesIndex: number;
series: any;
@ -20,62 +76,9 @@ interface Props {
observeChanges?: () => void;
excludeFilterKeys?: Array<string>;
canExclude?: boolean;
expandable?: boolean;
}
const FilterSeriesHeader = observer((props: {
expanded: boolean,
hidden: boolean,
seriesIndex: number,
series: any,
onRemove: (seriesIndex: any) => void,
canDelete: boolean | undefined,
toggleExpand: () => void
}) => {
const events = props.series.filter.filters.filter((i: any) => i && i.isEvent).length;
const filters = props.series.filter.filters.filter((i: any) => i && !i.isEvent).length;
const onUpdate = (name: any) => {
props.series.update('name', name)
}
return <div className={cn("border-b px-5 h-12 flex items-center relative", {hidden: props.hidden})}>
<Space className="mr-auto" size={30}>
<SeriesName
seriesIndex={props.seriesIndex}
name={props.series.name}
onUpdate={onUpdate}
/>
{!props.expanded && (
<Space>
{events > 0 && (
<Button type="primary" ghost size="small" onClick={props.toggleExpand}>
{`${events} Event${events > 1 ? 's' : ''}`}
</Button>
)}
{filters > 0 && (
<Button type="primary" ghost size="small" onClick={props.toggleExpand}>
{`${filters} Filter${filters > 1 ? 's' : ''}`}
</Button>
)}
</Space>
)}
{/*{events === 0 && filters === 0 && !props.expanded && (*/}
{/* <AddStepButton series={props.series} excludeFilterKeys={[]}/>*/}
{/*)}*/}
</Space>
<Space>
<Button onClick={props.onRemove}
size="small"
disabled={!props.canDelete}
icon={<Trash size={16}/>}/>
<Button onClick={props.toggleExpand}
size="small"
icon={props.expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
</Space>
</div>;
})
function FilterSeries(props: Props) {
const {
observeChanges = () => {
@ -86,8 +89,9 @@ function FilterSeries(props: Props) {
supportsEmpty = true,
excludeFilterKeys = [],
canExclude = false,
expandable = false
} = props;
const [expanded, setExpanded] = useState(true);
const [expanded, setExpanded] = useState(false);
const {series, seriesIndex} = props;
const onUpdateFilter = (filterIndex: any, filter: any) => {
@ -124,6 +128,15 @@ function FilterSeries(props: Props) {
toggleExpand={() => setExpanded(!expanded)}/>
)}
{expandable && !expanded && (
<Space className="justify-between w-full px-6 py-2">
<FilterCountLabels filters={series.filter.filters} toggleExpand={() => setExpanded(!expanded)}/>
<Button onClick={() => setExpanded(!expanded)}
size="small"
icon={expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
</Space>
)}
{expanded && (
<>
<div className="p-5">
@ -136,6 +149,13 @@ function FilterSeries(props: Props) {
supportsEmpty={supportsEmpty}
onFilterMove={onFilterMove}
excludeFilterKeys={excludeFilterKeys}
actions={[
expandable && (
<Button onClick={() => setExpanded(!expanded)}
size="small"
icon={expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
)
]}
/>
) : (
<div className="color-gray-medium">{emptyMessage}</div>

View file

@ -1,29 +1,24 @@
import React from 'react';
import {Card, Space, Typography, Button} from "antd";
import {useStore} from "App/mstore";
import FilterSelection from "Shared/Filters/FilterSelection/FilterSelection";
import {eventKeys} from "Types/filter/newFilter";
import {CLICKMAP, FUNNEL, INSIGHTS, RETENTION, TABLE, USER_PATH} from "App/constants/card";
import FilterSeries from "Components/Dashboard/components/FilterSeries/FilterSeries";
import {metricOf} from "App/constants/filterOptions";
import {AudioWaveform, PlusIcon} from "lucide-react";
import {AudioWaveform, ChevronDown, ChevronUp, PlusIcon} from "lucide-react";
import {observer} from "mobx-react-lite";
import AddStepButton from "Components/Dashboard/components/FilterSeries/AddStepButton";
interface Props {
}
function WidgetFormNew(props: Props) {
const [expanded, setExpanded] = React.useState(true);
function WidgetFormNew() {
// const [expanded, setExpanded] = React.useState(true);
const {metricStore, dashboardStore, aiFiltersStore} = useStore();
const metric: any = metricStore.instance;
const eventsLength = metric.series[0].filter.filters.filter((i: any) => i && i.isEvent).length;
const filtersLength = metric.series[0].filter.filters.filter((i: any) => i && !i.isEvent).length;
const isClickmap = metric.metricType === CLICKMAP;
const isClickMap = metric.metricType === CLICKMAP;
const isPathAnalysis = metric.metricType === USER_PATH;
const excludeFilterKeys = isClickmap || isPathAnalysis ? eventKeys : [];
const excludeFilterKeys = isClickMap || isPathAnalysis ? eventKeys : [];
const hasFilters = filtersLength > 0 || eventsLength > 0;
return (
@ -39,11 +34,7 @@ function WidgetFormNew(props: Props) {
)}
</Card>
{/*{eventsLength > 0 && !expanded && (*/}
{/*)}*/}
{hasFilters && expanded && (
{hasFilters && (
<FilterSection metric={metric} excludeFilterKeys={excludeFilterKeys}/>
)}
</>
@ -105,6 +96,7 @@ const FilterSection = observer(({metric, excludeFilterKeys}: any) => {
? 'Filter data using any event or attribute. Use Add Step button below to do so.'
: 'Add user event or filter to define the series by clicking Add Step.'
}
expandable={isSingleSeries}
/>
</div>
))
@ -114,7 +106,10 @@ const FilterSection = observer(({metric, excludeFilterKeys}: any) => {
<Card styles={{body: {padding: '4px'}}}>
<Button
type='link'
onClick={() => metric.addSeries()}
onClick={() => {
metric.addSeries();
}}
disabled={!canAddSeries}
size="small"
>

View file

@ -1,257 +1,312 @@
import { Segmented } from 'antd';
import { List } from 'immutable';
import { GripHorizontal } from 'lucide-react';
import { observer } from 'mobx-react-lite';
import React, { useEffect } from 'react';
import {Segmented} from 'antd';
import {List} from 'immutable';
import {GripHorizontal} from 'lucide-react';
import {observer} from 'mobx-react-lite';
import React, {useEffect} from 'react';
import { Tooltip } from 'UI';
import {Tooltip} from 'UI';
import FilterItem from '../FilterItem';
interface Props {
filter?: any; // event/filter
onUpdateFilter: (filterIndex: any, filter: any) => void;
onFilterMove?: (filters: any) => void;
onRemoveFilter: (filterIndex: any) => void;
onChangeEventsOrder: (e: any, { name, value }: any) => void;
hideEventsOrder?: boolean;
observeChanges?: () => void;
saveRequestPayloads?: boolean;
supportsEmpty?: boolean;
readonly?: boolean;
excludeFilterKeys?: Array<string>;
isConditional?: boolean;
filter?: any; // event/filter
onUpdateFilter: (filterIndex: any, filter: any) => void;
onFilterMove?: (filters: any) => void;
onRemoveFilter: (filterIndex: any) => void;
onChangeEventsOrder: (e: any, {name, value}: any) => void;
hideEventsOrder?: boolean;
observeChanges?: () => void;
saveRequestPayloads?: boolean;
supportsEmpty?: boolean;
readonly?: boolean;
excludeFilterKeys?: Array<string>;
isConditional?: boolean;
actions?: React.ReactNode[];
}
function FilterList(props: Props) {
const {
observeChanges = () => {},
filter,
hideEventsOrder = false,
saveRequestPayloads,
supportsEmpty = true,
excludeFilterKeys = [],
isConditional,
} = props;
const {
observeChanges = () => {
},
filter,
hideEventsOrder = false,
saveRequestPayloads,
supportsEmpty = true,
excludeFilterKeys = [],
isConditional,
actions = []
} = props;
const filters = List(filter.filters);
const eventsOrderSupport = filter.eventsOrderSupport;
const hasEvents = filters.filter((i: any) => i.isEvent).size > 0;
const hasFilters = filters.filter((i: any) => !i.isEvent).size > 0;
const filters = List(filter.filters);
const eventsOrderSupport = filter.eventsOrderSupport;
const hasEvents = filters.filter((i: any) => i.isEvent).size > 0;
const hasFilters = filters.filter((i: any) => !i.isEvent).size > 0;
let rowIndex = 0;
const cannotDeleteFilter = hasEvents && !supportsEmpty;
let rowIndex = 0;
const cannotDeleteFilter = hasEvents && !supportsEmpty;
useEffect(observeChanges, [filters]);
useEffect(observeChanges, [filters]);
const onRemoveFilter = (filterIndex: any) => {
props.onRemoveFilter(filterIndex);
};
const onRemoveFilter = (filterIndex: any) => {
props.onRemoveFilter(filterIndex);
};
const [hoveredItem, setHoveredItem] = React.useState<Record<string, any>>({
i: null,
position: null,
});
const [draggedInd, setDraggedItem] = React.useState<number | null>(null);
const [hoveredItem, setHoveredItem] = React.useState<Record<string, any>>({
i: null,
position: null,
});
const [draggedInd, setDraggedItem] = React.useState<number | null>(null);
const handleDragOverEv = (event: Record<string, any>, i: number) => {
event.preventDefault();
const target = event.currentTarget.getBoundingClientRect();
const hoverMiddleY = (target.bottom - target.top) / 2;
const hoverClientY = event.clientY - target.top;
const handleDragOverEv = (event: Record<string, any>, i: number) => {
event.preventDefault();
const target = event.currentTarget.getBoundingClientRect();
const hoverMiddleY = (target.bottom - target.top) / 2;
const hoverClientY = event.clientY - target.top;
const position = hoverClientY < hoverMiddleY ? 'top' : 'bottom';
setHoveredItem({ position, i });
};
const position = hoverClientY < hoverMiddleY ? 'top' : 'bottom';
setHoveredItem({position, i});
};
const calculateNewPosition = React.useCallback(
(draggedInd: number, hoveredIndex: number, hoveredPosition: string) => {
if (hoveredPosition === 'bottom') {
hoveredIndex++;
}
return draggedInd < hoveredIndex ? hoveredIndex - 1 : hoveredIndex;
},
[]
);
const calculateNewPosition = React.useCallback(
(draggedInd: number, hoveredIndex: number, hoveredPosition: string) => {
if (hoveredPosition === 'bottom') {
hoveredIndex++;
}
return draggedInd < hoveredIndex ? hoveredIndex - 1 : hoveredIndex;
},
[]
);
const handleDragStart = React.useCallback((
ev: Record<string, any>,
index: number,
elId: string
) => {
ev.dataTransfer.setData("text/plain", index.toString());
setDraggedItem(index);
const el = document.getElementById(elId);
if (el) {
ev.dataTransfer.setDragImage(el, 0, 0);
}
}, [])
const handleDragStart = React.useCallback((
ev: Record<string, any>,
index: number,
elId: string
) => {
ev.dataTransfer.setData("text/plain", index.toString());
setDraggedItem(index);
const el = document.getElementById(elId);
if (el) {
ev.dataTransfer.setDragImage(el, 0, 0);
}
}, [])
const handleDrop = React.useCallback(
(event: Record<string, any>) => {
event.preventDefault();
if (draggedInd === null) return;
const newItems = filters.toArray();
const newPosition = calculateNewPosition(
draggedInd,
hoveredItem.i,
hoveredItem.position
);
const handleDrop = React.useCallback(
(event: Record<string, any>) => {
event.preventDefault();
if (draggedInd === null) return;
const newItems = filters.toArray();
const newPosition = calculateNewPosition(
draggedInd,
hoveredItem.i,
hoveredItem.position
);
const reorderedItem = newItems.splice(draggedInd, 1)[0];
newItems.splice(newPosition, 0, reorderedItem);
const reorderedItem = newItems.splice(draggedInd, 1)[0];
newItems.splice(newPosition, 0, reorderedItem);
props.onFilterMove?.(List(newItems));
setHoveredItem({ i: null, position: null });
setDraggedItem(null);
},
[draggedInd, hoveredItem, filters, props.onFilterMove]
);
props.onFilterMove?.(List(newItems));
setHoveredItem({i: null, position: null});
setDraggedItem(null);
},
[draggedInd, hoveredItem, filters, props.onFilterMove]
);
const eventOrderItems = [
{
label: 'THEN',
value: 'then',
disabled: eventsOrderSupport && !eventsOrderSupport.includes('then'),
},
{
label: 'AND',
value: 'and',
disabled: eventsOrderSupport && !eventsOrderSupport.includes('and'),
},
{
label: 'OR',
value: 'or',
disabled: eventsOrderSupport && !eventsOrderSupport.includes('or'),
},
];
const eventOrderItems = [
{
label: 'THEN',
value: 'then',
disabled: eventsOrderSupport && !eventsOrderSupport.includes('then'),
const eventsNum = filters.filter((i: any) => i.isEvent).size
return (
<div className="flex flex-col">
{hasEvents && (
<>
<div className="flex items-center mb-2">
<div className="text-sm color-gray-medium mr-auto">
{filter.eventsHeader}
</div>
{!hideEventsOrder && (
<div className="flex items-center gap-2">
<div
className="color-gray-medium text-sm"
style={{ textDecoration: 'underline dotted' }}
>
<Tooltip
title={`Select the operator to be applied between events in your search.`}
>
<div>Events Order</div>
</Tooltip>
</div>
},
{
label: 'AND',
value: 'and',
disabled: eventsOrderSupport && !eventsOrderSupport.includes('and'),
},
{
label: 'OR',
value: 'or',
disabled: eventsOrderSupport && !eventsOrderSupport.includes('or'),
},
];
<Segmented
size={'small'}
onChange={(v) =>
props.onChangeEventsOrder(
null,
eventOrderItems.find((i) => i.value === v)
)
}
value={filter.eventsOrder}
options={eventOrderItems}
/>
</div>
)}
</div>
<div className={'flex flex-col'}>
{filters.map((filter: any, filterIndex: number) =>
filter.isEvent ? (
<div
style={{
pointerEvents: 'unset',
paddingTop:
hoveredItem.i === filterIndex &&
hoveredItem.position === 'top'
? '1.5rem'
: '0.5rem',
paddingBottom:
hoveredItem.i === filterIndex &&
hoveredItem.position === 'bottom'
? '1.5rem'
: '0.5rem',
marginLeft: '-1.25rem',
width: 'calc(100% + 2.5rem)',
}}
className={
'hover:bg-active-blue px-5 gap-2 items-center flex'
}
id={`${filter.key}-${filterIndex}`}
onDragOver={(e) => handleDragOverEv(e, filterIndex)}
onDrop={(e) => handleDrop(e)}
key={`${filter.key}-${filterIndex}`}
>
{!!props.onFilterMove && eventsNum > 1 ? (
<div
className={'p-2 cursor-grab'}
draggable={!!props.onFilterMove}
onDragStart={(e) =>
handleDragStart(
e,
filterIndex,
`${filter.key}-${filterIndex}`
)
}
>
<GripHorizontal size={16} />
const eventsNum = filters.filter((i: any) => i.isEvent).size
return (
<div className="flex flex-col">
{hasEvents && (
<>
<div className="flex items-center mb-2">
<div className="text-sm color-gray-medium mr-auto">
{filter.eventsHeader}
</div>
{!hideEventsOrder && (
<div className="flex items-center gap-2">
<div
className="color-gray-medium text-sm"
style={{textDecoration: 'underline dotted'}}
>
<Tooltip
title={`Select the operator to be applied between events in your search.`}
>
<div>Events Order</div>
</Tooltip>
</div>
<Segmented
size={'small'}
onChange={(v) =>
props.onChangeEventsOrder(
null,
eventOrderItems.find((i) => i.value === v)
)
}
value={filter.eventsOrder}
options={eventOrderItems}
/>
{actions && actions.map((action, index) => (
<div key={index}>{action}</div>
))}
</div>
)}
</div>
) : null}
<FilterItem
filterIndex={rowIndex++}
filter={filter}
onUpdate={(filter) =>
props.onUpdateFilter(filterIndex, filter)
}
onRemoveFilter={() => onRemoveFilter(filterIndex)}
saveRequestPayloads={saveRequestPayloads}
disableDelete={cannotDeleteFilter}
excludeFilterKeys={excludeFilterKeys}
readonly={props.readonly}
isConditional={isConditional}
/>
</div>
) : null
<div className={'flex flex-col'}>
{filters.map((filter: any, filterIndex: number) =>
filter.isEvent ? (
<div
style={{
pointerEvents: 'unset',
paddingTop:
hoveredItem.i === filterIndex &&
hoveredItem.position === 'top'
? '1.5rem'
: '0.5rem',
paddingBottom:
hoveredItem.i === filterIndex &&
hoveredItem.position === 'bottom'
? '1.5rem'
: '0.5rem',
marginLeft: '-1.25rem',
width: 'calc(100% + 2.5rem)',
}}
className={
'hover:bg-active-blue px-5 gap-2 items-center flex'
}
id={`${filter.key}-${filterIndex}`}
onDragOver={(e) => handleDragOverEv(e, filterIndex)}
onDrop={(e) => handleDrop(e)}
key={`${filter.key}-${filterIndex}`}
>
{!!props.onFilterMove && eventsNum > 1 ? (
<div
className={'p-2 cursor-grab'}
draggable={!!props.onFilterMove}
onDragStart={(e) =>
handleDragStart(
e,
filterIndex,
`${filter.key}-${filterIndex}`
)
}
>
<GripHorizontal size={16}/>
</div>
) : null}
<FilterItem
filterIndex={rowIndex++}
filter={filter}
onUpdate={(filter) =>
props.onUpdateFilter(filterIndex, filter)
}
onRemoveFilter={() => onRemoveFilter(filterIndex)}
saveRequestPayloads={saveRequestPayloads}
disableDelete={cannotDeleteFilter}
excludeFilterKeys={excludeFilterKeys}
readonly={props.readonly}
isConditional={isConditional}
/>
</div>
) : null
)}
</div>
<div className="mb-2"/>
</>
)}
</div>
<div className="mb-2" />
</>
)}
{hasFilters && (
<>
{hasEvents && <div className="border-t -mx-5 mb-4" />}
<div className="mb-2 text-sm color-gray-medium mr-auto">FILTERS</div>
{filters.map((filter: any, filterIndex: any) =>
!filter.isEvent ? (
<div className={'py-2 hover:bg-active-blue px-5'} style={{
marginLeft: '-1.25rem',
width: 'calc(100% + 2.5rem)',
}}>
<FilterItem
key={filterIndex}
readonly={props.readonly}
isFilter={true}
filterIndex={filterIndex}
filter={filter}
onUpdate={(filter) => props.onUpdateFilter(filterIndex, filter)}
onRemoveFilter={() => onRemoveFilter(filterIndex)}
excludeFilterKeys={excludeFilterKeys}
isConditional={isConditional}
/>
</div>
) : null
)}
</>
)}
</div>
);
{hasFilters && (
<>
{hasEvents && <div className="border-t -mx-5 mb-4"/>}
<div className="mb-2 text-sm color-gray-medium mr-auto">FILTERS</div>
{filters.map((filter: any, filterIndex: any) =>
!filter.isEvent ? (
<div className={'py-2 hover:bg-active-blue px-5'} style={{
marginLeft: '-1.25rem',
width: 'calc(100% + 2.5rem)',
}}>
<FilterItem
key={filterIndex}
readonly={props.readonly}
isFilter={true}
filterIndex={filterIndex}
filter={filter}
onUpdate={(filter) => props.onUpdateFilter(filterIndex, filter)}
onRemoveFilter={() => onRemoveFilter(filterIndex)}
excludeFilterKeys={excludeFilterKeys}
isConditional={isConditional}
/>
</div>
) : null
)}
</>
)}
</div>
);
}
export default observer(FilterList);
function EventsOrder(props: {
onChange: (e: any, v: any) => void,
filter: any,
eventsOrderSupport: any
}) {
const {filter, eventsOrderSupport, onChange} = props;
const options = [
{
label: 'THEN',
value: 'then',
disabled: eventsOrderSupport && !eventsOrderSupport.includes('then'),
},
{
label: 'AND',
value: 'and',
disabled: eventsOrderSupport && !eventsOrderSupport.includes('and'),
},
{
label: 'OR',
value: 'or',
disabled: eventsOrderSupport && !eventsOrderSupport.includes('or'),
},
];
return <div className="flex items-center gap-2">
<div
className="color-gray-medium text-sm"
style={{textDecoration: "underline dotted"}}
>
<Tooltip
title={`Select the operator to be applied between events in your search.`}
>
<div>Events Order</div>
</Tooltip>
</div>
<Segmented
size={"small"}
// onChange={props.onChange}
onChange={(v) => onChange(null, options.find((i) => i.value === v))}
value={filter.eventsOrder}
options={options}
/>
</div>;
}