Merge branch 'dashboards-redesign' of https://github.com/openreplay/openreplay into dashboards-redesign

This commit is contained in:
Sudheer Salavadi 2024-06-27 16:21:38 +05:30
commit aad75771b3
14 changed files with 315 additions and 159 deletions

View file

@ -2,6 +2,7 @@ import React from 'react';
import {Card, Col, Modal, Row, Typography} from "antd";
import {Grid2x2CheckIcon, Plus} from "lucide-react";
import NewDashboardModal from "Components/Dashboard/components/DashboardList/NewDashModal";
import {useStore} from "App/mstore";
interface Props {
open: boolean;
@ -9,6 +10,7 @@ interface Props {
}
function AddCardSelectionModal(props: Props) {
const {metricStore} = useStore();
const [open, setOpen] = React.useState(false);
const [isLibrary, setIsLibrary] = React.useState(false);
@ -18,6 +20,9 @@ function AddCardSelectionModal(props: Props) {
}
const onClick = (isLibrary: boolean) => {
if (!isLibrary) {
metricStore.init();
}
setIsLibrary(isLibrary);
setOpen(true);
}

View file

@ -8,7 +8,7 @@ interface Props {
}
function CreateDashboardButton({disabled = false}: Props) {
const [showModal, setShowModal] = React.useState(true);
const [showModal, setShowModal] = React.useState(false);
return <>
<Button

View file

@ -2,6 +2,7 @@ import ExampleFunnel from "./Examples/Funnel";
import ExamplePath from "./Examples/Path";
import ExampleTrend from "./Examples/Trend";
import PerfBreakdown from "./Examples/PerfBreakdown";
import BarChartCard from "./Examples/BarChart";
import SlowestDomain from "./Examples/SlowestDomain";
import ByBrowser from "./Examples/SessionsBy/ByBrowser";
import BySystem from "./Examples/SessionsBy/BySystem";
@ -315,21 +316,8 @@ export const CARD_LIST: CardType[] = [
metricOf: FilterKey.ERRORS_PER_DOMAINS,
category: CARD_CATEGORIES[3].key,
example: Bars,
data: {
total: 90,
values: [
{
"label": "company.domain.com",
"value": 89
},
{
"label": "openreplay.com",
"value": 15
}
]
}
data: generateRandomBarsData(),
},
{
title: 'Errors by Type',
key: FilterKey.ERRORS_PER_TYPE,
@ -375,6 +363,7 @@ export const CARD_LIST: CardType[] = [
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
{
@ -385,6 +374,7 @@ export const CARD_LIST: CardType[] = [
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
@ -396,6 +386,7 @@ export const CARD_LIST: CardType[] = [
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
@ -407,6 +398,7 @@ export const CARD_LIST: CardType[] = [
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
@ -418,6 +410,7 @@ export const CARD_LIST: CardType[] = [
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
@ -429,6 +422,7 @@ export const CARD_LIST: CardType[] = [
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
@ -440,6 +434,7 @@ export const CARD_LIST: CardType[] = [
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
@ -451,6 +446,7 @@ export const CARD_LIST: CardType[] = [
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
@ -462,6 +458,7 @@ export const CARD_LIST: CardType[] = [
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
@ -473,6 +470,7 @@ export const CARD_LIST: CardType[] = [
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
@ -484,6 +482,7 @@ export const CARD_LIST: CardType[] = [
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
{
@ -494,6 +493,7 @@ export const CARD_LIST: CardType[] = [
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
@ -505,6 +505,7 @@ export const CARD_LIST: CardType[] = [
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
@ -516,6 +517,7 @@ export const CARD_LIST: CardType[] = [
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
@ -527,6 +529,7 @@ export const CARD_LIST: CardType[] = [
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
@ -538,6 +541,7 @@ export const CARD_LIST: CardType[] = [
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
@ -549,6 +553,38 @@ export const CARD_LIST: CardType[] = [
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
]
function generateRandomBarsData(): { total: number, values: { label: string, value: number }[] } {
const labels = ["company.domain.com", "openreplay.com"];
const values = labels.map(label => ({
label,
value: Math.floor(Math.random() * 100)
}));
const total = values.reduce((acc, curr) => acc + curr.value, 0);
return {
total,
values: values.sort((a, b) => b.value - a.value)
};
}
function generateWebVitalData(): { value: number, chart: { timestamp: number, value: number }[], unit: string } {
const chart = Array.from({length: 7}, (_, i) => ({
timestamp: Date.now() + i * 86400000,
value: parseFloat((Math.random() * 10).toFixed(15))
}));
const value = chart.reduce((acc, curr) => acc + curr.value, 0) / chart.length;
return {
value,
chart,
unit: "%"
};
}

View file

@ -0,0 +1,74 @@
import {GitCommitHorizontal} from 'lucide-react';
import React from 'react';
import ExCard from './ExCard';
import {PERFORMANCE} from "App/constants/card";
import {Bar, BarChart, CartesianGrid, Legend, Rectangle, ResponsiveContainer, Tooltip, XAxis, YAxis} from "recharts";
import {Styles} from "Components/Dashboard/Widgets/common";
const _data = [
{
name: 'Jan',
uv: 4000,
pv: 2400,
},
{
name: 'Feb',
uv: 3000,
pv: 1398,
},
{
name: 'Mar',
uv: 2000,
pv: 9800,
},
{
name: 'Apr',
uv: 2780,
pv: 3908,
},
{
name: 'May',
uv: 1890,
pv: 4800,
},
{
name: 'Jun',
uv: 2390,
pv: 3800,
},
{
name: 'Jul',
uv: 3490,
pv: 4300,
},
];
function BarChartCard(props: any) {
return (
<ExCard
{...props}
>
<ResponsiveContainer width="100%" height="100%">
<BarChart
width={400}
height={280}
data={_data}
margin={Styles.chartMargins}
>
{/*<CartesianGrid strokeDasharray="3 3"/>*/}
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#EEEEEE"/>
<XAxis {...Styles.xaxis} dataKey="name"/>
<YAxis {...Styles.yaxis} />
<Tooltip {...Styles.tooltip} />
<Legend/>
<Bar dataKey="pv" fill="#8884d8" activeBar={<Rectangle fill="pink" stroke="blue"/>}/>
{/*<Bar dataKey="uv" fill="#82ca9d" activeBar={<Rectangle fill="gold" stroke="purple"/>}/>*/}
</BarChart>
</ResponsiveContainer>
</ExCard>
);
}
export default BarChartCard;

View file

@ -19,7 +19,9 @@ function ExCard({
style={{width: '100%', height: height || 286}}
>
<div className={'font-medium text-lg'}>{title}</div>
<div className={'flex flex-col gap-2 mt-2 cursor-pointer'} onClick={() => onCard(type)}>{children}</div>
<div className={'flex flex-col gap-2 mt-2 cursor-pointer'}
style={{height: height ? height - 50 : 236,}}
onClick={() => onCard(type)}>{children}</div>
</div>
);
}

View file

@ -6,10 +6,11 @@ interface Props {
title: string;
type: string;
onCard: (card: string) => void;
data?: any,
}
function WebVital(props: Props) {
const data = {
const data = props.data || {
"value": 8.33316146432396,
"chart": [
{

View file

@ -4,6 +4,7 @@ import {CARD_LIST, CARD_CATEGORIES, CardType} from './ExampleCards';
import {useStore} from 'App/mstore';
import Option from './Option';
import CardsLibrary from "Components/Dashboard/components/DashboardList/NewDashModal/CardsLibrary";
import {FUNNEL} from "App/constants/card";
interface SelectCardProps {
onClose: (refresh?: boolean) => void;
@ -20,22 +21,33 @@ const SelectCard: React.FC<SelectCardProps> = (props: SelectCardProps) => {
const dashboardId = window.location.pathname.split('/')[3];
const [libraryQuery, setLibraryQuery] = React.useState<string>('');
const handleCardSelection = (card: string) => {
console.log('card', card);
const selectedCard = CARD_LIST.find((c) => c.key === card) as CardType;
metricStore.merge({
const cardData: any = {
metricType: selectedCard.cardType,
name: selectedCard.title,
metricOf: selectedCard.metricOf,
});
};
if (selectedCard.cardType === FUNNEL) {
cardData.series = []
cardData.series.filter = []
}
metricStore.merge(cardData);
metricStore.instance.resetDefaults();
onCard();
};
const cardItems = useMemo(() => {
return CARD_LIST.filter((card) => card.category === selected).map((card) => (
<div key={card.key} className={card.width ? `col-span-${card.width}` : 'col-span-2'}>
<card.example onCard={handleCardSelection} type={card.key} title={card.title} data={card.data} height={card.height}/>
<card.example onCard={handleCardSelection}
type={card.key}
title={card.title}
data={card.data}
height={card.height}/>
</div>
));
}, [selected]);
@ -58,11 +70,6 @@ const SelectCard: React.FC<SelectCardProps> = (props: SelectCardProps) => {
return (
<>
{/*<Header selectedCount={selectedCards.length}*/}
{/* onAdd={onAddSelected}*/}
{/* title={dashboardId ? (isLibrary ? "Add Card" : "Create Card") : "Select a template to create a card"}*/}
{/*/>*/}
<Space className="items-center justify-between">
<div className="text-xl leading-4 font-medium">
{dashboardId ? (isLibrary ? "Add Card" : "Create Card") : "Select a template to create a card"}
@ -77,7 +84,6 @@ const SelectCard: React.FC<SelectCardProps> = (props: SelectCardProps) => {
<Input.Search
placeholder="Search"
// onSearch={(value) => setLibraryQuery(value)}
onChange={(value) => setLibraryQuery(value.target.value)}
style={{width: 200}}
/>
@ -86,32 +92,17 @@ const SelectCard: React.FC<SelectCardProps> = (props: SelectCardProps) => {
</Space>
{!isLibrary && <CategorySelector setSelected={setSelectedCategory} selected={selected}/>}
{isLibrary ? <CardsLibrary query={libraryQuery} selectedList={selectedCards} category={selected}
onCard={onCardClick}/> :
{isLibrary ?
<CardsLibrary query={libraryQuery}
selectedList={selectedCards}
category={selected}
onCard={onCardClick}/> :
<ExampleCardsGrid items={cardItems}/>}
</>
);
};
// interface HeaderProps {
// selectedCount?: number,
// onAdd?: () => void;
// title?: string;
// }
//
// const Header: React.FC<HeaderProps> = ({title = '', selectedCount = 0, onAdd = () => null}) => (
// <div className="flex items-center justify-between">
// <div className="text-lg leading-4 font-semibold">{title}</div>
// <div className="text-sm text-gray-500">
// {selectedCount > 0 ? (
// <Button type="link" onClick={onAdd}>
// Add {selectedCount} Selected
// </Button>
// ) : ''}
// </div>
// </div>
// );
interface CategorySelectorProps {
setSelected?: React.Dispatch<React.SetStateAction<string>>;
selected?: string;

View file

@ -105,6 +105,7 @@ function FilterSeries(props: Props) {
}
const onChangeEventsOrder = (_: any, {name, value}: any) => {
console.log(name, value)
series.filter.updateKey(name, value);
observeChanges();
};
@ -115,7 +116,7 @@ function FilterSeries(props: Props) {
};
return (
<div className="border rounded-lg shadow-sm bg-white ">
<div className="border rounded-lg shadow-sm bg-white">
{canExclude && <ExcludeFilters filter={series.filter}/>}
{!hideHeader && (
@ -129,7 +130,7 @@ function FilterSeries(props: Props) {
)}
{expandable && !expanded && (
<Space className="justify-between w-full px-6 py-2">
<Space className="justify-between w-full px-5 py-2">
<FilterCountLabels filters={series.filter.filters} toggleExpand={() => setExpanded(!expanded)}/>
<Button onClick={() => setExpanded(!expanded)}
size="small"
@ -162,7 +163,7 @@ function FilterSeries(props: Props) {
)}
</div>
<div className="border-t h-12 flex items-center">
<div className="-mx-4 px-6">
<div className="-mx-4 px-5">
<AddStepButton excludeFilterKeys={excludeFilterKeys} series={series}/>
</div>
</div>

View file

@ -2,15 +2,27 @@ import React from 'react';
import {Card, Space, Typography, Button} from "antd";
import {useStore} from "App/mstore";
import {eventKeys} from "Types/filter/newFilter";
import {CLICKMAP, FUNNEL, INSIGHTS, RETENTION, TABLE, USER_PATH} from "App/constants/card";
import {
CLICKMAP,
ERRORS,
FUNNEL,
INSIGHTS,
PERFORMANCE,
RESOURCE_MONITORING,
RETENTION,
TABLE,
USER_PATH, WEB_VITALS
} from "App/constants/card";
import FilterSeries from "Components/Dashboard/components/FilterSeries/FilterSeries";
import {metricOf} from "App/constants/filterOptions";
import {AudioWaveform, ChevronDown, ChevronUp, PlusIcon} from "lucide-react";
import {observer} from "mobx-react-lite";
import AddStepButton from "Components/Dashboard/components/FilterSeries/AddStepButton";
import {Icon} from "UI";
import FilterItem from "Shared/Filters/FilterItem";
import {FilterKey} from "Types/filter/filterType";
function WidgetFormNew() {
// const [expanded, setExpanded] = React.useState(true);
const {metricStore, dashboardStore, aiFiltersStore} = useStore();
const metric: any = metricStore.instance;
@ -20,9 +32,11 @@ function WidgetFormNew() {
const isPathAnalysis = metric.metricType === USER_PATH;
const excludeFilterKeys = isClickMap || isPathAnalysis ? eventKeys : [];
const hasFilters = filtersLength > 0 || eventsLength > 0;
const isPredefined = [ERRORS, PERFORMANCE, RESOURCE_MONITORING, WEB_VITALS].includes(metric.metricType);
return (
return isPredefined ? <PredefinedMessage/> : (
<>
<AdditionalFilters/>
<Card
styles={{
body: {padding: '0'},
@ -123,3 +137,37 @@ const FilterSection = observer(({metric, excludeFilterKeys}: any) => {
</>
);
})
const PathAnalysisFilter = observer(({metric}: any) => (
<div className='form-group flex flex-col'>
{metric.startType === 'start' ? 'Start Point' : 'End Point'}
<FilterItem
hideDelete
filter={metric.startPoint}
allowedFilterKeys={[FilterKey.LOCATION, FilterKey.CLICK, FilterKey.INPUT, FilterKey.CUSTOM]}
onUpdate={val => metric.updateStartPoint(val)}
onRemoveFilter={() => {
}}
/>
</div>
));
const AdditionalFilters = observer(() => {
const {metricStore, dashboardStore, aiFiltersStore} = useStore();
const metric: any = metricStore.instance;
return (
<>
{metric.metricType === USER_PATH && <PathAnalysisFilter metric={metric}/>}
</>
)
});
const PredefinedMessage = () => (
<div className='flex items-center my-6 justify-center'>
<Icon name='info-circle' size='18' color='gray-medium'/>
<div className='ml-2'>Filtering and drill-downs will be supported soon for this card type.</div>
</div>
);

View file

@ -0,0 +1,55 @@
import {observer} from "mobx-react-lite";
import {Tooltip} from "UI";
import {Segmented} from "antd";
import React from "react";
const EventsOrder = observer((props: {
onChange: (e: any, v: any) => void,
filter: any,
}) => {
const {filter, onChange} = props;
const eventsOrderSupport = filter.eventsOrderSupport;
const options = [
{
name: 'eventsOrder',
label: 'THEN',
value: 'then',
disabled: eventsOrderSupport && !eventsOrderSupport.includes('then'),
},
{
name: 'eventsOrder',
label: 'AND',
value: 'and',
disabled: eventsOrderSupport && !eventsOrderSupport.includes('and'),
},
{
name: 'eventsOrder',
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"}
className="text-sm"
onChange={(v) => onChange(null, options.find((i) => i.value === v))}
value={filter.eventsOrder}
options={options}
/>
</div>;
});
export default EventsOrder;

View file

@ -1,12 +1,11 @@
import {Segmented} from 'antd';
import {Space} 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 FilterItem from '../FilterItem';
import EventsOrder from "Shared/Filters/FilterList/EventsOrder";
interface Props {
filter?: any; // event/filter
@ -38,7 +37,6 @@ function FilterList(props: Props) {
} = 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;
@ -111,25 +109,6 @@ function FilterList(props: Props) {
[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 eventsNum = filters.filter((i: any) => i.isEvent).size
return (
<div className="flex flex-col">
@ -137,37 +116,16 @@ function FilterList(props: Props) {
<>
<div className="flex items-center mb-2">
<div className="text-sm color-gray-medium mr-auto">
{filter.eventsHeader}
{filter.eventsHeader || 'EVENTS'}
</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>
)}
<Space>
{!hideEventsOrder && <EventsOrder filter={filter}
onChange={props.onChangeEventsOrder}/>}
{actions && actions.map((action, index) => (
<div key={index}>{action}</div>
))}
</Space>
</div>
<div className={'flex flex-col'}>
{filters.map((filter: any, filterIndex: number) =>
@ -263,50 +221,3 @@ function FilterList(props: Props) {
}
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>;
}

View file

@ -1,9 +1,13 @@
import { makeAutoObservable, runInAction, observable, action } from "mobx"
import {makeAutoObservable, runInAction, observable, action} from "mobx"
import FilterItem from "./filterItem"
import { filtersMap, conditionalFiltersMap } from 'Types/filter/newFilter';
import {filtersMap, conditionalFiltersMap} from 'Types/filter/newFilter';
import {FilterKey} from "Types/filter/filterType";
export default class Filter {
public static get ID_KEY():string { return "filterId" }
public static get ID_KEY(): string {
return "filterId"
}
filterId: string = ''
name: string = ''
filters: FilterItem[] = []
@ -70,7 +74,7 @@ export default class Filter {
fromJson(json: any) {
this.name = json.name
this.filters = json.filters.map((i: Record<string, any>) =>
new FilterItem(undefined, this.isConditional, this.isMobile).fromJson(i)
new FilterItem(undefined, this.isConditional, this.isMobile).fromJson(i)
);
this.eventsOrder = json.eventsOrder
return this
@ -79,7 +83,7 @@ export default class Filter {
fromData(data) {
this.name = data.name
this.filters = data.filters.map((i: Record<string, any>) =>
new FilterItem(undefined, this.isConditional, this.isMobile).fromData(i)
new FilterItem(undefined, this.isConditional, this.isMobile).fromData(i)
)
this.eventsOrder = data.eventsOrder
return this
@ -121,4 +125,10 @@ export default class Filter {
removeExcludeFilter(index: number) {
this.excludes.splice(index, 1)
}
addFunnelDefaultFilters() {
this.filters = []
this.addFilter({...filtersMap[FilterKey.CLICK], value: [''], operator: 'onAny'})
this.addFilter({...filtersMap[FilterKey.CLICK], value: [''], operator: 'onAny'})
}
}

View file

@ -265,6 +265,16 @@ export default class Widget {
});
}
resetDefaults() {
if (this.metricType === FUNNEL) {
this.series = [];
this.series.push(new FilterSeries());
this.series[0].filter.addFunnelDefaultFilters();
this.series[0].filter.eventsOrder = 'then';
this.series[0].filter.eventsOrderSupport = ['then'];
}
}
exists() {
return this.metricId !== undefined;
}

View file

@ -10,6 +10,18 @@
background-color: var(--bg-teal);
}
:root{
--bg-teal: #394dfe;
}
.ant-btn{
border-radius: .5rem;
}
.ant-btn-primary{
background-color: var(--bg-teal);
}
.ml-15 { margin-left: 15px; }
.ph-10 { padding-left: 10px; padding-right: 10px; }