feat(ui) - dashboards wip

This commit is contained in:
Shekar Siri 2022-03-24 09:24:48 +01:00
parent 94370f028f
commit da336d0140
34 changed files with 730 additions and 87 deletions

View file

@ -15,7 +15,7 @@ import LiveSessionPure from 'Components/Session/LiveSession';
import AssistPure from 'Components/Assist';
import BugFinderPure from 'Components/BugFinder/BugFinder';
import DashboardPure from 'Components/Dashboard/NewDashboard';
import WidgetViewPure from 'Components/Dashboard/WidgetView';
import WidgetViewPure from 'Components/Dashboard/components/WidgetView';
import ErrorsPure from 'Components/Errors/Errors';
import Header from 'Components/Header/Header';
// import ResultsModal from 'Shared/Results/ResultsModal';
@ -49,10 +49,8 @@ const withSiteId = routes.withSiteId;
const withObTab = routes.withObTab;
const DASHBOARD_PATH = routes.dashboard();
// const DASHBOARD_WIDGET_CREATE_PATH = routes.dashboardMetricCreate();
// const DASHBOARD_WIDGET_DETAILS_PATH = routes.dashboardMetricDetails();
// const METRIC_CREATE_PATH = routes.metricCreate();
// const METRIC_DETAILS_PATH = routes.metricDetails();
const DASHBOARD_SELECT_PATH = routes.dashboardSelected();
const DASHBOARD_METRICS_PATH = routes.dashboardMetrics();
// const WIDGET_PATAH = routes.dashboardMetric();
const SESSIONS_PATH = routes.sessions();
@ -187,7 +185,11 @@ class Router extends React.Component {
{ siteIdList.length === 0 &&
<Redirect to={ routes.client(routes.CLIENT_TABS.SITES) } />
}
<Route path={ withSiteId(DASHBOARD_PATH, siteIdList) } component={ Dashboard } />
<Route index path={ withSiteId(DASHBOARD_METRICS_PATH, siteIdList) } component={ Dashboard } />
{/* <Route index path={ withSiteId(DASHBOARD_SELECT_PATH, siteIdList) } component={ Dashboard } /> */}
{/* <Route index path={ withSiteId(DASHBOARD_PATH, siteIdList) } component={ Dashboard } /> */}
{/* <Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
<Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
<Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />

View file

@ -5,12 +5,19 @@ import { observer } from "mobx-react-lite";
import { useDashboardStore } from './store/store';
import { withRouter } from 'react-router-dom';
import DashboardView from './components/DashboardView';
import { dashboardSelected, dashboardMetricDetails, dashboardMetricCreate, withSiteId } from 'App/routes';
import {
dashboardSelected,
dashboardMetricDetails,
dashboardMetricCreate,
withSiteId,
dashboardMetrics,
} from 'App/routes';
import DashboardSideMenu from './components/DashboardSideMenu';
import WidgetView from './WidgetView';
import WidgetView from './components/WidgetView';
import MetricsView from './components/MetricsView';
function NewDashboard(props) {
const { match: { params: { siteId, dashboardId, metricId } } } = props;
const { history, match: { params: { siteId, dashboardId, metricId } } } = props;
const store: any = useDashboardStore();
const dashboard = store.selectedDashboard;
@ -19,42 +26,66 @@ function NewDashboard(props) {
}, []);
useEffect(() => {
console.log('dashboardId', dashboardId);
if (!dashboard || !dashboard.dashboardId) {
if (dashboardId) {
store.selectDashboardById(dashboardId);
} else {
store.selectDefaultDashboard();
}
}
}
// 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);
return (
<div className="page-margin container-90">
<div className="side-menu">
<DashboardSideMenu />
</div>
<div className="side-menu-margined">
{ dashboard && dashboard.dashboardId && (
<Switch>
<Route exact strict path={withSiteId(dashboardSelected(dashboard.dashboardId), siteId)}>
<DashboardView dashboard={dashboard} />
</Route>
<Route exact strict path={withSiteId(dashboardMetricCreate(dashboard.dashboardId), siteId)}>
<WidgetView />
</Route>
<Route exact strict path={withSiteId(dashboardMetricDetails(dashboard.dashboardId, metricId), siteId)}>
<WidgetView />
</Route>
{/* <Route exact strict path={withSiteId((dashboard.dashboardId), siteId)}>
<WidgetView />
</Route> */}
<Redirect exact strict to={withSiteId(dashboardSelected(dashboard.dashboardId), siteId )} />
</Switch>
)}
</div>
</div>
<>
{/* { dashboard && dashboard.dashboardId && ( */}
<Switch>
<Route exact strict path={withSiteId(dashboardMetrics(), siteId)}>
<div className="page-margin container-90">
<div className="side-menu">
<DashboardSideMenu />
</div>
<div className="side-menu-margined">
<MetricsView />
</div>
</div>
</Route>
{ dashboardId && (
<>
<Route exact strict path={withSiteId(dashboardSelected(dashboardId), siteId)}>
<div className="page-margin container-90">
<div className="side-menu">
<DashboardSideMenu />
</div>
<div className="side-menu-margined">
<DashboardView dashboard={dashboard} />
</div>
</div>
</Route>
{/* <Route exact strict path={withSiteId(dashboardMetricCreate(dashboard.dashboardId), siteId)}>
<WidgetView />
</Route>
<Route exact strict path={withSiteId(dashboardMetricDetails(dashboard.dashboardId, metricId), siteId)}>
<WidgetView />
</Route> */}
{/* <Redirect exact strict to={withSiteId(dashboardSelected(dashboardId), siteId )} /> */}
</>
)}
</Switch>
{/* )} */}
</>
);
}

View file

@ -1,20 +0,0 @@
import React from 'react';
import { withRouter } from 'react-router-dom';
import { useDashboardStore } from '../store/store';
function WidgetView(props) {
console.log('WidgetView', props);
const store: any = useDashboardStore();
const widget = store.currentWidget;
return (
<div>
<div className="bg-white rounded border">
<div className="p-3">
<h1 className="mb-0 text-2xl">{widget.name}</h1>
</div>
</div>
</div>
);
}
export default withRouter(WidgetView);

View file

@ -3,17 +3,20 @@ import React from 'react';
import { SideMenuitem, SideMenuHeader, Icon } from 'UI';
import { withDashboardStore } from '../../store/store';
import { withRouter } from 'react-router-dom';
import { withSiteId, dashboardSelected } from 'App/routes';
import { withSiteId, dashboardSelected, dashboardMetrics } from 'App/routes';
function DashboardSideMenu(props) {
const { store, history } = props;
const { dashboardId } = store.selectedDashboard;
const redirect = (path) => {
history.push(path);
}
const onItemClick = (dashboard) => {
store.selectDashboardById(dashboard.dashboardId);
const path = withSiteId(dashboardSelected(dashboard.dashboardId), parseInt(store.siteId));
// console.log('path', path);
// history.push(path);
history.push(path);
};
return (
@ -41,7 +44,7 @@ function DashboardSideMenu(props) {
id="menu-manage-alerts"
title="Metrics"
iconName="bar-chart-line"
// onClick={() => setShowAlerts(true)}
onClick={() => redirect(withSiteId(dashboardMetrics(), store.siteId))}
/>
</div>
<div className="border-t w-full my-2" />

View file

@ -0,0 +1,111 @@
import React, { useState } from 'react';
import FilterList from 'Shared/Filters/FilterList';
import {
edit,
updateSeries,
addSeriesFilterFilter,
removeSeriesFilterFilter,
editSeriesFilterFilter,
editSeriesFilter,
} from 'Duck/customMetrics';
import { connect } from 'react-redux';
import { IconButton, Icon } from 'UI';
import FilterSelection from 'Shared/Filters/FilterSelection';
import SeriesName from './SeriesName';
import cn from 'classnames';
import { useDashboardStore } from '../../store/store';
import { observer, useObserver } from 'mobx-react-lite';
interface Props {
seriesIndex: number;
series: any;
edit: typeof edit;
updateSeries: typeof updateSeries;
onRemoveSeries: (seriesIndex) => void;
canDelete?: boolean;
addSeriesFilterFilter: typeof addSeriesFilterFilter;
editSeriesFilterFilter: typeof editSeriesFilterFilter;
editSeriesFilter: typeof editSeriesFilter;
removeSeriesFilterFilter: typeof removeSeriesFilterFilter;
hideHeader?: boolean;
emptyMessage?: any;
}
function FilterSeries(props: Props) {
const { canDelete, hideHeader = false, emptyMessage = 'Add user event or filter to define the series by clicking Add Step.' } = props;
const [expanded, setExpanded] = useState(true)
const { series, seriesIndex } = props;
const onAddFilter = (filter) => {
series.filter.addFilter(filter)
}
const onUpdateFilter = (filterIndex, filter) => {
series.filter.updateFilter(filterIndex, filter)
}
const onChangeEventsOrder = (e, { name, value }) => {
series.filter.updateKey(name, value)
// props.editSeriesFilter(seriesIndex, { eventsOrder: value });
}
const onRemoveFilter = (filterIndex) => {
series.filter.removeFilter(filterIndex)
// props.removeSeriesFilterFilter(seriesIndex, filterIndex);
}
return (
<div className="border rounded bg-white">
<div className={cn("border-b px-5 h-12 flex items-center relative", { 'hidden': hideHeader })}>
<div className="mr-auto">
<SeriesName seriesIndex={seriesIndex} name={series.name} onUpdate={(name) => props.updateSeries(seriesIndex, { name }) } />
</div>
<div className="flex items-center cursor-pointer">
<div onClick={props.onRemoveSeries} className={cn("ml-3", {'disabled': !canDelete})}>
<Icon name="trash" size="16" />
</div>
<div onClick={() => setExpanded(!expanded)} className="ml-3">
<Icon name="chevron-down" size="16" />
</div>
</div>
</div>
{ expanded && (
<>
<div className="p-5">
{ series.filter.filters.length > 0 ? (
<FilterList
filter={series.filter}
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
onChangeEventsOrder={onChangeEventsOrder}
/>
): (
<div className="color-gray-medium">{emptyMessage}</div>
)}
</div>
<div className="border-t h-12 flex items-center">
<div className="-mx-4 px-6">
<FilterSelection
filter={undefined}
onFilterClick={onAddFilter}
>
<IconButton primaryText label="ADD STEP" icon="plus" />
</FilterSelection>
</div>
</div>
</>
)}
</div>
);
}
export default connect(null, {
edit,
updateSeries,
addSeriesFilterFilter,
editSeriesFilterFilter,
editSeriesFilter,
removeSeriesFilterFilter,
})(observer(FilterSeries));

View file

@ -0,0 +1,57 @@
import React, { useState, useRef, useEffect } from 'react';
import { Icon } from 'UI';
interface Props {
name: string;
onUpdate: (name) => void;
seriesIndex?: number;
}
function SeriesName(props: Props) {
const { seriesIndex = 1 } = props;
const [editing, setEditing] = useState(false)
const [name, setName] = useState(props.name)
const ref = useRef<any>(null)
const write = ({ target: { value, name } }) => {
setName(value)
}
const onBlur = () => {
setEditing(false)
props.onUpdate(name)
}
useEffect(() => {
if (editing) {
ref.current.focus()
}
}, [editing])
useEffect(() => {
setName(props.name)
}, [props.name])
// const { name } = props;
return (
<div className="flex items-center">
{ editing ? (
<input
ref={ ref }
name="name"
className="fluid border-0 -mx-2 px-2 h-8"
value={name}
// readOnly={!editing}
onChange={write}
onBlur={onBlur}
onFocus={() => setEditing(true)}
/>
) : (
<div className="text-base h-8 flex items-center border-transparent">{name.trim() === '' ? 'Seriess ' + (seriesIndex + 1) : name }</div>
)}
<div className="ml-3 cursor-pointer" onClick={() => setEditing(true)}><Icon name="pencil" size="14" /></div>
</div>
);
}
export default SeriesName;

View file

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

View file

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

View file

@ -0,0 +1,16 @@
import React from 'react';
import { Button, PageTitle, Link } from 'UI';
import { withSiteId, dashboardMetricCreate } from 'App/routes';
function MetricsView(props) {
return (
<div>
<div className="flex items-center mb-4">
<PageTitle title="Metrics" className="mr-3" />
{/* <Link to={withSiteId(dashboardMetricCreate(dashboard.dashboardId), store.siteId)}><Button primary size="small">Add Metric</Button></Link> */}
</div>
</div>
);
}
export default MetricsView;

View file

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

View file

@ -0,0 +1,160 @@
import React from 'react';
import DropdownPlain from 'Shared/DropdownPlain';
import { metricTypes, metricOf, issueOptions } from 'App/constants/filterOptions';
import { FilterKey } from 'Types/filter/filterType';
import { useDashboardStore } from '../../store/store';
import { useObserver } from 'mobx-react-lite';
import { HelpText, Button, Icon } from 'UI'
import FilterSeries from '../FilterSeries';
interface Props {
// metric: any,
// editWidget: (metric, shouldFetch?) => void
}
function WidgetForm(props: Props) {
// const { metric } = props;
const store: any = useDashboardStore();
const metric = store.currentWidget;
const timeseriesOptions = metricOf.filter(i => i.type === 'timeseries');
const tableOptions = metricOf.filter(i => i.type === 'table');
const isTable = metric.metricType === 'table';
const isTimeSeries = metric.metricType === 'timeseries';
const _issueOptions = [{ text: 'All', value: 'all' }].concat(issueOptions);
const write = ({ target: { value, name } }) => store.editWidget({ [ name ]: value }, false);
const writeOption = (e, { value, name }) => {
store.editWidget({ [ name ]: value }, false);
if (name === 'metricValue') {
store.editWidget({ metricValue: [value] }, false);
}
if (name === 'metricOf') {
if (value === FilterKey.ISSUE) {
store.editWidget({ metricValue: ['all'] }, false);
}
}
if (name === 'metricType') {
if (value === 'timeseries') {
store.editWidget({ metricOf: timeseriesOptions[0].value, viewType: 'lineChart' }, false);
} else if (value === 'table') {
store.editWidget({ metricOf: tableOptions[0].value, viewType: 'table' }, false);
}
}
};
return useObserver(() => (
<div className="p-4">
<div className="form-group">
<label className="font-medium">Metric Type</label>
<div className="flex items-center">
<DropdownPlain
name="metricType"
options={metricTypes}
value={ metric.metricType }
onChange={ writeOption }
/>
{metric.metricType === 'timeseries' && (
<>
<span className="mx-3">of</span>
<DropdownPlain
name="metricOf"
options={timeseriesOptions}
value={ metric.metricOf }
onChange={ writeOption }
/>
</>
)}
{metric.metricType === 'table' && (
<>
<span className="mx-3">of</span>
<DropdownPlain
name="metricOf"
options={tableOptions}
value={ metric.metricOf }
onChange={ writeOption }
/>
</>
)}
{metric.metricOf === FilterKey.ISSUE && (
<>
<span className="mx-3">issue type</span>
<DropdownPlain
name="metricValue"
options={_issueOptions}
value={ metric.metricValue[0] }
onChange={ writeOption }
/>
</>
)}
{metric.metricType === 'table' && (
<>
<span className="mx-3">showing</span>
<DropdownPlain
name="metricFormat"
options={[
{ value: 'sessionCount', text: 'Session Count' },
]}
value={ metric.metricFormat }
onChange={ writeOption }
/>
</>
)}
</div>
</div>
<div className="form-group">
<label className="font-medium items-center">
{`${isTable ? 'Filter by' : 'Chart Series'}`}
{!isTable && (
<Button
className="ml-2"
primary plain size="small"
onClick={() => metric.addSeries()}
>Add Series</Button>
)}
</label>
{metric.series.length > 0 && metric.series.slice(0, isTable ? 1 : metric.series.length).map((series: any, index: number) => (
<div className="mb-2">
<FilterSeries
hideHeader={ isTable }
seriesIndex={index}
series={series}
// onRemoveSeries={() => removeSeries(index)}
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">
<Button primary size="small">Save</Button>
<div className="flex items-center">
<Button plain size="small" className="flex items-center">
<Icon name="trash" size="14" className="mr-2" color="teal"/>
Delete
</Button>
<Button plain size="small" className="flex items-center ml-2">
<Icon name="columns-gap" size="14" className="mr-2" color="teal"/>
Add to Dashboard
</Button>
</div>
</div>
</div>
));
}
export default WidgetForm;

View file

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

View file

@ -0,0 +1,87 @@
import React from 'react';
import cn from 'classnames';
import WidgetWrapper from '../../WidgetWrapper';
import { useDashboardStore } from '../../store/store';
import { Loader, NoContent, SegmentSelection, Icon } from 'UI';
import DateRange from 'Shared/DateRange';
import { useObserver } from 'mobx-react-lite';
interface Props {
className?: string;
}
function WidgetPreview(props: Props) {
const { className = '' } = props;
const store: any = useDashboardStore();
const metric = store.currentWidget;
const isTimeSeries = metric.metricType === 'timeseries';
const isTable = metric.metricType === 'table';
const chagneViewType = (e, { name, value }) => {
metric.update({ [ name ]: value });
}
const onDateChange = (changedDates) => {
// setPeriod({ ...changedDates, rangeName: changedDates.rangeValue })
metric.update({ ...changedDates, rangeName: changedDates.rangeValue });
}
return useObserver(() => (
<div className={cn(className)}>
<div className="flex items-center justify-between">
<h2 className="text-2xl">Trend</h2>
<div className="flex items-center">
{isTimeSeries && (
<>
<span className="color-gray-medium mr-2">Visualization</span>
<SegmentSelection
name="viewType"
className="my-3"
primary
icons={true}
onSelect={ chagneViewType }
value={{ value: metric.viewType }}
list={ [
{ value: 'lineChart', name: 'Chart', icon: 'graph-up-arrow' },
{ value: 'progress', name: 'Progress', icon: 'hash' },
]}
/>
</>
)}
{isTable && (
<>
<span className="mr-1 color-gray-medium">Visualization</span>
<SegmentSelection
name="viewType"
className="my-3"
primary={true}
icons={true}
onSelect={ chagneViewType }
value={{ value: metric.viewType }}
list={[
{ value: 'table', name: 'Table', icon: 'table' },
{ value: 'pieChart', name: 'Chart', icon: 'pie-chart-fill' },
]}
/>
</>
)}
<div className="mx-4" />
<span className="mr-1 color-gray-medium">Time Range</span>
<DateRange
rangeValue={metric.rangeName}
startDate={metric.startDate}
endDate={metric.endDate}
onDateChange={onDateChange}
customRangeRight
direction="left"
/>
</div>
</div>
<div className="bg-white rounded border p-4">
<WidgetWrapper widget={metric} />
</div>
</div>
));
}
export default WidgetPreview;

View file

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

View file

@ -0,0 +1,37 @@
import React from 'react';
import { NoContent } from 'UI';
import cn from 'classnames';
import { useDashboardStore } from '../../store/store';
import SessionItem from 'Shared/SessionItem';
interface Props {
className?: string;
}
function WidgetSessions(props: Props) {
const { className = '' } = props;
const store: any = useDashboardStore();
const widget = store.currentWidget;
return (
<div className={cn(className)}>
<div>
<h2 className="text-2xl">Sessions</h2>
{/* <div className="mr-auto">Showing all sessions between <span className="font-medium">{startTime}</span> and <span className="font-medium">{endTime}</span> </div> */}
</div>
<div className="mt-3">
<NoContent
title="No recordings found"
show={widget.sessions.length === 0}
icon="exclamation-circle"
>
{widget.sessions.map((session: any) => (
<SessionItem key={ session.sessionId } session={ session } />
))}
</NoContent>
</div>
</div>
);
}
export default WidgetSessions;

View file

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

View file

@ -0,0 +1,41 @@
import React, { useState } from 'react';
import { withRouter } from 'react-router-dom';
import { useDashboardStore } from '../../store/store';
import WidgetForm from '../WidgetForm';
import WidgetPreview from '../WidgetPreview';
import WidgetSessions from '../WidgetSessions';
import { Icon } from 'UI';
interface Props {
}
function WidgetView(props: Props) {
const [expanded, setExpanded] = useState(true);
const store: any = useDashboardStore();
const widget = store.currentWidget;
return (
<div className="page-margin container-70 mb-8">
<div className="bg-white rounded border">
<div className="p-4 flex justify-between items-center">
<h1 className="mb-0 text-2xl">{widget.name}</h1>
<div className="text-gray-600">
<div
onClick={() => setExpanded(!expanded)}
className="flex items-center cursor-pointer select-none"
>
<span className="mr-2 color-teal">{expanded ? 'Collapse' : 'Expand'}</span>
<Icon name={expanded ? 'chevron-up' : 'chevron-down'} size="16" color="teal" />
</div>
</div>
</div>
{ expanded && <WidgetForm />}
</div>
<WidgetPreview className="mt-8" />
<WidgetSessions className="mt-8" />
</div>
);
}
export default withRouter(WidgetView);

View file

@ -18,6 +18,7 @@ export default class DashboardStore {
selectedDashboard: observable,
isLoading: observable,
resetCurrentWidget: action,
addDashboard: action,
removeDashboard: action,
updateDashboard: action,
@ -31,6 +32,7 @@ export default class DashboardStore {
toJson: action,
fromJson: action,
setSiteId: action,
editWidget: action,
})
@ -48,6 +50,14 @@ export default class DashboardStore {
// }, 3000)
}
resetCurrentWidget() {
this.currentWidget = new Widget()
}
editWidget(widget: Widget) {
this.currentWidget.update(widget)
}
fetchList() {
this.isLoading = true
@ -210,7 +220,7 @@ function getRandomWidget() {
const widget = new Widget();
widget.widgetId = Math.floor(Math.random() * 100);
widget.name = randomMetricName();
widget.type = "random";
// widget.type = "random";
widget.colSpan = Math.floor(Math.random() * 2) + 1;
return widget;
}

View file

@ -0,0 +1,37 @@
import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx"
export default class Filter {
name: string = ''
filters: any[] = []
eventsOrder: string = 'then'
constructor() {
makeAutoObservable(this, {
addFilter: action,
removeFilter: action,
updateKey: action,
})
}
addFilter(filter: any) {
filter.value = [""]
if (filter.hasOwnProperty('filters')) {
filter.filters = filter.filters.map(i => ({ ...i, value: [""] }))
}
this.filters.push(filter)
console.log('addFilter', this.filters)
}
updateFilter(index: number, filter: any) {
this.filters[index] = filter
console.log('updateFilter', this.filters)
}
updateKey(key, value) {
this[key] = value
}
removeFilter(index: number) {
this.filters.splice(index, 1)
}
}

View file

@ -0,0 +1,22 @@
// import Filter from 'Types/filter';
import Filter from './filter'
import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx"
export default class FilterSeries {
seriesId?: any = undefined
name: string = "Series 1"
filter: Filter = new Filter()
constructor() {
makeAutoObservable(this, {
name: observable,
filter: observable,
update: action,
})
}
update(key, value) {
this[key] = value
}
}

View file

@ -1,9 +1,17 @@
import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx"
import Filter from 'Types/filter';
import FilterSeries from "./filterSeries";
export default class Widget {
widgetId: any = undefined
name: string = "New Metric"
type: string = ""
metricType: string = "timeseries"
metricOf: string = "sessionCount"
metricValue: string = ""
viewType: string = "lineChart"
series: FilterSeries[] = []
sessions: [] = []
position: number = 0
data: any = {}
isLoading: boolean = false
@ -15,26 +23,46 @@ export default class Widget {
makeAutoObservable(this, {
widgetId: observable,
name: observable,
type: observable,
metricType: observable,
metricOf: observable,
position: observable,
data: observable,
isLoading: observable,
isValid: observable,
dashboardId: observable,
addSeries: action,
colSpan: observable,
fromJson: action,
toJson: action,
validate: action,
update: action,
udpateKey: action,
})
const filterSeries = new FilterSeries()
this.series.push(filterSeries)
}
udpateKey(key: string, value: any) {
this[key] = value
}
removeSeries(index: number) {
this.series.splice(index, 1)
}
addSeries() {
const series = new FilterSeries()
series.name = "Series " + (this.series.length + 1)
this.series.push(series)
}
fromJson(json: any) {
runInAction(() => {
this.widgetId = json.widgetId
this.name = json.name
this.type = json.type
this.data = json.data
})
return this
@ -44,7 +72,6 @@ export default class Widget {
return {
widgetId: this.widgetId,
name: this.name,
type: this.type,
data: this.data
}
}

View file

@ -86,13 +86,15 @@ function FilterSeries(props: Props) {
<div className="color-gray-medium">{emptyMessage}</div>
)}
</div>
<div className="px-6 border-t h-12 flex items-center -mx-4">
<FilterSelection
filter={undefined}
onFilterClick={onAddFilter}
>
<IconButton primaryText label="ADD STEP" icon="plus" />
</FilterSelection>
<div className="border-t h-12 flex items-center">
<div className="-mx-4 px-6">
<FilterSelection
filter={undefined}
onFilterClick={onAddFilter}
>
<IconButton primaryText label="ADD STEP" icon="plus" />
</FilterSelection>
</div>
</div>
</>
)}

View file

@ -1,6 +1,6 @@
.dropdown {
display: flex !important;
padding: 4px;
padding: 4px 8px;
border-radius: 3px;
color: $gray-darkest;
font-weight: 500;

View file

@ -1,6 +1,8 @@
import React, { useState} from 'react';
import FilterItem from '../FilterItem';
import { SegmentSelection, Popup } from 'UI';
import { List } from 'immutable';
import { useObserver } from 'mobx-react-lite';
interface Props {
// filters: any[]; // event/filter
@ -12,16 +14,16 @@ interface Props {
}
function FilterList(props: Props) {
const { filter, hideEventsOrder = false } = props;
const filters = filter.filters;
const hasEvents = filter.filters.filter(i => i.isEvent).size > 0;
const hasFilters = filter.filters.filter(i => !i.isEvent).size > 0;
const filters = List(filter.filters);
const hasEvents = filters.filter((i: any) => i.isEvent).size > 0;
const hasFilters = filters.filter((i: any) => !i.isEvent).size > 0;
let rowIndex = 0;
const onRemoveFilter = (filterIndex) => {
props.onRemoveFilter(filterIndex);
}
return (
return useObserver(() => (
<div className="flex flex-col">
{ hasEvents && (
<>
@ -54,7 +56,7 @@ function FilterList(props: Props) {
</div>
)}
</div>
{filters.map((filter, filterIndex) => filter.isEvent ? (
{filters.map((filter: any, filterIndex: any) => filter.isEvent ? (
<FilterItem
key={filterIndex}
filterIndex={rowIndex++}
@ -71,7 +73,7 @@ function FilterList(props: Props) {
<>
{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, filterIndex) => !filter.isEvent ? (
{filters.map((filter: any, filterIndex: any) => !filter.isEvent ? (
<FilterItem
key={filterIndex}
isFilter={true}
@ -84,7 +86,7 @@ function FilterList(props: Props) {
</>
)}
</div>
);
));
}
export default FilterList;

View file

@ -21,7 +21,6 @@ function DropdownPlain({ name, label, options, onChange, defaultValue, wrapperSt
options={ options }
onChange={ onChange }
defaultValue={ defaultValue || options[ 0 ].value }
icon={null}
disabled={disabled}
icon={ <Icon name="chevron-down" color="gray-dark" size="14" className={stl.dropdownIcon} /> }
/>

View file

@ -101,6 +101,7 @@ export const testBuilderNew = () => '/test-builder';
export const testBuilder = (testId = ':testId') => `/test-builder/${ testId }`;
export const dashboard = () => '/dashboard';
export const dashboardMetrics = () => '/dashboard/metrics';
export const dashboardSelected = (id = ':dashboardId', hash) => hashed(`/dashboard/${ id }`, hash);
export const dashboardMetricDetails = (id = ':dashboardId', metricId = ':metricId', hash) => hashed(`/dashboard/${ id }/metric/${metricId}`, hash);
@ -121,6 +122,7 @@ const REQUIRED_SITE_ID_ROUTES = [
assist(),
dashboard(''),
dashboardSelected(''),
dashboardMetrics(''),
// dashboardMetricCreate(''),
dashboardMetricDetails(''),
metricCreate(''),
@ -152,7 +154,15 @@ export function isRoute(route, path){
routeParts.every((p, i) => p.startsWith(':') || p === pathParts[ i ]);
}
const SITE_CHANGE_AVALIABLE_ROUTES = [ sessions(), assist(), dashboard(), dashboardSelected(''), errors(), onboarding('')];
const SITE_CHANGE_AVALIABLE_ROUTES = [
sessions(),
assist(),
dashboard(),
dashboardMetrics(''),
dashboardSelected(''),
errors(),
onboarding('')
];
export const siteChangeAvaliable = path => SITE_CHANGE_AVALIABLE_ROUTES.some(r => isRoute(r, path));
export const redirects = Object.entries({

View file

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-bar-chart-line" viewBox="0 0 16 16">
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-bar-chart-line" viewBox="0 0 16 16">
<path d="M11 2a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v12h.5a.5.5 0 0 1 0 1H.5a.5.5 0 0 1 0-1H1v-3a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3h1V7a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v7h1V2zm1 12h2V2h-2v12zm-3 0V7H7v7h2zm-5 0v-3H2v3h2z"/>
</svg>

Before

Width:  |  Height:  |  Size: 351 B

After

Width:  |  Height:  |  Size: 308 B

View file

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-double-left" viewBox="0 0 16 16">
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-chevron-double-left" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8.354 1.646a.5.5 0 0 1 0 .708L2.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
<path fill-rule="evenodd" d="M12.354 1.646a.5.5 0 0 1 0 .708L6.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 447 B

After

Width:  |  Height:  |  Size: 404 B

View file

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-double-right" viewBox="0 0 16 16">
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-chevron-double-right" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M3.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L9.293 8 3.646 2.354a.5.5 0 0 1 0-.708z"/>
<path fill-rule="evenodd" d="M7.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L13.293 8 7.646 2.354a.5.5 0 0 1 0-.708z"/>
</svg>

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 407 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-columns-gap" viewBox="0 0 16 16">
<path d="M6 1v3H1V1h5zM1 0a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1V1a1 1 0 0 0-1-1H1zm14 12v3h-5v-3h5zm-5-1a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1h-5zM6 8v7H1V8h5zM1 7a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H1zm14-6v7h-5V1h5zm-5-1a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1V1a1 1 0 0 0-1-1h-5z"/>
</svg>

After

Width:  |  Height:  |  Size: 447 B

View file

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-controller" viewBox="0 0 16 16">
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-controller" viewBox="0 0 16 16">
<path d="M11.5 6.027a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0zm-1.5 1.5a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1zm2.5-.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0zm-1.5 1.5a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1zm-6.5-3h1v1h1v1h-1v1h-1v-1h-1v-1h1v-1z"/>
<path d="M3.051 3.26a.5.5 0 0 1 .354-.613l1.932-.518a.5.5 0 0 1 .62.39c.655-.079 1.35-.117 2.043-.117.72 0 1.443.041 2.12.126a.5.5 0 0 1 .622-.399l1.932.518a.5.5 0 0 1 .306.729c.14.09.266.19.373.297.408.408.78 1.05 1.095 1.772.32.733.599 1.591.805 2.466.206.875.34 1.78.364 2.606.024.816-.059 1.602-.328 2.21a1.42 1.42 0 0 1-1.445.83c-.636-.067-1.115-.394-1.513-.773-.245-.232-.496-.526-.739-.808-.126-.148-.25-.292-.368-.423-.728-.804-1.597-1.527-3.224-1.527-1.627 0-2.496.723-3.224 1.527-.119.131-.242.275-.368.423-.243.282-.494.575-.739.808-.398.38-.877.706-1.513.773a1.42 1.42 0 0 1-1.445-.83c-.27-.608-.352-1.395-.329-2.21.024-.826.16-1.73.365-2.606.206-.875.486-1.733.805-2.466.315-.722.687-1.364 1.094-1.772a2.34 2.34 0 0 1 .433-.335.504.504 0 0 1-.028-.079zm2.036.412c-.877.185-1.469.443-1.733.708-.276.276-.587.783-.885 1.465a13.748 13.748 0 0 0-.748 2.295 12.351 12.351 0 0 0-.339 2.406c-.022.755.062 1.368.243 1.776a.42.42 0 0 0 .426.24c.327-.034.61-.199.929-.502.212-.202.4-.423.615-.674.133-.156.276-.323.44-.504C4.861 9.969 5.978 9.027 8 9.027s3.139.942 3.965 1.855c.164.181.307.348.44.504.214.251.403.472.615.674.318.303.601.468.929.503a.42.42 0 0 0 .426-.241c.18-.408.265-1.02.243-1.776a12.354 12.354 0 0 0-.339-2.406 13.753 13.753 0 0 0-.748-2.295c-.298-.682-.61-1.19-.885-1.465-.264-.265-.856-.523-1.733-.708-.85-.179-1.877-.27-2.913-.27-1.036 0-2.063.091-2.913.27z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -1,3 +1,3 @@
<svg width="22" height="14" viewBox="0 0 22 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 22 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 0C0.895431 0 0 0.89543 0 2V12C0 13.1046 0.89543 14 2 14H20C21.1046 14 22 13.1046 22 12V2C22 0.895431 21.1046 0 20 0H2ZM11.0757 10V3.60156H9.98145V8.19385L7.10303 3.60156H6V10H7.10303V5.4165L9.97266 10H11.0757ZM15.0396 3.60156H15.3032L17.7202 10H16.5601L16.0423 8.50146H13.5654L13.0488 10H11.8931L14.3013 3.60156H14.5605H15.0396ZM13.8668 7.62695H15.7402L14.8024 4.91255L13.8668 7.62695Z" fill="#C4C4C4"/>
</svg>

Before

Width:  |  Height:  |  Size: 559 B

After

Width:  |  Height:  |  Size: 536 B

View file

@ -93,7 +93,7 @@ module.exports = {
// 'transitionProperty',
// 'transitionTimingFunction',
// 'translate',
// 'userSelect',
'userSelect',
// 'verticalAlign',
'visibility',
'whitespace',