feat(ui) - dashboards wip
|
|
@ -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 } />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
{/* )} */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SeriesName';
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './FilterSeries'
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './MetricsView';
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './WidgetForm';
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './WidgetPreview';
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './WidgetSessions';
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
37
frontend/app/components/Dashboard/store/filter.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
22
frontend/app/components/Dashboard/store/filterSeries.ts
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
.dropdown {
|
||||
display: flex !important;
|
||||
padding: 4px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
color: $gray-darkest;
|
||||
font-weight: 500;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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} /> }
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
3
frontend/app/svg/icons/columns-gap.svg
Normal 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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -93,7 +93,7 @@ module.exports = {
|
|||
// 'transitionProperty',
|
||||
// 'transitionTimingFunction',
|
||||
// 'translate',
|
||||
// 'userSelect',
|
||||
'userSelect',
|
||||
// 'verticalAlign',
|
||||
'visibility',
|
||||
'whitespace',
|
||||
|
|
|
|||