Merge branch 'dashboard' into api-v1.5.5

This commit is contained in:
Shekar Siri 2022-04-08 18:14:38 +02:00
commit 6c4664caa7
171 changed files with 32412 additions and 1203 deletions

View file

@ -69,7 +69,7 @@ For those who want to simply use OpenReplay as a service, [sign up](https://app.
Please refer to the [official OpenReplay documentation](https://docs.openreplay.com/). That should help you troubleshoot common issues. For additional help, you can reach out to us on one of these channels:
- [Slack](https://slack.openreplay.com) (Connect with our engineers and community)
- [Discord](https://discord.openreplay.com) (Connect with our engineers and community)
- [GitHub](https://github.com/openreplay/openreplay/issues) (Bug and issue reports)
- [Twitter](https://twitter.com/OpenReplayHQ) (Product updates, Great content)
- [Website chat](https://openreplay.com) (Talk to us)
@ -80,7 +80,7 @@ We're always on the lookout for contributions to OpenReplay, and we're glad you'
See our [Contributing Guide](CONTRIBUTING.md) for more details.
Also, feel free to join our [Slack](https://slack.openreplay.com) to ask questions, discuss ideas or connect with our contributors.
Also, feel free to join our [Discord](https://discord.openreplay.com) to ask questions, discuss ideas or connect with our contributors.
## Roadmap

View file

@ -1,3 +1,4 @@
import React, { lazy, Suspense } from 'react';
import { Switch, Route, Redirect } from 'react-router';
import { BrowserRouter, withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
@ -5,26 +6,29 @@ import { Notification } from 'UI';
import { Loader } from 'UI';
import { fetchUserInfo } from 'Duck/user';
import withSiteIdUpdater from 'HOCs/withSiteIdUpdater';
import Login from 'Components/Login/Login';
import ForgotPassword from 'Components/ForgotPassword/ForgotPassword';
import UpdatePassword from 'Components/UpdatePassword/UpdatePassword';
import ClientPure from 'Components/Client/Client';
import OnboardingPure from 'Components/Onboarding/Onboarding';
import SessionPure from 'Components/Session/Session';
import LiveSessionPure from 'Components/Session/LiveSession';
import AssistPure from 'Components/Assist';
import BugFinderPure from 'Components/BugFinder/BugFinder';
import DashboardPure from 'Components/Dashboard/Dashboard';
import ErrorsPure from 'Components/Errors/Errors';
const Login = lazy(() => import('Components/Login/Login'));
const ForgotPassword = lazy(() => import('Components/ForgotPassword/ForgotPassword'));
const UpdatePassword = lazy(() => import('Components/UpdatePassword/UpdatePassword'));
const SessionPure = lazy(() => import('Components/Session/Session'));
const LiveSessionPure = lazy(() => import('Components/Session/LiveSession'));
const OnboardingPure = lazy(() => import('Components/Onboarding/Onboarding'));
const ClientPure = lazy(() => import('Components/Client/Client'));
const AssistPure = lazy(() => import('Components/Assist'));
const BugFinderPure = lazy(() => import('Components/BugFinder/BugFinder'));
const DashboardPure = lazy(() => import('Components/Dashboard/NewDashboard'));
const ErrorsPure = lazy(() => import('Components/Errors/Errors'));
const FunnelDetails = lazy(() => import('Components/Funnels/FunnelDetails'));
const FunnelIssueDetails = lazy(() => import('Components/Funnels/FunnelIssueDetails'));
import WidgetViewPure from 'Components/Dashboard/components/WidgetView';
import Header from 'Components/Header/Header';
// import ResultsModal from 'Shared/Results/ResultsModal';
import FunnelDetails from 'Components/Funnels/FunnelDetails';
import FunnelIssueDetails from 'Components/Funnels/FunnelIssueDetails';
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 { dashboardService } from "App/services";
import { withStore } from 'App/mstore'
import APIClient from './api_client';
import * as routes from './routes';
@ -32,9 +36,12 @@ import { OB_DEFAULT_TAB } from 'App/routes';
import Signup from './components/Signup/Signup';
import { fetchTenants } from 'Duck/user';
import { setSessionPath } from 'Duck/sessions';
import { ModalProvider } from './components/Modal';
import ModalRoot from './components/Modal/ModalRoot';
const BugFinder = withSiteIdUpdater(BugFinderPure);
const Dashboard = withSiteIdUpdater(DashboardPure);
const WidgetView = withSiteIdUpdater(WidgetViewPure);
const Session = withSiteIdUpdater(SessionPure);
const LiveSession = withSiteIdUpdater(LiveSessionPure);
const Assist = withSiteIdUpdater(AssistPure);
@ -46,7 +53,15 @@ const FunnelIssue = withSiteIdUpdater(FunnelIssueDetails);
const withSiteId = routes.withSiteId;
const withObTab = routes.withObTab;
const METRICS_PATH = routes.metrics();
const METRICS_DETAILS = routes.metricDetails();
const DASHBOARD_PATH = routes.dashboard();
const DASHBOARD_SELECT_PATH = routes.dashboardSelected();
const DASHBOARD_METRIC_CREATE_PATH = routes.dashboardMetricCreate();
const DASHBOARD_METRIC_DETAILS_PATH = routes.dashboardMetricDetails();
// const WIDGET_PATAH = routes.dashboardMetric();
const SESSIONS_PATH = routes.sessions();
const ASSIST_PATH = routes.assist();
const ERRORS_PATH = routes.errors();
@ -62,6 +77,7 @@ const CLIENT_PATH = routes.client();
const ONBOARDING_PATH = routes.onboarding();
const ONBOARDING_REDIRECT_PATH = routes.onboarding(OB_DEFAULT_TAB);
@withStore
@withRouter
@connect((state) => {
const siteId = state.getIn([ 'user', 'siteId' ]);
@ -108,6 +124,8 @@ class Router extends React.Component {
fetchInitialData = () => {
Promise.all([
this.props.fetchUserInfo().then(() => {
const { mstore } = this.props
mstore.initClient();
this.props.fetchIntegrationVariables()
}),
this.props.fetchSiteList().then(() => {
@ -153,54 +171,78 @@ class Router extends React.Component {
{!hideHeader && <Header key="header"/>}
<Notification />
<Switch key="content" >
<Route path={ CLIENT_PATH } component={ Client } />
<Route path={ withSiteId(ONBOARDING_PATH, siteIdList)} component={ Onboarding } />
<Route
path="/integrations/"
render={
({ location }) => {
const client = new APIClient(jwt);
switch (location.pathname) {
case '/integrations/slack':
client.post('integrations/slack/add', {
code: location.search.split('=')[ 1 ],
state: tenantId,
});
break;
<Suspense fallback={<Loader loading={true} className="flex-1" />}>
<ModalProvider>
<ModalRoot />
<Switch key="content" >
<Route path={ CLIENT_PATH } component={ Client } />
<Route path={ withSiteId(ONBOARDING_PATH, siteIdList)} component={ Onboarding } />
<Route
path="/integrations/"
render={
({ location }) => {
const client = new APIClient(jwt);
switch (location.pathname) {
case '/integrations/slack':
client.post('integrations/slack/add', {
code: location.search.split('=')[ 1 ],
state: tenantId,
});
break;
}
return <Redirect to={ CLIENT_PATH } />;
}
return <Redirect to={ CLIENT_PATH } />;
}
}
/>
{ onboarding &&
<Redirect to={ withSiteId(ONBOARDING_REDIRECT_PATH, siteId)} />
}
{ siteIdList.length === 0 &&
<Redirect to={ routes.client(routes.CLIENT_TABS.SITES) } />
}
<Route exact strict path={ withSiteId(DASHBOARD_PATH, siteIdList) } component={ Dashboard } />
<Route exact strict path={ withSiteId(ASSIST_PATH, siteIdList) } component={ Assist } />
<Route exact strict path={ withSiteId(ERRORS_PATH, siteIdList) } component={ Errors } />
<Route exact strict path={ withSiteId(ERROR_PATH, siteIdList) } component={ Errors } />
<Route exact strict path={ withSiteId(FUNNEL_PATH, siteIdList) } component={ Funnels } />
<Route exact strict path={ withSiteId(FUNNEL_ISSUE_PATH, siteIdList) } component={ FunnelIssue } />
<Route exact strict path={ withSiteId(SESSIONS_PATH, siteIdList) } component={ BugFinder } />
<Route exact strict path={ withSiteId(SESSION_PATH, siteIdList) } component={ Session } />
<Route exact strict path={ withSiteId(LIVE_SESSION_PATH, siteIdList) } component={ LiveSession } />
<Route exact strict path={ withSiteId(LIVE_SESSION_PATH, siteIdList) } render={ (props) => <Session { ...props } live /> } />
{ routes.redirects.map(([ fr, to ]) => (
<Redirect key={ fr } exact strict from={ fr } to={ to } />
)) }
<Redirect to={ withSiteId(SESSIONS_PATH, siteId) } />
/>
{ onboarding &&
<Redirect to={ withSiteId(ONBOARDING_REDIRECT_PATH, siteId)} />
}
{ siteIdList.length === 0 &&
<Redirect to={ routes.client(routes.CLIENT_TABS.SITES) } />
}
<Route exact strict path={ withSiteId(METRICS_PATH, siteIdList) } component={ Dashboard } />
<Route exact strict path={ withSiteId(METRICS_DETAILS, siteIdList) } component={ Dashboard } />
<Route exact strict path={ withSiteId(DASHBOARD_PATH, siteIdList) } component={ Dashboard } />
<Route exact strict path={ withSiteId(DASHBOARD_SELECT_PATH, siteIdList) } component={ Dashboard } />
<Route exact strict path={ withSiteId(DASHBOARD_METRIC_CREATE_PATH, siteIdList) } component={ Dashboard } />
<Route exact strict path={ withSiteId(DASHBOARD_METRIC_DETAILS_PATH, siteIdList) } component={ Dashboard } />
{/* <Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
<Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
<Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
<Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
<Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } /> */}
<Route exact strict path={ withSiteId(ASSIST_PATH, siteIdList) } component={ Assist } />
<Route exact strict path={ withSiteId(ERRORS_PATH, siteIdList) } component={ Errors } />
<Route exact strict path={ withSiteId(ERROR_PATH, siteIdList) } component={ Errors } />
<Route exact strict path={ withSiteId(FUNNEL_PATH, siteIdList) } component={ Funnels } />
<Route exact strict path={ withSiteId(FUNNEL_ISSUE_PATH, siteIdList) } component={ FunnelIssue } />
<Route exact strict path={ withSiteId(SESSIONS_PATH, siteIdList) } component={ BugFinder } />
<Route exact strict path={ withSiteId(SESSION_PATH, siteIdList) } component={ Session } />
<Route exact strict path={ withSiteId(LIVE_SESSION_PATH, siteIdList) } component={ LiveSession } />
<Route exact strict path={ withSiteId(LIVE_SESSION_PATH, siteIdList) } render={ (props) => <Session { ...props } live /> } />
{ routes.redirects.map(([ fr, to ]) => (
<Redirect key={ fr } exact strict from={ fr } to={ to } />
)) }
<Redirect to={ withSiteId(SESSIONS_PATH, siteId) } />
</Switch>
</ModalProvider>
</Suspense>
</Loader>
:
<Suspense fallback={<Loader loading={true} className="flex-1" />}>
<Switch>
<Route exact strict path={ FORGOT_PASSWORD } component={ ForgotPassword } />
<Route exact strict path={ LOGIN_PATH } component={ changePassword ? UpdatePassword : Login } />
{ !existingTenant && <Route exact strict path={ SIGNUP_PATH } component={ Signup } /> }
<Redirect to={ LOGIN_PATH } />
</Switch>
</Loader> :
<Switch>
<Route exact strict path={ FORGOT_PASSWORD } component={ ForgotPassword } />
<Route exact strict path={ LOGIN_PATH } component={ changePassword ? UpdatePassword : Login } />
{ !existingTenant && <Route exact strict path={ SIGNUP_PATH } component={ Signup } /> }
<Redirect to={ LOGIN_PATH } />
</Switch>;
</Suspense>;
}
}

View file

@ -1,5 +1,4 @@
import store from 'App/store';
import { queried } from './routes';
const siteIdRequiredPaths = [
@ -24,6 +23,8 @@ const siteIdRequiredPaths = [
'/assist',
'/heatmaps',
'/custom_metrics',
'/dashboards',
'/metrics'
// '/custom_metrics/sessions',
];
@ -68,12 +69,16 @@ export default class APIClient {
this.siteId = siteId;
}
fetch(path, params, options = { clean: true }) {
fetch(path, params, options = { clean: true }) {
if (params !== undefined) {
const cleanedParams = options.clean ? clean(params) : params;
this.init.body = JSON.stringify(cleanedParams);
}
if (this.init.method === 'GET') {
delete this.init.body;
}
let fetch = window.fetch;

View file

@ -12,6 +12,7 @@
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet">
</head>
<body>
<div id="modal-root"></div>
<div id="app"><p style="color: #eee;text-align: center;height: 100%;padding: 25%;">Loading...</p></div>
</body>
</html>

View file

@ -45,7 +45,7 @@ function AlertFormModal(props: Props) {
const onDelete = async (instance) => {
if (await confirm({
header: 'Confirm',
confirmButton: 'Yes, Delete',
confirmButton: 'Yes, delete',
confirmation: `Are you sure you want to permanently delete this alert?`
})) {
props.remove(instance.alertId).then(() => {

View file

@ -32,7 +32,7 @@ const Alerts = props => {
const onDelete = async (instance) => {
if (await confirm({
header: 'Confirm',
confirmButton: 'Yes, Delete',
confirmButton: 'Yes, delete',
confirmation: `Are you sure you want to permanently delete this alert?`
})) {
props.remove(instance.alertId).then(() => {

View file

@ -114,7 +114,7 @@ class AutoComplete extends React.PureComponent {
render() {
const { ddOpen, query, loading, values } = this.state;
const {
const {
optionMapping = defaultOptionMapping,
valueToText = defaultValueToText,
placeholder = 'Type to search...',

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { DNDSource, DNDTarget } from 'Components/hocs/dnd';
// import { DNDSource, DNDTarget } from 'Components/hocs/dnd';
import Event, { TYPES } from 'Types/filter/event';
import { operatorOptions } from 'Types/filter';
import { editEvent, removeEvent, clearEvents, applyFilter } from 'Duck/filters';
@ -25,8 +25,8 @@ const getLabel = ({ type }) => {
return getPlaceholder({ type });
};
@DNDTarget('event')
@DNDSource('event')
// @DNDTarget('event')
// @DNDSource('event')
@connect(state => ({
isLastEvent: state.getIn([ 'filters', 'appliedFilter', 'events' ]).size === 1,
}), { editEvent, removeEvent, clearEvents, applyFilter })

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import { Input } from 'semantic-ui-react';
import { DNDContext } from 'Components/hocs/dnd';
// import { DNDContext } from 'Components/hocs/dnd';
import {
addEvent, applyFilter, moveEvent, clearEvents, edit,
addCustomFilter, addAttribute, setSearchQuery, setActiveFlow, setFilterOption
@ -45,7 +45,7 @@ import SaveFilterButton from 'Shared/SaveFilterButton';
setBlink,
edit,
})
@DNDContext
// @DNDContext
export default class EventFilter extends React.PureComponent {
state = { search: '', showFilterModal: false, showPlacehoder: true }
fetchEventList = debounce(this.props.fetchEventList, 500)

View file

@ -22,7 +22,7 @@ class SlackAddForm extends React.PureComponent {
remove = async (id) => {
if (await confirm({
header: 'Confirm',
confirmButton: 'Yes, Delete',
confirmButton: 'Yes, delete',
confirmation: `Are you sure you want to permanently delete this channel?`
})) {
this.props.remove(id);

View file

@ -0,0 +1,49 @@
import React, { useEffect } from 'react';
import withPageTitle from 'HOCs/withPageTitle';
import { observer, useObserver } from "mobx-react-lite";
import { useStore } from 'App/mstore';
import { withRouter } from 'react-router-dom';
import {
dashboardSelected,
withSiteId,
} from 'App/routes';
import DashboardSideMenu from './components/DashboardSideMenu';
import { Loader } from 'UI';
import DashboardRouter from './components/DashboardRouter';
function NewDashboard(props) {
const { history, match: { params: { siteId, dashboardId, metricId } } } = props;
const { dashboardStore } = useStore();
const loading = useObserver(() => dashboardStore.isLoading);
useEffect(() => {
dashboardStore.fetchList().then((resp) => {
if (parseInt(dashboardId) > 0) {
dashboardStore.selectDashboardById(dashboardId);
} else {
dashboardStore.selectDefaultDashboard().then(({ dashboardId }) => {
if (!history.location.pathname.includes('/metrics')) {
history.push(withSiteId(dashboardSelected(dashboardId), siteId));
}
});
}
});
}, []);
return (
<Loader loading={loading}>
<div className="page-margin container-90">
<div className="side-menu">
<DashboardSideMenu siteId={siteId} />
</div>
<div className="side-menu-margined">
<DashboardRouter siteId={siteId} />
</div>
</div>
</Loader>
);
}
export default withPageTitle('New Dashboard')(
withRouter(observer(NewDashboard))
);

View file

@ -22,7 +22,7 @@ function SideMenuSection({ title, items, onItemClick, setShowAlerts, siteId }) {
)}
<div className={stl.divider} />
<div className="my-3">
<div className="my-3">
<SideMenuitem
id="menu-manage-alerts"
title="Manage Alerts"

View file

@ -11,7 +11,7 @@ interface Props {
onClick?: (event, index) => void;
}
function CustomMetriLineChart(props: Props) {
const { data, params, seriesMap, colors, onClick = () => null } = props;
const { data, params, seriesMap = [], colors, onClick = () => null } = props;
return (
<ResponsiveContainer height={ 240 } width="100%">
<LineChart

View file

@ -0,0 +1,75 @@
import React from 'react'
import { Styles } from '../../common';
import { AreaChart, ResponsiveContainer, XAxis, YAxis, CartesianGrid, Area, Tooltip } from 'recharts';
import { LineChart, Line, Legend } from 'recharts';
import cn from 'classnames';
import CountBadge from '../../common/CountBadge';
import { numberWithCommas } from 'App/utils';
interface Props {
data: any;
// onClick?: (event, index) => void;
}
function CustomMetricOverviewChart(props: Props) {
const { data } = props;
console.log('data', data)
const gradientDef = Styles.gradientDef();
return (
<div className="relative -mx-4">
<div className="absolute flex items-start flex-col justify-center inset-0 p-3">
<div className="mb-2 flex items-center" >
</div>
<div className="flex items-center">
<CountBadge
// title={subtext}
count={ countView(Math.round(data.value), data.unit) }
change={ data.progress || 0 }
unit={ data.unit }
// className={textClass}
/>
</div>
</div>
<ResponsiveContainer height={ 100 } width="100%">
<AreaChart
data={ data.chart }
margin={ {
top: 85, right: 0, left: 0, bottom: 5,
} }
>
{gradientDef}
<Tooltip {...Styles.tooltip} />
<XAxis hide {...Styles.xaxis} interval={4} dataKey="time" />
<YAxis hide interval={ 0 } />
<Area
name={''}
// unit={unit && ' ' + unit}
type="monotone"
dataKey="value"
stroke={Styles.colors[0]}
fillOpacity={ 1 }
strokeWidth={ 2 }
strokeOpacity={ 0.8 }
fill={'url(#colorCount)'}
/>
</AreaChart>
</ResponsiveContainer>
</div>
)
}
export default CustomMetricOverviewChart
const countView = (avg, unit) => {
if (unit === 'mb') {
if (!avg) return 0;
const count = Math.trunc(avg / 1024 / 1024);
return numberWithCommas(count);
}
if (unit === 'min') {
if (!avg) return 0;
const count = Math.trunc(avg);
return numberWithCommas(count > 1000 ? count +'k' : count);
}
return avg ? numberWithCommas(avg): 0;
}

View file

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

View file

@ -35,8 +35,7 @@ function CustomMetricPieChart(props: Props) {
}
}
return (
<div>
<NoContent size="small" show={data.values && data.values.length === 0} >
<NoContent size="small" show={!data.values || data.values.length === 0} style={{ minHeight: '240px'}}>
<ResponsiveContainer height={ 220 } width="100%">
<PieChart>
<Pie
@ -52,105 +51,77 @@ function CustomMetricPieChart(props: Props) {
activeIndex={1}
onClick={onClickHandler}
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);
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;
let name = data.values[index].name || 'Unidentified';
name = name.length > 20 ? name.substring(0, 20) + '...' : name;
if (percentage<3){
return null;
}
return (
<text
x={x}
y={y}
fontWeight="400"
fontSize="12px"
// fontFamily="'Source Sans Pro', 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'"
textAnchor={x > cx ? "start" : "end"}
dominantBaseline="central"
fill='#666'
>
{name || 'Unidentified'} {numberWithCommas(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>
// );
// }}
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;
let name = data.values[index].name || 'Unidentified';
name = name.length > 20 ? name.substring(0, 20) + '...' : name;
if (percentage<3){
return null;
}
return (
<text
x={x}
y={y}
fontWeight="400"
fontSize="12px"
// fontFamily="'Source Sans Pro', 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'"
textAnchor={x > cx ? "start" : "end"}
dominantBaseline="central"
fill='#666'
>
{name || 'Unidentified'} {numberWithCommas(value)}
</text>
);
}}
>
{data.values.map((entry, index) => (
<Cell key={`cell-${index}`} fill={Styles.colorsPie[index % Styles.colorsPie.length]} />
))}
{data && data.values && data.values.map((entry, index) => (
<Cell key={`cell-${index}`} fill={Styles.colorsPie[index % Styles.colorsPie.length]} />
))}
</Pie>
<Tooltip {...Styles.tooltip} />
</PieChart>
</ResponsiveContainer>
<div className="text-sm color-gray-medium">Top 5 </div>
</NoContent>
</div>
</NoContent>
)
}

View file

@ -56,29 +56,29 @@ function CustomMetricWidget(props: Props) {
const isTable = metric.viewType === 'table';
const isPieChart = metric.viewType === 'pieChart';
useEffect(() => {
new APIClient()['post'](`/custom_metrics/${metricParams.metricId}/chart`, { ...metricParams, q: metric.name })
.then(response => response.json())
.then(({ errors, data }) => {
if (errors) {
console.log('err', errors)
} else {
const namesMap = data
.map(i => Object.keys(i))
.flat()
.filter(i => i !== 'time' && i !== 'timestamp')
.reduce((unique: any, item: any) => {
if (!unique.includes(item)) {
unique.push(item);
}
return unique;
}, []);
// useEffect(() => {
// new APIClient()['post'](`/custom_metrics/${metricParams.metricId}/chart`, { ...metricParams, q: metric.name })
// .then(response => response.json())
// .then(({ errors, data }) => {
// if (errors) {
// console.log('err', errors)
// } else {
// const namesMap = data
// .map(i => Object.keys(i))
// .flat()
// .filter(i => i !== 'time' && i !== 'timestamp')
// .reduce((unique: any, item: any) => {
// if (!unique.includes(item)) {
// unique.push(item);
// }
// return unique;
// }, []);
setSeriesMap(namesMap);
setData(getChartFormatter(period)(data));
}
}).finally(() => setLoading(false));
}, [period])
// setSeriesMap(namesMap);
// setData(getChartFormatter(period)(data));
// }
// }).finally(() => setLoading(false));
// }, [period])
const clickHandlerTable = (filters) => {
const activeWidget = {

View file

@ -61,27 +61,27 @@ function CustomMetricWidget(props: Props) {
setLoading(true);
// fetch new data for the widget preview
new APIClient()['post']('/custom_metrics/try', { ...metricParams, ...metric.toSaveData() })
.then(response => response.json())
.then(({ errors, data }) => {
if (errors) {
console.log('err', errors)
} else {
const namesMap = data
.map(i => Object.keys(i))
.flat()
.filter(i => i !== 'time' && i !== 'timestamp')
.reduce((unique: any, item: any) => {
if (!unique.includes(item)) {
unique.push(item);
}
return unique;
}, []);
// new APIClient()['post']('/custom_metrics/try', { ...metricParams, ...metric.toSaveData() })
// .then(response => response.json())
// .then(({ errors, data }) => {
// if (errors) {
// console.log('err', errors)
// } else {
// const namesMap = data
// .map(i => Object.keys(i))
// .flat()
// .filter(i => i !== 'time' && i !== 'timestamp')
// .reduce((unique: any, item: any) => {
// if (!unique.includes(item)) {
// unique.push(item);
// }
// return unique;
// }, []);
setSeriesMap(namesMap);
setData(getChartFormatter(period)(data));
}
}).finally(() => setLoading(false));
// setSeriesMap(namesMap);
// setData(getChartFormatter(period)(data));
// }
// }).finally(() => setLoading(false));
}, [metric])
const onDateChange = (changedDates) => {

View file

@ -1,5 +1,5 @@
.bar {
height: 10px;
height: 5px;
background-color: red;
width: 100%;
border-radius: 3px;

View file

@ -10,7 +10,7 @@ const Bar = ({ className = '', width = 0, avg, domain, color }) => {
<span className="font-medium">{`${avg}`}</span>
</div>
</div>
<div className="text-sm leading-3">{domain}</div>
<div className="text-sm leading-3 color-gray-medium">{domain}</div>
</div>
)
}

View file

@ -0,0 +1,48 @@
import React from 'react';
import { NoContent } from 'UI';
import { Styles } from '../../common';
import {
AreaChart, Area,
BarChart, Bar, CartesianGrid, Tooltip,
LineChart, Line, Legend, ResponsiveContainer,
XAxis, YAxis
} from 'recharts';
interface Props {
data: any
}
function BreakdownOfLoadedResources(props: Props) {
const { data } = props;
const gradientDef = Styles.gradientDef();
const params = { density: 28 }
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">
<BarChart
data={ data.chart }
margin={ Styles.chartMargins }
>
{gradientDef}
<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 Resources" }}
/>
<Legend />
<Tooltip {...Styles.tooltip} />
<Bar minPointSize={1} name="CSS" dataKey="stylesheet" stackId="a" fill={Styles.colors[0]} />
<Bar name="Images" dataKey="img" stackId="a" fill={Styles.colors[2]} />
<Bar name="Scripts" dataKey="script" stackId="a" fill={Styles.colors[3]} />
</BarChart>
</ResponsiveContainer>
</NoContent>
);
}
export default BreakdownOfLoadedResources;

View file

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

View file

@ -0,0 +1,56 @@
import React from 'react';
import { NoContent } from 'UI';
import { Styles } from '../../common';
import {
AreaChart, Area,
BarChart, Bar, CartesianGrid, Tooltip,
LineChart, Line, Legend, ResponsiveContainer,
XAxis, YAxis
} from 'recharts';
interface Props {
data: any
}
function CPULoad(props: Props) {
const { data } = props;
const gradientDef = Styles.gradientDef();
const params = { density: 70 }
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">
<AreaChart
data={ data.chart }
margin={ Styles.chartMargins }
>
{gradientDef}
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
<YAxis
{...Styles.yaxis}
allowDecimals={false}
tickFormatter={val => Styles.tickFormatter(val)}
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
/>
<Tooltip {...Styles.tooltip} />
<Area
name="Avg"
type="monotone"
unit="%"
dataKey="avgCpu"
stroke={Styles.colors[0]}
fillOpacity={ 1 }
strokeWidth={ 2 }
strokeOpacity={ 0.8 }
fill={'url(#colorCount)'}
/>
</AreaChart>
</ResponsiveContainer>
</NoContent>
);
}
export default CPULoad;

View file

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

View file

@ -0,0 +1,49 @@
import React from 'react';
import { NoContent } from 'UI';
import { Styles } from '../../common';
import {
BarChart, Bar, CartesianGrid, Tooltip,
LineChart, Line, Legend, ResponsiveContainer,
XAxis, YAxis
} from 'recharts';
interface Props {
data: any
}
function CallsErrors4xx(props: Props) {
const { data } = props;
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">
<BarChart
data={data.chart}
margin={Styles.chartMargins}
syncId="errorsPerType"
// syncId={ showSync ? "errorsPerType" : undefined }
>
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis
{...Styles.xaxis}
dataKey="time"
// interval={params.density/7}
/>
<YAxis
{...Styles.yaxis}
label={{ ...Styles.axisLabelLeft, value: "Number of Errors" }}
allowDecimals={false}
/>
<Legend />
<Tooltip {...Styles.tooltip} />
{/* { data.namesMap.map((key, index) => (
<Line key={key} name={key} type="monotone" dataKey={key} stroke={Styles.colors[index]} fillOpacity={ 1 } strokeWidth={ 2 } strokeOpacity={ 0.8 } fill="url(#colorCount)" dot={false} />
))} */}
</BarChart>
</ResponsiveContainer>
</NoContent>
);
}
export default CallsErrors4xx;

View file

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

View file

@ -0,0 +1,49 @@
import React from 'react';
import { NoContent } from 'UI';
import { Styles } from '../../common';
import {
BarChart, Bar, CartesianGrid, Tooltip,
LineChart, Line, Legend, ResponsiveContainer,
XAxis, YAxis
} from 'recharts';
interface Props {
data: any
}
function CallsErrors5xx(props: Props) {
const { data } = props;
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">
<BarChart
data={data.chart}
margin={Styles.chartMargins}
syncId="errorsPerType"
// syncId={ showSync ? "errorsPerType" : undefined }
>
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis
{...Styles.xaxis}
dataKey="time"
// interval={params.density/7}
/>
<YAxis
{...Styles.yaxis}
label={{ ...Styles.axisLabelLeft, value: "Number of Errors" }}
allowDecimals={false}
/>
<Legend />
<Tooltip {...Styles.tooltip} />
{/* { data.namesMap.map((key, index) => (
<Line key={key} name={key} type="monotone" dataKey={key} stroke={Styles.colors[index]} fillOpacity={ 1 } strokeWidth={ 2 } strokeOpacity={ 0.8 } fill="url(#colorCount)" dot={false} />
))} */}
</BarChart>
</ResponsiveContainer>
</NoContent>
);
}
export default CallsErrors5xx;

View file

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

View file

@ -0,0 +1,55 @@
import React from 'react';
import { NoContent } from 'UI';
import { Styles } from '../../common';
import {
AreaChart, Area,
BarChart, Bar, CartesianGrid, Tooltip,
LineChart, Line, Legend, ResponsiveContainer,
XAxis, YAxis
} from 'recharts';
interface Props {
data: any
}
function Crashes(props: Props) {
const { data } = props;
const gradientDef = Styles.gradientDef();
const params = { density: 70 }
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">
<AreaChart
data={ data.chart }
margin={ Styles.chartMargins }
>
{gradientDef}
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
<YAxis
{...Styles.yaxis}
allowDecimals={false}
tickFormatter={val => Styles.tickFormatter(val)}
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
/>
<Tooltip {...Styles.tooltip} />
<Area
name="Crashes"
type="monotone"
unit="%"
dataKey="avgCpu"
stroke={Styles.colors[0]}
fillOpacity={ 1 }
strokeWidth={ 2 }
strokeOpacity={ 0.8 }
fill={'url(#colorCount)'}
/>
</AreaChart>
</ResponsiveContainer>
</NoContent>
);
}
export default Crashes;

View file

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

View file

@ -0,0 +1,91 @@
import React from 'react';
import { NoContent } from 'UI';
import { Styles, AvgLabel } from '../../common';
import { withRequest } from 'HOCs'
import {
AreaChart, Area,
BarChart, Bar, CartesianGrid, Tooltip,
LineChart, Line, Legend, ResponsiveContainer,
XAxis, YAxis
} from 'recharts';
import WidgetAutoComplete from 'Shared/WidgetAutoComplete';
import { toUnderscore } from 'App/utils';
const WIDGET_KEY = 'pagesDomBuildtime';
interface Props {
data: any
optionsLoading: any
fetchOptions: any
options: any
}
function DomBuildingTime(props: Props) {
const { data, optionsLoading } = props;
const gradientDef = Styles.gradientDef();
const params = { density: 70 }
const onSelect = (params) => {
const _params = { density: 70 }
console.log('params', params) // TODO reload the data with new params;
// this.props.fetchWidget(WIDGET_KEY, dashbaordStore.period, props.platform, { ..._params, url: params.value })
}
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
>
<>
<div className="flex items-center mb-3">
<WidgetAutoComplete
loading={optionsLoading}
fetchOptions={props.fetchOptions}
options={props.options}
onSelect={onSelect}
placeholder="Search for Page"
/>
<AvgLabel className="ml-auto" text="Avg" count={Math.round(data.avg)} unit="ms" />
</div>
<ResponsiveContainer height={ 207 } width="100%">
<AreaChart
data={ data.chart }
margin={ Styles.chartMargins }
>
{gradientDef}
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
<YAxis
{...Styles.yaxis}
allowDecimals={false}
tickFormatter={val => Styles.tickFormatter(val)}
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
/>
<Tooltip {...Styles.tooltip} />
<Area
name="Avg"
type="monotone"
unit="%"
dataKey="avgCpu"
stroke={Styles.colors[0]}
fillOpacity={ 1 }
strokeWidth={ 2 }
strokeOpacity={ 0.8 }
fill={'url(#colorCount)'}
/>
</AreaChart>
</ResponsiveContainer>
</>
</NoContent>
);
}
export default withRequest({
dataName: "options",
initialData: [],
dataWrapper: data => data,
loadingName: 'optionsLoading',
requestName: "fetchOptions",
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
method: 'GET'
})(DomBuildingTime)

View file

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

View file

@ -0,0 +1,48 @@
import React from 'react';
import { NoContent } from 'UI';
import { Styles } from '../../common';
import {
BarChart, Bar, CartesianGrid, Tooltip,
LineChart, Line, Legend, ResponsiveContainer,
XAxis, YAxis
} from 'recharts';
interface Props {
data: any
}
function ErrorsByOrigin(props: Props) {
const { data } = props;
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">
<BarChart
data={data.chart}
margin={Styles.chartMargins}
syncId="errorsPerType"
// syncId={ showSync ? "errorsPerType" : undefined }
>
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis
{...Styles.xaxis}
dataKey="time"
// interval={params.density/7}
/>
<YAxis
{...Styles.yaxis}
label={{ ...Styles.axisLabelLeft, value: "Number of Errors" }}
allowDecimals={false}
/>
<Legend />
<Tooltip {...Styles.tooltip} />
<Bar minPointSize={1} name={<span className="float">1<sup>st</sup> Party</span>} dataKey="firstParty" stackId="a" fill={Styles.colors[0]} />
<Bar name={<span className="float">3<sup>rd</sup> Party</span>} dataKey="thirdParty" stackId="a" fill={Styles.colors[2]} />
</BarChart>
</ResponsiveContainer>
</NoContent>
);
}
export default ErrorsByOrigin;

View file

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

View file

@ -0,0 +1,50 @@
import React from 'react';
import { NoContent } from 'UI';
import { Styles } from '../../common';
import {
BarChart, Bar, CartesianGrid, Tooltip,
LineChart, Line, Legend, ResponsiveContainer,
XAxis, YAxis
} from 'recharts';
interface Props {
data: any
}
function ErrorsByType(props: Props) {
const { data } = props;
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">
<BarChart
data={data.chart}
margin={Styles.chartMargins}
syncId="errorsPerType"
// syncId={ showSync ? "errorsPerType" : undefined }
>
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis
{...Styles.xaxis}
dataKey="time"
// interval={params.density/7}
/>
<YAxis
{...Styles.yaxis}
label={{ ...Styles.axisLabelLeft, value: "Number of Errors" }}
allowDecimals={false}
/>
<Legend />
<Tooltip {...Styles.tooltip} />
<Bar minPointSize={1} name="Integrations" dataKey="integrations" stackId="a" fill={Styles.colors[0]}/>
<Bar name="4xx" dataKey="4xx" stackId="a" fill={Styles.colors[1]} />
<Bar name="5xx" dataKey="5xx" stackId="a" fill={Styles.colors[2]} />
<Bar name="Javascript" dataKey="js" stackId="a" fill={Styles.colors[3]} />
</BarChart>
</ResponsiveContainer>
</NoContent>
);
}
export default ErrorsByType;

View file

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

View file

@ -0,0 +1,36 @@
import React from 'react';
import { NoContent } from 'UI';
import { Styles } from '../../common';
import { numberWithCommas } from 'App/utils';
import Bar from 'App/components/Dashboard/Widgets/ErrorsPerDomain/Bar';
interface Props {
data: any
}
function ErrorsPerDomain(props: Props) {
const { data } = props;
console.log('ErrorsPerDomain', data);
// const firstAvg = 10;
const firstAvg = data.chart[0] && data.chart[0].errorsCount;
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
>
<div className="w-full" style={{ height: '240px' }}>
{data.chart.map((item, i) =>
<Bar
key={i}
className="mb-2"
avg={numberWithCommas(Math.round(item.errorsCount))}
width={Math.round((item.errorsCount * 100) / firstAvg) - 10}
domain={item.domain}
color={Styles.colors[i]}
/>
)}
</div>
</NoContent>
);
}
export default ErrorsPerDomain;

View file

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

View file

@ -0,0 +1,60 @@
import React from 'react';
import { NoContent } from 'UI';
import { Styles, AvgLabel } from '../../common';
import {
AreaChart, Area,
BarChart, Bar, CartesianGrid, Tooltip,
LineChart, Line, Legend, ResponsiveContainer,
XAxis, YAxis
} from 'recharts';
interface Props {
data: any
}
function FPS(props: Props) {
const { data } = props;
const gradientDef = Styles.gradientDef();
const params = { density: 70 }
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
>
<>
<div className="flex items-center justify-end mb-3">
<AvgLabel text="Avg" className="ml-3" count={data.avgFps} />
</div>
<ResponsiveContainer height={ 207 } width="100%">
<AreaChart
data={ data.chart }
margin={ Styles.chartMargins }
>
{gradientDef}
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
<YAxis
{...Styles.yaxis}
allowDecimals={false}
tickFormatter={val => Styles.tickFormatter(val)}
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
/>
<Tooltip {...Styles.tooltip} />
<Area
name="Avg"
type="monotone"
dataKey="avgFps"
stroke={Styles.colors[0]}
fillOpacity={ 1 }
strokeWidth={ 2 }
strokeOpacity={ 0.8 }
fill={'url(#colorCount)'}
/>
</AreaChart>
</ResponsiveContainer>
</>
</NoContent>
);
}
export default FPS;

View file

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

View file

@ -0,0 +1,61 @@
import React from 'react';
import { NoContent } from 'UI';
import { Styles, AvgLabel } from '../../common';
import {
AreaChart, Area,
BarChart, Bar, CartesianGrid, Tooltip,
LineChart, Line, Legend, ResponsiveContainer,
XAxis, YAxis
} from 'recharts';
interface Props {
data: any
}
function MemoryConsumption(props: Props) {
const { data } = props;
const gradientDef = Styles.gradientDef();
const params = { density: 70 }
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
>
<>
<div className="flex items-center justify-end mb-3">
<AvgLabel text="Avg" unit="mb" className="ml-3" count={data.avgUsedJsHeapSize} />
</div>
<ResponsiveContainer height={ 207 } width="100%">
<AreaChart
data={ data.chart }
margin={ Styles.chartMargins }
>
{gradientDef}
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
<YAxis
{...Styles.yaxis}
allowDecimals={false}
tickFormatter={val => Styles.tickFormatter(val)}
label={{ ...Styles.axisLabelLeft, value: "JS Heap Size (mb)" }}
/>
<Tooltip {...Styles.tooltip} />
<Area
name="Avg"
unit=" mb"
type="monotone"
dataKey="avgFps"
stroke={Styles.colors[0]}
fillOpacity={ 1 }
strokeWidth={ 2 }
strokeOpacity={ 0.8 }
fill={'url(#colorCount)'}
/>
</AreaChart>
</ResponsiveContainer>
</>
</NoContent>
);
}
export default MemoryConsumption;

View file

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

View file

@ -0,0 +1,16 @@
import { AreaChart, Area } from 'recharts';
import { Styles } from '../../common';
const Chart = ({ data, compare }) => {
const colors = compare ? Styles.compareColors : Styles.colors;
return (
<AreaChart width={ 90 } height={ 30 } data={ data.chart } >
<Area type="monotone" dataKey="count" stroke={colors[0]} fill={colors[3]} fillOpacity={ 0.5 } />
</AreaChart>
);
}
Chart.displayName = 'Chart';
export default Chart;

View file

@ -0,0 +1,23 @@
import React from 'react'
import copy from 'copy-to-clipboard'
import { useState } from 'react'
const CopyPath = ({ data }) => {
const [copied, setCopied] = useState(false)
const copyHandler = () => {
copy(data.url);
setCopied(true);
setTimeout(function() {
setCopied(false)
}, 500);
}
return (
<div className="cursor-pointer color-teal" onClick={copyHandler}>
{ copied ? 'Copied' : 'Copy Path'}
</div>
)
}
export default CopyPath

View file

@ -0,0 +1,62 @@
import React from 'react';
import { NoContent } from 'UI';
import { Styles, Table } from '../../common';
import { List } from 'immutable';
import Chart from './Chart';
import ResourceInfo from './ResourceInfo';
import CopyPath from './CopyPath';
const cols = [
{
key: 'resource',
title: 'Resource',
Component: ResourceInfo,
width: '40%',
},
{
key: 'sessions',
title: 'Sessions',
toText: count => `${ count > 1000 ? Math.trunc(count / 1000) : count }${ count > 1000 ? 'k' : '' }`,
width: '20%',
},
{
key: 'trend',
title: 'Trend',
Component: Chart,
width: '20%',
},
{
key: 'copy-path',
title: '',
Component: CopyPath,
cellClass: 'invisible group-hover:visible text-right',
width: '20%',
}
];
interface Props {
data: any
}
function MissingResources(props: Props) {
const { data } = props;
return (
<NoContent
title="No resources missing."
size="small"
show={ data.chart.length === 0 }
>
<div style={{ height: '240px'}}>
<Table
small
cols={ cols }
rows={ List(data.chart) }
rowClass="group"
/>
</div>
</NoContent>
);
}
export default MissingResources;

View file

@ -0,0 +1,18 @@
import { diffFromNowString } from 'App/date';
import { TextEllipsis } from 'UI';
import styles from './resourceInfo.css';
export default class ResourceInfo extends React.PureComponent {
render() {
const { data } = this.props;
return (
<div className="flex flex-col" >
<TextEllipsis className={ styles.name } text={ data.name } hintText={ data.url } />
<div className={ styles.timings }>
{ data.endedAt && data.startedAt && `${ diffFromNowString(data.endedAt) } ago - ${ diffFromNowString(data.startedAt) } old` }
</div>
</div>
);
}
}

View file

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

View file

@ -0,0 +1,10 @@
.name {
letter-spacing: -.04em;
font-size: .9rem;
cursor: pointer;
}
.timings {
color: $gray-medium;
font-size: 12px;
}

View file

@ -0,0 +1,70 @@
import React from 'react';
import { NoContent } from 'UI';
import { Styles } from '../../common';
import {
ComposedChart, Bar, CartesianGrid, Line, Legend, ResponsiveContainer,
XAxis, YAxis, Tooltip
} from 'recharts';
interface Props {
data: any
}
function ResourceLoadedVsResponseEnd(props: Props) {
const { data } = props;
const params = { density: 70 }
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">
<ComposedChart
data={data.chart}
margin={ Styles.chartMargins}
>
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis
{...Styles.xaxis}
dataKey="time"
// interval={3}
interval={(params.density / 7)}
/>
<YAxis
{...Styles.yaxis}
label={{ ...Styles.axisLabelLeft, value: "Number of Resources" }}
yAxisId="left"
tickFormatter={val => Styles.tickFormatter(val, 'ms')}
/>
<YAxis
{...Styles.yaxis}
label={{
...Styles.axisLabelLeft,
value: "Response End (ms)",
position: "insideRight",
offset: 0
}}
yAxisId="right"
orientation="right"
tickFormatter={val => Styles.tickFormatter(val, 'ms')}
/>
<Tooltip {...Styles.tooltip} />
<Legend />
<Bar minPointSize={1} yAxisId="left" name="XHR" dataKey="xhr" stackId="a" fill={Styles.colors[0]} />
<Bar yAxisId="left" name="Other" dataKey="total" stackId="a" fill={Styles.colors[2]} />
<Line
yAxisId="right"
strokeWidth={2}
name="Response End"
type="monotone"
dataKey="avgResponseEnd"
stroke={Styles.lineColor}
dot={false}
/>
</ComposedChart>
</ResponsiveContainer>
</NoContent>
);
}
export default ResourceLoadedVsResponseEnd;

View file

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

View file

@ -0,0 +1,73 @@
import React from 'react';
import { NoContent } from 'UI';
import { Styles } from '../../common';
import {
ComposedChart, Bar, CartesianGrid, Line, Legend, ResponsiveContainer,
XAxis, YAxis, Tooltip
} from 'recharts';
interface Props {
data: any
}
function ResourceLoadedVsVisuallyComplete(props: Props) {
const { data } = props;
const gradientDef = Styles.gradientDef();
const params = { density: 70 }
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">
<ComposedChart
data={data.chart}
margin={ Styles.chartMargins}
>
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis
{...Styles.xaxis}
dataKey="time"
// interval={3}
interval={(params.density / 7)}
/>
<YAxis
{...Styles.yaxis}
label={{ ...Styles.axisLabelLeft, value: "Visually Complete (ms)" }}
yAxisId="left"
tickFormatter={val => Styles.tickFormatter(val, 'ms')}
/>
<YAxis
{...Styles.yaxis}
label={{
...Styles.axisLabelLeft,
value: "Number of Resources",
position: "insideRight",
offset: 0
}}
yAxisId="right"
orientation="right"
tickFormatter={val => Styles.tickFormatter(val)}
/>
<Tooltip {...Styles.tooltip} />
<Legend />
<Bar minPointSize={1} yAxisId="right" name="Images" type="monotone" dataKey="types.img" stackId="a" fill={Styles.colors[0]} />
<Bar yAxisId="right" name="Scripts" type="monotone" dataKey="types.script" stackId="a" fill={Styles.colors[2]} />
<Bar yAxisId="right" name="CSS" type="monotone" dataKey="types.stylesheet" stackId="a" fill={Styles.colors[4]} />
<Line
yAxisId="left"
name="Visually Complete"
type="monotone"
dataKey="avgTimeToRender"
stroke={Styles.lineColor }
dot={false}
unit=" ms"
strokeWidth={2}
/>
</ComposedChart>
</ResponsiveContainer>
</NoContent>
);
}
export default ResourceLoadedVsVisuallyComplete;

View file

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

View file

@ -0,0 +1,122 @@
import React from 'react';
import { NoContent, DropdownPlain } from 'UI';
import { Styles, AvgLabel } from '../../common';
import { withRequest } from 'HOCs'
import {
AreaChart, Area,
BarChart, Bar, CartesianGrid, Tooltip,
LineChart, Line, Legend, ResponsiveContainer,
XAxis, YAxis
} from 'recharts';
import WidgetAutoComplete from 'Shared/WidgetAutoComplete';
import { toUnderscore } from 'App/utils';
const WIDGET_KEY = 'resourcesLoadingTime';
export const RESOURCE_OPTIONS = [
{ text: 'All', value: 'all', },
{ text: 'JS', value: "SCRIPT", },
{ text: 'CSS', value: "STYLESHEET", },
{ text: 'Fetch', value: "REQUEST", },
{ text: 'Image', value: "IMG", },
{ text: 'Media', value: "MEDIA", },
{ text: 'Other', value: "OTHER", },
];
interface Props {
data: any
optionsLoading: any
fetchOptions: any
options: any
}
function ResourceLoadingTime(props: Props) {
const { data, optionsLoading } = props;
const gradientDef = Styles.gradientDef();
const params = { density: 70 }
const [autoCompleteSelected, setSutoCompleteSelected] = React.useState('');
const [type, setType] = React.useState('');
const onSelect = (params) => {
const _params = { density: 70 }
setSutoCompleteSelected(params.value);
console.log('params', params) // TODO reload the data with new params;
// this.props.fetchWidget(WIDGET_KEY, dashbaordStore.period, props.platform, { ..._params, url: params.value })
}
const writeOption = (e, { name, value }) => {
// this.setState({ [name]: value })
setType(value);
const _params = { density: 70 } // TODO reload the data with new params;
// this.props.fetchWidget(WIDGET_KEY, this.props.period, this.props.platform, { ..._params, [ name ]: value === 'all' ? null : value })
}
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
>
<>
<div className="flex items-center mb-3">
<WidgetAutoComplete
loading={optionsLoading}
fetchOptions={props.fetchOptions}
options={props.options}
onSelect={onSelect}
placeholder="Search for Page"
/>
<DropdownPlain
disabled={!!autoCompleteSelected}
name="type"
label="Resource"
options={ RESOURCE_OPTIONS }
onChange={ writeOption }
defaultValue={'all'}
wrapperStyle={{
position: 'absolute',
top: '12px',
left: '170px',
}}
/>
<AvgLabel className="ml-auto" text="Avg" count={Math.round(data.avg)} unit="ms" />
</div>
<ResponsiveContainer height={ 200 } width="100%">
<AreaChart
data={ data.chart }
margin={ Styles.chartMargins }
>
{gradientDef}
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
<YAxis
{...Styles.yaxis}
allowDecimals={false}
tickFormatter={val => Styles.tickFormatter(val)}
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
/>
<Tooltip {...Styles.tooltip} />
<Area
name="Avg"
unit=" ms"
type="monotone"
dataKey="avg"
stroke={Styles.colors[0]}
fillOpacity={ 1 }
strokeWidth={ 2 }
strokeOpacity={ 0.8 }
fill={'url(#colorCount)'}
/>
</AreaChart>
</ResponsiveContainer>
</>
</NoContent>
);
}
export default withRequest({
dataName: "options",
initialData: [],
dataWrapper: data => data,
loadingName: 'optionsLoading',
requestName: "fetchOptions",
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
method: 'GET'
})(ResourceLoadingTime)

View file

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

View file

@ -0,0 +1,91 @@
import React from 'react';
import { NoContent } from 'UI';
import { Styles, AvgLabel } from '../../common';
import { withRequest } from 'HOCs'
import {
AreaChart, Area,
BarChart, Bar, CartesianGrid, Tooltip,
LineChart, Line, Legend, ResponsiveContainer,
XAxis, YAxis
} from 'recharts';
import WidgetAutoComplete from 'Shared/WidgetAutoComplete';
import { toUnderscore } from 'App/utils';
const WIDGET_KEY = 'pagesResponseTime';
interface Props {
data: any
optionsLoading: any
fetchOptions: any
options: any
}
function ResponseTime(props: Props) {
const { data, optionsLoading } = props;
const gradientDef = Styles.gradientDef();
const params = { density: 70 }
const onSelect = (params) => {
const _params = { density: 70 }
console.log('params', params) // TODO reload the data with new params;
// this.props.fetchWidget(WIDGET_KEY, dashbaordStore.period, props.platform, { ..._params, url: params.value })
}
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
>
<>
<div className="flex items-center mb-3">
<WidgetAutoComplete
loading={optionsLoading}
fetchOptions={props.fetchOptions}
options={props.options}
onSelect={onSelect}
placeholder="Search for Page"
/>
<AvgLabel className="ml-auto" text="Avg" count={Math.round(data.avg)} unit="ms" />
</div>
<ResponsiveContainer height={ 207 } width="100%">
<AreaChart
data={ data.chart }
margin={ Styles.chartMargins }
>
{gradientDef}
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
<YAxis
{...Styles.yaxis}
allowDecimals={false}
tickFormatter={val => Styles.tickFormatter(val)}
label={{ ...Styles.axisLabelLeft, value: "Page Response Time (ms)" }}
/>
<Tooltip {...Styles.tooltip} />
<Area
name="Avg"
type="monotone"
unit=" ms"
dataKey="avgCpu"
stroke={Styles.colors[0]}
fillOpacity={ 1 }
strokeWidth={ 2 }
strokeOpacity={ 0.8 }
fill={'url(#colorCount)'}
/>
</AreaChart>
</ResponsiveContainer>
</>
</NoContent>
);
}
export default withRequest({
dataName: "options",
initialData: [],
dataWrapper: data => data,
loadingName: 'optionsLoading',
requestName: "fetchOptions",
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
method: 'GET'
})(ResponseTime)

View file

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

View file

@ -0,0 +1,47 @@
import React from 'react';
import { NoContent } from 'UI';
import { Styles } from '../../common';
import {
BarChart, Bar, CartesianGrid, Tooltip,
LineChart, Line, Legend, ResponsiveContainer,
XAxis, YAxis
} from 'recharts';
interface Props {
data: any
}
function SessionsAffectedByJSErrors(props: Props) {
const { data } = props;
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">
<BarChart
data={data.chart}
margin={Styles.chartMargins}
syncId="errorsPerType"
// syncId={ showSync ? "errorsPerType" : undefined }
>
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis
{...Styles.xaxis}
dataKey="time"
// interval={params.density/7}
/>
<YAxis
{...Styles.yaxis}
label={{ ...Styles.axisLabelLeft, value: "Number of Errors" }}
allowDecimals={false}
/>
<Legend />
<Tooltip {...Styles.tooltip} />
<Bar minPointSize={1} name="Sessions" dataKey="sessionsCount" stackId="a" fill={Styles.colors[0]} />
</BarChart>
</ResponsiveContainer>
</NoContent>
);
}
export default SessionsAffectedByJSErrors;

View file

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

View file

@ -0,0 +1,55 @@
import React from 'react';
import { NoContent } from 'UI';
import { Styles } from '../../common';
import {
AreaChart, Area,
BarChart, Bar, CartesianGrid, Tooltip,
LineChart, Line, Legend, ResponsiveContainer,
XAxis, YAxis
} from 'recharts';
interface Props {
data: any
}
function SessionsImpactedBySlowRequests(props: Props) {
const { data } = props;
const gradientDef = Styles.gradientDef();
const params = { density: 70 }
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">
<AreaChart
data={ data.chart }
margin={ Styles.chartMargins }
>
{gradientDef}
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
<YAxis
{...Styles.yaxis}
allowDecimals={false}
tickFormatter={val => Styles.tickFormatter(val)}
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
/>
<Tooltip {...Styles.tooltip} />
<Area
name="Sessions"
type="monotone"
dataKey="count"
stroke={Styles.colors[0]}
fillOpacity={ 1 }
strokeWidth={ 2 }
strokeOpacity={ 0.8 }
fill={'url(#colorCount)'}
/>
</AreaChart>
</ResponsiveContainer>
</NoContent>
);
}
export default SessionsImpactedBySlowRequests;

View file

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

View file

@ -0,0 +1,34 @@
import React from 'react';
import { NoContent } from 'UI';
import { Styles } from '../../common';
import { numberWithCommas } from 'App/utils';
import Bar from 'App/components/Dashboard/Widgets/SlowestDomains/Bar';
interface Props {
data: any
}
function SlowestDomains(props: Props) {
const { data } = props;
const firstAvg = data.chart[0] && data.chart[0].errorsCount;
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
>
<div className="w-full" style={{ height: '240px' }}>
{data.chart.map((item, i) =>
<Bar
key={i}
className="mb-2"
avg={numberWithCommas(Math.round(item.errorsCount))}
width={Math.round((item.errorsCount * 100) / firstAvg) - 10}
domain={item.domain}
color={Styles.colors[i]}
/>
)}
</div>
</NoContent>
);
}
export default SlowestDomains;

View file

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

View file

@ -0,0 +1,91 @@
import React from 'react';
import { NoContent } from 'UI';
import { Styles, AvgLabel } from '../../common';
import { withRequest } from 'HOCs'
import {
AreaChart, Area,
BarChart, Bar, CartesianGrid, Tooltip,
LineChart, Line, Legend, ResponsiveContainer,
XAxis, YAxis
} from 'recharts';
import WidgetAutoComplete from 'Shared/WidgetAutoComplete';
import { toUnderscore } from 'App/utils';
const WIDGET_KEY = 'timeToRender';
interface Props {
data: any
optionsLoading: any
fetchOptions: any
options: any
}
function TimeToRender(props: Props) {
const { data, optionsLoading } = props;
const gradientDef = Styles.gradientDef();
const params = { density: 70 }
const onSelect = (params) => {
const _params = { density: 70 }
console.log('params', params) // TODO reload the data with new params;
// this.props.fetchWidget(WIDGET_KEY, dashbaordStore.period, props.platform, { ..._params, url: params.value })
}
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
>
<>
<div className="flex items-center mb-3">
<WidgetAutoComplete
loading={optionsLoading}
fetchOptions={props.fetchOptions}
options={props.options}
onSelect={onSelect}
placeholder="Search for Page"
/>
<AvgLabel className="ml-auto" text="Avg" count={Math.round(data.avg)} unit="ms" />
</div>
<ResponsiveContainer height={ 200 } width="100%">
<AreaChart
data={ data.chart }
margin={ Styles.chartMargins }
>
{gradientDef}
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
<YAxis
{...Styles.yaxis}
// allowDecimals={false}
tickFormatter={val => Styles.tickFormatter(val)}
label={{ ...Styles.axisLabelLeft, value: "Time to Render (ms)" }}
/>
<Tooltip {...Styles.tooltip} />
<Area
name="Avg"
type="monotone"
unit=" ms"
dataKey="avgCpu"
stroke={Styles.colors[0]}
fillOpacity={ 1 }
strokeWidth={ 2 }
strokeOpacity={ 0.8 }
fill={'url(#colorCount)'}
/>
</AreaChart>
</ResponsiveContainer>
</>
</NoContent>
);
}
export default withRequest({
dataName: "options",
initialData: [],
dataWrapper: data => data,
loadingName: 'optionsLoading',
requestName: "fetchOptions",
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
method: 'GET'
})(TimeToRender)

View file

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

View file

@ -0,0 +1,50 @@
import React from 'react';
import { useStore } from 'App/mstore';
import { SideMenuitem, SideMenuHeader, Icon, Button } from 'UI';
import { withSiteId, dashboardSelected, metrics } from 'App/routes';
import { withRouter } from 'react-router-dom';
import { useModal } from 'App/components/Modal';
interface Props {
siteId: string
history: any
}
function DashbaordListModal(props: Props) {
const { dashboardStore } = useStore();
const { hideModal } = useModal();
const dashboards = dashboardStore.dashboards;
const activeDashboardId = dashboardStore.selectedDashboard?.dashboardId;
const onItemClick = (dashboard) => {
dashboardStore.selectDashboardById(dashboard.dashboardId);
const path = withSiteId(dashboardSelected(dashboard.dashboardId), parseInt(props.siteId));
props.history.push(path);
hideModal();
};
return (
<div className="bg-white h-screen" style={{ width: '300px'}}>
<div className="color-gray-medium uppercase p-4 text-lg">Dashboards</div>
<div>
{dashboards.map((item: any) => (
<div key={ item.dashboardId } className="px-4">
<SideMenuitem
key={ item.dashboardId }
active={item.dashboardId === activeDashboardId}
title={ item.name }
iconName={ item.icon }
onClick={() => onItemClick(item)} // TODO add click handler
leading = {(
<div className="ml-2 flex items-center">
{item.isPublic && <div className="p-1"><Icon name="user-friends" color="gray-light" size="16" /></div>}
{item.isPinned && <div className="p-1"><Icon name="pin-fill" size="16" /></div>}
</div>
)}
/>
</div>
))}
</div>
</div>
);
}
export default withRouter(DashbaordListModal);

View file

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

View file

@ -0,0 +1,86 @@
import { useObserver } from 'mobx-react-lite';
import React from 'react';
import { Button, Modal, Form, Icon, Checkbox } from 'UI';
import { useStore } from 'App/mstore'
interface Props {
show: boolean;
// dashboard: any;
closeHandler?: () => void;
}
function DashboardEditModal(props: Props) {
const { show, closeHandler } = props;
const { dashboardStore } = useStore();
const dashboard = useObserver(() => dashboardStore.dashboardInstance);
const onSave = () => {
dashboardStore.save(dashboard).then(closeHandler);
}
const write = ({ target: { value, name } }) => dashboard.update({ [ name ]: value })
const writeOption = (e, { checked, name }) => {
dashboard.update({ [name]: checked });
}
return useObserver(() => (
<Modal size="tiny" open={ show }>
<Modal.Header className="flex items-center justify-between">
<div>{ 'Edit Dashboard' }</div>
<Icon
role="button"
tabIndex="-1"
color="gray-dark"
size="14"
name="close"
onClick={ closeHandler }
/>
</Modal.Header>
<Modal.Content>
<Form onSubmit={onSave}>
<Form.Field>
<label>{'Title:'}</label>
<input
autoFocus={ true }
className=""
name="name"
value={ dashboard.name }
onChange={write}
placeholder="Title"
/>
</Form.Field>
<Form.Field>
<div className="flex items-center">
<Checkbox
name="isPublic"
className="font-medium mr-3"
type="checkbox"
checked={ dashboard.isPublic }
onClick={ writeOption }
/>
<div className="flex items-center cursor-pointer" onClick={ () => dashboard.update({ 'isPublic': !dashboard.isPublic }) }>
<Icon name="user-friends" size="16" />
<span className="ml-2"> Team can see and edit the dashboard.</span>
</div>
</div>
</Form.Field>
</Form>
</Modal.Content>
<Modal.Actions>
<div className="-mx-2 px-2">
<Button
primary
onClick={ onSave }
// loading={ loading }
>
Save
</Button>
<Button className="" marginRight onClick={ closeHandler }>{ 'Cancel' }</Button>
</div>
</Modal.Actions>
</Modal>
));
}
export default DashboardEditModal;

View file

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

View file

@ -0,0 +1,60 @@
import { useObserver } from 'mobx-react-lite';
import React from 'react';
import { Input } from 'UI';
import { useDashboardStore } from '../../store/store';
import cn from 'classnames';
import { useStore } from 'App/mstore';
interface Props {
}
function DashboardForm(props: Props) {
const { dashboardStore } = useStore();
const dashboard = dashboardStore.dashboardInstance;
const write = ({ target: { value, name } }) => dashboard.update({ [ name ]: value })
const writeRadio = ({ target: { value, name } }) => {
dashboard.update({ [name]: value === 'team' });
}
return useObserver(() => (
<div className="mb-8 grid grid-cols-2 gap-8">
<div className="form-field flex flex-col">
<label htmlFor="name" className="font-medium mb-2">Title</label>
<Input type="text" name="name" onChange={write} value={dashboard.name} />
</div>
<div className="form-field flex flex-col">
<label htmlFor="name" className="font-medium mb-2">Visibility and Editing</label>
<div className="flex items-center py-2">
<label className="inline-flex items-center mr-6">
<input
type="radio"
className="form-radio h-5 w-5"
name="isPublic"
value="team"
checked={dashboard.isPublic}
onChange={writeRadio}
/>
<span className={cn("ml-2", { 'color-teal' : dashboard.isPublic})}>Team</span>
</label>
<label className="inline-flex items-center">
<input
type="radio"
className="form-radio h-5 w-5"
name="isPublic"
value="personal"
checked={!dashboard.isPublic}
onChange={writeRadio}
/>
<span className={cn("ml-2", { 'color-teal' : !dashboard.isPublic})}>Personal</span>
</label>
</div>
</div>
</div>
));
}
export default DashboardForm;

View file

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

View file

@ -0,0 +1,111 @@
import React, { useEffect } from 'react';
import WidgetWrapper from '../WidgetWrapper';
import { useObserver } from 'mobx-react-lite';
import cn from 'classnames';
import { useStore } from 'App/mstore';
function WidgetCategoryItem({ category, isSelected, onClick, selectedWidgetIds, unSelectCategory }) {
const selectedCategoryWidgetsCount = useObserver(() => {
return category.widgets.filter(widget => selectedWidgetIds.includes(widget.metricId)).length;
});
return (
<div
className={cn("rounded p-4 shadow border cursor-pointer", { 'bg-active-blue border-color-teal':isSelected, 'bg-white': !isSelected })}
onClick={() => onClick(category)}
>
<div className="font-medium text-lg mb-2 capitalize">{category.name}</div>
<div className="mb-2">{category.description}</div>
{selectedCategoryWidgetsCount > 0 && (
<div className="flex items-center">
<input type="checkbox" checked={true} onChange={() => unSelectCategory(category)} />
<span className="color-gray-medium ml-2">{`Selected ${selectedCategoryWidgetsCount} of ${category.widgets.length}`}</span>
</div>
)}
</div>
);
}
function DashboardMetricSelection(props) {
const { dashboardStore } = useStore();
let widgetCategories: any[] = useObserver(() => dashboardStore.widgetCategories);
const [activeCategory, setActiveCategory] = React.useState<any>();
const selectedWidgetIds = useObserver(() => dashboardStore.selectedWidgets.map((widget: any) => widget.metricId));
useEffect(() => {
dashboardStore?.fetchTemplates().then(templates => {
setActiveCategory(dashboardStore.widgetCategories[0]);
});
}, []);
const handleWidgetCategoryClick = (category: any) => {
setActiveCategory(category);
};
const toggleAllWidgets = ({ target: { checked }}) => {
dashboardStore.toggleAllSelectedWidgets(checked);
}
return useObserver(() => (
<div >
<div className="grid grid-cols-12 gap-4 my-3 items-end">
<div className="col-span-3">
<div className="uppercase color-gray-medium text-lg">Categories</div>
</div>
<div className="col-span-9 flex items-center">
{activeCategory && (
<>
<div className="flex items-baseline">
<h2 className="text-2xl capitalize">{activeCategory.name}</h2>
<span className="text-2xl color-gray-medium ml-2">{activeCategory.widgets.length}</span>
</div>
<div className="ml-auto flex items-center">
<span className="color-gray-medium">Showing past 7 days data for visual clue</span>
<div className="flex items-center ml-3">
<input type="checkbox" onChange={toggleAllWidgets} />
<span className="ml-2">Select All</span>
</div>
</div>
</>
)}
</div>
</div>
<div className="grid grid-cols-12 gap-4">
<div className="col-span-3">
<div className="grid grid-cols-1 gap-4">
{activeCategory && widgetCategories.map((category, index) =>
<WidgetCategoryItem
key={category.name}
onClick={handleWidgetCategoryClick}
category={category}
isSelected={activeCategory.name === category.name}
selectedWidgetIds={selectedWidgetIds}
unSelectCategory={dashboardStore.removeSelectedWidgetByCategory}
/>
)}
</div>
</div>
<div className="col-span-9">
<div
className="grid grid-cols-4 gap-4 -mx-4 px-4 pb-40 items-start"
style={{ height: "calc(100vh - 165px)", overflowY: 'auto' }}
>
{activeCategory && activeCategory.widgets.map((widget: any) => (
<WidgetWrapper
key={widget.metricId}
widget={widget}
active={selectedWidgetIds.includes(widget.metricId)}
isTemplate={true}
isWidget={widget.metricType === 'predefined'}
onClick={() => dashboardStore.toggleWidgetSelection(widget)}
/>
))}
</div>
</div>
</div>
</div>
));
}
export default DashboardMetricSelection;

View file

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

View file

@ -0,0 +1,70 @@
import React from 'react';
import { useObserver } from 'mobx-react-lite';
import DashboardMetricSelection from '../DashboardMetricSelection';
import DashboardForm from '../DashboardForm';
import { Button } from 'UI';
import { withRouter } from 'react-router-dom';
import { useStore } from 'App/mstore';
import { useModal } from 'App/components/Modal';
import { dashboardMetricCreate, withSiteId } from 'App/routes';
interface Props {
history: any
siteId?: string
dashboardId?: string
}
function DashboardModal(props) {
const { history, siteId, dashboardId } = props;
const { dashboardStore } = useStore();
const { hideModal } = useModal();
const dashboard = useObserver(() => dashboardStore.dashboardInstance);
const loading = useObserver(() => dashboardStore.isSaving);
const onSave = () => {
dashboardStore.save(dashboard).then(hideModal)
}
const handleCreateNew = () => {
const path = withSiteId(dashboardMetricCreate(dashboardId), siteId);
history.push(path);
hideModal();
}
return useObserver(() => (
<div
className="fixed border-r shadow p-4 h-screen"
style={{ backgroundColor: '#FAFAFA', zIndex: '9999', width: '85%'}}
>
<div className="mb-6 flex items-end justify-between">
<div>
<h1 className="text-2xl">
{ dashboard.exists() ? "Add metric(s) to dashboard" : "Create Dashboard" }
</h1>
</div>
<div>
{dashboard.exists() && <Button outline size="small" onClick={handleCreateNew}>Create New</Button>}
</div>
</div>
{ !dashboard.exists() && (
<>
<DashboardForm />
<p>Create new dashboard by choosing from the range of predefined metrics that you care about. You can always add your custom metrics later.</p>
</>
)}
<DashboardMetricSelection />
<div className="flex absolute bottom-0 left-0 right-0 bg-white border-t p-3">
<Button
primary
className=""
disabled={!dashboard.isValid || loading}
onClick={onSave}
>
{ dashboard.exists() ? "Add Selected to Dashboard" : "Create and Add to Dashboard" }
</Button>
</div>
</div>
));
}
export default withRouter(DashboardModal);

View file

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

View file

@ -0,0 +1,55 @@
import React from 'react';
import { Switch, Route } from 'react-router';
import { withRouter } from 'react-router-dom';
import {
metrics,
metricDetails,
dashboardSelected,
dashboardMetricCreate,
dashboardMetricDetails,
withSiteId,
dashboard,
} from 'App/routes';
import DashboardView from '../DashboardView';
import MetricsView from '../MetricsView';
import WidgetView from '../WidgetView';
interface Props {
history: any
match: any
}
function DashboardRouter(props: Props) {
const { match: { params: { siteId, dashboardId, metricId } } } = props;
return (
<div>
<Switch>
<Route exact strict path={withSiteId(metrics(), siteId)}>
<MetricsView siteId={siteId} />
</Route>
<Route exact strict path={withSiteId(metricDetails(), siteId)}>
<WidgetView siteId={siteId} {...props} />
</Route>
<Route exact strict path={withSiteId(dashboardMetricDetails(dashboardId), siteId)}>
<WidgetView siteId={siteId} {...props} />
</Route>
<Route exact strict path={withSiteId(dashboardMetricCreate(dashboardId), siteId)}>
<WidgetView siteId={siteId} {...props} />
</Route>
<Route exact strict path={withSiteId(dashboard(''), siteId)}>
<>Nothing...</>
</Route>
<Route exact strict path={withSiteId(dashboardSelected(dashboardId), siteId)}>
<DashboardView siteId={siteId} dashboardId={dashboardId} />
</Route>
</Switch>
</div>
);
}
export default withRouter(DashboardRouter);

View file

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

View file

@ -0,0 +1,73 @@
import { useObserver } from 'mobx-react-lite';
import React from 'react';
import { Button, Modal, Form, Icon } from 'UI';
import { useStore } from 'App/mstore'
import DropdownPlain from 'Shared/DropdownPlain';
interface Props {
metricId: string,
show: boolean;
closeHandler?: () => void;
}
function DashboardSelectionModal(props: Props) {
const { show, metricId, closeHandler } = props;
const { dashboardStore } = useStore();
const dashboardOptions = dashboardStore.dashboards.map((i: any) => ({
key: i.id,
text: i.name,
value: i.dashboardId,
}));
const [selectedId, setSelectedId] = React.useState(dashboardOptions[0].value);
const onSave = () => {
const dashboard = dashboardStore.getDashboard(selectedId)
if (dashboard) {
dashboardStore.addWidgetToDashboard(dashboard, [metricId]).then(closeHandler)
}
}
return useObserver(() => (
<Modal size="tiny" open={ show }>
<Modal.Header className="flex items-center justify-between">
<div>{ 'Add to selected Dashboard' }</div>
<Icon
role="button"
tabIndex="-1"
color="gray-dark"
size="14"
name="close"
onClick={ closeHandler }
/>
</Modal.Header>
<Modal.Content>
<div className="py-4">
<Form onSubmit={onSave}>
<Form.Field>
<label>{'Dashbaord:'}</label>
<DropdownPlain
options={dashboardOptions}
value={selectedId}
onChange={(e, { value }) => setSelectedId(value)}
/>
</Form.Field>
</Form>
</div>
</Modal.Content>
<Modal.Actions>
<div className="-mx-2 px-2">
<Button
primary
onClick={ onSave }
// loading={ loading }
>
Add
</Button>
<Button className="" marginRight onClick={ closeHandler }>{ 'Cancel' }</Button>
</div>
</Modal.Actions>
</Modal>
));
}
export default DashboardSelectionModal;

View file

@ -0,0 +1,104 @@
import { useObserver } from 'mobx-react-lite';
import React from 'react';
import { SideMenuitem, SideMenuHeader, Icon, Button } from 'UI';
import { useStore } from 'App/mstore';
import { withRouter } from 'react-router-dom';
import { withSiteId, dashboardSelected, metrics } from 'App/routes';
import { useModal } from 'App/components/Modal';
import DashbaordListModal from '../DashbaordListModal';
import DashboardModal from '../DashboardModal';
import cn from 'classnames';
const SHOW_COUNT = 5;
interface Props {
siteId: string
history: any
}
function DashboardSideMenu(props: Props) {
const { history, siteId } = props;
const { hideModal, showModal } = useModal();
const { dashboardStore } = useStore();
const dashboardId = dashboardStore.selectedDashboard?.dashboardId;
const dashboardsPicked = useObserver(() => dashboardStore.dashboards.slice(0, SHOW_COUNT));
const remainingDashboardsCount = dashboardStore.dashboards.length - SHOW_COUNT;
const redirect = (path) => {
history.push(path);
}
const onItemClick = (dashboard) => {
dashboardStore.selectDashboardById(dashboard.dashboardId);
const path = withSiteId(dashboardSelected(dashboard.dashboardId), parseInt(siteId));
history.push(path);
};
const onAddDashboardClick = (e) => {
dashboardStore.initDashboard();
showModal(<DashboardModal />, {})
}
const togglePinned = (dashboard) => {
dashboardStore.updatePinned(dashboard.dashboardId);
}
return useObserver(() => (
<div>
<SideMenuHeader className="mb-4" text="Dashboards" />
{dashboardsPicked.sort((a: any, b: any) => a.isPinned === b.isPinned ? 0 : a.isPinned ? -1 : 1 ).map((item: any) => (
<SideMenuitem
key={ item.dashboardId }
active={item.dashboardId === dashboardId}
title={ item.name }
iconName={ item.icon }
onClick={() => onItemClick(item)}
className="group"
leading = {(
<div className="ml-2 flex items-center">
{item.isPublic && <div className="p-1"><Icon name="user-friends" color="gray-light" size="16" /></div>}
{<div className={cn("p-1 group-hover:visible", { 'invisible' : !item.isPinned })} onClick={() => togglePinned(item)}><Icon name="pin-fill" size="16" /></div>}
</div>
)}
/>
))}
<div>
{remainingDashboardsCount > 0 && (
<div
className="my-2 py-2 color-teal cursor-pointer"
onClick={() => showModal(<DashbaordListModal siteId={siteId} />, {})}
>
{remainingDashboardsCount} More
</div>
)}
</div>
<div className="border-t w-full my-2" />
<div className="w-full">
<SideMenuitem
id="menu-manage-alerts"
title="Create Dashboard"
iconName="plus"
onClick={onAddDashboardClick}
/>
</div>
<div className="border-t w-full my-2" />
<div className="w-full">
<SideMenuitem
id="menu-manage-alerts"
title="Metrics"
iconName="bar-chart-line"
onClick={() => redirect(withSiteId(metrics(), siteId))}
/>
</div>
<div className="border-t w-full my-2" />
<div className="my-3 w-full">
<SideMenuitem
id="menu-manage-alerts"
title="Alerts"
iconName="bell-plus"
// onClick={() => setShowAlerts(true)}
/>
</div>
</div>
));
}
export default withRouter(DashboardSideMenu);

View file

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

View file

@ -0,0 +1,113 @@
import React, { useEffect } from 'react';
import { observer, useObserver } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
import { Button, PageTitle, Link, Loader, NoContent, ItemMenu } from 'UI';
import { withSiteId, dashboardMetricCreate, dashboardSelected, dashboard } from 'App/routes';
import withModal from 'App/components/Modal/withModal';
import DashboardWidgetGrid from '../DashboardWidgetGrid';
import { confirm } from 'UI/Confirmation';
import { withRouter } from 'react-router-dom';
import { useModal } from 'App/components/Modal';
import DashboardModal from '../DashboardModal';
import DashboardEditModal from '../DashboardEditModal';
import DateRange from 'Shared/DateRange';
interface Props {
siteId: number;
history: any
match: any
dashboardId: any
}
function DashboardView(props: Props) {
const { siteId, dashboardId } = props;
const { dashboardStore } = useStore();
const { hideModal, showModal } = useModal();
const loading = useObserver(() => dashboardStore.fetchingDashboard);
const dashboard: any = dashboardStore.selectedDashboard
const period = useObserver(() => dashboardStore.period);
const [showEditModal, setShowEditModal] = React.useState(false);
useEffect(() => {
dashboardStore.fetch(dashboardId)
}, []);
const onAddWidgets = () => {
dashboardStore.initDashboard(dashboard)
showModal(<DashboardModal siteId={siteId} dashboardId={dashboardId} />, {})
}
const onEdit = () => {
dashboardStore.initDashboard(dashboard)
setShowEditModal(true)
}
const onDelete = async () => {
if (await confirm({
header: 'Confirm',
confirmButton: 'Yes, delete',
confirmation: `Are you sure you want to permanently delete this Dashboard?`
})) {
dashboardStore.deleteDashboard(dashboard).then(() => {
dashboardStore.selectDefaultDashboard().then(({ dashboardId }) => {
props.history.push(withSiteId(dashboard(), siteId));
});
});
}
}
return useObserver(() => (
<Loader loading={loading}>
<NoContent
show={!dashboard || !dashboard.dashboardId}
title="No data available."
size="small"
>
<div>
<DashboardEditModal
show={showEditModal}
closeHandler={() => setShowEditModal(false)}
/>
<div className="flex items-center mb-4 justify-between">
<div className="flex items-center">
<PageTitle title={dashboard?.name} className="mr-3" />
<Button primary size="small" onClick={onAddWidgets}>Add Metric</Button>
</div>
<div className="flex items-center">
<div className="flex items-center">
<span className="mr-2 color-gray-medium">Time Range</span>
<DateRange
rangeValue={period.rangeName}
startDate={period.start}
endDate={period.end}
onDateChange={(period) => dashboardStore.setPeriod(period)}
customRangeRight
direction="left"
/>
</div>
<div className="mx-4" />
<ItemMenu
items={[
{
text: 'Edit',
onClick: onEdit
},
{
text: 'Delete Dashboard',
onClick: onDelete
},
]}
/>
</div>
</div>
<DashboardWidgetGrid
siteId={siteId}
dashboardId={dashboardId}
onEditHandler={onAddWidgets}
/>
</div>
</NoContent>
</Loader>
));
}
export default withRouter(withModal(observer(DashboardView)));

View file

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

View file

@ -0,0 +1,50 @@
import React from 'react';
import { useStore } from 'App/mstore';
import WidgetWrapper from '../WidgetWrapper';
import { NoContent, Button, Loader } from 'UI';
import { useObserver } from 'mobx-react-lite';
interface Props {
siteId: string,
dashboardId: string;
onEditHandler: () => void;
}
function DashboardWidgetGrid(props) {
const { dashboardId, siteId } = props;
const { dashboardStore } = useStore();
const loading = useObserver(() => dashboardStore.isLoading);
const dashbaord: any = dashboardStore.selectedDashboard;
const list: any = useObserver(() => dashbaord?.widgets);
return useObserver(() => (
<Loader loading={loading}>
<NoContent
show={list.length === 0}
icon="exclamation-circle"
title="No metrics added to this dashboard"
subtext={
<div>
<p>Metrics helps you visualize trends from sessions captured by OpenReplay</p>
<Button size="small" primary onClick={props.onEditHandler}>Add Metric</Button>
</div>
}
>
<div className="grid gap-4 grid-cols-4 items-start pb-10">
{list && list.map((item, index) => (
<WidgetWrapper
index={index}
widget={item}
key={item.widgetId}
moveListItem={(dragIndex, hoverIndex) => dashbaord.swapWidgetPosition(dragIndex, hoverIndex)}
dashboardId={dashboardId}
siteId={siteId}
isWidget={true}
/>
))}
</div>
</NoContent>
</Loader>
));
}
export default DashboardWidgetGrid;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,47 @@
import React from 'react';
import { Icon, NoContent, Label, Link, Pagination } from 'UI';
import { checkForRecent, formatDateTimeDefault, convertTimestampToUtcTimestamp } from 'App/date';
interface Props {
metric: any;
}
function DashboardLink({ dashboards}) {
return (
dashboards.map(dashboard => (
<Link to={`/dashboard/${dashboard.dashboardId}`} className="">
<div className="flex items-center mb-1">
<div className="mr-2 text-4xl no-underline" style={{ textDecoration: 'none'}}>·</div>
<span className="link leading-4">{dashboard.name}</span>
</div>
</Link>
))
);
}
function MetricListItem(props: Props) {
const { metric } = props;
return (
<div className="grid grid-cols-12 p-3 border-t select-none">
<div className="col-span-3">
<Link to={`/metrics/${metric.metricId}`} className="link">
{metric.name}
</Link>
</div>
<div><Label className="capitalize">{metric.metricType}</Label></div>
<div className="col-span-2">
<DashboardLink dashboards={metric.dashboards} />
</div>
<div className="col-span-3">{metric.owner}</div>
<div>
<div className="flex items-center">
<Icon name={metric.isPublic ? "user-friends" : "person-fill"} className="mr-2" />
<span>{metric.isPublic ? 'Team' : 'Private'}</span>
</div>
</div>
<div className="col-span-2">{metric.lastModified && checkForRecent(metric.lastModified, 'LLL dd, yyyy, hh:mm a')}</div>
</div>
);
}
export default MetricListItem;

View file

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

View file

@ -0,0 +1,55 @@
import { useObserver } from 'mobx-react-lite';
import React from 'react';
import { NoContent, Pagination } from 'UI';
import { useStore } from 'App/mstore';
import { getRE } from 'App/utils';
import MetricListItem from '../MetricListItem';
import { sliceListPerPage } from 'App/utils';
interface Props { }
function MetricsList(props: Props) {
const { metricStore } = useStore();
const metrics = useObserver(() => metricStore.metrics);
const metricsSearch = useObserver(() => metricStore.metricsSearch);
const filterList = (list) => {
const filterRE = getRE(metricsSearch, 'i');
let _list = list.filter(w => {
const dashbaordNames = w.dashboards.map(d => d.name).join(' ');
return filterRE.test(w.name) || filterRE.test(w.metricType) || filterRE.test(w.owner) || filterRE.test(dashbaordNames);
});
return _list
}
const list: any = metricsSearch !== '' ? filterList(metrics) : metrics;
const lenth = list.length;
return useObserver(() => (
<NoContent show={lenth === 0} icon="exclamation-circle">
<div className="mt-3 border rounded bg-white">
<div className="grid grid-cols-12 p-3 font-medium">
<div className="col-span-3">Title</div>
<div>Type</div>
<div className="col-span-2">Dashboards</div>
<div className="col-span-3">Owner</div>
<div>Visibility</div>
<div className="col-span-2">Last Modified</div>
</div>
{sliceListPerPage(list, metricStore.page - 1, metricStore.pageSize).map((metric: any) => (
<MetricListItem metric={metric} />
))}
</div>
<div className="w-full flex items-center justify-center py-6">
<Pagination
page={metricStore.page}
totalPages={Math.ceil(lenth / metricStore.pageSize)}
onPageChange={(page) => metricStore.updateKey('page', page)}
limit={metricStore.pageSize}
debounceRequest={100}
/>
</div>
</NoContent>
));
}
export default MetricsList;

View file

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

View file

@ -0,0 +1,34 @@
import { useObserver } from 'mobx-react-lite';
import React, { useEffect, useState } from 'react';
import { useStore } from 'App/mstore';
import { Icon } from 'UI';
import { debounce } from 'App/utils';
let debounceUpdate: any = () => {}
function MetricsSearch(props) {
const { metricStore } = useStore();
const [query, setQuery] = useState(metricStore.metricsSearch);
useEffect(() => {
debounceUpdate = debounce((key, value) => metricStore.updateKey(key, value), 500);
}, [])
const write = ({ target: { name, value } }) => {
setQuery(value);
debounceUpdate('metricsSearch', value);
}
return useObserver(() => (
<div className="relative">
<Icon name="search" className="absolute top-0 bottom-0 ml-2 m-auto" size="16" />
<input
value={query}
name="metricsSearch"
className="bg-white p-2 border rounded w-full pl-10"
placeholder="Filter by title, type, dashboard and owner"
onChange={write}
/>
</div>
));
}
export default MetricsSearch;

View file

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

View file

@ -0,0 +1,37 @@
import React from 'react';
import { Button, PageTitle, Icon, Link } from 'UI';
import { withSiteId, metricCreate } from 'App/routes';
import MetricsList from '../MetricsList';
import MetricsSearch from '../MetricsSearch';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
interface Props{
siteId: number;
}
function MetricsView(props: Props) {
const { siteId } = props;
const { metricStore } = useStore();
const metricsCount = useObserver(() => metricStore.metrics.length);
React.useEffect(() => {
metricStore.fetchList();
}, []);
return useObserver(() => (
<div>
<div className="flex items-center mb-4 justify-between">
<div className="flex items-baseline mr-3">
<PageTitle title="Metrics" className="" />
<span className="text-2xl color-gray-medium ml-2">{metricsCount}</span>
</div>
<Link to={'/metrics/create'}><Button primary size="small">Add Metric</Button></Link>
<div className="ml-auto w-1/3">
<MetricsSearch />
</div>
</div>
<MetricsList />
</div>
));
}
export default MetricsView;

View file

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

View file

@ -0,0 +1,100 @@
import React, { useState, useRef, useEffect } from 'react';
import CustomMetriLineChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetriLineChart';
import CustomMetricPercentage from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage';
import CustomMetricTable from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTable';
import CustomMetricPieChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart';
import { Styles } from 'App/components/Dashboard/Widgets/common';
import { observer, useObserver, useLocalObservable } from 'mobx-react-lite';
import { Loader } from 'UI';
import { useStore } from 'App/mstore';
import WidgetPredefinedChart from '../WidgetPredefinedChart';
import CustomMetricOverviewChart from '../../Widgets/CustomMetricsWidgets/CustomMetricOverviewChart';
interface Props {
metric: any;
isWidget?: boolean
}
function WidgetChart(props: Props) {
const { isWidget = false, metric } = props;
// const metric = useObserver(() => props.metric);
const { dashboardStore } = useStore();
const period = useObserver(() => dashboardStore.period);
const colors = Styles.customMetricColors;
const [loading, setLoading] = useState(false)
const [seriesMap, setSeriesMap] = useState<any>([]);
const params = { density: 70 }
const metricParams = { ...params, metricId: metric.metricId, viewType: 'lineChart' }
const prevMetricRef = useRef<any>();
const [data, setData] = useState<any>(metric.data);
useEffect(() => {
if (prevMetricRef.current && prevMetricRef.current.name !== metric.name) {
prevMetricRef.current = metric;
return
};
prevMetricRef.current = metric;
setLoading(true);
const data = isWidget ? {} : { ...metricParams, ...metric.toJson() };
dashboardStore.fetchMetricChartData(metric, data, isWidget).then((res: any) => {
setData(res);
}).finally(() => {
setLoading(false);
});
}, [period]);
const renderChart = () => {
const { metricType, viewType, predefinedKey } = metric;
if (metricType === 'predefined') {
if (viewType === 'overview') {
return <CustomMetricOverviewChart data={data} />
}
return <WidgetPredefinedChart data={data} predefinedKey={metric.predefinedKey} />
}
if (metricType === 'timeseries') {
if (viewType === 'lineChart') {
return (
<CustomMetriLineChart
data={metric.data}
seriesMap={seriesMap}
colors={colors}
params={params}
/>
)
} else if (viewType === 'progress') {
return (
<CustomMetricPercentage
data={metric.data[0]}
colors={colors}
params={params}
/>
)
}
}
if (metricType === 'table') {
if (viewType === 'table') {
return <CustomMetricTable metric={metric} data={metric.data[0]} />;
} else if (viewType === 'pieChart') {
return (
<CustomMetricPieChart
metric={metric}
data={metric.data[0]}
colors={colors}
params={params}
/>
)
}
}
return <div>Unknown</div>;
}
return useObserver(() => (
<Loader loading={loading}>
{renderChart()}
</Loader>
));
}
export default WidgetChart;

View file

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

View file

@ -0,0 +1,215 @@
import React, { useState } from 'react';
import DropdownPlain from 'Shared/DropdownPlain';
import { metricTypes, metricOf, issueOptions } from 'App/constants/filterOptions';
import { FilterKey } from 'Types/filter/filterType';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
import { HelpText, Button, Icon } from 'UI'
import FilterSeries from '../FilterSeries';
import { withRouter } from 'react-router-dom';
import { confirm } from 'UI/Confirmation';
import { withSiteId, dashboardMetricDetails, metricDetails } from 'App/routes'
import DashboardSelectionModal from '../DashboardSelectionModal/DashboardSelectionModal';
interface Props {
history: any;
match: any;
onDelete: () => void;
}
function WidgetForm(props: Props) {
const [showDashboardSelectionModal, setShowDashboardSelectionModal] = useState(false);
const { history, match: { params: { siteId, dashboardId, metricId } } } = props;
const { metricStore } = useStore();
const isSaving = useObserver(() => metricStore.isSaving);
const metric: any = useObserver(() => metricStore.instance);
const timeseriesOptions = metricOf.filter(i => i.type === 'timeseries');
const tableOptions = metricOf.filter(i => i.type === 'table');
const isTable = metric.metricType === 'table';
const isTimeSeries = metric.metricType === 'timeseries';
const _issueOptions = [{ text: 'All', value: 'all' }].concat(issueOptions);
const write = ({ target: { value, name } }) => metricStore.merge({ [ name ]: value });
const writeOption = (e, { value, name }) => {
metricStore.merge({ [ name ]: value });
if (name === 'metricValue') {
metricStore.merge({ metricValue: [value] });
}
if (name === 'metricOf') {
if (value === FilterKey.ISSUE) {
metricStore.merge({ metricValue: ['all'] });
}
}
if (name === 'metricType') {
if (value === 'timeseries') {
metricStore.merge({ metricOf: timeseriesOptions[0].value, viewType: 'lineChart' });
} else if (value === 'table') {
metricStore.merge({ metricOf: tableOptions[0].value, viewType: 'table' });
}
}
};
const onSave = () => {
const wasCreating = !metric.exists()
metricStore.save(metric, dashboardId).then((metric) => {
if (wasCreating) {
if (parseInt(dashboardId) > 0) {
history.push(withSiteId(dashboardMetricDetails(parseInt(dashboardId), metric.metricId), siteId));
} else {
history.push(withSiteId(metricDetails(metric.metricId), siteId));
}
}
});
}
const onDelete = async () => {
if (await confirm({
header: 'Confirm',
confirmButton: 'Yes, delete',
confirmation: `Are you sure you want to permanently delete this metric?`
})) {
metricStore.delete(metric).then(props.onDelete);
}
}
const onObserveChanges = () => {
console.log('observe changes');
// metricStore.fetchMetricChartData(metric);
}
return useObserver(() => (
<div className="p-4">
<div className="form-group">
<label className="font-medium">Metric Type</label>
<div className="flex items-center">
<DropdownPlain
name="metricType"
options={metricTypes}
value={ metric.metricType }
onChange={ writeOption }
/>
{metric.metricType === 'timeseries' && (
<>
<span className="mx-3">of</span>
<DropdownPlain
name="metricOf"
options={timeseriesOptions}
value={ metric.metricOf }
onChange={ writeOption }
/>
</>
)}
{metric.metricType === 'table' && (
<>
<span className="mx-3">of</span>
<DropdownPlain
name="metricOf"
options={tableOptions}
value={ metric.metricOf }
onChange={ writeOption }
/>
</>
)}
{metric.metricOf === FilterKey.ISSUE && (
<>
<span className="mx-3">issue type</span>
<DropdownPlain
name="metricValue"
options={_issueOptions}
value={ metric.metricValue[0] }
onChange={ writeOption }
/>
</>
)}
{metric.metricType === 'table' && (
<>
<span className="mx-3">showing</span>
<DropdownPlain
name="metricFormat"
options={[
{ value: 'sessionCount', text: 'Session Count' },
]}
value={ metric.metricFormat }
onChange={ writeOption }
/>
</>
)}
</div>
</div>
<div className="form-group">
<label className="font-medium items-center">
{`${isTable ? 'Filter by' : 'Chart Series'}`}
{!isTable && (
<Button
className="ml-2"
primary plain size="small"
onClick={() => metric.addSeries()}
>Add Series</Button>
)}
</label>
{metric.series.length > 0 && metric.series.slice(0, isTable ? 1 : metric.series.length).map((series: any, index: number) => (
<div className="mb-2">
<FilterSeries
hideHeader={ isTable }
seriesIndex={index}
series={series}
// onRemoveSeries={() => removeSeries(index)}
onRemoveSeries={() => metric.removeSeries(index)}
canDelete={metric.series.length > 1}
emptyMessage={isTable ?
'Filter data using any event or attribute. Use Add Step button below to do so.' :
'Add user event or filter to define the series by clicking Add Step.'
}
observeChanges={onObserveChanges}
/>
</div>
))}
</div>
<div className="form-groups flex items-center justify-between">
<Button
primary
size="small"
onClick={onSave}
disabled={isSaving}
>
{metric.exists() ? 'Update' : 'Create'}
</Button>
<div className="flex items-center">
{metric.exists() && (
<>
<Button plain size="small" onClick={onDelete} className="flex items-center">
<Icon name="trash" size="14" className="mr-2" color="teal"/>
Delete
</Button>
<Button plain size="small" className="flex items-center ml-2" onClick={() => setShowDashboardSelectionModal(true)}>
<Icon name="columns-gap" size="14" className="mr-2" color="teal"/>
Add to Dashboard
</Button>
</>
)}
</div>
</div>
<DashboardSelectionModal
metricId={metric.metricId}
show={showDashboardSelectionModal}
closeHandler={() => setShowDashboardSelectionModal(false)}
/>
</div>
));
}
export default WidgetForm;

View file

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

Some files were not shown because too many files have changed in this diff Show more