openreplay/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx
Delirium d604f9920b
feat ui: dashboards redesign (#2230)
* feat ui: dashboards redesign start

* more cards

* fix ui: more different cards...

* feat ui: finish cards, all trigger, all icons

* change(ui): added missin const

* feature(ui): new dashboard modal

* feature(ui): new dashboard modal

* change(ui): new cards

* change(ui): dashboard redesign

* change(ui): dashboard redesign

* change(ui): dashboard redesign

* change(ui): modal context and alert form

* change(ui): table card show more with modal

* change(ui): examples

* change(ui): example categorize and other improvements

* change(ui): example categorize and other improvements

* change(ui): performance cards

* change(ui): insights card

* Various style updates in dashboards and other pages. (#2308)

* Various minor style updates

* Various style improvements

* Update ExampleCards.tsx

* change(ui): fixed an issue with card create

* change(ui): fixed an issue with card create

* change(ui): default filters and events order

* change(ui): random data

* Dashboards redesign - improvments (#2313)

* Various minor style updates

* Various style improvements

* Update ExampleCards.tsx

* various minor improvements in dashbaords.

* revised dashboard widget header

* change(ui): sessions by user

* change(ui): funnel example

* change(ui): modal height and scroll

* change(ui): example cards with data

* change(ui): example cards with data

* change(ui): funnel bar text color

* change(ui): example cards overlay click

* change(ui): path analysis filter card

---------

Co-authored-by: Shekar Siri <sshekarsiri@gmail.com>
Co-authored-by: Sudheer Salavadi <connect.uxmaster@gmail.com>
2024-06-27 19:47:34 +02:00

367 lines
15 KiB
TypeScript

import React, {useEffect, useState} from 'react';
import {metricOf, issueOptions, issueCategories} from 'App/constants/filterOptions';
import {FilterKey} from 'Types/filter/filterType';
import {useStore} from 'App/mstore';
import {observer} from 'mobx-react-lite';
import {Button, Icon, confirm, Tooltip} from 'UI';
import FilterSeries from '../FilterSeries';
import Select from 'Shared/Select';
import {withSiteId, dashboardMetricDetails, metricDetails} from 'App/routes';
import MetricTypeDropdown from './components/MetricTypeDropdown';
import MetricSubtypeDropdown from './components/MetricSubtypeDropdown';
import {
TIMESERIES,
TABLE,
CLICKMAP,
FUNNEL,
ERRORS,
RESOURCE_MONITORING,
PERFORMANCE,
WEB_VITALS,
INSIGHTS,
USER_PATH,
RETENTION
} from 'App/constants/card';
import {eventKeys} from 'App/types/filter/newFilter';
import {renderClickmapThumbnail} from './renderMap';
import Widget from 'App/mstore/types/widget';
import FilterItem from 'Shared/Filters/FilterItem';
import {Input} from 'antd'
interface Props {
history: any;
match: any;
onDelete: () => void;
expanded?: boolean;
}
function WidgetForm(props: Props) {
const {
history,
match: {
params: {siteId, dashboardId}
}
} = props;
const [aiQuery, setAiQuery] = useState('')
const [aiAskChart, setAiAskChart] = useState('')
const {metricStore, dashboardStore, aiFiltersStore} = useStore();
const isSaving = metricStore.isSaving;
const metric: any = metricStore.instance;
const [initialInstance, setInitialInstance] = useState();
const timeseriesOptions = metricOf.filter((i) => i.type === 'timeseries');
const tableOptions = metricOf.filter((i) => i.type === 'table');
const isTable = metric.metricType === TABLE;
const isClickmap = metric.metricType === CLICKMAP;
const isFunnel = metric.metricType === FUNNEL;
const isInsights = metric.metricType === INSIGHTS;
const isPathAnalysis = metric.metricType === USER_PATH;
const isRetention = metric.metricType === RETENTION;
const canAddSeries = metric.series.length < 3;
const eventsLength = metric.series[0].filter.filters.filter((i: any) => i && i.isEvent).length;
const cannotSaveFunnel = isFunnel && (!metric.series[0] || eventsLength <= 1);
const isPredefined = [ERRORS, PERFORMANCE, RESOURCE_MONITORING, WEB_VITALS].includes(
metric.metricType
);
const excludeFilterKeys = isClickmap || isPathAnalysis ? eventKeys : [];
useEffect(() => {
if (!!metric && !initialInstance) {
setInitialInstance(metric.toJson());
}
}, [metric]);
const writeOption = ({value, name}: { value: any; name: any }) => {
value = Array.isArray(value) ? value : value.value;
const obj: any = {[name]: value};
if (name === 'metricType') {
switch (value) {
case TIMESERIES:
obj.metricOf = timeseriesOptions[0].value;
break;
case TABLE:
obj.metricOf = tableOptions[0].value;
break;
}
}
metricStore.merge(obj);
};
const onSave = async () => {
const wasCreating = !metric.exists();
if (isClickmap) {
try {
metric.thumbnail = await renderClickmapThumbnail();
} catch (e) {
console.error(e);
}
}
const savedMetric = await metricStore.save(metric);
setInitialInstance(metric.toJson());
if (wasCreating) {
if (parseInt(dashboardId, 10) > 0) {
history.replace(
withSiteId(dashboardMetricDetails(dashboardId, savedMetric.metricId), siteId)
);
void dashboardStore.addWidgetToDashboard(
dashboardStore.getDashboard(parseInt(dashboardId, 10))!,
[savedMetric.metricId]
);
} else {
history.replace(withSiteId(metricDetails(savedMetric.metricId), siteId));
}
}
};
const onDelete = async () => {
if (
await confirm({
header: 'Confirm',
confirmButton: 'Yes, delete',
confirmation: `Are you sure you want to permanently delete this card?`
})
) {
metricStore.delete(metric).then(props.onDelete);
}
};
const undoChanges = () => {
const w = new Widget();
metricStore.merge(w.fromJson(initialInstance), false);
};
const fetchResults = () => {
aiFiltersStore.getCardFilters(aiQuery, metric.metricType)
.then((f) => {
metric.createSeries(f.filters);
})
};
const fetchChartData = () => {
void aiFiltersStore.getCardData(aiAskChart, metric.toJson())
}
const handleKeyDown = (event: any) => {
if (event.key === 'Enter') {
fetchResults();
}
};
const handleChartKeyDown = (event: any) => {
if (event.key === 'Enter') {
fetchChartData();
}
};
const testingKey = localStorage.getItem('__mauricio_testing_access') === 'true';
return (
<div className='p-6'>
{/*
<div className='form-group'>
<div className='flex items-center'>
<span className='mr-2'>Card showing</span>
<MetricTypeDropdown onSelect={writeOption}/>
<MetricSubtypeDropdown onSelect={writeOption}/>
{isPathAnalysis && (
<>
<span className='mx-3'></span>
<Select
name='startType'
options={[
{value: 'start', label: 'With Start Point'},
{value: 'end', label: 'With End Point'}
]}
defaultValue={metric.startType}
// value={metric.metricOf}
onChange={writeOption}
placeholder='All Issues'
/>
<span className='mx-3'>showing</span>
<Select
name='metricValue'
options={[
{value: 'location', label: 'Pages'},
{value: 'click', label: 'Clicks'},
{value: 'input', label: 'Input'},
{value: 'custom', label: 'Custom'},
]}
defaultValue={metric.metricValue}
isMulti={true}
// value={metric.metricValue}
onChange={writeOption}
placeholder='All Issues'
/>
</>
)}
{metric.metricOf === FilterKey.ISSUE && metric.metricType === TABLE && (
<>
<span className='mx-3'>issue type</span>
<Select
name='metricValue'
options={issueOptions}
value={metric.metricValue}
onChange={writeOption}
isMulti={true}
placeholder='All Issues'
/>
</>
)}
{metric.metricType === INSIGHTS && (
<>
<span className='mx-3'>of</span>
<Select
name='metricValue'
options={issueCategories}
value={metric.metricValue}
onChange={writeOption}
isMulti={true}
placeholder='All Categories'
/>
</>
)}
{metric.metricType === 'table' &&
!(metric.metricOf === FilterKey.ERRORS || metric.metricOf === FilterKey.SESSIONS) && (
<>
<span className='mx-3'>showing</span>
<Select
name='metricFormat'
options={[{value: 'sessionCount', label: 'Session Count'}]}
defaultValue={metric.metricFormat}
onChange={writeOption}
/>
</>
)}
</div>
</div>
*/}
{isPathAnalysis && (
<div className='form-group flex flex-col'>
{metric.startType === 'start' ? 'Start Point' : 'End Point'}
<FilterItem
hideDelete={true}
filter={metric.startPoint}
allowedFilterKeys={[FilterKey.LOCATION, FilterKey.CLICK, FilterKey.INPUT, FilterKey.CUSTOM]}
onUpdate={(val) => {
metric.updateStartPoint(val);
}} onRemoveFilter={() => {
}}/>
</div>
)}
{isPredefined && (
<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>
)}
{testingKey ? <Input
placeholder="AI Query"
value={aiQuery}
onChange={(e: any) => setAiQuery(e.target.value)}
className="w-full mb-2"
onKeyDown={handleKeyDown}
/> : null}
{testingKey ? <Input
placeholder="AI Ask Chart"
value={aiAskChart}
onChange={(e: any) => setAiAskChart(e.target.value)}
className="w-full mb-2"
onKeyDown={handleChartKeyDown}
/> : null}
{aiFiltersStore.isLoading ? (
<div>
<div className='flex items-center font-medium py-2'>
Loading
</div>
</div>
) : null}
{!isPredefined && (
<div>
<div className='flex items-center font-medium py-2'>
{`${isTable || isFunnel || isClickmap || isInsights || isPathAnalysis || isRetention ? 'Filter by' : 'Chart Series'}`}
{!isTable && !isFunnel && !isClickmap && !isInsights && !isPathAnalysis && !isRetention && (
<Button
className='ml-2'
variant='text-primary'
onClick={() => metric.addSeries()}
disabled={!canAddSeries}
>
ADD
</Button>
)}
</div>
{metric.series.length > 0 &&
metric.series
.slice(0, isTable || isFunnel || isClickmap || isInsights || isRetention ? 1 : metric.series.length)
.map((series: any, index: number) => (
<div className='mb-2' key={series.name}>
<FilterSeries
canExclude={isPathAnalysis}
supportsEmpty={!isClickmap && !isPathAnalysis}
excludeFilterKeys={excludeFilterKeys}
observeChanges={() => metric.updateKey('hasChanged', true)}
hideHeader={isTable || isClickmap || isInsights || isPathAnalysis || isFunnel}
seriesIndex={index}
series={series}
onRemoveSeries={() => metric.removeSeries(index)}
canDelete={metric.series.length > 1}
emptyMessage={
isTable
? '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.'
}
/>
</div>
))}
</div>
)}
<div className='form-groups flex items-center justify-between'>
<Tooltip
title='Cannot save funnel metric without at least 2 events'
disabled={!cannotSaveFunnel}
>
<div className='flex items-center'>
<Button variant='primary' onClick={onSave} disabled={isSaving || cannotSaveFunnel}>
{metric.exists()
? 'Update'
: parseInt(dashboardId) > 0
? 'Create & Add to Dashboard'
: 'Create'}
</Button>
{metric.exists() && metric.hasChanged && (
<Button onClick={undoChanges} variant='text' icon='arrow-counterclockwise' className='ml-2'>
Undo
</Button>
)}
</div>
</Tooltip>
<div className='flex items-center'>
{metric.exists() && (
<Button variant='text-primary' onClick={onDelete}>
<Icon name='trash' size='14' className='mr-2' color='teal'/>
Delete
</Button>
)}
</div>
</div>
</div>
);
}
export default observer(WidgetForm);