Merge pull request #359 from openreplay/custom-metrics-improvements
Custom metrics - wip
This commit is contained in:
commit
f6b3a05ab5
69 changed files with 893 additions and 343 deletions
|
|
@ -36,8 +36,7 @@ def get_session2(projectId: int, sessionId: Union[int, str], context: schemas.Cu
|
||||||
include_fav_viewed=True, group_metadata=True)
|
include_fav_viewed=True, group_metadata=True)
|
||||||
if data is None:
|
if data is None:
|
||||||
return {"errors": ["session not found"]}
|
return {"errors": ["session not found"]}
|
||||||
if not data.get("live"):
|
sessions_favorite_viewed.view_session(project_id=projectId, user_id=context.user_id, session_id=sessionId)
|
||||||
sessions_favorite_viewed.view_session(project_id=projectId, user_id=context.user_id, session_id=sessionId)
|
|
||||||
return {
|
return {
|
||||||
'data': data
|
'data': data
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func getTimeoutContext() context.Context {
|
func getTimeoutContext() context.Context {
|
||||||
ctx, _ := context.WithTimeout(context.Background(), time.Duration(time.Second*10))
|
ctx, _ := context.WithTimeout(context.Background(), time.Duration(time.Second*30))
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,7 @@ def update(tenant_id, user_id, changes):
|
||||||
(SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name != 'Owner' LIMIT 1)))""")
|
(SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name != 'Owner' LIMIT 1)))""")
|
||||||
else:
|
else:
|
||||||
sub_query_users.append(f"{helper.key_to_snake_case(key)} = %({key})s")
|
sub_query_users.append(f"{helper.key_to_snake_case(key)} = %({key})s")
|
||||||
|
changes["role_id"] = changes.get("roleId", changes.get("role_id"))
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
if len(sub_query_users) > 0:
|
if len(sub_query_users) > 0:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ import Header from 'Components/Header/Header';
|
||||||
import FunnelDetails from 'Components/Funnels/FunnelDetails';
|
import FunnelDetails from 'Components/Funnels/FunnelDetails';
|
||||||
import FunnelIssueDetails from 'Components/Funnels/FunnelIssueDetails';
|
import FunnelIssueDetails from 'Components/Funnels/FunnelIssueDetails';
|
||||||
import { fetchList as fetchIntegrationVariables } from 'Duck/customField';
|
import { fetchList as fetchIntegrationVariables } from 'Duck/customField';
|
||||||
|
import { fetchList as fetchSiteList } from 'Duck/site';
|
||||||
|
import { fetchList as fetchAnnouncements } from 'Duck/announcements';
|
||||||
|
import { fetchList as fetchAlerts } from 'Duck/alerts';
|
||||||
|
import { fetchWatchdogStatus } from 'Duck/watchdogs';
|
||||||
|
|
||||||
import APIClient from './api_client';
|
import APIClient from './api_client';
|
||||||
import * as routes from './routes';
|
import * as routes from './routes';
|
||||||
|
|
@ -80,7 +84,14 @@ const ONBOARDING_REDIRECT_PATH = routes.onboarding(OB_DEFAULT_TAB);
|
||||||
onboarding: state.getIn([ 'user', 'onboarding' ])
|
onboarding: state.getIn([ 'user', 'onboarding' ])
|
||||||
};
|
};
|
||||||
}, {
|
}, {
|
||||||
fetchUserInfo, fetchTenants, setSessionPath, fetchIntegrationVariables
|
fetchUserInfo,
|
||||||
|
fetchTenants,
|
||||||
|
setSessionPath,
|
||||||
|
fetchIntegrationVariables,
|
||||||
|
fetchSiteList,
|
||||||
|
fetchAnnouncements,
|
||||||
|
fetchAlerts,
|
||||||
|
fetchWatchdogStatus,
|
||||||
})
|
})
|
||||||
class Router extends React.Component {
|
class Router extends React.Component {
|
||||||
state = {
|
state = {
|
||||||
|
|
@ -93,6 +104,14 @@ class Router extends React.Component {
|
||||||
props.fetchUserInfo().then(() => {
|
props.fetchUserInfo().then(() => {
|
||||||
props.fetchIntegrationVariables()
|
props.fetchIntegrationVariables()
|
||||||
}),
|
}),
|
||||||
|
props.fetchSiteList().then(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
props.fetchAnnouncements();
|
||||||
|
props.fetchAlerts();
|
||||||
|
props.fetchWatchdogStatus();
|
||||||
|
}, 100);
|
||||||
|
}),
|
||||||
|
// props.fetchAnnouncements(),
|
||||||
])
|
])
|
||||||
// .then(() => this.onLoginLogout());
|
// .then(() => this.onLoginLogout());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,9 @@ class Notifications extends React.Component {
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
setTimeout(() => {
|
// setTimeout(() => {
|
||||||
props.fetchList();
|
// props.fetchList();
|
||||||
}, 1000);
|
// }, 1000);
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
props.fetchList();
|
props.fetchList();
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,7 @@ import { withRouter } from 'react-router-dom';
|
||||||
@withToggle('visible', 'toggleVisisble')
|
@withToggle('visible', 'toggleVisisble')
|
||||||
@withRouter
|
@withRouter
|
||||||
class Announcements extends React.Component {
|
class Announcements extends React.Component {
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
props.fetchList();
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateToUrl = url => {
|
navigateToUrl = url => {
|
||||||
if (url) {
|
if (url) {
|
||||||
if (url.startsWith(window.ENV.ORIGIN)) {
|
if (url.startsWith(window.ENV.ORIGIN)) {
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,11 @@ import withPageTitle from 'HOCs/withPageTitle';
|
||||||
import {
|
import {
|
||||||
fetchFavoriteList as fetchFavoriteSessionList
|
fetchFavoriteList as fetchFavoriteSessionList
|
||||||
} from 'Duck/sessions';
|
} from 'Duck/sessions';
|
||||||
import { countries } from 'App/constants';
|
|
||||||
import { applyFilter, clearEvents, addAttribute } from 'Duck/filters';
|
import { applyFilter, clearEvents, addAttribute } from 'Duck/filters';
|
||||||
import { fetchList as fetchFunnelsList } from 'Duck/funnels';
|
import { fetchList as fetchFunnelsList } from 'Duck/funnels';
|
||||||
import { defaultFilters, preloadedFilters } from 'Types/filter';
|
|
||||||
import { KEYS } from 'Types/filter/customFilter';
|
import { KEYS } from 'Types/filter/customFilter';
|
||||||
import SessionList from './SessionList';
|
import SessionList from './SessionList';
|
||||||
import stl from './bugFinder.css';
|
import stl from './bugFinder.css';
|
||||||
import { fetchList as fetchSiteList } from 'Duck/site';
|
|
||||||
import withLocationHandlers from "HOCs/withLocationHandlers";
|
import withLocationHandlers from "HOCs/withLocationHandlers";
|
||||||
import { fetch as fetchFilterVariables } from 'Duck/sources';
|
import { fetch as fetchFilterVariables } from 'Duck/sources';
|
||||||
import { fetchSources } from 'Duck/customField';
|
import { fetchSources } from 'Duck/customField';
|
||||||
|
|
@ -68,7 +65,6 @@ const allowedQueryKeys = [
|
||||||
fetchSources,
|
fetchSources,
|
||||||
clearEvents,
|
clearEvents,
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
fetchSiteList,
|
|
||||||
fetchFunnelsList,
|
fetchFunnelsList,
|
||||||
resetFunnel,
|
resetFunnel,
|
||||||
resetFunnelFilters,
|
resetFunnelFilters,
|
||||||
|
|
@ -81,7 +77,6 @@ export default class BugFinder extends React.PureComponent {
|
||||||
state = {showRehydratePanel: false}
|
state = {showRehydratePanel: false}
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
// props.fetchFavoriteSessionList();
|
|
||||||
|
|
||||||
// TODO should cache the response
|
// TODO should cache the response
|
||||||
// props.fetchSources().then(() => {
|
// props.fetchSources().then(() => {
|
||||||
|
|
@ -115,29 +110,6 @@ export default class BugFinder extends React.PureComponent {
|
||||||
this.setState({ showRehydratePanel: !this.state.showRehydratePanel })
|
this.setState({ showRehydratePanel: !this.state.showRehydratePanel })
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetchPreloadedFilters = () => {
|
|
||||||
// this.props.fetchFilterVariables('filterValues').then(function() {
|
|
||||||
// const { filterValues } = this.props;
|
|
||||||
// const keys = [
|
|
||||||
// {key: KEYS.USER_OS, label: 'OS'},
|
|
||||||
// {key: KEYS.USER_BROWSER, label: 'Browser'},
|
|
||||||
// {key: KEYS.USER_DEVICE, label: 'Device'},
|
|
||||||
// {key: KEYS.REFERRER, label: 'Referrer'},
|
|
||||||
// {key: KEYS.USER_COUNTRY, label: 'Country'},
|
|
||||||
// ]
|
|
||||||
// if (filterValues && filterValues.size != 0) {
|
|
||||||
// keys.forEach(({key, label}) => {
|
|
||||||
// const _keyFilters = filterValues.get(key)
|
|
||||||
// if (key === KEYS.USER_COUNTRY) {
|
|
||||||
// preloadedFilters.push(_keyFilters.map(item => ({label, type: key, key, value: item, actualValue: countries[item], isFilter: true})));
|
|
||||||
// } else {
|
|
||||||
// preloadedFilters.push(_keyFilters.map(item => ({label, type: key, key, value: item, isFilter: true})));
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }.bind(this));
|
|
||||||
// }
|
|
||||||
|
|
||||||
setActiveTab = tab => {
|
setActiveTab = tab => {
|
||||||
this.props.setActiveTab(tab);
|
this.props.setActiveTab(tab);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import DateRangeDropdown from 'Shared/DateRangeDropdown';
|
||||||
})
|
})
|
||||||
export default class DateRange extends React.PureComponent {
|
export default class DateRange extends React.PureComponent {
|
||||||
onDateChange = (e) => {
|
onDateChange = (e) => {
|
||||||
this.props.fetchFunnelsList(e.rangeValue)
|
// this.props.fetchFunnelsList(e.rangeValue)
|
||||||
this.props.applyFilter(e)
|
this.props.applyFilter(e)
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,9 @@ function SessionsMenu(props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
fetchWatchdogStatus()
|
// fetchWatchdogStatus()
|
||||||
}, [])
|
// }, [])
|
||||||
|
|
||||||
const capturingAll = props.captureRate && props.captureRate.get('captureAll');
|
const capturingAll = props.captureRate && props.captureRate.get('captureAll');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { withRouter } from 'react-router-dom';
|
||||||
import { Switch, Route, Redirect } from 'react-router';
|
import { Switch, Route, Redirect } from 'react-router';
|
||||||
import { CLIENT_TABS, client as clientRoute } from 'App/routes';
|
import { CLIENT_TABS, client as clientRoute } from 'App/routes';
|
||||||
import { fetchList as fetchMemberList } from 'Duck/member';
|
import { fetchList as fetchMemberList } from 'Duck/member';
|
||||||
import { fetchList as fetchSiteList } from 'Duck/site';
|
|
||||||
|
|
||||||
import ProfileSettings from './ProfileSettings';
|
import ProfileSettings from './ProfileSettings';
|
||||||
import Integrations from './Integrations';
|
import Integrations from './Integrations';
|
||||||
|
|
@ -21,7 +20,6 @@ import Roles from './Roles';
|
||||||
appearance: state.getIn([ 'user', 'account', 'appearance' ]),
|
appearance: state.getIn([ 'user', 'account', 'appearance' ]),
|
||||||
}), {
|
}), {
|
||||||
fetchMemberList,
|
fetchMemberList,
|
||||||
fetchSiteList,
|
|
||||||
})
|
})
|
||||||
@withRouter
|
@withRouter
|
||||||
export default class Client extends React.PureComponent {
|
export default class Client extends React.PureComponent {
|
||||||
|
|
|
||||||
|
|
@ -240,7 +240,6 @@ export default class Dashboard extends React.PureComponent {
|
||||||
Custom Metrics are not supported for comparison.
|
Custom Metrics are not supported for comparison.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* <CustomMetrics /> */}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { Styles } from '../../common';
|
||||||
|
import { ResponsiveContainer, XAxis, YAxis, CartesianGrid, Area, Tooltip } from 'recharts';
|
||||||
|
import { LineChart, Line, Legend } from 'recharts';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: any;
|
||||||
|
params: any;
|
||||||
|
seriesMap: any;
|
||||||
|
colors: any;
|
||||||
|
onClick?: (event, index) => void;
|
||||||
|
}
|
||||||
|
function CustomMetriLineChart(props: Props) {
|
||||||
|
const { data, params, seriesMap, colors, onClick = () => null } = props;
|
||||||
|
return (
|
||||||
|
<ResponsiveContainer height={ 240 } width="100%">
|
||||||
|
<LineChart
|
||||||
|
data={ data }
|
||||||
|
margin={Styles.chartMargins}
|
||||||
|
// syncId={ showSync ? "domainsErrors_4xx" : undefined }
|
||||||
|
onClick={onClick}
|
||||||
|
isAnimationActive={ false }
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||||
|
<XAxis
|
||||||
|
{...Styles.xaxis}
|
||||||
|
dataKey="time"
|
||||||
|
interval={params.density/7}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
{...Styles.yaxis}
|
||||||
|
allowDecimals={false}
|
||||||
|
label={{
|
||||||
|
...Styles.axisLabelLeft,
|
||||||
|
value: "Number of Sessions"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Legend />
|
||||||
|
<Tooltip {...Styles.tooltip} />
|
||||||
|
{ seriesMap.map((key, index) => (
|
||||||
|
<Line
|
||||||
|
key={key}
|
||||||
|
name={key}
|
||||||
|
type="monotone"
|
||||||
|
dataKey={key}
|
||||||
|
stroke={colors[index]}
|
||||||
|
fillOpacity={ 1 }
|
||||||
|
strokeWidth={ 2 }
|
||||||
|
strokeOpacity={ 0.6 }
|
||||||
|
// fill="url(#colorCount)"
|
||||||
|
dot={false}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomMetriLineChart
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './CustomMetriLineChart';
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: any;
|
||||||
|
params: any;
|
||||||
|
colors: any;
|
||||||
|
onClick?: (event, index) => void;
|
||||||
|
}
|
||||||
|
function CustomMetriPercentage(props: Props) {
|
||||||
|
const { data = {} } = props;
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center" style={{ height: '240px'}}>
|
||||||
|
<div className="text-6xl">{data.count}</div>
|
||||||
|
<div className="text-lg mt-6">{`${data.previousCount} ( ${data.countProgress}% ) from previous period.`}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomMetriPercentage;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './CustomMetricPercentage';
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
.wrapper {
|
||||||
|
background-color: white;
|
||||||
|
/* border: solid thin $gray-medium; */
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { ResponsiveContainer, XAxis, YAxis, CartesianGrid, Area, Tooltip } from 'recharts';
|
||||||
|
import { LineChart, Line, Legend, PieChart, Pie, Cell } from 'recharts';
|
||||||
|
import { Styles } from '../../common';
|
||||||
|
|
||||||
|
|
||||||
|
function renderCustomizedLabel({
|
||||||
|
cx, cy, midAngle, innerRadius, outerRadius, value, color, startAngle, endAngle}) {
|
||||||
|
const RADIAN = Math.PI / 180;
|
||||||
|
const diffAngle = endAngle - startAngle;
|
||||||
|
const delta = ((360-diffAngle)/15)-1;
|
||||||
|
const radius = innerRadius + (outerRadius - innerRadius);
|
||||||
|
const x = cx + (radius+delta) * Math.cos(-midAngle * RADIAN);
|
||||||
|
const y = cy + (radius+(delta*delta)) * Math.sin(-midAngle * RADIAN);
|
||||||
|
return (
|
||||||
|
<text x={x} y={y} fill={color} textAnchor={x > cx ? 'start' : 'end'} dominantBaseline="central" fontSize={12} fontWeight="normal">
|
||||||
|
{value}
|
||||||
|
</text>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
function renderCustomizedLabelLine(props){
|
||||||
|
let { cx, cy, midAngle, innerRadius, outerRadius, color, startAngle, endAngle } = props;
|
||||||
|
const RADIAN = Math.PI / 180;
|
||||||
|
const diffAngle = endAngle - startAngle;
|
||||||
|
const radius = 10 + innerRadius + (outerRadius - innerRadius);
|
||||||
|
let path='';
|
||||||
|
for(let i=0;i<((360-diffAngle)/15);i++){
|
||||||
|
path += `${(cx + (radius+i) * Math.cos(-midAngle * RADIAN))},${(cy + (radius+i*i) * Math.sin(-midAngle * RADIAN))} `
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<polyline points={path} stroke={color} fill="none" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
interface Props {
|
||||||
|
data: any;
|
||||||
|
params: any;
|
||||||
|
// seriesMap: any;
|
||||||
|
colors: any;
|
||||||
|
onClick?: (event, index) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CustomMetricPieChart(props: Props) {
|
||||||
|
const { data = { values: [] }, params, colors, onClick = () => null } = props;
|
||||||
|
return (
|
||||||
|
<ResponsiveContainer height={ 240 } width="100%">
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
isAnimationActive={ false }
|
||||||
|
data={data.values}
|
||||||
|
dataKey="sessionCount"
|
||||||
|
nameKey="name"
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
// innerRadius={40}
|
||||||
|
outerRadius={70}
|
||||||
|
// fill={colors[0]}
|
||||||
|
activeIndex={1}
|
||||||
|
labelLine={({
|
||||||
|
cx,
|
||||||
|
cy,
|
||||||
|
midAngle,
|
||||||
|
innerRadius,
|
||||||
|
outerRadius,
|
||||||
|
value,
|
||||||
|
index
|
||||||
|
}) => {
|
||||||
|
const RADIAN = Math.PI / 180;
|
||||||
|
let radius1 = 15 + innerRadius + (outerRadius - innerRadius);
|
||||||
|
let radius2 = innerRadius + (outerRadius - innerRadius);
|
||||||
|
let x2 = cx + radius1 * Math.cos(-midAngle * RADIAN);
|
||||||
|
let y2 = cy + radius1 * Math.sin(-midAngle * RADIAN);
|
||||||
|
let x1 = cx + radius2 * Math.cos(-midAngle * RADIAN);
|
||||||
|
let y1 = cy + radius2 * Math.sin(-midAngle * RADIAN);
|
||||||
|
|
||||||
|
const percentage = value * 100 / data.values.reduce((a, b) => a + b.sessionCount, 0);
|
||||||
|
|
||||||
|
if (percentage<3){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return(
|
||||||
|
<line x1={x1} y1={y1} x2={x2} y2={y2} stroke="#3EAAAF" strokeWidth={1} />
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
label={({
|
||||||
|
cx,
|
||||||
|
cy,
|
||||||
|
midAngle,
|
||||||
|
innerRadius,
|
||||||
|
outerRadius,
|
||||||
|
value,
|
||||||
|
index
|
||||||
|
}) => {
|
||||||
|
const RADIAN = Math.PI / 180;
|
||||||
|
let radius = 20 + innerRadius + (outerRadius - innerRadius);
|
||||||
|
let x = cx + radius * Math.cos(-midAngle * RADIAN);
|
||||||
|
let y = cy + radius * Math.sin(-midAngle * RADIAN);
|
||||||
|
const percentage = (value / data.values.reduce((a, b) => a + b.sessionCount, 0)) * 100;
|
||||||
|
if (percentage<3){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<text
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
fontWeight="300"
|
||||||
|
fontSize="12px"
|
||||||
|
// fontFamily="'Source Sans Pro', 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'"
|
||||||
|
textAnchor={x > cx ? "start" : "end"}
|
||||||
|
dominantBaseline="central"
|
||||||
|
fill='#3EAAAF'
|
||||||
|
>
|
||||||
|
{data.values[index].name} - ({value})
|
||||||
|
</text>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
// label={({
|
||||||
|
// cx,
|
||||||
|
// cy,
|
||||||
|
// midAngle,
|
||||||
|
// innerRadius,
|
||||||
|
// outerRadius,
|
||||||
|
// value,
|
||||||
|
// index
|
||||||
|
// }) => {
|
||||||
|
// const RADIAN = Math.PI / 180;
|
||||||
|
// const radius = 30 + innerRadius + (outerRadius - innerRadius);
|
||||||
|
// const x = cx + radius * Math.cos(-midAngle * RADIAN);
|
||||||
|
// const y = cy + radius * Math.sin(-midAngle * RADIAN);
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <text
|
||||||
|
// x={x}
|
||||||
|
// y={y}
|
||||||
|
// fill="#3EAAAF"
|
||||||
|
// textAnchor={x > cx ? "start" : "end"}
|
||||||
|
// dominantBaseline="top"
|
||||||
|
// fontSize={10}
|
||||||
|
// >
|
||||||
|
// {data.values[index].name} ({value})
|
||||||
|
// </text>
|
||||||
|
// );
|
||||||
|
// }}
|
||||||
|
>
|
||||||
|
{data.values.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={Styles.colorsPie[index % Styles.colorsPie.length]} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip {...Styles.tooltip} />
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomMetricPieChart;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './CustomMetricPieChart';
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
.wrapper {
|
||||||
|
background-color: white;
|
||||||
|
/* border: solid thin $gray-medium; */
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { Table } from '../../common';
|
||||||
|
import { List } from 'immutable';
|
||||||
|
|
||||||
|
const cols = [
|
||||||
|
{
|
||||||
|
key: 'name',
|
||||||
|
title: 'Resource',
|
||||||
|
toText: name => name || 'Unidentified',
|
||||||
|
width: '70%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sessionCount',
|
||||||
|
title: 'Sessions',
|
||||||
|
toText: sessions => sessions,
|
||||||
|
width: '30%',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: any;
|
||||||
|
onClick?: (event, index) => void;
|
||||||
|
}
|
||||||
|
function CustomMetriTable(props: Props) {
|
||||||
|
const { data = { values: [] }, onClick = () => null } = props;
|
||||||
|
const rows = List(data.values);
|
||||||
|
return (
|
||||||
|
<div className="" style={{ height: '240px'}}>
|
||||||
|
<Table
|
||||||
|
small
|
||||||
|
cols={ cols }
|
||||||
|
rows={ rows }
|
||||||
|
rowClass="group"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomMetriTable;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './CustomMetricTable';
|
||||||
|
|
@ -2,22 +2,25 @@ import React, { useEffect, useState } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Loader, NoContent, Icon, Popup } from 'UI';
|
import { Loader, NoContent, Icon, Popup } from 'UI';
|
||||||
import { Styles } from '../../common';
|
import { Styles } from '../../common';
|
||||||
import { ResponsiveContainer, AreaChart, XAxis, YAxis, CartesianGrid, Area, Tooltip } from 'recharts';
|
import { ResponsiveContainer } from 'recharts';
|
||||||
import { LineChart, Line, Legend } from 'recharts';
|
|
||||||
import { LAST_24_HOURS, LAST_30_MINUTES, YESTERDAY, LAST_7_DAYS } from 'Types/app/period';
|
import { LAST_24_HOURS, LAST_30_MINUTES, YESTERDAY, LAST_7_DAYS } from 'Types/app/period';
|
||||||
import stl from './CustomMetricWidget.css';
|
import stl from './CustomMetricWidget.css';
|
||||||
import { getChartFormatter, getStartAndEndTimestampsByDensity } from 'Types/dashboard/helper';
|
import { getChartFormatter, getStartAndEndTimestampsByDensity } from 'Types/dashboard/helper';
|
||||||
import { init, edit, remove, setAlertMetricId, setActiveWidget, updateActiveState } from 'Duck/customMetrics';
|
import { init, edit, remove, setAlertMetricId, setActiveWidget, updateActiveState } from 'Duck/customMetrics';
|
||||||
import APIClient from 'App/api_client';
|
import APIClient from 'App/api_client';
|
||||||
import { setShowAlerts } from 'Duck/dashboard';
|
import { setShowAlerts } from 'Duck/dashboard';
|
||||||
|
import CustomMetriLineChart from '../CustomMetriLineChart';
|
||||||
|
import CustomMetricPieChart from '../CustomMetricPieChart';
|
||||||
|
import CustomMetricPercentage from '../CustomMetricPercentage';
|
||||||
|
import CustomMetricTable from '../CustomMetricTable';
|
||||||
|
|
||||||
const customParams = rangeName => {
|
const customParams = rangeName => {
|
||||||
const params = { density: 70 }
|
const params = { density: 70 }
|
||||||
|
|
||||||
if (rangeName === LAST_24_HOURS) params.density = 70
|
// if (rangeName === LAST_24_HOURS) params.density = 70
|
||||||
if (rangeName === LAST_30_MINUTES) params.density = 70
|
// if (rangeName === LAST_30_MINUTES) params.density = 70
|
||||||
if (rangeName === YESTERDAY) params.density = 70
|
// if (rangeName === YESTERDAY) params.density = 70
|
||||||
if (rangeName === LAST_7_DAYS) params.density = 70
|
// if (rangeName === LAST_7_DAYS) params.density = 70
|
||||||
|
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
@ -47,11 +50,10 @@ function CustomMetricWidget(props: Props) {
|
||||||
|
|
||||||
const colors = Styles.customMetricColors;
|
const colors = Styles.customMetricColors;
|
||||||
const params = customParams(period.rangeName)
|
const params = customParams(period.rangeName)
|
||||||
const gradientDef = Styles.gradientDef();
|
|
||||||
const metricParams = { ...params, metricId: metric.metricId, viewType: 'lineChart', startDate: period.start, endDate: period.end }
|
const metricParams = { ...params, metricId: metric.metricId, viewType: 'lineChart', startDate: period.start, endDate: period.end }
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
new APIClient()['post']('/custom_metrics/chart', { ...metricParams, q: metric.name })
|
new APIClient()['post'](`/custom_metrics/${metricParams.metricId}/chart`, { ...metricParams, q: metric.name })
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(({ errors, data }) => {
|
.then(({ errors, data }) => {
|
||||||
if (errors) {
|
if (errors) {
|
||||||
|
|
@ -78,8 +80,19 @@ function CustomMetricWidget(props: Props) {
|
||||||
if (event) {
|
if (event) {
|
||||||
const payload = event.activePayload[0].payload;
|
const payload = event.activePayload[0].payload;
|
||||||
const timestamp = payload.timestamp;
|
const timestamp = payload.timestamp;
|
||||||
const { startTimestamp, endTimestamp } = getStartAndEndTimestampsByDensity(timestamp, period.start, period.end, params.density);
|
const periodTimestamps = metric.metricType === 'timeseries' ?
|
||||||
props.setActiveWidget({ widget: metric, startTimestamp, endTimestamp, timestamp: payload.timestamp, index })
|
getStartAndEndTimestampsByDensity(timestamp, period.start, period.end, params.density) :
|
||||||
|
period.toTimestamps();
|
||||||
|
|
||||||
|
const activeWidget = {
|
||||||
|
widget: metric,
|
||||||
|
period: period,
|
||||||
|
...periodTimestamps,
|
||||||
|
timestamp: payload.timestamp,
|
||||||
|
index,
|
||||||
|
}
|
||||||
|
|
||||||
|
props.setActiveWidget(activeWidget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,7 +102,7 @@ function CustomMetricWidget(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={stl.wrapper}>
|
<div className={stl.wrapper}>
|
||||||
<div className="flex items-center mb-10 p-2">
|
<div className="flex items-center p-2">
|
||||||
<div className="font-medium">{metric.name}</div>
|
<div className="font-medium">{metric.name}</div>
|
||||||
<div className="ml-auto flex items-center">
|
<div className="ml-auto flex items-center">
|
||||||
<WidgetIcon className="cursor-pointer mr-6" icon="bell-plus" tooltip="Set Alert" onClick={props.onAlertClick} />
|
<WidgetIcon className="cursor-pointer mr-6" icon="bell-plus" tooltip="Set Alert" onClick={props.onAlertClick} />
|
||||||
|
|
@ -97,56 +110,51 @@ function CustomMetricWidget(props: Props) {
|
||||||
<WidgetIcon className="cursor-pointer" icon="close" tooltip="Hide Metric" onClick={() => updateActiveState(metric.metricId, false)} />
|
<WidgetIcon className="cursor-pointer" icon="close" tooltip="Hide Metric" onClick={() => updateActiveState(metric.metricId, false)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="px-3">
|
||||||
<Loader loading={ loading } size="small">
|
<Loader loading={ loading } size="small">
|
||||||
<NoContent
|
<NoContent
|
||||||
size="small"
|
size="small"
|
||||||
show={ data.length === 0 }
|
show={ data.length === 0 }
|
||||||
>
|
>
|
||||||
<ResponsiveContainer height={ 240 } width="100%">
|
<ResponsiveContainer height={ 240 } width="100%">
|
||||||
<LineChart
|
<>
|
||||||
data={ data }
|
{metric.viewType === 'lineChart' && (
|
||||||
margin={Styles.chartMargins}
|
<CustomMetriLineChart
|
||||||
syncId={ showSync ? "domainsErrors_4xx" : undefined }
|
data={ data }
|
||||||
onClick={clickHandler}
|
params={ params }
|
||||||
>
|
seriesMap={ seriesMap }
|
||||||
<defs>
|
colors={ colors }
|
||||||
<linearGradient id="colorCount" x1="0" y1="0" x2="0" y2="1">
|
onClick={ clickHandler }
|
||||||
<stop offset="5%" stopColor={colors[4]} stopOpacity={ 0.9 } />
|
|
||||||
<stop offset="95%" stopColor={colors[4]} stopOpacity={ 0.2 } />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
|
||||||
<XAxis
|
|
||||||
{...Styles.xaxis}
|
|
||||||
dataKey="time"
|
|
||||||
interval={params.density/7}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
{...Styles.yaxis}
|
|
||||||
allowDecimals={false}
|
|
||||||
label={{
|
|
||||||
...Styles.axisLabelLeft,
|
|
||||||
value: "Number of Sessions"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Legend />
|
|
||||||
<Tooltip {...Styles.tooltip} />
|
|
||||||
{ seriesMap.map((key, index) => (
|
|
||||||
<Line
|
|
||||||
key={key}
|
|
||||||
name={key}
|
|
||||||
type="monotone"
|
|
||||||
dataKey={key}
|
|
||||||
stroke={colors[index]}
|
|
||||||
fillOpacity={ 1 }
|
|
||||||
strokeWidth={ 2 }
|
|
||||||
strokeOpacity={ 0.8 }
|
|
||||||
fill="url(#colorCount)"
|
|
||||||
dot={false}
|
|
||||||
/>
|
/>
|
||||||
))}
|
)}
|
||||||
</LineChart>
|
|
||||||
|
{metric.viewType === 'pieChart' && (
|
||||||
|
<CustomMetricPieChart
|
||||||
|
data={ data[0] }
|
||||||
|
params={ params }
|
||||||
|
colors={ colors }
|
||||||
|
onClick={ clickHandler }
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{metric.viewType === 'progress' && (
|
||||||
|
<CustomMetricPercentage
|
||||||
|
data={ data[0] }
|
||||||
|
params={ params }
|
||||||
|
colors={ colors }
|
||||||
|
onClick={ clickHandler }
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{metric.viewType === 'table' && (
|
||||||
|
<CustomMetricTable
|
||||||
|
data={ data[0] }
|
||||||
|
// params={ params }
|
||||||
|
// colors={ colors }
|
||||||
|
onClick={ clickHandler }
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</NoContent>
|
</NoContent>
|
||||||
</Loader>
|
</Loader>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,13 @@
|
||||||
.wrapper {
|
.wrapper {
|
||||||
background-color: white;
|
background-color: $gray-light;
|
||||||
/* border: solid thin $gray-medium; */
|
/* border: solid thin $gray-medium; */
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 10px;
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.innerWapper {
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 70%;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
@ -1,16 +1,20 @@
|
||||||
import React, { useEffect, useState, useRef } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Loader, NoContent, Icon } from 'UI';
|
import { Loader, NoContent, SegmentSelection, Icon } from 'UI';
|
||||||
import { Styles } from '../../common';
|
import { Styles } from '../../common';
|
||||||
import { ResponsiveContainer, XAxis, YAxis, CartesianGrid, Tooltip, LineChart, Line, Legend } from 'recharts';
|
// import { ResponsiveContainer, XAxis, YAxis, CartesianGrid, Tooltip, LineChart, Line, Legend } from 'recharts';
|
||||||
import Period, { LAST_24_HOURS, LAST_30_MINUTES, YESTERDAY, LAST_7_DAYS } from 'Types/app/period';
|
import Period, { LAST_24_HOURS, LAST_30_MINUTES, YESTERDAY, LAST_7_DAYS } from 'Types/app/period';
|
||||||
import stl from './CustomMetricWidgetPreview.css';
|
import stl from './CustomMetricWidgetPreview.css';
|
||||||
import { getChartFormatter } from 'Types/dashboard/helper';
|
import { getChartFormatter } from 'Types/dashboard/helper';
|
||||||
import { remove } from 'Duck/customMetrics';
|
import { remove } from 'Duck/customMetrics';
|
||||||
import DateRange from 'Shared/DateRange';
|
import DateRange from 'Shared/DateRange';
|
||||||
import { edit } from 'Duck/customMetrics';
|
import { edit } from 'Duck/customMetrics';
|
||||||
|
import CustomMetriLineChart from '../CustomMetriLineChart';
|
||||||
|
import CustomMetricPercentage from '../CustomMetricPercentage';
|
||||||
|
import CustomMetricTable from '../CustomMetricTable';
|
||||||
|
|
||||||
import APIClient from 'App/api_client';
|
import APIClient from 'App/api_client';
|
||||||
|
import CustomMetricPieChart from '../CustomMetricPieChart';
|
||||||
|
|
||||||
const customParams = rangeName => {
|
const customParams = rangeName => {
|
||||||
const params = { density: 70 }
|
const params = { density: 70 }
|
||||||
|
|
@ -43,8 +47,9 @@ function CustomMetricWidget(props: Props) {
|
||||||
const params = customParams(period.rangeName)
|
const params = customParams(period.rangeName)
|
||||||
const gradientDef = Styles.gradientDef();
|
const gradientDef = Styles.gradientDef();
|
||||||
const metricParams = { ...params, metricId: metric.metricId, viewType: 'lineChart' }
|
const metricParams = { ...params, metricId: metric.metricId, viewType: 'lineChart' }
|
||||||
|
|
||||||
const prevMetricRef = useRef<any>();
|
const prevMetricRef = useRef<any>();
|
||||||
|
const isTimeSeries = metric.metricType === 'timeseries';
|
||||||
|
const isTable = metric.metricType === 'table';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check for title change
|
// Check for title change
|
||||||
|
|
@ -83,11 +88,46 @@ function CustomMetricWidget(props: Props) {
|
||||||
props.edit({ ...changedDates, rangeName: changedDates.rangeValue });
|
props.edit({ ...changedDates, rangeName: changedDates.rangeValue });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const chagneViewType = (e, { name, value }) => {
|
||||||
|
props.edit({ [ name ]: value });
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-10">
|
<div className="mb-10">
|
||||||
<div className="flex items-center mb-4">
|
<div className="flex items-center mb-4">
|
||||||
<div className="mr-auto font-medium">Preview</div>
|
<div className="mr-auto font-medium">Preview</div>
|
||||||
<div>
|
<div className="flex items-center">
|
||||||
|
{isTimeSeries && (
|
||||||
|
<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 && (
|
||||||
|
<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: 'graph-up-arrow' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="mx-2" />
|
||||||
|
<span className="mr-1 color-gray-medium">Time Range</span>
|
||||||
<DateRange
|
<DateRange
|
||||||
rangeValue={metric.rangeName}
|
rangeValue={metric.rangeName}
|
||||||
startDate={metric.startDate}
|
startDate={metric.startDate}
|
||||||
|
|
@ -99,50 +139,50 @@ function CustomMetricWidget(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={stl.wrapper}>
|
<div className={stl.wrapper}>
|
||||||
<div>
|
<div className={stl.innerWapper}>
|
||||||
<Loader loading={ loading } size="small">
|
<Loader loading={ loading } size="small">
|
||||||
<NoContent
|
<NoContent
|
||||||
size="small"
|
size="small"
|
||||||
show={ data.length === 0 }
|
show={ data.length === 0 }
|
||||||
>
|
>
|
||||||
<ResponsiveContainer height={ 240 } width="100%">
|
<div className="p-4 font-medium">
|
||||||
<LineChart
|
{metric.name}
|
||||||
data={ data }
|
</div>
|
||||||
margin={Styles.chartMargins}
|
<div className="px-4 pb-4">
|
||||||
syncId={ showSync ? "domainsErrors_4xx" : undefined }
|
{ isTimeSeries && (
|
||||||
>
|
<>
|
||||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
{ metric.viewType === 'progress' && (
|
||||||
<XAxis
|
<CustomMetricPercentage
|
||||||
{...Styles.xaxis}
|
data={data[0]}
|
||||||
dataKey="time"
|
colors={colors}
|
||||||
interval={params.density/7}
|
params={params}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
)}
|
||||||
{...Styles.yaxis}
|
{ metric.viewType === 'lineChart' && (
|
||||||
allowDecimals={false}
|
<CustomMetriLineChart
|
||||||
label={{
|
data={data}
|
||||||
...Styles.axisLabelLeft,
|
seriesMap={seriesMap}
|
||||||
value: "Number of Sessions"
|
colors={colors}
|
||||||
}}
|
params={params}
|
||||||
/>
|
/>
|
||||||
<Legend />
|
)}
|
||||||
<Tooltip {...Styles.tooltip} />
|
</>
|
||||||
{ seriesMap.map((key, index) => (
|
)}
|
||||||
<Line
|
|
||||||
key={key}
|
{ isTable && (
|
||||||
name={key}
|
<>
|
||||||
type="monotone"
|
{ metric.viewType === 'table' ? (
|
||||||
dataKey={key}
|
<CustomMetricTable data={data[0]} />
|
||||||
stroke={colors[index]}
|
) : (
|
||||||
fillOpacity={ 1 }
|
<CustomMetricPieChart
|
||||||
strokeWidth={ 2 }
|
data={data[0]}
|
||||||
strokeOpacity={ 0.6 }
|
colors={colors}
|
||||||
// fill="url(#colorCount)"
|
params={params}
|
||||||
dot={false}
|
/>
|
||||||
/>
|
)}
|
||||||
))}
|
</>
|
||||||
</LineChart>
|
)}
|
||||||
</ResponsiveContainer>
|
</div>
|
||||||
</NoContent>
|
</NoContent>
|
||||||
</Loader>
|
</Loader>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import CustomMetricWidget from './CustomMetricWidget';
|
||||||
import AlertFormModal from 'App/components/Alerts/AlertFormModal';
|
import AlertFormModal from 'App/components/Alerts/AlertFormModal';
|
||||||
import { init as initAlert } from 'Duck/alerts';
|
import { init as initAlert } from 'Duck/alerts';
|
||||||
import LazyLoad from 'react-lazyload';
|
import LazyLoad from 'react-lazyload';
|
||||||
|
import CustomMetrics from 'App/components/shared/CustomMetrics';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
fetchList: Function;
|
fetchList: Function;
|
||||||
|
|
@ -22,7 +23,7 @@ function CustomMetricsWidgets(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{list.filter(item => item.active).map((item: any) => (
|
{list.map((item: any) => (
|
||||||
<LazyLoad>
|
<LazyLoad>
|
||||||
<CustomMetricWidget
|
<CustomMetricWidget
|
||||||
key={item.metricId}
|
key={item.metricId}
|
||||||
|
|
@ -36,6 +37,13 @@ function CustomMetricsWidgets(props: Props) {
|
||||||
</LazyLoad>
|
</LazyLoad>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{list.size === 0 && (
|
||||||
|
<div className="flex items-center py-2">
|
||||||
|
<div className="mr-2">Be proactive by monitoring the metrics you care about the most.</div>
|
||||||
|
<CustomMetrics />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<AlertFormModal
|
<AlertFormModal
|
||||||
showModal={!!activeMetricId}
|
showModal={!!activeMetricId}
|
||||||
metricId={activeMetricId}
|
metricId={activeMetricId}
|
||||||
|
|
@ -46,5 +54,5 @@ function CustomMetricsWidgets(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(state => ({
|
export default connect(state => ({
|
||||||
list: state.getIn(['customMetrics', 'list']),
|
list: state.getIn(['customMetrics', 'list']).filter(item => item.active),
|
||||||
}), { fetchList, initAlert })(CustomMetricsWidgets);
|
}), { fetchList, initAlert })(CustomMetricsWidgets);
|
||||||
|
|
@ -5,6 +5,7 @@ const colorsx = ['#256669', '#38999e', '#3eaaaf', '#51b3b7', '#78c4c7', '#9fd5d7
|
||||||
const compareColors = ['#394EFF', '#4D5FFF', '#808DFF', '#B3BBFF', '#E5E8FF'];
|
const compareColors = ['#394EFF', '#4D5FFF', '#808DFF', '#B3BBFF', '#E5E8FF'];
|
||||||
const compareColorsx = ["#222F99", "#2E3ECC", "#394EFF", "#6171FF", "#8895FF", "#B0B8FF", "#D7DCFF"].reverse();
|
const compareColorsx = ["#222F99", "#2E3ECC", "#394EFF", "#6171FF", "#8895FF", "#B0B8FF", "#D7DCFF"].reverse();
|
||||||
const customMetricColors = ['#3EAAAF', '#394EFF', '#666666'];
|
const customMetricColors = ['#3EAAAF', '#394EFF', '#666666'];
|
||||||
|
const colorsPie = colors.concat(["#DDDDDD"]);
|
||||||
|
|
||||||
const countView = count => {
|
const countView = count => {
|
||||||
const isMoreThanK = count >= 1000;
|
const isMoreThanK = count >= 1000;
|
||||||
|
|
@ -14,6 +15,7 @@ const countView = count => {
|
||||||
export default {
|
export default {
|
||||||
customMetricColors,
|
customMetricColors,
|
||||||
colors,
|
colors,
|
||||||
|
colorsPie,
|
||||||
colorsx,
|
colorsx,
|
||||||
compareColors,
|
compareColors,
|
||||||
compareColorsx,
|
compareColorsx,
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@ export default class Table extends React.PureComponent {
|
||||||
rowProps,
|
rowProps,
|
||||||
rowClass = '',
|
rowClass = '',
|
||||||
small = false,
|
small = false,
|
||||||
compare = false
|
compare = false,
|
||||||
|
maxHeight = 200,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { showAll } = this.state;
|
const { showAll } = this.state;
|
||||||
|
|
||||||
|
|
@ -30,7 +31,7 @@ export default class Table extends React.PureComponent {
|
||||||
<div key={ key } style={ { width } } className={ stl.header }>{ title }</div>)
|
<div key={ key } style={ { width } } className={ stl.header }>{ title }</div>)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div className={ cn(stl.content, "thin-scrollbar") }>
|
<div className={ cn(stl.content, "thin-scrollbar") } style={{ maxHeight: maxHeight + 'px'}}>
|
||||||
{ rows.take(showAll ? 10 : (small ? 3 : 5)).map(row => (
|
{ rows.take(showAll ? 10 : (small ? 3 : 5)).map(row => (
|
||||||
<div className={ cn(rowClass, stl.row, { [stl.small]: small}) } key={ row.key }>
|
<div className={ cn(rowClass, stl.row, { [stl.small]: small}) } key={ row.key }>
|
||||||
{ cols.map(({ cellClass = '', className = '', Component, key, toText = t => t, width }) => (
|
{ cols.map(({ cellClass = '', className = '', Component, key, toText = t => t, width }) => (
|
||||||
|
|
@ -41,21 +42,20 @@ export default class Table extends React.PureComponent {
|
||||||
</div>
|
</div>
|
||||||
)) }
|
)) }
|
||||||
</div>
|
</div>
|
||||||
)) }
|
)) }
|
||||||
|
</div>
|
||||||
{ rows.size > (small ? 3 : 5) && !showAll &&
|
{ rows.size > (small ? 3 : 5) && !showAll &&
|
||||||
<div className="w-full flex justify-center mt-3">
|
<div className="w-full flex justify-center">
|
||||||
<Button
|
<Button
|
||||||
onClick={ this.onLoadMoreClick }
|
onClick={ this.onLoadMoreClick }
|
||||||
plain
|
plain
|
||||||
small
|
small
|
||||||
className="text-center"
|
className="text-center"
|
||||||
>
|
>
|
||||||
{ 'Load More' }
|
{ rows.size + ' More' }
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,10 +66,6 @@ const Header = (props) => {
|
||||||
}
|
}
|
||||||
}, [showTrackingModal])
|
}, [showTrackingModal])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchSiteList()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ cn(styles.header, showTrackingModal ? styles.placeOnTop : '') }>
|
<div className={ cn(styles.header, showTrackingModal ? styles.placeOnTop : '') }>
|
||||||
<NavLink to={ withSiteId(SESSIONS_PATH, siteId) }>
|
<NavLink to={ withSiteId(SESSIONS_PATH, siteId) }>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import cn from 'classnames';
|
||||||
import NewSiteForm from '../Client/Sites/NewSiteForm';
|
import NewSiteForm from '../Client/Sites/NewSiteForm';
|
||||||
import { clearSearch } from 'Duck/search';
|
import { clearSearch } from 'Duck/search';
|
||||||
import { fetchList as fetchIntegrationVariables } from 'Duck/customField';
|
import { fetchList as fetchIntegrationVariables } from 'Duck/customField';
|
||||||
|
import { fetchList as fetchAlerts } from 'Duck/alerts';
|
||||||
|
import { fetchWatchdogStatus } from 'Duck/watchdogs';
|
||||||
|
|
||||||
@withRouter
|
@withRouter
|
||||||
@connect(state => ({
|
@connect(state => ({
|
||||||
|
|
@ -23,13 +25,15 @@ import { fetchList as fetchIntegrationVariables } from 'Duck/customField';
|
||||||
init,
|
init,
|
||||||
clearSearch,
|
clearSearch,
|
||||||
fetchIntegrationVariables,
|
fetchIntegrationVariables,
|
||||||
|
fetchAlerts,
|
||||||
|
fetchWatchdogStatus,
|
||||||
})
|
})
|
||||||
export default class SiteDropdown extends React.PureComponent {
|
export default class SiteDropdown extends React.PureComponent {
|
||||||
state = { showProductModal: false }
|
state = { showProductModal: false }
|
||||||
|
|
||||||
componentDidMount() {
|
// componentDidMount() {
|
||||||
this.props.fetchIntegrationVariables();
|
// this.props.fetchIntegrationVariables();
|
||||||
}
|
// }
|
||||||
|
|
||||||
closeModal = (e, newSite) => {
|
closeModal = (e, newSite) => {
|
||||||
this.setState({ showProductModal: false })
|
this.setState({ showProductModal: false })
|
||||||
|
|
@ -44,6 +48,8 @@ export default class SiteDropdown extends React.PureComponent {
|
||||||
this.props.setSiteId(siteId);
|
this.props.setSiteId(siteId);
|
||||||
this.props.clearSearch();
|
this.props.clearSearch();
|
||||||
this.props.fetchIntegrationVariables();
|
this.props.fetchIntegrationVariables();
|
||||||
|
this.props.fetchAlerts();
|
||||||
|
this.props.fetchWatchdogStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,11 @@ const SESSIONS_ROUTE = sessionsRoute();
|
||||||
function Session({
|
function Session({
|
||||||
sessionId,
|
sessionId,
|
||||||
loading,
|
loading,
|
||||||
hasErrors,
|
hasErrors,
|
||||||
session,
|
session,
|
||||||
fetchSession,
|
fetchSession,
|
||||||
fetchSlackList,
|
fetchSlackList,
|
||||||
hasSessionsPath
|
hasSessionsPath
|
||||||
}) {
|
}) {
|
||||||
usePageTitle("OpenReplay Session Player");
|
usePageTitle("OpenReplay Session Player");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -51,7 +51,7 @@ function Session({
|
||||||
<Loader className="flex-1" loading={ loading || sessionId !== session.sessionId }>
|
<Loader className="flex-1" loading={ loading || sessionId !== session.sessionId }>
|
||||||
{ session.isIOS
|
{ session.isIOS
|
||||||
? <IOSPlayer session={session} />
|
? <IOSPlayer session={session} />
|
||||||
: (session.live && !hasSessionsPath ? <LivePlayer /> : <WebPlayer />)
|
: <WebPlayer />
|
||||||
}
|
}
|
||||||
</Loader>
|
</Loader>
|
||||||
</NoContent>
|
</NoContent>
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,15 @@ export default connect(state => ({
|
||||||
metadata: state.getIn([ 'sessions', 'current', 'metadata' ]),
|
metadata: state.getIn([ 'sessions', 'current', 'metadata' ]),
|
||||||
}))(function Metadata ({ metadata }) {
|
}))(function Metadata ({ metadata }) {
|
||||||
const [ visible, setVisible ] = useState(false);
|
const [ visible, setVisible ] = useState(false);
|
||||||
const toggle = useCallback(() => metadata.length > 0 && setVisible(v => !v), []);
|
const metaLenth = Object.keys(metadata).length;
|
||||||
|
const toggle = useCallback(() => metaLenth > 0 && setVisible(v => !v), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popup
|
<Popup
|
||||||
trigger={
|
trigger={
|
||||||
<IconButton
|
<IconButton
|
||||||
className={cn("w-full", { 'opacity-25' : metadata.length === 0 })}
|
className={cn("w-full", { 'opacity-25' : metaLenth === 0 })}
|
||||||
onClick={ toggle }
|
onClick={ toggle }
|
||||||
icon="id-card"
|
icon="id-card"
|
||||||
plain
|
plain
|
||||||
|
|
@ -33,17 +35,17 @@ export default connect(state => ({
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
on="click"
|
on="click"
|
||||||
disabled={metadata.length > 0}
|
disabled={metaLenth > 0}
|
||||||
size="tiny"
|
size="tiny"
|
||||||
inverted
|
inverted
|
||||||
position="top center"
|
position="top center"
|
||||||
/>
|
/>
|
||||||
{ visible &&
|
{ visible &&
|
||||||
<div className={ stl.modal } >
|
<div className={ stl.modal } >
|
||||||
<NoContent show={ metadata.size === 0 } size="small">
|
<NoContent show={ metaLenth === 0 } size="small">
|
||||||
{ metadata.map((i) => {
|
{ Object.keys(metadata).map((key) => {
|
||||||
const key = Object.keys(i)[0]
|
// const key = Object.keys(i)[0]
|
||||||
const value = i[key]
|
const value = metadata[key]
|
||||||
return <MetadataItem item={ { value, key } } key={ key } />
|
return <MetadataItem item={ { value, key } } key={ key } />
|
||||||
}) }
|
}) }
|
||||||
</NoContent>
|
</NoContent>
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ export default class extends React.PureComponent {
|
||||||
content={ open && <SessionList similarSessions={ similarSessions } loading={ loading } /> }
|
content={ open && <SessionList similarSessions={ similarSessions } loading={ loading } /> }
|
||||||
onClose={ open ? this.switchOpen : () => null }
|
onClose={ open ? this.switchOpen : () => null }
|
||||||
/>
|
/>
|
||||||
<div className={ cn("flex justify-between items-center p-3", stl.field) } >
|
<div className={ cn("flex justify-between items-center p-3 capitalize", stl.field) } >
|
||||||
<div>
|
<div>
|
||||||
<div className={ stl.key }>{ item.key }</div>
|
<div className={ stl.key }>{ item.key }</div>
|
||||||
<TextEllipsis
|
<TextEllipsis
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ const HeapTooltip = ({ active, payload}) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const NodesCountTooltip = ({ active, payload} ) => {
|
const NodesCountTooltip = ({ active, payload} ) => {
|
||||||
if (!active || payload.length === 0) return null;
|
if (!active || !payload || payload.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<div className={ stl.tooltipWrapper } >
|
<div className={ stl.tooltipWrapper } >
|
||||||
<p>
|
<p>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Form, SegmentSelection, Button, IconButton } from 'UI';
|
import { Form, Button, IconButton } from 'UI';
|
||||||
import FilterSeries from '../FilterSeries';
|
import FilterSeries from '../FilterSeries';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { edit as editMetric, save, addSeries, removeSeries, remove } from 'Duck/customMetrics';
|
import { edit as editMetric, save, addSeries, removeSeries, remove } from 'Duck/customMetrics';
|
||||||
|
|
@ -7,7 +7,8 @@ import CustomMetricWidgetPreview from 'App/components/Dashboard/Widgets/CustomMe
|
||||||
import { confirm } from 'UI/Confirmation';
|
import { confirm } from 'UI/Confirmation';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
|
import DropdownPlain from '../../DropdownPlain';
|
||||||
|
import { metricTypes, metricOf, issueOptions } from 'App/constants/filterOptions';
|
||||||
interface Props {
|
interface Props {
|
||||||
metric: any;
|
metric: any;
|
||||||
editMetric: (metric, shouldFetch?) => void;
|
editMetric: (metric, shouldFetch?) => void;
|
||||||
|
|
@ -21,6 +22,13 @@ interface Props {
|
||||||
|
|
||||||
function CustomMetricForm(props: Props) {
|
function CustomMetricForm(props: Props) {
|
||||||
const { metric, loading } = props;
|
const { metric, loading } = props;
|
||||||
|
// const metricOfOptions = metricOf.filter(i => i.key === metric.metricType);
|
||||||
|
const timeseriesOptions = metricOf.filter(i => i.key === 'timeseries');
|
||||||
|
const tableOptions = metricOf.filter(i => i.key === 'table');
|
||||||
|
const isTable = metric.metricType === 'table';
|
||||||
|
const isTimeSeries = metric.metricType === 'timeseries';
|
||||||
|
const _issueOptions = [{ text: 'All', value: '' }].concat(issueOptions);
|
||||||
|
|
||||||
|
|
||||||
const addSeries = () => {
|
const addSeries = () => {
|
||||||
props.addSeries();
|
props.addSeries();
|
||||||
|
|
@ -30,12 +38,33 @@ function CustomMetricForm(props: Props) {
|
||||||
props.removeSeries(index);
|
props.removeSeries(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
const write = ({ target: { value, name } }) => props.editMetric({ ...metric, [ name ]: value }, false);
|
const write = ({ target: { value, name } }) => props.editMetric({ [ name ]: value }, false);
|
||||||
|
const writeOption = (e, { value, name }) => {
|
||||||
|
props.editMetric({ [ name ]: value }, false);
|
||||||
|
|
||||||
const changeConditionTab = (e, { name, value }) => {
|
if (name === 'metricValue') {
|
||||||
props.editMetric({[ 'viewType' ]: value });
|
props.editMetric({ metricValue: [value] }, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'metricOf') {
|
||||||
|
if (value === 'ISSUES') {
|
||||||
|
props.editMetric({ metricValue: [''] }, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'metricType') {
|
||||||
|
if (value === 'timeseries') {
|
||||||
|
props.editMetric({ metricOf: timeseriesOptions[0].value, viewType: 'lineChart' }, false);
|
||||||
|
} else if (value === 'table') {
|
||||||
|
props.editMetric({ metricOf: tableOptions[0].value, viewType: 'table' }, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// const changeConditionTab = (e, { name, value }) => {
|
||||||
|
// props.editMetric({[ 'viewType' ]: value });
|
||||||
|
// };
|
||||||
|
|
||||||
const save = () => {
|
const save = () => {
|
||||||
props.save(metric).then(() => {
|
props.save(metric).then(() => {
|
||||||
toast.success(metric.exists() ? 'Updated succesfully.' : 'Created succesfully.');
|
toast.success(metric.exists() ? 'Updated succesfully.' : 'Created succesfully.');
|
||||||
|
|
@ -79,30 +108,71 @@ function CustomMetricForm(props: Props) {
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="font-medium">Metric Type</label>
|
<label className="font-medium">Metric Type</label>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="bg-white p-1 px-2 border rounded" style={{ height: '30px'}}>Timeseries</span>
|
<DropdownPlain
|
||||||
<span className="mx-2 color-gray-medium">of</span>
|
name="metricType"
|
||||||
<div>
|
options={metricTypes}
|
||||||
<SegmentSelection
|
value={ metric.metricType }
|
||||||
primary
|
onChange={ writeOption }
|
||||||
name="viewType"
|
/>
|
||||||
small={true}
|
|
||||||
// className="my-3"
|
{metric.metricType === 'timeseries' && (
|
||||||
onSelect={ changeConditionTab }
|
<>
|
||||||
value={{ value: metric.viewType }}
|
<span className="mx-3">of</span>
|
||||||
list={ [
|
<DropdownPlain
|
||||||
{ name: 'Session Count', value: 'lineChart' },
|
name="metricOf"
|
||||||
{ name: 'Session Percentage', value: 'progress', disabled: true },
|
options={timeseriesOptions}
|
||||||
]}
|
value={ metric.metricOf }
|
||||||
/>
|
onChange={ writeOption }
|
||||||
</div>
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{metric.metricType === 'table' && (
|
||||||
|
<>
|
||||||
|
<span className="mx-3">of</span>
|
||||||
|
<DropdownPlain
|
||||||
|
name="metricOf"
|
||||||
|
options={tableOptions}
|
||||||
|
value={ metric.metricOf }
|
||||||
|
onChange={ writeOption }
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{metric.metricOf === 'ISSUES' && (
|
||||||
|
<>
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="font-medium">Chart Series</label>
|
<label className="font-medium">Chart Series</label>
|
||||||
{metric.series && metric.series.size > 0 && metric.series.map((series: any, index: number) => (
|
{metric.series && metric.series.size > 0 && metric.series.take(isTable ? 1 : metric.series.size).map((series: any, index: number) => (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<FilterSeries
|
<FilterSeries
|
||||||
|
hideHeader={ isTable }
|
||||||
seriesIndex={index}
|
seriesIndex={index}
|
||||||
series={series}
|
series={series}
|
||||||
onRemoveSeries={() => removeSeries(index)}
|
onRemoveSeries={() => removeSeries(index)}
|
||||||
|
|
@ -112,9 +182,11 @@ function CustomMetricForm(props: Props) {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cn("flex justify-end -my-4", {'disabled' : metric.series.size > 2})}>
|
{ isTimeSeries && (
|
||||||
<IconButton hover type="button" onClick={addSeries} primaryText label="SERIES" icon="plus" />
|
<div className={cn("flex justify-end -my-4", {'disabled' : metric.series.size > 2})}>
|
||||||
</div>
|
<IconButton hover type="button" onClick={addSeries} primaryText label="SERIES" icon="plus" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="my-8" />
|
<div className="my-8" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { IconButton } from 'UI';
|
import { IconButton } from 'UI';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { edit, init } from 'Duck/customMetrics';
|
import { edit, init } from 'Duck/customMetrics';
|
||||||
|
|
@ -7,6 +7,9 @@ interface Props {
|
||||||
init: (instance?, setDefault?) => void;
|
init: (instance?, setDefault?) => void;
|
||||||
}
|
}
|
||||||
function CustomMetrics(props: Props) {
|
function CustomMetrics(props: Props) {
|
||||||
|
useEffect(() => { // TODO remove this block
|
||||||
|
props.init()
|
||||||
|
}, [])
|
||||||
return (
|
return (
|
||||||
<div className="self-start">
|
<div className="self-start">
|
||||||
<IconButton plain outline icon="plus" label="CREATE METRIC" onClick={() => props.init()} />
|
<IconButton plain outline icon="plus" label="CREATE METRIC" onClick={() => props.init()} />
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,12 @@ interface Props {
|
||||||
editSeriesFilterFilter: typeof editSeriesFilterFilter;
|
editSeriesFilterFilter: typeof editSeriesFilterFilter;
|
||||||
editSeriesFilter: typeof editSeriesFilter;
|
editSeriesFilter: typeof editSeriesFilter;
|
||||||
removeSeriesFilterFilter: typeof removeSeriesFilterFilter;
|
removeSeriesFilterFilter: typeof removeSeriesFilterFilter;
|
||||||
|
hideHeader?: boolean;
|
||||||
|
emptyMessage?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FilterSeries(props: Props) {
|
function FilterSeries(props: Props) {
|
||||||
const { canDelete } = 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 [expanded, setExpanded] = useState(true)
|
||||||
const { series, seriesIndex } = props;
|
const { series, seriesIndex } = props;
|
||||||
|
|
||||||
|
|
@ -51,7 +53,7 @@ function FilterSeries(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border rounded bg-white">
|
<div className="border rounded bg-white">
|
||||||
<div className="border-b px-5 h-12 flex items-center relative">
|
<div className={cn("border-b px-5 h-12 flex items-center relative", { 'hidden': hideHeader })}>
|
||||||
<div className="mr-auto">
|
<div className="mr-auto">
|
||||||
<SeriesName name={series.name} onUpdate={(name) => props.updateSeries(seriesIndex, { name }) } />
|
<SeriesName name={series.name} onUpdate={(name) => props.updateSeries(seriesIndex, { name }) } />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -78,10 +80,10 @@ function FilterSeries(props: Props) {
|
||||||
onChangeEventsOrder={onChangeEventsOrder}
|
onChangeEventsOrder={onChangeEventsOrder}
|
||||||
/>
|
/>
|
||||||
): (
|
): (
|
||||||
<div className="color-gray-medium">Add user event or filter to define the series by clicking Add Step.</div>
|
<div className="color-gray-medium">{emptyMessage}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="px-5 border-t h-12 flex items-center">
|
<div className="px-6 border-t h-12 flex items-center -mx-4">
|
||||||
<FilterSelection
|
<FilterSelection
|
||||||
filter={undefined}
|
filter={undefined}
|
||||||
onFilterClick={onAddFilter}
|
onFilterClick={onAddFilter}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import stl from './SessionListModal.css';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { fetchSessionList, setActiveWidget } from 'Duck/customMetrics';
|
import { fetchSessionList, setActiveWidget } from 'Duck/customMetrics';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
list: any;
|
list: any;
|
||||||
|
|
@ -57,9 +56,9 @@ function SessionListModal(props: Props) {
|
||||||
|
|
||||||
const writeOption = (e, { name, value }) => setActiveSeries(value);
|
const writeOption = (e, { name, value }) => setActiveSeries(value);
|
||||||
const filteredSessions = getListSessionsBySeries(activeSeries);
|
const filteredSessions = getListSessionsBySeries(activeSeries);
|
||||||
|
|
||||||
const startTime = DateTime.fromMillis(activeWidget.startTimestamp).toFormat('LLL dd, yyyy HH:mm a');
|
const startTime = DateTime.fromMillis(activeWidget.startTimestamp).toFormat('LLL dd, yyyy HH:mm a');
|
||||||
const endTime = DateTime.fromMillis(activeWidget.endTimestamp).toFormat('LLL dd, yyyy HH:mm a');
|
const endTime = DateTime.fromMillis(activeWidget.endTimestamp).toFormat('LLL dd, yyyy HH:mm a');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SlideModal
|
<SlideModal
|
||||||
title={ activeWidget && (
|
title={ activeWidget && (
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
color: $gray-darkest;
|
color: $gray-darkest;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
background-color: white;
|
||||||
|
border: solid thin $gray-light;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $gray-light;
|
background-color: $gray-light;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,25 +3,30 @@ import stl from './DropdownPlain.css';
|
||||||
import { Dropdown, Icon } from 'UI';
|
import { Dropdown, Icon } from 'UI';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
name?: string;
|
||||||
options: any[];
|
options: any[];
|
||||||
onChange: (e, { name, value }) => void;
|
onChange: (e, { name, value }) => void;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
direction?: string;
|
direction?: string;
|
||||||
value: any;
|
value: any;
|
||||||
|
multiple?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DropdownPlain(props: Props) {
|
export default function DropdownPlain(props: Props) {
|
||||||
const { value, options, icon = "chevron-down", direction = "left" } = props;
|
const { name = "sort", value, options, icon = "chevron-down", direction = "right", multiple = false } = props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
value={value}
|
value={value}
|
||||||
name="sort"
|
name={name}
|
||||||
className={ stl.dropdown }
|
className={ stl.dropdown }
|
||||||
direction={direction}
|
direction={direction}
|
||||||
options={ options }
|
options={ options }
|
||||||
onChange={ props.onChange }
|
onChange={ props.onChange }
|
||||||
|
// floating
|
||||||
scrolling
|
scrolling
|
||||||
|
multiple={ multiple }
|
||||||
|
selectOnBlur={ false }
|
||||||
// defaultValue={ value }
|
// defaultValue={ value }
|
||||||
icon={ icon ? <Icon name="chevron-down" color="gray-dark" size="14" className={stl.dropdownIcon} /> : null }
|
icon={ icon ? <Icon name="chevron-down" color="gray-dark" size="14" className={stl.dropdownIcon} /> : null }
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -100,18 +100,20 @@ const FilterDropdown = props => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showDropdown && (
|
{showDropdown && (
|
||||||
<div className="absolute mt-2 bg-white rounded border p-3 z-20" id="filter-dropdown" style={{ width: '200px'}}>
|
<div className="absolute mt-2 bg-white rounded border z-20" id="filter-dropdown" style={{ width: '200px'}}>
|
||||||
<div className="font-medium mb-2 tracking-widest color-gray-dark">SELECT FILTER</div>
|
<div className="font-medium mb-2 tracking-widest color-gray-dark p-3">SELECT FILTER</div>
|
||||||
{filterKeys.filter(f => !filterKeyMaps.includes(f.key)).map(f => (
|
<div className="px-3" style={{ maxHeight: '200px', overflowY: 'auto'}} >
|
||||||
<div
|
{filterKeys.filter(f => !filterKeyMaps.includes(f.key)).map(f => (
|
||||||
key={f.key}
|
<div
|
||||||
onClick={() => onFilterKeySelect(f.key)}
|
key={f.key}
|
||||||
className={cn(stl.filterItem, 'py-3 -mx-3 px-3 flex items-center cursor-pointer')}
|
onClick={() => onFilterKeySelect(f.key)}
|
||||||
>
|
className={cn(stl.filterItem, 'py-3 -mx-3 px-3 flex items-center cursor-pointer')}
|
||||||
<Icon name={f.icon} size="16" />
|
>
|
||||||
<span className="ml-3 capitalize">{f.name}</span>
|
<Icon name={f.icon} size="16" />
|
||||||
</div>
|
<span className="ml-3 capitalize">{f.name}</span>
|
||||||
))}
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{filterKey && (
|
{filterKey && (
|
||||||
|
|
|
||||||
|
|
@ -47,15 +47,17 @@ function FilterAutoComplete(props: Props) {
|
||||||
const requestValues = (q) => {
|
const requestValues = (q) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
return new APIClient()[method?.toLowerCase()](endpoint, { ...params, q })
|
return new APIClient()[method?.toLocaleLowerCase()](endpoint, { ...params, q })
|
||||||
.then(response => response.json())
|
.then(response => {
|
||||||
.then(({ errors, data }) => {
|
if (response.ok) {
|
||||||
if (errors) {
|
return response.json();
|
||||||
// this.setError();
|
}
|
||||||
} else {
|
throw new Error(response.statusText);
|
||||||
setOptions(data);
|
})
|
||||||
}
|
.then(({ data }) => {
|
||||||
}).finally(() => setLoading(false));
|
setOptions(data);
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
const debouncedRequestValues = React.useCallback(debounce(requestValues, 300), []);
|
const debouncedRequestValues = React.useCallback(debounce(requestValues, 300), []);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import FilterSelection from '../FilterSelection';
|
||||||
import FilterValue from '../FilterValue';
|
import FilterValue from '../FilterValue';
|
||||||
import { Icon } from 'UI';
|
import { Icon } from 'UI';
|
||||||
import FilterSource from '../FilterSource';
|
import FilterSource from '../FilterSource';
|
||||||
|
import { FilterType } from 'App/types/filter/filterType';
|
||||||
|
import SubFilterItem from '../SubFilterItem';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
filterIndex: number;
|
filterIndex: number;
|
||||||
|
|
@ -15,9 +17,14 @@ interface Props {
|
||||||
function FilterItem(props: Props) {
|
function FilterItem(props: Props) {
|
||||||
const { isFilter = false, filterIndex, filter } = props;
|
const { isFilter = false, filterIndex, filter } = props;
|
||||||
const canShowValues = !(filter.operator === "isAny" || filter.operator === "onAny" || filter.operator === "isUndefined");
|
const canShowValues = !(filter.operator === "isAny" || filter.operator === "onAny" || filter.operator === "isUndefined");
|
||||||
|
const isSubFilter = filter.type === FilterType.SUB_FILTERS;
|
||||||
|
|
||||||
const replaceFilter = (filter) => {
|
const replaceFilter = (filter) => {
|
||||||
props.onUpdate({ ...filter, value: [""]});
|
props.onUpdate({
|
||||||
|
...filter,
|
||||||
|
value: [""],
|
||||||
|
subFilters: filter.subFilters ? filter.subFilters.map(i => ({ ...i, value: [""] })) : []
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onOperatorChange = (e, { name, value }) => {
|
const onOperatorChange = (e, { name, value }) => {
|
||||||
|
|
@ -28,6 +35,19 @@ function FilterItem(props: Props) {
|
||||||
props.onUpdate({ ...filter, sourceOperator: value })
|
props.onUpdate({ ...filter, sourceOperator: value })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onUpdateSubFilter = (subFilter, subFilterIndex) => {
|
||||||
|
props.onUpdate({
|
||||||
|
...filter,
|
||||||
|
subFilters: filter.subFilters.map((i, index) => {
|
||||||
|
if (index === subFilterIndex) {
|
||||||
|
return subFilter;
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center hover:bg-active-blue -mx-5 px-5 py-2">
|
<div className="flex items-center hover:bg-active-blue -mx-5 px-5 py-2">
|
||||||
<div className="flex items-start w-full">
|
<div className="flex items-start w-full">
|
||||||
|
|
@ -48,14 +68,31 @@ function FilterItem(props: Props) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Filter values */}
|
{/* Filter values */}
|
||||||
<FilterOperator
|
{ !isSubFilter && (
|
||||||
options={filter.operatorOptions}
|
<>
|
||||||
onChange={onOperatorChange}
|
<FilterOperator
|
||||||
className="mx-2 flex-shrink-0"
|
options={filter.operatorOptions}
|
||||||
value={filter.operator}
|
onChange={onOperatorChange}
|
||||||
/>
|
className="mx-2 flex-shrink-0"
|
||||||
{ canShowValues && (<FilterValue filter={filter} onUpdate={props.onUpdate} />) }
|
value={filter.operator}
|
||||||
|
/>
|
||||||
|
{ canShowValues && (<FilterValue filter={filter} onUpdate={props.onUpdate} />) }
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* SubFilters */}
|
||||||
|
{isSubFilter && (
|
||||||
|
<div className="grid grid-col ml-3 w-full">
|
||||||
|
{filter.subFilters.map((subFilter, subFilterIndex) => (
|
||||||
|
<SubFilterItem
|
||||||
|
filterIndex={subFilterIndex}
|
||||||
|
filter={subFilter}
|
||||||
|
onUpdate={(f) => onUpdateSubFilter(f, subFilterIndex)}
|
||||||
|
onRemoveFilter={props.onRemoveFilter}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-shrink-0 self-start mt-1 ml-auto px-2">
|
<div className="flex flex-shrink-0 self-start mt-1 ml-auto px-2">
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ function FilterList(props: Props) {
|
||||||
<div className="mr-2 color-gray-medium text-sm" style={{ textDecoration: 'underline dotted'}}>
|
<div className="mr-2 color-gray-medium text-sm" style={{ textDecoration: 'underline dotted'}}>
|
||||||
<Popup
|
<Popup
|
||||||
trigger={<div>Events Order</div>}
|
trigger={<div>Events Order</div>}
|
||||||
content={ `Events Order` }
|
content={ `Select the operator to be applied between events in your search.` }
|
||||||
size="tiny"
|
size="tiny"
|
||||||
inverted
|
inverted
|
||||||
position="top center"
|
position="top center"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { filter } from 'App/components/BugFinder/ManageFilters/savedFilterList.css'
|
||||||
|
import React from 'react'
|
||||||
|
import FilterOperator from '../FilterOperator';
|
||||||
|
import FilterValue from '../FilterValue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
filterIndex: number;
|
||||||
|
filter: any; // event/filter
|
||||||
|
onUpdate: (filter) => void;
|
||||||
|
onRemoveFilter: () => void;
|
||||||
|
isFilter?: boolean;
|
||||||
|
}
|
||||||
|
export default function SubFilterItem(props: Props) {
|
||||||
|
const { isFilter = false, filterIndex, filter } = props;
|
||||||
|
const canShowValues = !(filter.operator === "isAny" || filter.operator === "onAny" || filter.operator === "isUndefined");
|
||||||
|
|
||||||
|
const onOperatorChange = (e, { name, value }) => {
|
||||||
|
props.onUpdate({ ...filter, operator: value })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center hover:bg-active-blue pb-4">
|
||||||
|
<div className="flex-shrink-0 py-1">{filter.label}</div>
|
||||||
|
<FilterOperator
|
||||||
|
options={filter.operatorOptions}
|
||||||
|
onChange={onOperatorChange}
|
||||||
|
className="mx-2 flex-shrink-0"
|
||||||
|
value={filter.operator}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{ canShowValues && (<FilterValue filter={filter} onUpdate={props.onUpdate} />) }
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './SubFilterItem';
|
||||||
|
|
@ -18,12 +18,6 @@ function FunnelSearch(props: Props) {
|
||||||
|
|
||||||
const onAddFilter = (filter) => {
|
const onAddFilter = (filter) => {
|
||||||
props.addFilter(filter);
|
props.addFilter(filter);
|
||||||
// filter.value = [""]
|
|
||||||
// const newFilters = appliedFilter.filters.concat(filter);
|
|
||||||
// props.edit({
|
|
||||||
// ...appliedFilter.filter,
|
|
||||||
// filters: newFilters,
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onUpdateFilter = (filterIndex, filter) => {
|
const onUpdateFilter = (filterIndex, filter) => {
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,6 @@ function SessionSearch(props: Props) {
|
||||||
|
|
||||||
const onAddFilter = (filter) => {
|
const onAddFilter = (filter) => {
|
||||||
props.addFilter(filter);
|
props.addFilter(filter);
|
||||||
// filter.value = [""]
|
|
||||||
// const newFilters = appliedFilter.filters.concat(filter);
|
|
||||||
// props.edit({
|
|
||||||
// ...appliedFilter.filter,
|
|
||||||
// filters: newFilters,
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onUpdateFilter = (filterIndex, filter) => {
|
const onUpdateFilter = (filterIndex, filter) => {
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,25 @@
|
||||||
import React from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { Icon } from 'UI'
|
import { Icon } from 'UI'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { onboarding as onboardingRoute } from 'App/routes'
|
import { onboarding as onboardingRoute } from 'App/routes'
|
||||||
import { withSiteId } from 'App/routes';
|
import { withSiteId } from 'App/routes';
|
||||||
|
import { isGreaterOrEqualVersion } from 'App/utils'
|
||||||
|
|
||||||
const TrackerUpdateMessage= (props) => {
|
const TrackerUpdateMessage= (props) => {
|
||||||
// const { site } = props;
|
const [needUpdate, setNeedUpdate] = React.useState(false)
|
||||||
const { site, sites, match: { params: { siteId } } } = props;
|
const { sites, match: { params: { siteId } } } = props;
|
||||||
const activeSite = sites.find(s => s.id == siteId);
|
const activeSite = sites.find(s => s.id == siteId);
|
||||||
const hasSessions = !!activeSite && !activeSite.recorded;
|
|
||||||
const appVersionInt = parseInt(window.ENV.TRACKER_VERSION.split(".").join(""))
|
useEffect(() => {
|
||||||
const trackerVersionInt = site.trackerVersion ? parseInt(site.trackerVersion.split(".").join("")) : 0
|
if (!activeSite || !activeSite.trackerVersion) return;
|
||||||
const needUpdate = !hasSessions && appVersionInt > trackerVersionInt;
|
|
||||||
|
const isLatest = isGreaterOrEqualVersion(activeSite.trackerVersion, window.ENV.TRACKER_VERSION);
|
||||||
|
if (!isLatest && activeSite.recorded) {
|
||||||
|
setNeedUpdate(true)
|
||||||
|
}
|
||||||
|
}, [activeSite])
|
||||||
|
|
||||||
return needUpdate ? (
|
return needUpdate ? (
|
||||||
<>
|
<>
|
||||||
{(
|
{(
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,14 @@ class SegmentSelection extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { className, list, small = false, extraSmall = false, primary = false, size = "normal" } = this.props;
|
const { className, list, small = false, extraSmall = false, primary = false, size = "normal", icons = false } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ cn(styles.wrapper, {
|
<div className={ cn(styles.wrapper, {
|
||||||
[styles.primary] : primary,
|
[styles.primary] : primary,
|
||||||
[styles.small] : size === 'small' || small,
|
[styles.small] : size === 'small' || small,
|
||||||
[styles.extraSmall] : extraSmall,
|
[styles.extraSmall] : size === 'extraSmall' || extraSmall,
|
||||||
|
[styles.icons] : icons === true,
|
||||||
}, className) }
|
}, className) }
|
||||||
>
|
>
|
||||||
{ list.map(item => (
|
{ list.map(item => (
|
||||||
|
|
@ -27,8 +28,8 @@ class SegmentSelection extends React.Component {
|
||||||
data-active={ this.props.value && this.props.value.value === item.value }
|
data-active={ this.props.value && this.props.value.value === item.value }
|
||||||
onClick={ () => !item.disabled && this.setActiveItem(item) }
|
onClick={ () => !item.disabled && this.setActiveItem(item) }
|
||||||
>
|
>
|
||||||
{ item.icon && <Icon name={ item.icon } size="20" marginRight="10" /> }
|
{ item.icon && <Icon name={ item.icon } size={(size === "extraSmall" || icons) ? 14 : 20} marginRight={ item.name ? "6" : "" } /> }
|
||||||
<div>{ item.name }</div>
|
<div className="leading-none">{ item.name }</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
disabled={!item.disabled}
|
disabled={!item.disabled}
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,13 @@
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-right: solid thin $teal;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: $gray-lightest;
|
background-color: $gray-lightest;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
border-right: solid thin $gray-light;
|
||||||
|
|
||||||
& span svg {
|
& span svg {
|
||||||
fill: $gray-medium;
|
fill: $gray-medium;
|
||||||
|
|
@ -53,9 +53,16 @@
|
||||||
& .item {
|
& .item {
|
||||||
color: $teal;
|
color: $teal;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
border-right: solid thin $teal;
|
||||||
|
& svg {
|
||||||
|
fill: $teal !important;
|
||||||
|
}
|
||||||
&[data-active=true] {
|
&[data-active=true] {
|
||||||
background-color: $teal;
|
background-color: $teal;
|
||||||
color: white;
|
color: white;
|
||||||
|
& svg {
|
||||||
|
fill: white !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -65,6 +72,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.extraSmall .item {
|
.extraSmall .item {
|
||||||
padding: 0 4px;
|
padding: 2px 4px !important;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icons .item {
|
||||||
|
padding: 4px !important;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,24 @@
|
||||||
import styles from './slideModal.css';
|
import styles from './slideModal.css';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
export default class SlideModal extends React.PureComponent {
|
export default class SlideModal extends React.PureComponent {
|
||||||
componentDidMount() {
|
// componentDidMount() {
|
||||||
document.addEventListener('keydown', this.keyPressHandler);
|
// document.addEventListener('keydown', this.keyPressHandler);
|
||||||
}
|
// }
|
||||||
|
|
||||||
componentWillUnmount() {
|
// componentWillUnmount() {
|
||||||
document.removeEventListener('keydown', this.keyPressHandler);
|
// document.removeEventListener('keydown', this.keyPressHandler);
|
||||||
|
// }
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (prevProps.isDisplayed !== this.props.isDisplayed) {
|
||||||
|
if (this.props.isDisplayed) {
|
||||||
|
document.addEventListener('keydown', this.keyPressHandler);
|
||||||
|
document.body.classList.add('no-scroll');
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('keydown', this.keyPressHandler);
|
||||||
|
document.body.classList.remove('no-scroll');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
keyPressHandler = (e) => {
|
keyPressHandler = (e) => {
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,36 @@ export const customOperators = [
|
||||||
{ key: '>=', text: '>=', value: '>=' },
|
{ key: '>=', text: '>=', value: '>=' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const metricTypes = [
|
||||||
|
{ text: 'Timeseries', value: 'timeseries' },
|
||||||
|
{ text: 'Table', value: 'table' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const metricOf = [
|
||||||
|
{ text: 'Session Count', value: 'sessionCount', key: 'timeseries' },
|
||||||
|
{ text: 'Users', value: 'USERID', key: 'table' },
|
||||||
|
{ text: 'Issues', value: 'ISSUES', key: 'table' },
|
||||||
|
{ text: 'Browser', value: 'USERBROWSER', key: 'table' },
|
||||||
|
{ text: 'Device', value: 'USERDEVICE', key: 'table' },
|
||||||
|
{ text: 'Country', value: 'USERCOUNTRY', key: 'table' },
|
||||||
|
{ text: 'URL', value: 'VISITED_URL', key: 'table' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const issueOptions = [
|
||||||
|
{ text: 'Click Rage', value: 'click_rage' },
|
||||||
|
{ text: 'Dead Click', value: 'dead_click' },
|
||||||
|
{ text: 'Excessive Scrolling', value: 'excessive_scrolling' },
|
||||||
|
{ text: 'Bad Request', value: 'bad_request' },
|
||||||
|
{ text: 'Missing Resource', value: 'missing_resource' },
|
||||||
|
{ text: 'Memory', value: 'memory' },
|
||||||
|
{ text: 'CPU', value: 'cpu' },
|
||||||
|
{ text: 'Slow Resource', value: 'slow_resource' },
|
||||||
|
{ text: 'Slow Page Load', value: 'slow_page_load' },
|
||||||
|
{ text: 'Crash', value: 'crash' },
|
||||||
|
{ text: 'Custom', value: 'custom' },
|
||||||
|
{ text: 'JS Exception', value: 'js_exception' },
|
||||||
|
]
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
options,
|
options,
|
||||||
baseOperators,
|
baseOperators,
|
||||||
|
|
@ -62,4 +92,7 @@ export default {
|
||||||
booleanOperators,
|
booleanOperators,
|
||||||
customOperators,
|
customOperators,
|
||||||
getOperatorsByKeys,
|
getOperatorsByKeys,
|
||||||
|
metricTypes,
|
||||||
|
metricOf,
|
||||||
|
issueOptions,
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { fromJS, List, Map, Set } from 'immutable';
|
import { List, Map, Set } from 'immutable';
|
||||||
import { errors as errorsRoute, isRoute } from "App/routes";
|
import { errors as errorsRoute, isRoute } from "App/routes";
|
||||||
import Filter from 'Types/filter';
|
import Filter from 'Types/filter';
|
||||||
import SavedFilter from 'Types/filter/savedFilter';
|
import SavedFilter from 'Types/filter/savedFilter';
|
||||||
|
|
@ -8,15 +8,6 @@ import withRequestState, { RequestTypes } from './requestStateCreator';
|
||||||
import { fetchList as fetchSessionList } from './sessions';
|
import { fetchList as fetchSessionList } from './sessions';
|
||||||
import { fetchList as fetchErrorsList } from './errors';
|
import { fetchList as fetchErrorsList } from './errors';
|
||||||
import { fetchListType, fetchType, saveType, editType, initType, removeType } from './funcTools/crud/types';
|
import { fetchListType, fetchType, saveType, editType, initType, removeType } from './funcTools/crud/types';
|
||||||
import logger from 'App/logger';
|
|
||||||
|
|
||||||
import { newFiltersList } from 'Types/filter'
|
|
||||||
import NewFilter, { filtersMap } from 'Types/filter/newFilter';
|
|
||||||
|
|
||||||
|
|
||||||
// for (var i = 0; i < newFiltersList.length; i++) {
|
|
||||||
// filterOptions[newFiltersList[i].category] = newFiltersList.filter(filter => filter.category === newFiltersList[i].category)
|
|
||||||
// }
|
|
||||||
|
|
||||||
const ERRORS_ROUTE = errorsRoute();
|
const ERRORS_ROUTE = errorsRoute();
|
||||||
|
|
||||||
|
|
@ -44,11 +35,8 @@ const ADD_ATTRIBUTE = 'filters/ADD_ATTRIBUTE';
|
||||||
const EDIT_ATTRIBUTE = 'filters/EDIT_ATTRIBUTE';
|
const EDIT_ATTRIBUTE = 'filters/EDIT_ATTRIBUTE';
|
||||||
const REMOVE_ATTRIBUTE = 'filters/REMOVE_ATTRIBUTE';
|
const REMOVE_ATTRIBUTE = 'filters/REMOVE_ATTRIBUTE';
|
||||||
const SET_ACTIVE_FLOW = 'filters/SET_ACTIVE_FLOW';
|
const SET_ACTIVE_FLOW = 'filters/SET_ACTIVE_FLOW';
|
||||||
|
|
||||||
const UPDATE_VALUE = 'filters/UPDATE_VALUE';
|
const UPDATE_VALUE = 'filters/UPDATE_VALUE';
|
||||||
|
|
||||||
const REFRESH_FILTER_OPTIONS = 'filters/REFRESH_FILTER_OPTIONS';
|
|
||||||
|
|
||||||
const initialState = Map({
|
const initialState = Map({
|
||||||
instance: Filter(),
|
instance: Filter(),
|
||||||
activeFilter: null,
|
activeFilter: null,
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ export const applySavedSearch = (filter) => (dispatch, getState) => {
|
||||||
|
|
||||||
export const fetchSessions = (filter) => (dispatch, getState) => {
|
export const fetchSessions = (filter) => (dispatch, getState) => {
|
||||||
const _filter = filter ? filter : getState().getIn([ 'search', 'instance']);
|
const _filter = filter ? filter : getState().getIn([ 'search', 'instance']);
|
||||||
return dispatch(applyFilter(_filter));
|
// return dispatch(applyFilter(_filter)); // TODO uncomment this line
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateSeries = (index, series) => ({
|
export const updateSeries = (index, series) => ({
|
||||||
|
|
@ -233,6 +233,10 @@ export const hasFilterApplied = (filters, filter) => {
|
||||||
|
|
||||||
export const addFilter = (filter) => (dispatch, getState) => {
|
export const addFilter = (filter) => (dispatch, getState) => {
|
||||||
filter.value = checkFilterValue(filter.value);
|
filter.value = checkFilterValue(filter.value);
|
||||||
|
filter.subFilters = filter.subFilters ? filter.subFilters.map(subFilter => ({
|
||||||
|
...subFilter,
|
||||||
|
value: checkFilterValue(subFilter.value),
|
||||||
|
})) : null;
|
||||||
const instance = getState().getIn([ 'search', 'instance']);
|
const instance = getState().getIn([ 'search', 'instance']);
|
||||||
|
|
||||||
if (hasFilterApplied(instance.filters, filter)) {
|
if (hasFilterApplied(instance.filters, filter)) {
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ export default class AssistManager {
|
||||||
if (document.hidden && getState().calling === CallingState.NoCall) {
|
if (document.hidden && getState().calling === CallingState.NoCall) {
|
||||||
this.socket?.close()
|
this.socket?.close()
|
||||||
}
|
}
|
||||||
}, 15000)
|
}, 30000)
|
||||||
} else {
|
} else {
|
||||||
inactiveTimeout && clearTimeout(inactiveTimeout)
|
inactiveTimeout && clearTimeout(inactiveTimeout)
|
||||||
this.socket?.open()
|
this.socket?.open()
|
||||||
|
|
|
||||||
|
|
@ -141,4 +141,10 @@
|
||||||
|
|
||||||
margin: 25px 0;
|
margin: 25px 0;
|
||||||
background-color: $gray-light;
|
background-color: $gray-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-scroll {
|
||||||
|
height: 100vh;
|
||||||
|
overflow-y: hidden;
|
||||||
|
padding-right: 15px;
|
||||||
}
|
}
|
||||||
3
frontend/app/svg/icons/graph-up-arrow.svg
Normal file
3
frontend/app/svg/icons/graph-up-arrow.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-graph-up-arrow" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M0 0h1v15h15v1H0V0Zm10 3.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-1 0V4.9l-3.613 4.417a.5.5 0 0 1-.74.037L7.06 6.767l-3.656 5.027a.5.5 0 0 1-.808-.588l4-5.5a.5.5 0 0 1 .758-.06l2.609 2.61L13.445 4H10.5a.5.5 0 0 1-.5-.5Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 402 B |
3
frontend/app/svg/icons/hash.svg
Normal file
3
frontend/app/svg/icons/hash.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-hash" viewBox="0 0 16 16">
|
||||||
|
<path d="M8.39 12.648a1.32 1.32 0 0 0-.015.18c0 .305.21.508.5.508.266 0 .492-.172.555-.477l.554-2.703h1.204c.421 0 .617-.234.617-.547 0-.312-.188-.53-.617-.53h-.985l.516-2.524h1.265c.43 0 .618-.227.618-.547 0-.313-.188-.524-.618-.524h-1.046l.476-2.304a1.06 1.06 0 0 0 .016-.164.51.51 0 0 0-.516-.516.54.54 0 0 0-.539.43l-.523 2.554H7.617l.477-2.304c.008-.04.015-.118.015-.164a.512.512 0 0 0-.523-.516.539.539 0 0 0-.531.43L6.53 5.484H5.414c-.43 0-.617.22-.617.532 0 .312.187.539.617.539h.906l-.515 2.523H4.609c-.421 0-.609.219-.609.531 0 .313.188.547.61.547h.976l-.516 2.492c-.008.04-.015.125-.015.18 0 .305.21.508.5.508.265 0 .492-.172.554-.477l.555-2.703h2.242l-.515 2.492zm-1-6.109h2.266l-.515 2.563H6.859l.532-2.563z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 855 B |
3
frontend/app/svg/icons/table.svg
Normal file
3
frontend/app/svg/icons/table.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-table" viewBox="0 0 16 16">
|
||||||
|
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2zm15 2h-4v3h4V4zm0 4h-4v3h4V8zm0 4h-4v3h3a1 1 0 0 0 1-1v-2zm-5 3v-3H6v3h4zm-5 0v-3H1v2a1 1 0 0 0 1 1h3zm-4-4h4V8H1v3zm0-4h4V4H1v3zm5-3v3h4V4H6zm4 4H6v3h4V8z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 371 B |
|
|
@ -103,5 +103,11 @@ export default Record({
|
||||||
endTimestamp: this.end,
|
endTimestamp: this.end,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
toTimestampstwo() {
|
||||||
|
return {
|
||||||
|
startTimestamp: this.start / 1000,
|
||||||
|
endTimestamp: this.end / 1000,
|
||||||
|
};
|
||||||
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -27,7 +27,11 @@ export const FilterSeries = Record({
|
||||||
export default Record({
|
export default Record({
|
||||||
metricId: undefined,
|
metricId: undefined,
|
||||||
name: 'Series',
|
name: 'Series',
|
||||||
viewType: 'lineChart',
|
metricType: 'table',
|
||||||
|
metricOf: 'USERID',
|
||||||
|
metricValue: ['sessionCount'],
|
||||||
|
metricFormat: 'sessionCount',
|
||||||
|
viewType: 'pieChart',
|
||||||
series: List(),
|
series: List(),
|
||||||
isPublic: true,
|
isPublic: true,
|
||||||
startDate: '',
|
startDate: '',
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ export enum FilterType {
|
||||||
NUMBER = "NUMBER",
|
NUMBER = "NUMBER",
|
||||||
DURATION = "DURATION",
|
DURATION = "DURATION",
|
||||||
MULTIPLE = "MULTIPLE",
|
MULTIPLE = "MULTIPLE",
|
||||||
|
SUB_FILTERS = "SUB_FILTERS",
|
||||||
COUNTRY = "COUNTRY",
|
COUNTRY = "COUNTRY",
|
||||||
DROPDOWN = "DROPDOWN",
|
DROPDOWN = "DROPDOWN",
|
||||||
MULTIPLE_DROPDOWN = "MULTIPLE_DROPDOWN",
|
MULTIPLE_DROPDOWN = "MULTIPLE_DROPDOWN",
|
||||||
|
|
@ -61,4 +62,8 @@ export enum FilterKey {
|
||||||
AVG_CPU_LOAD = "AVG_CPU_LOAD",
|
AVG_CPU_LOAD = "AVG_CPU_LOAD",
|
||||||
AVG_MEMORY_USAGE = "AVG_MEMORY_USAGE",
|
AVG_MEMORY_USAGE = "AVG_MEMORY_USAGE",
|
||||||
FETCH_FAILED = "FETCH_FAILED",
|
FETCH_FAILED = "FETCH_FAILED",
|
||||||
|
FETCH = "FETCH",
|
||||||
|
FETCH_URL = "FETCH_URL",
|
||||||
|
FETCH_STATUS = "FETCH_STATUS",
|
||||||
|
FETCH_METHOD = "FETCH_METHOD",
|
||||||
}
|
}
|
||||||
|
|
@ -236,22 +236,4 @@ export const operatorOptions = (filter) => {
|
||||||
case KEYS.CLICK_RAGE:
|
case KEYS.CLICK_RAGE:
|
||||||
return [{ key: 'onAnything', text: 'on anything', value: 'true' }]
|
return [{ key: 'onAnything', text: 'on anything', value: 'true' }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const NewFilterType = (key, category, label, icon, isEvent = false) => {
|
|
||||||
return {
|
|
||||||
key: key,
|
|
||||||
category: category,
|
|
||||||
label: label,
|
|
||||||
icon: icon,
|
|
||||||
isEvent: isEvent,
|
|
||||||
operators: operatorOptions({ key }),
|
|
||||||
value: [""]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const newFiltersList = [
|
|
||||||
NewFilterType(TYPES.CLICK, 'Gear', 'Click', 'filters/click', true),
|
|
||||||
NewFilterType(TYPES.CLICK, 'Gear', 'Input', 'filters/click', true),
|
|
||||||
NewFilterType(TYPES.CONSOLE, 'Other', 'Console', 'filters/click', true),
|
|
||||||
];
|
|
||||||
|
|
@ -6,21 +6,6 @@ import { capitalize } from 'App/utils';
|
||||||
const countryOptions = Object.keys(countries).map(i => ({ text: countries[i], value: i }));
|
const countryOptions = Object.keys(countries).map(i => ({ text: countries[i], value: i }));
|
||||||
const containsFilters = [{ key: 'contains', text: 'contains', value: 'contains' }]
|
const containsFilters = [{ key: 'contains', text: 'contains', value: 'contains' }]
|
||||||
|
|
||||||
const ISSUE_OPTIONS = [
|
|
||||||
{ text: 'Click Rage', value: 'click_rage' },
|
|
||||||
{ text: 'Dead Click', value: 'dead_click' },
|
|
||||||
{ text: 'Excessive Scrolling', value: 'excessive_scrolling' },
|
|
||||||
{ text: 'Bad Request', value: 'bad_request' },
|
|
||||||
{ text: 'Missing Resource', value: 'missing_resource' },
|
|
||||||
{ text: 'Memory', value: 'memory' },
|
|
||||||
{ text: 'CPU', value: 'cpu' },
|
|
||||||
{ text: 'Slow Resource', value: 'slow_resource' },
|
|
||||||
{ text: 'Slow Page Load', value: 'slow_page_load' },
|
|
||||||
{ text: 'Crash', value: 'crash' },
|
|
||||||
{ text: 'Custom', value: 'custom' },
|
|
||||||
{ text: 'JS Exception', value: 'js_exception' },
|
|
||||||
]
|
|
||||||
|
|
||||||
export const filtersMap = {
|
export const filtersMap = {
|
||||||
// EVENTS
|
// EVENTS
|
||||||
[FilterKey.CLICK]: { key: FilterKey.CLICK, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Click', operator: 'on', operatorOptions: filterOptions.targetOperators, icon: 'filters/click', isEvent: true },
|
[FilterKey.CLICK]: { key: FilterKey.CLICK, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Click', operator: 'on', operatorOptions: filterOptions.targetOperators, icon: 'filters/click', isEvent: true },
|
||||||
|
|
@ -48,13 +33,18 @@ export const filtersMap = {
|
||||||
[FilterKey.USERANONYMOUSID]: { key: FilterKey.USERANONYMOUSID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User AnonymousId', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/userid' },
|
[FilterKey.USERANONYMOUSID]: { key: FilterKey.USERANONYMOUSID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User AnonymousId', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/userid' },
|
||||||
|
|
||||||
// PERFORMANCE
|
// PERFORMANCE
|
||||||
|
[FilterKey.FETCH]: { key: FilterKey.FETCH, type: FilterType.SUB_FILTERS, category: FilterCategory.PERFORMANCE, label: 'Fetch Request', subFilters: [
|
||||||
|
{ key: FilterKey.FETCH_URL, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with URL', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
|
||||||
|
{ key: FilterKey.FETCH_STATUS, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with status code', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
|
||||||
|
{ key: FilterKey.FETCH_METHOD, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with method', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
|
||||||
|
], icon: 'filters/fetch-failed', isEvent: true },
|
||||||
[FilterKey.DOM_COMPLETE]: { key: FilterKey.DOM_COMPLETE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'DOM Complete', operator: 'isAny', operatorOptions: filterOptions.stringOperators, source: [], icon: 'filters/dom-complete', isEvent: true, hasSource: true, sourceOperator: '=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
[FilterKey.DOM_COMPLETE]: { key: FilterKey.DOM_COMPLETE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'DOM Complete', operator: 'isAny', operatorOptions: filterOptions.stringOperators, source: [], icon: 'filters/dom-complete', isEvent: true, hasSource: true, sourceOperator: '=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
||||||
[FilterKey.LARGEST_CONTENTFUL_PAINT_TIME]: { key: FilterKey.LARGEST_CONTENTFUL_PAINT_TIME, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Largest Contentful Paint', operator: 'isAny', operatorOptions: filterOptions.stringOperators, source: [], icon: 'filters/lcpt', isEvent: true, hasSource: true, sourceOperator: '=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
[FilterKey.LARGEST_CONTENTFUL_PAINT_TIME]: { key: FilterKey.LARGEST_CONTENTFUL_PAINT_TIME, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Largest Contentful Paint', operator: 'isAny', operatorOptions: filterOptions.stringOperators, source: [], icon: 'filters/lcpt', isEvent: true, hasSource: true, sourceOperator: '=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
||||||
[FilterKey.TTFB]: { key: FilterKey.TTFB, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Time to First Byte', operator: 'isAny', operatorOptions: filterOptions.stringOperators, source: [], icon: 'filters/ttfb', isEvent: true, hasSource: true, sourceOperator: '=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
[FilterKey.TTFB]: { key: FilterKey.TTFB, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Time to First Byte', operator: 'isAny', operatorOptions: filterOptions.stringOperators, source: [], icon: 'filters/ttfb', isEvent: true, hasSource: true, sourceOperator: '=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
||||||
[FilterKey.AVG_CPU_LOAD]: { key: FilterKey.AVG_CPU_LOAD, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg CPU Load', operator: 'isAny', operatorOptions: filterOptions.stringOperators, source: [], icon: 'filters/cpu-load', isEvent: true, hasSource: true, sourceOperator: '=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
[FilterKey.AVG_CPU_LOAD]: { key: FilterKey.AVG_CPU_LOAD, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg CPU Load', operator: 'isAny', operatorOptions: filterOptions.stringOperators, source: [], icon: 'filters/cpu-load', isEvent: true, hasSource: true, sourceOperator: '=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
||||||
[FilterKey.AVG_MEMORY_USAGE]: { key: FilterKey.AVG_MEMORY_USAGE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg Memory Usage', operator: 'isAny', operatorOptions: filterOptions.stringOperators, source: [], icon: 'filters/memory-load', isEvent: true, hasSource: true, sourceOperator: '=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
[FilterKey.AVG_MEMORY_USAGE]: { key: FilterKey.AVG_MEMORY_USAGE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg Memory Usage', operator: 'isAny', operatorOptions: filterOptions.stringOperators, source: [], icon: 'filters/memory-load', isEvent: true, hasSource: true, sourceOperator: '=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
||||||
[FilterKey.FETCH_FAILED]: { key: FilterKey.FETCH_FAILED, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Failed Request', operator: 'isAny', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch-failed', isEvent: true },
|
[FilterKey.FETCH_FAILED]: { key: FilterKey.FETCH_FAILED, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Failed Request', operator: 'isAny', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch-failed', isEvent: true },
|
||||||
[FilterKey.ISSUE]: { key: FilterKey.ISSUE, type: FilterType.ISSUE, category: FilterCategory.JAVASCRIPT, label: 'Issue', operator: 'is', operatorOptions: filterOptions.baseOperators, icon: 'filters/click', options: ISSUE_OPTIONS },
|
[FilterKey.ISSUE]: { key: FilterKey.ISSUE, type: FilterType.ISSUE, category: FilterCategory.JAVASCRIPT, label: 'Issue', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/click', options: filterOptions.issueOptions },
|
||||||
}
|
}
|
||||||
|
|
||||||
export const liveFiltersMap = {
|
export const liveFiltersMap = {
|
||||||
|
|
@ -121,17 +111,19 @@ export default Record({
|
||||||
isEvent: false,
|
isEvent: false,
|
||||||
index: 0,
|
index: 0,
|
||||||
options: [],
|
options: [],
|
||||||
|
|
||||||
|
subFilters: [],
|
||||||
}, {
|
}, {
|
||||||
keyKey: "_key",
|
keyKey: "_key",
|
||||||
fromJS: ({ value, key, type, ...filter }) => {
|
fromJS: ({ value, key, type, ...filter }) => {
|
||||||
// const _filter = filtersMap[key] || filtersMap[type] || {};
|
|
||||||
const _filter = filtersMap[type];
|
const _filter = filtersMap[type];
|
||||||
return {
|
return {
|
||||||
...filter,
|
...filter,
|
||||||
..._filter,
|
..._filter,
|
||||||
key: _filter.key,
|
key: _filter.key,
|
||||||
type: _filter.type, // camelCased(filter.type.toLowerCase()),
|
type: _filter.type, // camelCased(filter.type.toLowerCase()),
|
||||||
value: value.length === 0 ? [""] : value, // make sure there an empty value
|
value: value.length === 0 ? [""] : value,
|
||||||
|
// subFilters: filter.subFilters.map(this),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -142,33 +134,29 @@ export default Record({
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const generateFilterOptions = (map) => {
|
export const generateFilterOptions = (map) => {
|
||||||
const _options = {};
|
const filterSection = {};
|
||||||
Object.keys(map).forEach(key => {
|
Object.keys(map).forEach(key => {
|
||||||
const filter = map[key];
|
const filter = map[key];
|
||||||
if (_options.hasOwnProperty(filter.category)) {
|
if (filterSection.hasOwnProperty(filter.category)) {
|
||||||
_options[filter.category].push(filter);
|
filterSection[filter.category].push(filter);
|
||||||
} else {
|
} else {
|
||||||
_options[filter.category] = [filter];
|
filterSection[filter.category] = [filter];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return _options;
|
return filterSection;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generateLiveFilterOptions = (map) => {
|
export const generateLiveFilterOptions = (map) => {
|
||||||
const _options = {};
|
const filterSection = {};
|
||||||
|
|
||||||
Object.keys(map).filter(i => map[i].isLive).forEach(key => {
|
Object.keys(map).filter(i => map[i].isLive).forEach(key => {
|
||||||
const filter = map[key];
|
const filter = map[key];
|
||||||
filter.operator = 'contains';
|
filter.operator = 'contains';
|
||||||
// filter.type = FilterType.STRING;
|
if (filterSection.hasOwnProperty(filter.category)) {
|
||||||
// filter.type = FilterType.AUTOCOMPLETE_LOCAL;
|
filterSection[filter.category].push(filter);
|
||||||
// filter.options = countryOptions;
|
|
||||||
// filter.operatorOptions = [{ key: 'contains', text: 'contains', value: 'contains' }]
|
|
||||||
if (_options.hasOwnProperty(filter.category)) {
|
|
||||||
_options[filter.category].push(filter);
|
|
||||||
} else {
|
} else {
|
||||||
_options[filter.category] = [filter];
|
filterSection[filter.category] = [filter];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return _options;
|
return filterSection;
|
||||||
}
|
}
|
||||||
|
|
@ -226,4 +226,10 @@ export const iceServerConfigFromString = (str) => {
|
||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isGreaterOrEqualVersion = (version, compareTo) => {
|
||||||
|
const [major, minor, patch] = version.split("-")[0].split('.');
|
||||||
|
const [majorC, minorC, patchC] = compareTo.split("-")[0].split('.');
|
||||||
|
return (major > majorC) || (major === majorC && minor > minorC) || (major === majorC && minor === minorC && patch >= patchC);
|
||||||
}
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ const oss = {
|
||||||
ORIGIN: () => 'window.location.origin',
|
ORIGIN: () => 'window.location.origin',
|
||||||
API_EDP: () => 'window.location.origin + "/api"',
|
API_EDP: () => 'window.location.origin + "/api"',
|
||||||
ASSETS_HOST: () => 'window.location.origin + "/assets"',
|
ASSETS_HOST: () => 'window.location.origin + "/assets"',
|
||||||
VERSION: '1.5.1',
|
VERSION: '1.5.2',
|
||||||
SOURCEMAP: true,
|
SOURCEMAP: true,
|
||||||
MINIO_ENDPOINT: process.env.MINIO_ENDPOINT,
|
MINIO_ENDPOINT: process.env.MINIO_ENDPOINT,
|
||||||
MINIO_PORT: process.env.MINIO_PORT,
|
MINIO_PORT: process.env.MINIO_PORT,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ fatal()
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
version="v1.5.1"
|
version="v1.5.2"
|
||||||
usr=`whoami`
|
usr=`whoami`
|
||||||
|
|
||||||
# Installing k3s
|
# Installing k3s
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
fromVersion: "v1.5.1"
|
fromVersion: "v1.5.2"
|
||||||
# Databases specific variables
|
# Databases specific variables
|
||||||
postgresql: &postgres
|
postgresql: &postgres
|
||||||
# For generating passwords
|
# For generating passwords
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,7 @@ module.exports = {
|
||||||
wsRouter,
|
wsRouter,
|
||||||
start: (server) => {
|
start: (server) => {
|
||||||
io = _io(server, {
|
io = _io(server, {
|
||||||
maxHttpBufferSize: 1e6,
|
maxHttpBufferSize: 5e6,
|
||||||
cors: {
|
cors: {
|
||||||
origin: "*",
|
origin: "*",
|
||||||
methods: ["GET", "POST", "PUT"]
|
methods: ["GET", "POST", "PUT"]
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue