Merge pull request #428 from openreplay/dashboard

Dashboard
This commit is contained in:
Shekar Siri 2022-04-15 14:36:32 +02:00 committed by GitHub
commit d69b6220b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
160 changed files with 2373 additions and 720 deletions

View file

@ -3,7 +3,7 @@ on:
workflow_dispatch:
push:
branches:
- dev
- api-v1.5.5
paths:
- frontend/**

View file

@ -22,7 +22,7 @@ const FunnelIssueDetails = lazy(() => import('Components/Funnels/FunnelIssueDeta
import WidgetViewPure from 'Components/Dashboard/components/WidgetView';
import Header from 'Components/Header/Header';
// import ResultsModal from 'Shared/Results/ResultsModal';
import { fetchList as fetchIntegrationVariables } from 'Duck/customField';
import { fetchList as fetchMetadata } from 'Duck/customField';
import { fetchList as fetchSiteList } from 'Duck/site';
import { fetchList as fetchAnnouncements } from 'Duck/announcements';
import { fetchList as fetchAlerts } from 'Duck/alerts';
@ -80,7 +80,7 @@ const ONBOARDING_REDIRECT_PATH = routes.onboarding(OB_DEFAULT_TAB);
@withStore
@withRouter
@connect((state) => {
const siteId = state.getIn([ 'user', 'siteId' ]);
const siteId = state.getIn([ 'site', 'siteId' ]);
const jwt = state.get('jwt');
const changePassword = state.getIn([ 'user', 'account', 'changePassword' ]);
const userInfoLoading = state.getIn([ 'user', 'fetchUserInfoRequest', 'loading' ]);
@ -88,7 +88,7 @@ const ONBOARDING_REDIRECT_PATH = routes.onboarding(OB_DEFAULT_TAB);
jwt,
siteId,
changePassword,
sites: state.getIn([ 'user', 'client', 'sites' ]),
sites: state.getIn([ 'site', 'list' ]),
isLoggedIn: jwt !== null && !changePassword,
loading: siteId === null || userInfoLoading,
email: state.getIn([ 'user', 'account', 'email' ]),
@ -103,7 +103,7 @@ const ONBOARDING_REDIRECT_PATH = routes.onboarding(OB_DEFAULT_TAB);
fetchUserInfo,
fetchTenants,
setSessionPath,
fetchIntegrationVariables,
fetchMetadata,
fetchSiteList,
fetchAnnouncements,
fetchAlerts,
@ -124,17 +124,18 @@ 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(() => {
setTimeout(() => {
this.props.fetchAnnouncements();
this.props.fetchAlerts();
this.props.fetchWatchdogStatus();
}, 100);
}),
this.props.fetchSiteList().then(() => {
const { mstore } = this.props
mstore.initClient();
setTimeout(() => {
this.props.fetchMetadata()
this.props.fetchAnnouncements();
this.props.fetchAlerts();
this.props.fetchWatchdogStatus();
}, 100);
})
})
])
}
@ -197,25 +198,17 @@ class Router extends React.Component {
{ onboarding &&
<Redirect to={ withSiteId(ONBOARDING_REDIRECT_PATH, siteId)} />
}
{ siteIdList.length === 0 &&
{/* { siteIdList.length === 0 &&
<Redirect to={ routes.client(routes.CLIENT_TABS.SITES) } />
}
} */}
{/* DASHBOARD and Metrics */}
<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 } />

View file

@ -56,7 +56,7 @@ export const clean = (obj, forbidenValues = [ undefined, '' ]) => {
export default class APIClient {
constructor() {
const jwt = store.getState().get('jwt');
const siteId = store.getState().getIn([ 'user', 'siteId' ]);
const siteId = store.getState().getIn([ 'site', 'siteId' ]);
this.init = {
headers: {
Accept: 'application/json',

View file

@ -113,7 +113,7 @@ class Notifications extends React.Component {
<NoContent
title=""
subtext="There are no alerts to show."
icon="exclamation-circle"
animatedIcon="no-results"
show={ !loading && notifications.size === 0 }
size="small"
>

View file

@ -71,7 +71,7 @@ class Announcements extends React.Component {
<NoContent
title=""
subtext="There are no announcements to show."
icon="exclamation-circle"
animatedIcon="no-results"
show={ !loading && announcements.size === 0 }
size="small"
>
@ -96,6 +96,6 @@ class Announcements extends React.Component {
export default connect(state => ({
announcements: state.getIn(['announcements', 'list']),
loading: state.getIn(['announcements', 'fetchList', 'loading']),
siteId: state.getIn([ 'user', 'siteId' ]),
siteId: state.getIn([ 'site', 'siteId' ]),
sites: state.getIn([ 'site', 'list' ]),
}), { fetchList, setLastRead })(Announcements);

View file

@ -8,11 +8,15 @@ interface Props {
loading: boolean,
list: any,
session: any,
fetchLiveList: () => void,
fetchLiveList: (params: any) => void,
}
function SessionList(props: Props) {
useEffect(() => {
props.fetchLiveList();
const params: any = {}
if (props.session.userId) {
params.userId = props.session.userId
}
props.fetchLiveList(params);
}, [])
return (

View file

@ -20,7 +20,7 @@ import { LAST_7_DAYS } from 'Types/app/period';
import { resetFunnel } from 'Duck/funnels';
import { resetFunnelFilters } from 'Duck/funnelFilters'
import NoSessionsMessage from 'Shared/NoSessionsMessage';
import TrackerUpdateMessage from 'Shared/TrackerUpdateMessage';
// import TrackerUpdateMessage from 'Shared/TrackerUpdateMessage';
import SessionSearch from 'Shared/SessionSearch';
import MainSearchBar from 'Shared/MainSearchBar';
import { clearSearch, fetchSessions } from 'Duck/search';
@ -53,7 +53,7 @@ const allowedQueryKeys = [
sources: state.getIn([ 'customFields', 'sources' ]),
filterValues: state.get('filterValues'),
favoriteList: state.getIn([ 'sessions', 'favoriteList' ]),
currentProjectId: state.getIn([ 'user', 'siteId' ]),
currentProjectId: state.getIn([ 'site', 'siteId' ]),
sites: state.getIn([ 'site', 'list' ]),
watchdogs: state.getIn(['watchdogs', 'list']),
activeFlow: state.getIn([ 'filters', 'activeFlow' ]),
@ -130,7 +130,7 @@ export default class BugFinder extends React.PureComponent {
/>
</div>
<div className={cn("side-menu-margined", stl.searchWrapper) }>
<TrackerUpdateMessage />
{/* <TrackerUpdateMessage /> */}
<NoSessionsMessage />
<div className="mb-5">
<MainSearchBar />

View file

@ -72,7 +72,7 @@ const SessionCaptureRate = props => {
}
export default connect(state => ({
currentProjectId: state.getIn([ 'user', 'siteId' ]),
currentProjectId: state.getIn([ 'site', 'siteId' ]),
captureRate: state.getIn(['watchdogs', 'captureRate']),
loading: state.getIn(['watchdogs', 'savingCaptureRate', 'loading']),
}), {

View file

@ -11,7 +11,7 @@ function SessionFlowList({ activeTab, savedFilters, loading }) {
<NoContent
title="No Flows Found!"
subtext="Please try changing your search parameters."
icon="exclamation-circle"
animatedIcon="no-results"
show={ !loading && savedFilters.size === 0 }
>
<Loader loading={ loading }>

View file

@ -1,12 +1,12 @@
import { connect } from 'react-redux';
import { Loader, NoContent, Button, LoadMoreButton, Pagination } from 'UI';
import { Loader, NoContent, Button, Pagination } from 'UI';
import { applyFilter, addAttribute, addEvent } from 'Duck/filters';
import { fetchSessions, addFilterByKeyAndValue, updateCurrentPage } from 'Duck/search';
import { fetchSessions, addFilterByKeyAndValue, updateCurrentPage, setScrollPosition } from 'Duck/search';
import SessionItem from 'Shared/SessionItem';
import SessionListHeader from './SessionListHeader';
import { FilterKey } from 'Types/filter/filterType';
const ALL = 'all';
// const ALL = 'all';
const PER_PAGE = 10;
const AUTOREFRESH_INTERVAL = 3 * 60 * 1000;
var timeoutId;
@ -21,6 +21,7 @@ var timeoutId;
filters: state.getIn([ 'search', 'instance', 'filters' ]),
metaList: state.getIn(['customFields', 'list']).map(i => i.key),
currentPage: state.getIn([ 'search', 'currentPage' ]),
scrollY: state.getIn([ 'search', 'scrollY' ]),
lastPlayedSessionId: state.getIn([ 'sessions', 'lastPlayedSessionId' ]),
}), {
applyFilter,
@ -29,24 +30,15 @@ var timeoutId;
fetchSessions,
addFilterByKeyAndValue,
updateCurrentPage,
setScrollPosition,
})
export default class SessionList extends React.PureComponent {
state = {
showPages: 1,
}
constructor(props) {
super(props);
this.timeout();
}
componentDidUpdate(prevProps) {
if (prevProps.loading && !this.props.loading) {
this.setState({ showPages: 1 });
}
}
addPage = () => this.setState({ showPages: this.state.showPages + 1 })
onUserClick = (userId, userAnonymousId) => {
if (userId) {
this.props.addFilterByKeyAndValue(FilterKey.USERID, userId);
@ -76,17 +68,21 @@ export default class SessionList extends React.PureComponent {
}
componentWillUnmount() {
this.props.setScrollPosition(window.scrollY)
clearTimeout(timeoutId)
}
componentDidMount() {
const { scrollY } = this.props;
window.scrollTo(0, scrollY);
}
renderActiveTabContent(list) {
const {
loading,
filters,
onMenuItemClick,
allList,
// onMenuItemClick,
// allList,
activeTab,
metaList,
currentPage,
@ -95,19 +91,17 @@ export default class SessionList extends React.PureComponent {
} = this.props;
const _filterKeys = filters.map(i => i.key);
const hasUserFilter = _filterKeys.includes(FilterKey.USERID) || _filterKeys.includes(FilterKey.USERANONYMOUSID);
const { showPages } = this.state;
const displayedCount = Math.min(showPages * PER_PAGE, list.size);
return (
<NoContent
title={this.getNoContentMessage(activeTab)}
// subtext="Please try changing your search parameters."
icon="exclamation-circle"
animatedIcon="no-results"
show={ !loading && list.size === 0}
subtext={
<div>
<div>Please try changing your search parameters.</div>
{allList.size > 0 && (
{/* {allList.size > 0 && (
<div className="pt-2">
However, we found other sessions based on your search parameters.
<div>
@ -117,7 +111,7 @@ export default class SessionList extends React.PureComponent {
>See All</Button>
</div>
</div>
)}
)} */}
</div>
}
>
@ -148,23 +142,23 @@ export default class SessionList extends React.PureComponent {
render() {
const { activeTab, allList, total } = this.props;
var filteredList;
// var filteredList;
if (activeTab.type !== ALL && activeTab.type !== 'bookmark' && activeTab.type !== 'live') { // Watchdog sessions
filteredList = allList.filter(session => activeTab.fits(session))
} else {
filteredList = allList
}
// if (activeTab.type !== ALL && activeTab.type !== 'bookmark' && activeTab.type !== 'live') { // Watchdog sessions
// filteredList = allList.filter(session => activeTab.fits(session))
// } else {
// filteredList = allList
// }
if (activeTab.type === 'bookmark') {
filteredList = filteredList.filter(item => item.favorite)
}
const _total = activeTab.type === 'all' ? total : filteredList.size
// if (activeTab.type === 'bookmark') {
// filteredList = filteredList.filter(item => item.favorite)
// }
// const _total = activeTab.type === 'all' ? total : allList.size
return (
<div className="">
<SessionListHeader activeTab={activeTab} count={_total}/>
{ this.renderActiveTabContent(filteredList) }
<SessionListHeader activeTab={activeTab} count={total}/>
{ this.renderActiveTabContent(allList) }
</div>
);
}

View file

@ -13,7 +13,7 @@ import { confirm } from 'UI/Confirmation';
fields: state.getIn(['customFields', 'list']).sortBy(i => i.index),
field: state.getIn(['customFields', 'instance']),
loading: state.getIn(['customFields', 'fetchRequest', 'loading']),
sites: state.getIn([ 'user', 'client', 'sites' ]),
sites: state.getIn([ 'site', 'list' ]),
errors: state.getIn([ 'customFields', 'saveRequest', 'errors' ]),
}), {
init,

View file

@ -4,8 +4,8 @@ import SiteDropdown from 'Shared/SiteDropdown';
import { save, init, edit, remove, fetchList } from 'Duck/integrations/actions';
@connect((state, { name, customPath }) => ({
sites: state.getIn([ 'user', 'client', 'sites' ]),
initialSiteId: state.getIn([ 'user', 'siteId' ]),
sites: state.getIn([ 'site', 'list' ]),
initialSiteId: state.getIn([ 'site', 'siteId' ]),
list: state.getIn([ name, 'list' ]),
config: state.getIn([ name, 'instance']),
saving: state.getIn([ customPath || name, 'saveRequest', 'loading']),

View file

@ -1,7 +1,8 @@
import { connect } from 'react-redux';
import { Input, Button, Label } from 'UI';
import { save, edit, update , fetchList } from 'Duck/site';
import { pushNewSite, setSiteId } from 'Duck/user';
import { pushNewSite } from 'Duck/user';
import { setSiteId } from 'Duck/site';
import { withRouter } from 'react-router-dom';
import styles from './siteForm.css';

View file

@ -59,7 +59,7 @@ class Webhooks extends React.PureComponent {
title="No webhooks available."
size="small"
show={ noSlackWebhooks.size === 0 }
icon
animatedIcon="no-results"
>
<div className={ styles.list }>
{ noSlackWebhooks.map(webhook => (

View file

@ -18,7 +18,7 @@
& .tabContent {
background-color: white;
padding: 25px;
margin-top: -30px;
/* margin-top: -30px; */
margin-right: -20px;
width: 100%;
}

View file

@ -212,8 +212,7 @@ export default class Dashboard extends React.PureComponent {
show={ noWidgets }
title="You haven't added any insights widgets!"
subtext="Add new to keep track of Processed Sessions, Application Activity, Errors and lot more."
icon
empty
animatedIcon="empty-state"
>
<WidgetSection
title="Overview"

View file

@ -35,6 +35,6 @@ const DashboardHeader = props => {
export default connect(state => ({
period: state.getIn([ 'dashboard', 'period' ]),
platform: state.getIn([ 'dashboard', 'platform' ]),
currentProjectId: state.getIn([ 'user', 'siteId' ]),
currentProjectId: state.getIn([ 'site', 'siteId' ]),
sites: state.getIn([ 'site', 'list' ]),
}), { setPeriod, setPlatform })(DashboardHeader)

View file

@ -3,47 +3,45 @@ 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';
import cn from 'classnames';
function NewDashboard(props) {
const { history, match: { params: { siteId, dashboardId, metricId } } } = props;
const { dashboardStore } = useStore();
const loading = useObserver(() => dashboardStore.isLoading);
const isMetricDetails = history.location.pathname.includes('/metrics/') || history.location.pathname.includes('/metric/');
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));
}
});
}
}
// else {
// dashboardStore.selectDefaultDashboard().then(({ dashboardId }) => {
// console.log('dashboardId', dashboardId)
// // if (!history.location.pathname.includes('/metrics')) {
// // history.push(withSiteId(dashboardSelected(dashboardId), siteId));
// // }
// });
// }
});
}, []);
}, [siteId]);
return (
return useObserver(() => (
<Loader loading={loading}>
<div className="page-margin container-90">
<div className="side-menu">
<div className={cn("side-menu", { 'hidden' : isMetricDetails })}>
<DashboardSideMenu siteId={siteId} />
</div>
<div className="side-menu-margined">
<div className={cn({ "side-menu-margined" : !isMetricDetails, "container-70" : isMetricDetails })}>
<DashboardRouter siteId={siteId} />
</div>
</div>
</Loader>
);
));
}
export default withPageTitle('New Dashboard')(
withRouter(observer(NewDashboard))
);
export default withPageTitle('New Dashboard')(withRouter(NewDashboard));

View file

@ -41,5 +41,5 @@ function SideMenuSection({ title, items, onItemClick, setShowAlerts, siteId }) {
SideMenuSection.displayName = "SideMenuSection";
export default connect(state => ({
siteId: state.getIn([ 'user', 'siteId' ])
siteId: state.getIn([ 'site', 'siteId' ])
}), { setShowAlerts })(SideMenuSection);

View file

@ -6,16 +6,17 @@ import { LineChart, Line, Legend } from 'recharts';
interface Props {
data: any;
params: any;
seriesMap: any;
// seriesMap: any;
colors: any;
onClick?: (event, index) => void;
}
function CustomMetriLineChart(props: Props) {
const { data, params, seriesMap = [], colors, onClick = () => null } = props;
const { data = { chart: [], namesMap: [] }, params, colors, onClick = () => null } = props;
return (
<ResponsiveContainer height={ 240 } width="100%">
<LineChart
data={ data }
data={ data.chart }
margin={Styles.chartMargins}
// syncId={ showSync ? "domainsErrors_4xx" : undefined }
onClick={onClick}
@ -37,18 +38,18 @@ function CustomMetriLineChart(props: Props) {
/>
<Legend />
<Tooltip {...Styles.tooltip} />
{ seriesMap.map((key, index) => (
{ Array.isArray(data.namesMap) && data.namesMap.map((key, index) => (
<Line
key={key}
name={key}
type="monotone"
dataKey={key}
stroke={colors[index]}
fillOpacity={ 1 }
strokeWidth={ 2 }
strokeOpacity={ 0.6 }
// fill="url(#colorCount)"
dot={false}
key={key}
name={key}
type="monotone"
dataKey={key}
stroke={colors[index]}
fillOpacity={ 1 }
strokeWidth={ 2 }
strokeOpacity={ 0.6 }
// fill="url(#colorCount)"
dot={false}
/>
))}
</LineChart>

View file

@ -12,28 +12,28 @@ interface Props {
}
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="absolute flex items-start flex-col justify-start 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}
/>
<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,
top: 50, right: 0, left: 0, bottom: 5,
} }
>
{gradientDef}

View file

@ -12,7 +12,7 @@ function CustomMetriPercentage(props: Props) {
return (
<div className="flex flex-col items-center justify-center" style={{ height: '240px'}}>
<div className="text-6xl">{numberWithCommas(data.count)}</div>
<div className="text-lg mt-6">{`${data.previousCount} ( ${data.countProgress}% )`}</div>
<div className="text-lg mt-6">{`${parseInt(data.previousCount || 0)} ( ${parseInt(data.countProgress || 0).toFixed(1)}% )`}</div>
<div className="color-gray-medium">from previous period.</div>
</div>
)

View file

@ -33,6 +33,7 @@ function CustomMetriTable(props: Props) {
const rows = List(data.values);
const onClickHandler = (event, data) => {
console.log('onClickHandler', data);
const filters = Array<any>();
let filter = { ...filtersMap[metric.metricOf] }
filter.value = [data.name]

View file

@ -136,7 +136,7 @@ function CustomMetricWidget(props: Props) {
<CustomMetriLineChart
data={ data }
params={ params }
seriesMap={ seriesMap }
// seriesMap={ seriesMap }
colors={ colors }
onClick={ clickHandler }
/>

View file

@ -168,7 +168,7 @@ function CustomMetricWidget(props: Props) {
{ metric.viewType === 'lineChart' && (
<CustomMetriLineChart
data={data}
seriesMap={seriesMap}
// seriesMap={seriesMap}
colors={colors}
params={params}
/>

View file

@ -10,25 +10,25 @@ import {
interface Props {
data: any
metric?: any
}
function BreakdownOfLoadedResources(props: Props) {
const { data } = props;
const { data, metric } = props;
const gradientDef = Styles.gradientDef();
const params = { density: 28 }
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
show={ metric.data.chart.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">
<BarChart
data={ data.chart }
data={ metric.data.chart }
margin={ Styles.chartMargins }
>
{gradientDef}
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis {...Styles.xaxis} dataKey="time" interval={params.density/7} />
<XAxis {...Styles.xaxis} dataKey="time" interval={metric.params.density/7} />
<YAxis
{...Styles.yaxis}
allowDecimals={false}

View file

@ -10,25 +10,26 @@ import {
interface Props {
data: any
metric?: any
}
function CPULoad(props: Props) {
const { data } = props;
const { data, metric } = props;
const gradientDef = Styles.gradientDef();
const params = { density: 70 }
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
show={ metric.data.chart.length === 0 }
style={ { height: '240px' } }
>
<ResponsiveContainer height={ 240 } width="100%">
<AreaChart
data={ data.chart }
data={ metric.data.chart }
margin={ Styles.chartMargins }
>
{gradientDef}
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
<XAxis {...Styles.xaxis} dataKey="time" interval={(metric.params.density/7)} />
<YAxis
{...Styles.yaxis}
allowDecimals={false}
@ -40,7 +41,7 @@ function CPULoad(props: Props) {
name="Avg"
type="monotone"
unit="%"
dataKey="avgCpu"
dataKey="value"
stroke={Styles.colors[0]}
fillOpacity={ 1 }
strokeWidth={ 2 }

View file

@ -0,0 +1,81 @@
import React from 'react';
import { Loader, NoContent } from 'UI';
import { Styles, Table } from '../../common';
import { getRE } from 'App/utils';
import ImageInfo from './ImageInfo';
import MethodType from './MethodType';
import cn from 'classnames';
import stl from './callWithErrors.css';
const cols = [
{
key: 'method',
title: 'Method',
className: 'text-left',
Component: MethodType,
cellClass: 'ml-2',
width: '8%',
},
{
key: 'urlHostpath',
title: 'Path',
Component: ImageInfo,
width: '40%',
},
{
key: 'allRequests',
title: 'Requests',
className: 'text-left',
width: '15%',
},
{
key: '4xx',
title: '4xx',
className: 'text-left',
width: '15%',
},
{
key: '5xx',
title: '5xx',
className: 'text-left',
width: '15%',
}
];
interface Props {
data: any
metric?: any
}
function CallWithErrors(props: Props) {
const { data, metric } = props;
const [search, setSearch] = React.useState('')
const test = (value = '', serach) => getRE(serach, 'i').test(value);
const _data = search ? metric.data.chart.filter(i => test(i.urlHostpath, search)) : metric.data.chart.images;
console.log('data', metric.data)
const write = ({ target: { name, value } }) => {
setSearch(value)
};
return (
<NoContent
size="small"
show={ metric.data.chart.length === 0 }
style={{ height: '240px'}}
>
<div style={{ height: '240px'}}>
<div className={ cn(stl.topActions, 'py-3 flex text-right')}>
<input disabled={metric.data.chart.length === 0} className={stl.searchField} name="search" placeholder="Filter by Path" onChange={write} />
</div>
<Table
cols={ cols }
rows={ _data }
/>
</div>
</NoContent>
);
}
export default CallWithErrors;

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,12 @@
import { Popup, Icon, TextEllipsis } from 'UI';
import styles from './imageInfo.css';
const ImageInfo = ({ data }) => (
<div className={ styles.name }>
<TextEllipsis text={data.urlHostpath} />
</div>
);
ImageInfo.displayName = 'ImageInfo';
export default ImageInfo;

View file

@ -0,0 +1,10 @@
import React from 'react'
import { Label } from 'UI';
const MethodType = ({ data }) => {
return (
<Label className="ml-1">{data.method}</Label>
)
}
export default MethodType

View file

@ -0,0 +1,22 @@
.topActions {
position: absolute;
top: -4px;
right: 50px;
display: flex;
justify-content: flex-end;
}
.searchField {
padding: 4px 5px;
border-bottom: dotted thin $gray-light;
border-radius: 3px;
&:focus,
&:active {
border: solid thin transparent !important;
box-shadow: none;
background-color: $gray-light;
}
&:hover {
border: solid thin $gray-light !important;
}
}

View file

@ -0,0 +1,39 @@
.name {
display: flex;
align-items: center;
& > span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 60%;
}
}
.imagePreview {
max-width: 200px;
max-height: 200px;
}
.imageWrapper {
display: flex;
flex-flow: column;
align-items: center;
width: 40px;
text-align: center;
margin-right: 10px;
& > span {
height: 16px;
}
& .label {
font-size: 9px;
color: $gray-light;
}
}
.popup {
background-color: #f5f5f5 !important;
&:before {
background-color: #f5f5f5 !important;
}
}

View file

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

View file

@ -2,33 +2,33 @@ import React from 'react';
import { NoContent } from 'UI';
import { Styles } from '../../common';
import {
BarChart, Bar, CartesianGrid, Tooltip,
CartesianGrid, Tooltip,
LineChart, Line, Legend, ResponsiveContainer,
XAxis, YAxis
} from 'recharts';
interface Props {
data: any
metric?: any
}
function CallsErrors4xx(props: Props) {
const { data } = props;
const { data, metric } = props;
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
show={ metric.data.chart.length === 0 }
style={ { height: '240px' } }
>
<ResponsiveContainer height={ 240 } width="100%">
<BarChart
data={data.chart}
<LineChart
data={metric.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}
interval={metric.params.density/7}
/>
<YAxis
{...Styles.yaxis}
@ -37,10 +37,10 @@ function CallsErrors4xx(props: Props) {
/>
<Legend />
<Tooltip {...Styles.tooltip} />
{/* { data.namesMap.map((key, index) => (
{ Array.isArray(metric.data.namesMap) && metric.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>
))}
</LineChart>
</ResponsiveContainer>
</NoContent>
);

View file

@ -9,26 +9,26 @@ import {
interface Props {
data: any
metric?: any
}
function CallsErrors5xx(props: Props) {
const { data } = props;
const { data, metric } = props;
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
show={ metric.data.chart.length === 0 }
style={ { height: '240px' } }
>
<ResponsiveContainer height={ 240 } width="100%">
<BarChart
data={data.chart}
<LineChart
data={metric.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}
interval={metric.params.density/7}
/>
<YAxis
{...Styles.yaxis}
@ -37,10 +37,10 @@ function CallsErrors5xx(props: Props) {
/>
<Legend />
<Tooltip {...Styles.tooltip} />
{/* { data.namesMap.map((key, index) => (
{ Array.isArray(metric.data.namesMap) && metric.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>
))}
</LineChart>
</ResponsiveContainer>
</NoContent>
);

View file

@ -10,36 +10,36 @@ import {
interface Props {
data: any
metric?: any
}
function Crashes(props: Props) {
const { data } = props;
const { data, metric } = props;
const gradientDef = Styles.gradientDef();
const params = { density: 70 }
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
show={ metric.data.chart.length === 0 }
style={ { height: '240px' } }
>
<ResponsiveContainer height={ 240 } width="100%">
<AreaChart
data={ data.chart }
data={ metric.data.chart }
margin={ Styles.chartMargins }
>
{gradientDef}
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
<XAxis {...Styles.xaxis} dataKey="time" interval={(metric.params.density/7)} />
<YAxis
{...Styles.yaxis}
allowDecimals={false}
tickFormatter={val => Styles.tickFormatter(val)}
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
label={{ ...Styles.axisLabelLeft, value: "Number of Crashes" }}
/>
<Tooltip {...Styles.tooltip} />
<Area
name="Crashes"
type="monotone"
unit="%"
dataKey="avgCpu"
dataKey="count"
stroke={Styles.colors[0]}
fillOpacity={ 1 }
strokeWidth={ 2 }

View file

@ -18,15 +18,14 @@ interface Props {
optionsLoading: any
fetchOptions: any
options: any
metric?: any
}
function DomBuildingTime(props: Props) {
const { data, optionsLoading } = props;
const { data, optionsLoading, metric } = props;
const gradientDef = Styles.gradientDef();
const params = { density: 70 }
const onSelect = (params) => {
const _params = { density: 70 }
// 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 })
}
@ -34,39 +33,39 @@ function DomBuildingTime(props: Props) {
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
show={ metric.data.chart.length === 0 }
>
<>
<div className="flex items-center mb-3">
<WidgetAutoComplete
{/* <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" />
/> */}
<AvgLabel className="ml-auto" text="Avg" count={Math.round(metric.data.avg)} unit="ms" />
</div>
<ResponsiveContainer height={ 207 } width="100%">
<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)} />
<XAxis {...Styles.xaxis} dataKey="time" interval={(metric.params.density/7)} />
<YAxis
{...Styles.yaxis}
allowDecimals={false}
tickFormatter={val => Styles.tickFormatter(val)}
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
label={{ ...Styles.axisLabelLeft, value: "DOM Build Time (ms)" }}
/>
<Tooltip {...Styles.tooltip} />
<Area
name="Avg"
type="monotone"
unit="%"
dataKey="avgCpu"
// unit="%"
dataKey="value"
stroke={Styles.colors[0]}
fillOpacity={ 1 }
strokeWidth={ 2 }

View file

@ -9,26 +9,26 @@ import {
interface Props {
data: any
metric?: any
}
function ErrorsByOrigin(props: Props) {
const { data } = props;
const { data, metric } = props;
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
show={ metric.data.chart.length === 0 }
style={ { height: '240px' } }
>
<ResponsiveContainer height={ 240 } width="100%">
<BarChart
data={data.chart}
data={metric.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}
interval={metric.params.density/7}
/>
<YAxis
{...Styles.yaxis}

View file

@ -9,26 +9,26 @@ import {
interface Props {
data: any
metric?: any
}
function ErrorsByType(props: Props) {
const { data } = props;
const { data, metric } = props;
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
show={ metric.data.chart.length === 0 }
style={ { height: '240px' } }
>
<ResponsiveContainer height={ 240 } width="100%">
<BarChart
data={data.chart}
data={metric.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}
interval={metric.params.density/7}
/>
<YAxis
{...Styles.yaxis}

View file

@ -6,19 +6,20 @@ import Bar from 'App/components/Dashboard/Widgets/ErrorsPerDomain/Bar';
interface Props {
data: any
metric?: any
}
function ErrorsPerDomain(props: Props) {
const { data } = props;
console.log('ErrorsPerDomain', data);
const { data, metric } = props;
// const firstAvg = 10;
const firstAvg = data.chart[0] && data.chart[0].errorsCount;
const firstAvg = metric.data.chart[0] && metric.data.chart[0].errorsCount;
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
show={ metric.data.chart.length === 0 }
style={{ height: '240px'}}
>
<div className="w-full" style={{ height: '240px' }}>
{data.chart.map((item, i) =>
{metric.data.chart.map((item, i) =>
<Bar
key={i}
className="mb-2"

View file

@ -10,16 +10,16 @@ import {
interface Props {
data: any
metric?: any
}
function FPS(props: Props) {
const { data } = props;
const { data, metric } = props;
const gradientDef = Styles.gradientDef();
const params = { density: 70 }
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
show={ metric.data.chart.length === 0 }
>
<>
<div className="flex items-center justify-end mb-3">
@ -27,23 +27,23 @@ function FPS(props: Props) {
</div>
<ResponsiveContainer height={ 207 } width="100%">
<AreaChart
data={ data.chart }
data={ metric.data.chart }
margin={ Styles.chartMargins }
>
{gradientDef}
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
<XAxis {...Styles.xaxis} dataKey="time" interval={(metric.params.density/7)} />
<YAxis
{...Styles.yaxis}
allowDecimals={false}
tickFormatter={val => Styles.tickFormatter(val)}
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
label={{ ...Styles.axisLabelLeft, value: "Frames Per Second" }}
/>
<Tooltip {...Styles.tooltip} />
<Area
name="Avg"
type="monotone"
dataKey="avgFps"
dataKey="value"
stroke={Styles.colors[0]}
fillOpacity={ 1 }
strokeWidth={ 2 }

View file

@ -10,16 +10,16 @@ import {
interface Props {
data: any
metric?: any
}
function MemoryConsumption(props: Props) {
const { data } = props;
const { data, metric } = props;
const gradientDef = Styles.gradientDef();
const params = { density: 70 }
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
show={ metric.data.chart.length === 0 }
>
<>
<div className="flex items-center justify-end mb-3">
@ -27,12 +27,12 @@ function MemoryConsumption(props: Props) {
</div>
<ResponsiveContainer height={ 207 } width="100%">
<AreaChart
data={ data.chart }
data={ metric.data.chart }
margin={ Styles.chartMargins }
>
{gradientDef}
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
<XAxis {...Styles.xaxis} dataKey="time" interval={(metric.params.density/7)} />
<YAxis
{...Styles.yaxis}
allowDecimals={false}
@ -44,7 +44,7 @@ function MemoryConsumption(props: Props) {
name="Avg"
unit=" mb"
type="monotone"
dataKey="avgFps"
dataKey="value"
stroke={Styles.colors[0]}
fillOpacity={ 1 }
strokeWidth={ 2 }

View file

@ -37,21 +37,22 @@ const cols = [
interface Props {
data: any
metric?: any
}
function MissingResources(props: Props) {
const { data } = props;
const { data, metric } = props;
return (
<NoContent
title="No resources missing."
size="small"
show={ data.chart.length === 0 }
show={ metric.data.chart.length === 0 }
>
<div style={{ height: '240px'}}>
<Table
small
cols={ cols }
rows={ List(data.chart) }
rows={ List(metric.data.chart) }
rowClass="group"
/>
</div>

View file

@ -8,19 +8,19 @@ import {
interface Props {
data: any
metric?: any
}
function ResourceLoadedVsResponseEnd(props: Props) {
const { data } = props;
const params = { density: 70 }
const { data, metric } = props;
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
show={ metric.data.chart.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">
<ComposedChart
data={data.chart}
data={metric.data.chart}
margin={ Styles.chartMargins}
>
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
@ -28,7 +28,7 @@ function ResourceLoadedVsResponseEnd(props: Props) {
{...Styles.xaxis}
dataKey="time"
// interval={3}
interval={(params.density / 7)}
interval={(metric.params.density / 7)}
/>
<YAxis
{...Styles.yaxis}

View file

@ -8,28 +8,27 @@ import {
interface Props {
data: any
metric?: any
}
function ResourceLoadedVsVisuallyComplete(props: Props) {
const { data } = props;
const { data, metric } = props;
const gradientDef = Styles.gradientDef();
const params = { density: 70 }
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
show={ metric.data.chart.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">
<ComposedChart
data={data.chart}
data={metric.data.chart}
margin={ Styles.chartMargins}
>
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis
{...Styles.xaxis}
dataKey="time"
// interval={3}
interval={(params.density / 7)}
interval={(metric.params.density / 7)}
/>
<YAxis
{...Styles.yaxis}

View file

@ -27,16 +27,16 @@ interface Props {
optionsLoading: any
fetchOptions: any
options: any
metric?: any
}
function ResourceLoadingTime(props: Props) {
const { data, optionsLoading } = props;
const { data, optionsLoading, metric } = 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 }
// 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 })
@ -52,11 +52,11 @@ function ResourceLoadingTime(props: Props) {
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
show={ metric.data.chart.length === 0 }
>
<>
<div className="flex items-center mb-3">
<WidgetAutoComplete
{/* <WidgetAutoComplete
loading={optionsLoading}
fetchOptions={props.fetchOptions}
options={props.options}
@ -75,22 +75,22 @@ function ResourceLoadingTime(props: Props) {
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 }
data={ metric.data.chart }
margin={ Styles.chartMargins }
>
{gradientDef}
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
<XAxis {...Styles.xaxis} dataKey="time" interval={(metric.params.density/7)} />
<YAxis
{...Styles.yaxis}
allowDecimals={false}
tickFormatter={val => Styles.tickFormatter(val)}
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
label={{ ...Styles.axisLabelLeft, value: "Resource Fetch Time (ms)" }}
/>
<Tooltip {...Styles.tooltip} />
<Area

View file

@ -18,15 +18,15 @@ interface Props {
optionsLoading: any
fetchOptions: any
options: any
metric?: any
}
function ResponseTime(props: Props) {
const { data, optionsLoading } = props;
const { data, optionsLoading, metric } = props;
const gradientDef = Styles.gradientDef();
const params = { density: 70 }
const onSelect = (params) => {
const _params = { density: 70 }
// 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 })
}
@ -34,27 +34,27 @@ function ResponseTime(props: Props) {
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
show={ metric.data.chart.length === 0 }
>
<>
<div className="flex items-center mb-3">
<WidgetAutoComplete
{/* <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" />
/> */}
<AvgLabel className="ml-auto" text="Avg" count={Math.round(metric.data.avg)} unit="ms" />
</div>
<ResponsiveContainer height={ 207 } width="100%">
<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)} />
<XAxis {...Styles.xaxis} dataKey="time" interval={(metric.params.density/7)} />
<YAxis
{...Styles.yaxis}
allowDecimals={false}
@ -66,7 +66,7 @@ function ResponseTime(props: Props) {
name="Avg"
type="monotone"
unit=" ms"
dataKey="avgCpu"
dataKey="value"
stroke={Styles.colors[0]}
fillOpacity={ 1 }
strokeWidth={ 2 }

View file

@ -0,0 +1,128 @@
import React from 'react';
import { Loader, NoContent } from 'UI';
import { Styles, AvgLabel } from '../../common';
import {
ComposedChart, Bar, BarChart, CartesianGrid, ResponsiveContainer,
XAxis, YAxis, ReferenceLine, Tooltip, Legend
} from 'recharts';
const PercentileLine = props => {
const {
viewBox: { x, y },
xoffset,
yheight,
height,
label
} = props;
return (
<svg>
<line
x1={x + xoffset}
x2={x + xoffset}
y1={height + 10}
y2={205}
{...props}
/>
<text
x={x + 5}
y={height + 20}
fontSize="8"
fontFamily="Roboto"
fill="#000000"
textAnchor="start"
>
{label}
</text>
</svg>
);
};
interface Props {
data: any
metric?: any
}
function ResponseTimeDistribution(props: Props) {
const { data, metric } = props;
const colors = Styles.colors;
return (
<NoContent
size="small"
show={ metric.data.chart.length === 0 }
style={ { height: '240px' } }
>
<div className="flex items-center justify-end mb-3">
<AvgLabel text="Avg" unit="ms" className="ml-3" count={metric.data.avg} />
</div>
<div className="flex mb-4">
<ResponsiveContainer height={ 240 } width="100%">
<ComposedChart
data={metric.data.chart}
margin={Styles.chartMargins}
barSize={50}
>
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis
{...Styles.xaxis}
dataKey="responseTime"
label={{
...Styles.axisLabelLeft,
angle: 0,
offset: 0,
value: "Page Response Time (ms)",
style: { textAnchor: 'middle' },
position: "insideBottom"
}}
/>
<YAxis
{...Styles.yaxis}
allowDecimals={false}
label={{
...Styles.axisLabelLeft,
value: "Number of Calls"
}}
/>
<Bar minPointSize={1} name="Calls" dataKey="count" stackId="a" fill={colors[2]} label="Backend" />
<Tooltip {...Styles.tooltip} labelFormatter={val => 'Page Response Time: ' + val} />
{ metric.data.percentiles.map((item, i) => (
<ReferenceLine
key={i}
label={
<PercentileLine
xoffset={0}
// y={130}
height={i * 20}
stroke={'#000'}
strokeWidth={0.5}
strokeOpacity={1}
label={`${item.percentile}th Percentile (${item.responseTime}ms)`}
/>
}
allowDecimals={false}
x={item.responseTime}
strokeWidth={0}
strokeOpacity={1}
/>
))}
</ComposedChart>
</ResponsiveContainer>
<ResponsiveContainer height={ 240 } width="10%">
<BarChart
data={metric.data.extremeValues}
margin={Styles.chartMargins}
barSize={40}
>
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis {...Styles.xaxis} dataKey="time" />
<YAxis hide type="number" domain={[0, metric.data.total]} {...Styles.yaxis} allowDecimals={false} />
<Tooltip {...Styles.tooltip} />
<Bar minPointSize={1} name="Extreme Values" dataKey="count" stackId="a" fill={colors[0]} />
</BarChart>
</ResponsiveContainer>
</div>
</NoContent>
);
}
export default ResponseTimeDistribution;

View file

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

View file

@ -9,26 +9,26 @@ import {
interface Props {
data: any
metric?: any
}
function SessionsAffectedByJSErrors(props: Props) {
const { data } = props;
const { data, metric } = props;
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
show={ metric.data.chart.length === 0 }
style={ { height: '240px' } }
>
<ResponsiveContainer height={ 240 } width="100%">
<BarChart
data={data.chart}
data={metric.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}
interval={metric.params.density/7}
/>
<YAxis
{...Styles.yaxis}

View file

@ -10,30 +10,32 @@ import {
interface Props {
data: any
metric?: any
}
function SessionsImpactedBySlowRequests(props: Props) {
const { data } = props;
const { data, metric } = props;
const gradientDef = Styles.gradientDef();
const params = { density: 70 }
console.log('SessionsImpactedBySlowRequests', metric.data)
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
show={ metric.data.chart.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">
<AreaChart
data={ data.chart }
data={ metric.data.chart }
margin={ Styles.chartMargins }
>
{gradientDef}
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
<XAxis {...Styles.xaxis} dataKey="time" interval={(metric.params.density/7)} />
<YAxis
{...Styles.yaxis}
allowDecimals={false}
tickFormatter={val => Styles.tickFormatter(val)}
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
label={{ ...Styles.axisLabelLeft, value: "Number of Sessions" }}
/>
<Tooltip {...Styles.tooltip} />
<Area

View file

@ -0,0 +1,42 @@
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
metric?: any
}
function SessionsPerBrowser(props: Props) {
const { data, metric } = props;
const firstAvg = metric.data.chart[0] && metric.data.chart[0].count;
const getVersions = item => {
return Object.keys(item)
.filter(i => i !== 'browser' && i !== 'count')
.map(i => ({ key: 'v' +i, value: item[i]}))
}
return (
<NoContent
size="small"
show={ metric.data.chart.length === 0 }
>
<div className="w-full" style={{ height: '240px' }}>
{metric.data.chart.map((item, i) =>
<Bar
key={i}
className="mb-4"
avg={Math.round(item.count)}
versions={getVersions(item)}
width={Math.round((item.count * 100) / firstAvg) - 10}
domain={item.browser}
colors={Styles.colors}
/>
)}
</div>
</NoContent>
);
}
export default SessionsPerBrowser;

View file

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

View file

@ -6,17 +6,19 @@ import Bar from 'App/components/Dashboard/Widgets/SlowestDomains/Bar';
interface Props {
data: any
metric?: any
}
function SlowestDomains(props: Props) {
const { data } = props;
const firstAvg = data.chart[0] && data.chart[0].errorsCount;
const { data, metric } = props;
const firstAvg = metric.data.chart[0] && metric.data.chart[0].errorsCount;
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
show={ metric.data.chart.length === 0 }
style={{ height: '240px' }}
>
<div className="w-full" style={{ height: '240px' }}>
{data.chart.map((item, i) =>
{metric.data.chart.map((item, i) =>
<Bar
key={i}
className="mb-2"

View file

@ -0,0 +1,15 @@
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="avg" 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,27 @@
import { Popup } from 'UI';
import cn from 'classnames';
import styles from './imageInfo.css';
const supportedTypes = ['png', 'jpg', 'jpeg', 'svg'];
const ImageInfo = ({ data }) => {
const canPreview = supportedTypes.includes(data.type);
return (
<div className={ styles.name }>
<Popup
className={ styles.popup }
trigger={
<div className={cn({ [styles.hasPreview]: canPreview})}>
<div className={ styles.label }>{data.name}</div>
</div>
}
disabled={!canPreview}
content={ <img src={ `${ data.url }` } className={ styles.imagePreview } alt="One of the slowest images" /> }
/>
</div>
)
};
ImageInfo.displayName = 'ImageInfo';
export default ImageInfo;

View file

@ -0,0 +1,12 @@
import React from 'react'
import cn from 'classnames'
const ResourceType = ({ data : { type = 'js' }, compare }) => {
return (
<div className={ cn("rounded-full p-2 color-white h-12 w-12 flex items-center justify-center", { 'bg-teal': compare, 'bg-tealx': !compare})}>
<span className="overflow-hidden whitespace-no-wrap text-xs">{ type.toUpperCase() }</span>
</div>
)
}
export default ResourceType

View file

@ -0,0 +1,81 @@
import React from 'react';
import { NoContent } from 'UI';
import { Styles, Table } from '../../common';
import { List } from 'immutable';
import { numberWithCommas } from 'App/utils';
import Chart from './Chart';
import ImageInfo from './ImageInfo';
import ResourceType from './ResourceType';
import CopyPath from './CopyPath';
export const RESOURCE_OPTIONS = [
{ text: 'All', value: 'ALL', },
{ text: 'CSS', value: 'STYLESHEET', },
{ text: 'JS', value: 'SCRIPT', },
];
const cols = [
{
key: 'type',
title: 'Type',
Component: ResourceType,
className: 'text-center justify-center',
cellClass: 'ml-2',
width: '8%',
},
{
key: 'name',
title: 'File Name',
Component: ImageInfo,
cellClass: '-ml-2',
width: '40%',
},
{
key: 'avg',
title: 'Load Time',
toText: avg => `${ avg ? numberWithCommas(Math.trunc(avg)) : 0} ms`,
className: 'justify-center',
width: '15%',
},
{
key: 'trend',
title: 'Trend',
Component: Chart,
width: '15%',
},
{
key: 'copy-path',
title: '',
Component: CopyPath,
cellClass: 'invisible group-hover:visible text-right',
width: '15%',
}
];
interface Props {
data: any
metric?: any
}
function SlowestResources(props: Props) {
const { data, metric } = props;
return (
<NoContent
title="No resources missing."
size="small"
show={ metric.data.chart.length === 0 }
>
<div style={{ height: '240px', marginBottom:'10px'}}>
<Table
small
cols={ cols }
rows={ List(metric.data.chart) }
rowClass="group"
/>
</div>
</NoContent>
);
}
export default SlowestResources;

View file

@ -0,0 +1,52 @@
.name {
display: flex;
align-items: center;
& > span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 60%;
}
& .label {
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.hasPreview {
/* text-decoration: underline; */
border-bottom: 1px dotted;
cursor: pointer;
}
.imagePreview {
max-width: 200px;
max-height: 200px;
}
.imageWrapper {
display: flex;
flex-flow: column;
align-items: center;
width: 40px;
text-align: center;
margin-right: 10px;
& > span {
height: 16px;
}
& .label {
font-size: 9px;
color: $gray-light;
}
}
.popup {
background-color: #f5f5f5 !important;
&:before {
background-color: #f5f5f5 !important;
}
}

View file

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

View file

@ -0,0 +1,24 @@
import React from 'react'
import { Styles } from '../../common';
import cn from 'classnames';
import stl from './scale.css';
function Scale({ colors }) {
const lastIndex = (Styles.colors.length - 1)
return (
<div className={ cn(stl.bars, 'absolute bottom-0 mb-4')}>
{colors.map((c, i) => (
<div
key={i}
style={{ backgroundColor: c, width: '6px', height: '15px', marginBottom: '1px' }}
className="flex items-center justify-center"
>
{ i === 0 && <div className="text-xs pl-12">Slow</div>}
{ i === lastIndex && <div className="text-xs pl-12">Fast</div>}
</div>
))}
</div>
)
}
export default Scale

View file

@ -0,0 +1,114 @@
import React, { useEffect } from 'react';
import { NoContent } from 'UI';
import { Styles, AvgLabel } from '../../common';
import Scale from './Scale';
import { threeLetter } from 'App/constants/countries';
import { colorScale } from 'App/utils';
import { observer } from 'mobx-react-lite';
import * as DataMap from "datamaps";
import { numberWithCommas } from 'App/utils';
// interface Props {
// metric?: any
// }
function SpeedIndexByLocation(props) {
const { metric } = props;
const wrapper: any = React.useRef(null);
let map: any = null;
const getSeries = data => {
const series: any[] = [];
data.forEach(item => {
const d = [threeLetter[item.userCountry], Math.round(item.avg)]
series.push(d)
})
return series;
}
useEffect(() => {
if (wrapper.current && !map && metric.data.chart.length > 0) {
const dataset = getDataset();
map = new DataMap({
element: wrapper.current,
fills: { defaultFill: '#E8E8E8' },
data: dataset,
// responsive: true,
// height: null, //if not null, datamaps will grab the height of 'element'
// width: null, //if not null, datamaps will grab the width of 'element'
geographyConfig: {
borderColor: '#FFFFFF',
borderWidth: 0.5,
highlightBorderWidth: 1,
popupOnHover: true,
// don't change color on mouse hover
highlightFillColor: function(geo) {
return '#999999';
// return geo['fillColor'] || '#F5F5F5';
},
// only change border
highlightBorderColor: '#B7B7B7',
// show desired information in tooltip
popupTemplate: function(geo, data) {
// don't show tooltip if country don't present in dataset
if (!data) { return ; }
// tooltip content
return ['<div class="hoverinfo speedIndexPopup">',
'<strong>', geo.properties.name, '</strong>',
'<span>Avg: <strong>', numberWithCommas(data.numberOfThings), '</strong><span>',
'</div>'].join('');
}
}
});
}
}, [])
useEffect(() => {
if (map && map.updateChoropleth) {
const series = getSeries(metric.data.chart);
// console.log('series', series)
// map.updateChoropleth(series, {reset: true});
}
}, [])
const getDataset = () => {
const { metric } = props;
const colors = Styles.colors;
var dataset = {};
const series = getSeries(metric.data.chart);
var onlyValues = series.map(function(obj){ return obj[1]; });
const paletteScale = colorScale(onlyValues, [...colors].reverse());
// fill dataset in appropriate format
series.forEach(function(item){
var iso = item[0], value = item[1];
dataset[iso] = { numberOfThings: value, fillColor: paletteScale(value) };
});
return dataset;
}
return (
<NoContent
size="small"
show={ metric.data.chart.length === 0 }
style={ { height: '240px' } }
>
<div className="w-full flex justify-end">
<AvgLabel text="Avg" count={Math.round(metric.data.avg)} unit="ms" />
</div>
<Scale colors={Styles.colors} />
<div
style={{
height: '220px',
width: '90%',
margin: '0 auto',
display: 'flex',
}}
ref={ wrapper }
/>
</NoContent>
);
}
export default observer(SpeedIndexByLocation);

View file

@ -0,0 +1,21 @@
.maps {
height: auto;
width: 110%;
stroke: $gray-medium;
stroke-width: 1;
stroke-linecap: round;
stroke-linejoin: round;
margin-top: -20px;
}
.location {
fill: $gray-light !important;
cursor: pointer;
&:focus,
&:hover {
fill: #b8e2b3;
outline: 0;
}
}

View file

@ -0,0 +1,102 @@
import React, { useEffect } from 'react';
import { NoContent } from 'UI';
import { Styles, AvgLabel } from '../../common';
import Scale from './Scale';
import { threeLetter } from 'App/constants/countries';
import { colorScale } from 'App/utils';
import { observer } from 'mobx-react-lite';
import { numberWithCommas } from 'App/utils';
import WorldMap from "@svg-maps/world";
import { SVGMap } from "react-svg-map";
import "react-svg-map/lib/index.css";
import stl from './SpeedIndexByLocation.css';
interface Props {
metric?: any
}
function SpeedIndexByLocation(props: Props) {
const { metric } = props;
const wrapper: any = React.useRef(null);
let map: any = null;
const getSeries = data => {
const series: any[] = [];
data.forEach(item => {
const d = [threeLetter[item.userCountry], Math.round(item.avg)]
series.push(d)
})
return series;
}
useEffect(() => {
// if (!wrapper && !wrapper.current) return
}, [])
// useEffect(() => {
// if (map && map.updateChoropleth) {
// const series = getSeries(metric.data.chart);
// // console.log('series', series)
// // map.updateChoropleth(series, {reset: true});
// }
// }, [])
const getDataset = () => {
const { metric } = props;
const colors = Styles.colors;
var dataset = {};
const series = getSeries(metric.data.chart);
var onlyValues = series.map(function(obj){ return obj[1]; });
const paletteScale = colorScale(onlyValues, [...colors].reverse());
// fill dataset in appropriate format
series.forEach(function(item){
var iso = item[0], value = item[1];
dataset[iso] = { numberOfThings: value, fillColor: paletteScale(value) };
});
return dataset;
}
const getLocationClassName = (location, index) => {
// Generate random heat map
return `svg-map__location svg-map__location--heat${index % 4}`;
}
return (
<NoContent
size="small"
show={ metric.data.chart.length === 0 }
style={ { height: '240px' } }
className="relative"
>
<div className="absolute right-0 mr-4 top=0 w-full flex justify-end">
<AvgLabel text="Avg" count={Math.round(metric.data.avg)} unit="ms" />
</div>
<Scale colors={Styles.colors} />
<div className="map-target"></div>
<div
style={{
height: '220px',
width: '100%',
margin: '0 auto',
display: 'flex',
}}
ref={ wrapper }
>
<SVGMap
map={WorldMap}
className={stl.maps}
locationClassName={stl.location}
// onLocationMouseOver={(e) => console.log(e.target)}
/>
</div>
{/* <div className="examples__block__map__tooltip" style={this.state.tooltipStyle}>
{this.state.pointedLocation}
</div> */}
</NoContent>
);
}
export default observer(SpeedIndexByLocation);

View file

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

View file

@ -0,0 +1,11 @@
.bars {
& div:first-child {
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
& div:last-child {
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
}

View file

@ -18,15 +18,15 @@ interface Props {
optionsLoading: any
fetchOptions: any
options: any
metric?: any
}
function TimeToRender(props: Props) {
const { data, optionsLoading } = props;
const { data, optionsLoading, metric } = props;
const gradientDef = Styles.gradientDef();
const params = { density: 70 }
const onSelect = (params) => {
const _params = { density: 70 }
// 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 })
}
@ -34,27 +34,27 @@ function TimeToRender(props: Props) {
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
show={ metric.data.chart.length === 0 }
>
<>
<div className="flex items-center mb-3">
<WidgetAutoComplete
{/* <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 }
data={ metric.data.chart }
margin={ Styles.chartMargins }
>
{gradientDef}
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
<XAxis {...Styles.xaxis} dataKey="time" interval={(metric.params.density/7)} />
<YAxis
{...Styles.yaxis}
// allowDecimals={false}
@ -66,7 +66,7 @@ function TimeToRender(props: Props) {
name="Avg"
type="monotone"
unit=" ms"
dataKey="avgCpu"
dataKey="value"
stroke={Styles.colors[0]}
fillOpacity={ 1 }
strokeWidth={ 2 }

View file

@ -17,8 +17,8 @@ function WidgetCategoryItem({ category, isSelected, onClick, selectedWidgetIds,
<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>
{/* <input type="checkbox" checked={true} onChange={() => unSelectCategory(category)} /> */}
<span className="color-gray-medium text-sm">{`Selected ${selectedCategoryWidgetsCount} of ${category.widgets.length}`}</span>
</div>
)}
</div>
@ -29,6 +29,7 @@ function DashboardMetricSelection(props) {
const { dashboardStore } = useStore();
let widgetCategories: any[] = useObserver(() => dashboardStore.widgetCategories);
const [activeCategory, setActiveCategory] = React.useState<any>();
const [selectAllCheck, setSelectAllCheck] = React.useState(false);
const selectedWidgetIds = useObserver(() => dashboardStore.selectedWidgets.map((widget: any) => widget.metricId));
useEffect(() => {
@ -39,10 +40,17 @@ function DashboardMetricSelection(props) {
const handleWidgetCategoryClick = (category: any) => {
setActiveCategory(category);
setSelectAllCheck(false);
};
const toggleAllWidgets = ({ target: { checked }}) => {
dashboardStore.toggleAllSelectedWidgets(checked);
// dashboardStore.toggleAllSelectedWidgets(checked);
setSelectAllCheck(checked);
if (checked) {
dashboardStore.selectWidgetsByCategory(activeCategory.name);
} else {
dashboardStore.removeSelectedWidgetByCategory(activeCategory);
}
}
return useObserver(() => (
@ -62,10 +70,10 @@ function DashboardMetricSelection(props) {
<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>
<label className="flex items-center ml-3 cursor-pointer select-none">
<input type="checkbox" onChange={toggleAllWidgets} checked={selectAllCheck} />
<div className="ml-2">Select All</div>
</label>
</div>
</>
)}
@ -89,7 +97,7 @@ function DashboardMetricSelection(props) {
<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' }}
style={{ maxHeight: "calc(100vh - 165px)", overflowY: 'auto' }}
>
{activeCategory && activeCategory.widgets.map((widget: any) => (
<WidgetWrapper

View file

@ -16,6 +16,7 @@ interface Props {
function DashboardModal(props) {
const { history, siteId, dashboardId } = props;
const { dashboardStore } = useStore();
const selectedWidgetsCount = useObserver(() => dashboardStore.selectedWidgets.length);
const { hideModal } = useModal();
const dashboard = useObserver(() => dashboardStore.dashboardInstance);
const loading = useObserver(() => dashboardStore.isSaving);
@ -33,7 +34,7 @@ function DashboardModal(props) {
return useObserver(() => (
<div
className="fixed border-r shadow p-4 h-screen"
style={{ backgroundColor: '#FAFAFA', zIndex: '9999', width: '85%'}}
style={{ backgroundColor: '#FAFAFA', zIndex: '999', width: '85%', maxWidth: '1300px' }}
>
<div className="mb-6 flex items-end justify-between">
<div>
@ -53,15 +54,16 @@ function DashboardModal(props) {
)}
<DashboardMetricSelection />
<div className="flex absolute bottom-0 left-0 right-0 bg-white border-t p-3">
<div className="flex items-center 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" }
{ dashboard.exists() ? "Add Selected to Dashboard" : "Create" }
</Button>
<span className="ml-2 color-gray-medium">{selectedWidgetsCount} Widgets</span>
</div>
</div>
));

View file

@ -15,6 +15,12 @@ import DashboardView from '../DashboardView';
import MetricsView from '../MetricsView';
import WidgetView from '../WidgetView';
function DashboardViewSelected({ siteId, dashboardId}) {
return (
<DashboardView siteId={siteId} dashboardId={dashboardId} />
)
}
interface Props {
history: any
match: any
@ -32,6 +38,10 @@ function DashboardRouter(props: Props) {
<WidgetView siteId={siteId} {...props} />
</Route>
<Route exact strict path={withSiteId(dashboard(''), siteId)}>
<DashboardView siteId={siteId} dashboardId={dashboardId} />
</Route>
<Route exact strict path={withSiteId(dashboardMetricDetails(dashboardId), siteId)}>
<WidgetView siteId={siteId} {...props} />
</Route>
@ -40,12 +50,8 @@ function DashboardRouter(props: Props) {
<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} />
<DashboardViewSelected siteId={siteId} dashboardId={dashboardId} />
</Route>
</Switch>
</div>

View file

@ -29,7 +29,7 @@ function DashboardSelectionModal(props: Props) {
return useObserver(() => (
<Modal size="tiny" open={ show }>
<Modal.Header className="flex items-center justify-between">
<div>{ 'Add to selected Dashboard' }</div>
<div>{ 'Add to selected dashboard' }</div>
<Icon
role="button"
tabIndex="-1"

View file

@ -8,19 +8,24 @@ import { useModal } from 'App/components/Modal';
import DashbaordListModal from '../DashbaordListModal';
import DashboardModal from '../DashboardModal';
import cn from 'classnames';
import { Tooltip } from 'react-tippy';
import { connect } from 'react-redux';
import { setShowAlerts } from 'Duck/dashboard';
const SHOW_COUNT = 5;
const SHOW_COUNT = 8;
interface Props {
siteId: string
history: any
setShowAlerts: (show: boolean) => void
}
function DashboardSideMenu(props: Props) {
const { history, siteId } = props;
const { history, siteId, setShowAlerts } = props;
const { hideModal, showModal } = useModal();
const { dashboardStore } = useStore();
const dashboardId = dashboardStore.selectedDashboard?.dashboardId;
const dashboardId = useObserver(() => dashboardStore.selectedDashboard?.dashboardId);
const dashboardsPicked = useObserver(() => dashboardStore.dashboards.slice(0, SHOW_COUNT));
const remainingDashboardsCount = dashboardStore.dashboards.length - SHOW_COUNT;
const isMetric = history.location.pathname.includes('metrics');
const redirect = (path) => {
history.push(path);
@ -53,9 +58,24 @@ function DashboardSideMenu(props: Props) {
onClick={() => onItemClick(item)}
className="group"
leading = {(
<div className="ml-2 flex items-center">
<div className="ml-2 flex items-center cursor-default">
{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>}
{item.isPinned && <div className="p-1 pointer-events-none"><Icon name="pin-fill" size="16" /></div>}
{!item.isPinned && (
<Tooltip
delay={500}
arrow
title="Set as default dashboard"
hideOnClick={true}
>
<div
className={cn("p-1 invisible group-hover:visible cursor-pointer")}
onClick={() => togglePinned(item)}
>
<Icon name="pin-fill" size="16" color="gray-light" />
</div>
</Tooltip>
)}
</div>
)}
/>
@ -82,6 +102,7 @@ function DashboardSideMenu(props: Props) {
<div className="border-t w-full my-2" />
<div className="w-full">
<SideMenuitem
active={isMetric}
id="menu-manage-alerts"
title="Metrics"
iconName="bar-chart-line"
@ -94,11 +115,11 @@ function DashboardSideMenu(props: Props) {
id="menu-manage-alerts"
title="Alerts"
iconName="bell-plus"
// onClick={() => setShowAlerts(true)}
onClick={() => setShowAlerts(true)}
/>
</div>
</div>
));
}
export default withRouter(DashboardSideMenu);
export default connect(null, { setShowAlerts })(withRouter(DashboardSideMenu));

View file

@ -11,6 +11,7 @@ import { useModal } from 'App/components/Modal';
import DashboardModal from '../DashboardModal';
import DashboardEditModal from '../DashboardEditModal';
import DateRange from 'Shared/DateRange';
import AlertFormModal from 'App/components/Alerts/AlertFormModal';
interface Props {
siteId: number;
@ -22,13 +23,21 @@ function DashboardView(props: Props) {
const { siteId, dashboardId } = props;
const { dashboardStore } = useStore();
const { hideModal, showModal } = useModal();
const showAlertModal = useObserver(() => dashboardStore.showAlertModal);
const loading = useObserver(() => dashboardStore.fetchingDashboard);
const dashboard: any = dashboardStore.selectedDashboard
const dashboards = useObserver(() => dashboardStore.dashboards);
const dashboard: any = useObserver(() => dashboardStore.selectedDashboard);
const period = useObserver(() => dashboardStore.period);
const [showEditModal, setShowEditModal] = React.useState(false);
useEffect(() => {
dashboardStore.fetch(dashboardId)
if (!dashboard || !dashboard.dashboardId) return;
dashboardStore.fetch(dashboard.dashboardId)
}, [dashboard]);
useEffect(() => {
if (dashboardId) return;
dashboardStore.selectDefaultDashboard();
}, []);
const onAddWidgets = () => {
@ -58,11 +67,16 @@ function DashboardView(props: Props) {
return useObserver(() => (
<Loader loading={loading}>
<NoContent
show={!dashboard || !dashboard.dashboardId}
title="No data available."
show={dashboards.length === 0 || !dashboard || !dashboard.dashboardId}
icon="no-metrics-chart"
title="No dashboards available."
size="small"
iconSize={180}
subtext={
<Button primary size="small" onClick={onAddWidgets}>Create Dashboard</Button>
}
>
<div>
<div style={{ maxWidth: '1300px', margin: 'auto'}}>
<DashboardEditModal
show={showEditModal}
closeHandler={() => setShowEditModal(false)}
@ -74,7 +88,7 @@ function DashboardView(props: Props) {
</div>
<div className="flex items-center">
<div className="flex items-center">
<span className="mr-2 color-gray-medium">Time Range</span>
{/* <span className="mr-2 color-gray-medium">Time Range</span> */}
<DateRange
rangeValue={period.rangeName}
startDate={period.start}
@ -85,18 +99,15 @@ function DashboardView(props: Props) {
/>
</div>
<div className="mx-4" />
<ItemMenu
items={[
{
text: 'Edit',
onClick: onEdit
},
{
text: 'Delete Dashboard',
onClick: onDelete
},
]}
/>
<div className="flex items-center">
<ItemMenu
label="Options"
items={[
{ text: 'Rename', onClick: onEdit },
{ text: 'Delete', onClick: onDelete },
]}
/>
</div>
</div>
</div>
<DashboardWidgetGrid
@ -104,10 +115,14 @@ function DashboardView(props: Props) {
dashboardId={dashboardId}
onEditHandler={onAddWidgets}
/>
<AlertFormModal
showModal={showAlertModal}
onClose={() => dashboardStore.updateKey('showAlertModal', false)}
/>
</div>
</NoContent>
</Loader>
));
}
export default withRouter(withModal(observer(DashboardView)));
export default withRouter(withModal(DashboardView));

View file

@ -20,7 +20,7 @@ function DashboardWidgetGrid(props) {
<Loader loading={loading}>
<NoContent
show={list.length === 0}
icon="exclamation-circle"
icon="no-metrics-chart"
title="No metrics added to this dashboard"
subtext={
<div>

View file

@ -23,7 +23,7 @@ function MetricsList(props: Props) {
const lenth = list.length;
return useObserver(() => (
<NoContent show={lenth === 0} icon="exclamation-circle">
<NoContent show={lenth === 0} animatedIcon="no-results">
<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>

View file

@ -4,28 +4,55 @@ import CustomMetricPercentage from 'App/components/Dashboard/Widgets/CustomMetri
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 { useObserver } from 'mobx-react-lite';
import { Loader } from 'UI';
import { useStore } from 'App/mstore';
import WidgetPredefinedChart from '../WidgetPredefinedChart';
import CustomMetricOverviewChart from '../../Widgets/CustomMetricsWidgets/CustomMetricOverviewChart';
import CustomMetricOverviewChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricOverviewChart';
import { getStartAndEndTimestampsByDensity } from 'Types/dashboard/helper';
interface Props {
metric: any;
isWidget?: boolean
onClick?: () => void;
}
function WidgetChart(props: Props) {
const { isWidget = false, metric } = props;
// const metric = useObserver(() => props.metric);
const { dashboardStore } = useStore();
const period = useObserver(() => dashboardStore.period);
const drillDownFilter = useObserver(() => dashboardStore.drillDownFilter);
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 isOverviewWidget = metric.metricType === 'predefined' && metric.viewType === 'overview';
const params = { density: isOverviewWidget ? 7 : 70 }
const metricParams = { ...params }
const prevMetricRef = useRef<any>();
const [data, setData] = useState<any>(metric.data);
const isTableWidget = metric.metricType === 'table' && metric.viewType === 'table';
const isPieChart = metric.metricType === 'table' && metric.viewType === 'pieChart';
const onChartClick = (event: any) => {
if (event) {
if (isTableWidget || isPieChart) {
const periodTimestamps = period.toTimestamps()
drillDownFilter.merge({
filters: event,
startTimestamp: periodTimestamps.startTimestamp,
endTimestamp: periodTimestamps.endTimestamp,
});
} else {
const payload = event.activePayload[0].payload;
const timestamp = payload.timestamp;
const periodTimestamps = getStartAndEndTimestampsByDensity(timestamp, period.start, period.end, params.density);
drillDownFilter.merge({
startTimestamp: periodTimestamps.startTimestamp,
endTimestamp: periodTimestamps.endTimestamp,
});
}
}
}
useEffect(() => {
if (prevMetricRef.current && prevMetricRef.current.name !== metric.name) {
prevMetricRef.current = metric;
@ -34,8 +61,8 @@ function WidgetChart(props: Props) {
prevMetricRef.current = metric;
setLoading(true);
const data = isWidget ? {} : { ...metricParams, ...metric.toJson() };
dashboardStore.fetchMetricChartData(metric, data, isWidget).then((res: any) => {
const payload = isWidget ? { ...params } : { ...metricParams, ...metric.toJson() };
dashboardStore.fetchMetricChartData(metric, payload, isWidget).then((res: any) => {
setData(res);
}).finally(() => {
setLoading(false);
@ -43,29 +70,29 @@ function WidgetChart(props: Props) {
}, [period]);
const renderChart = () => {
const { metricType, viewType, predefinedKey } = metric;
const { metricType, viewType } = metric;
if (metricType === 'predefined') {
if (viewType === 'overview') {
if (isOverviewWidget) {
return <CustomMetricOverviewChart data={data} />
}
return <WidgetPredefinedChart data={data} predefinedKey={metric.predefinedKey} />
return <WidgetPredefinedChart metric={metric} data={data} predefinedKey={metric.predefinedKey} />
}
if (metricType === 'timeseries') {
if (viewType === 'lineChart') {
return (
<CustomMetriLineChart
data={metric.data}
seriesMap={seriesMap}
data={data}
colors={colors}
params={params}
onClick={onChartClick}
/>
)
} else if (viewType === 'progress') {
return (
<CustomMetricPercentage
data={metric.data[0]}
data={data[0]}
colors={colors}
params={params}
/>
@ -75,14 +102,18 @@ function WidgetChart(props: Props) {
if (metricType === 'table') {
if (viewType === 'table') {
return <CustomMetricTable metric={metric} data={metric.data[0]} />;
return <CustomMetricTable
metric={metric} data={data[0]}
onClick={onChartClick}
/>;
} else if (viewType === 'pieChart') {
return (
<CustomMetricPieChart
metric={metric}
data={metric.data[0]}
data={data[0]}
colors={colors}
params={params}
onClick={onChartClick}
/>
)
}

View file

@ -20,7 +20,8 @@ interface Props {
function WidgetForm(props: Props) {
const [showDashboardSelectionModal, setShowDashboardSelectionModal] = useState(false);
const { history, match: { params: { siteId, dashboardId, metricId } } } = props;
const { metricStore } = useStore();
const { metricStore, dashboardStore } = useStore();
const dashboards = dashboardStore.dashboards;
const isSaving = useObserver(() => metricStore.isSaving);
const metric: any = useObserver(() => metricStore.instance);
@ -29,6 +30,7 @@ function WidgetForm(props: Props) {
const isTable = metric.metricType === 'table';
const isTimeSeries = metric.metricType === 'timeseries';
const _issueOptions = [{ text: 'All', value: 'all' }].concat(issueOptions);
const canAddToDashboard = metric.exists() && dashboards.length > 0;
const write = ({ target: { value, name } }) => metricStore.merge({ [ name ]: value });
const writeOption = (e, { value, name }) => {
@ -193,7 +195,12 @@ function WidgetForm(props: Props) {
<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)}>
<Button
plain size="small"
className="flex items-center ml-2"
onClick={() => setShowDashboardSelectionModal(true)}
disabled={!canAddToDashboard}
>
<Icon name="columns-gap" size="14" className="mr-2" color="teal"/>
Add to Dashboard
</Button>
@ -201,13 +208,13 @@ function WidgetForm(props: Props) {
)}
</div>
</div>
<DashboardSelectionModal
metricId={metric.metricId}
show={showDashboardSelectionModal}
closeHandler={() => setShowDashboardSelectionModal(false)}
/>
{ canAddToDashboard && (
<DashboardSelectionModal
metricId={metric.metricId}
show={showDashboardSelectionModal}
closeHandler={() => setShowDashboardSelectionModal(false)}
/>
)}
</div>
));
}

View file

@ -22,65 +22,77 @@ import ResourceLoadingTime from 'App/components/Dashboard/Widgets/PredefinedWidg
import BreakdownOfLoadedResources from 'App/components/Dashboard/Widgets/PredefinedWidgets/BreakdownOfLoadedResources';
import MissingResources from 'App/components/Dashboard/Widgets/PredefinedWidgets/MissingResources';
import ResourceLoadedVsResponseEnd from 'App/components/Dashboard/Widgets/PredefinedWidgets/ResourceLoadedVsResponseEnd';
import SessionsPerBrowser from 'App/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser';
import CallWithErrors from '../../Widgets/PredefinedWidgets/CallWithErrors';
import SpeedIndexByLocation from '../../Widgets/PredefinedWidgets/SpeedIndexByLocation';
import SlowestResources from '../../Widgets/PredefinedWidgets/SlowestResources';
import ResponseTimeDistribution from '../../Widgets/PredefinedWidgets/ResponseTimeDistribution';
interface Props {
data: any;
predefinedKey: string
metric?: any;
}
function WidgetPredefinedChart(props: Props) {
const { data, predefinedKey } = props;
const { data, predefinedKey, metric } = props;
const renderWidget = () => {
switch (predefinedKey) {
// ERRORS
case 'errors_per_type':
return <ErrorsByType data={data} />
return <ErrorsByType data={data} metric={metric} />
case 'errors_per_domains':
return <ErrorsPerDomain data={data} />
return <ErrorsPerDomain data={data} metric={metric} />
case 'resources_by_party':
return <ErrorsByOrigin data={data} />
return <ErrorsByOrigin data={data} metric={metric} />
case 'impacted_sessions_by_js_errors':
return <SessionsAffectedByJSErrors data={data} />
return <SessionsAffectedByJSErrors data={data} metric={metric} />
case 'domains_errors_4xx':
return <CallsErrors4xx data={data} />
return <CallsErrors4xx data={data} metric={metric} />
case 'domains_errors_5xx':
return <CallsErrors5xx data={data} />
return <CallsErrors5xx data={data} metric={metric} />
case 'calls_errors':
return <CallWithErrors data={data} metric={metric} />
// PERFORMANCE
// case 'impacted_sessions_by_slow_pages':
// case 'pages_response_time_distribution':
// case 'speed_location':
case 'impacted_sessions_by_slow_pages':
return <SessionsImpactedBySlowRequests data={data} metric={metric} />
case 'pages_response_time_distribution':
return <ResponseTimeDistribution data={data} metric={metric} />
case 'speed_location':
return <SpeedIndexByLocation metric={metric} />
case 'cpu':
return <CPULoad data={data} />
return <CPULoad data={data} metric={metric} />
case 'crashes':
return <Crashes data={data} />
return <Crashes data={data} metric={metric} />
case 'pages_dom_buildtime':
return <DomBuildingTime data={data} />
return <DomBuildingTime data={data} metric={metric} />
case 'fps':
return <FPS data={data} />
return <FPS data={data} metric={metric} />
case 'memory_consumption':
return <MemoryConsumption data={data} />
return <MemoryConsumption data={data} metric={metric} />
case 'pages_response_time':
return <ResponseTime data={data} />
return <ResponseTime data={data} metric={metric} />
case 'resources_vs_visually_complete':
return <ResourceLoadedVsVisuallyComplete data={data} />
return <ResourceLoadedVsVisuallyComplete data={data} metric={metric} />
case 'sessions_per_browser':
return <SessionsImpactedBySlowRequests data={data} />
return <SessionsPerBrowser data={data} metric={metric} />
case 'slowest_domains':
return <SlowestDomains data={data} />
return <SlowestDomains data={data} metric={metric} />
case 'time_to_render':
return <TimeToRender data={data} />
return <TimeToRender data={data} metric={metric} />
// Resources
case 'resources_count_by_type':
return <BreakdownOfLoadedResources data={data} />
return <BreakdownOfLoadedResources data={data} metric={metric} />
case 'missing_resources':
return <MissingResources data={data} />
return <MissingResources data={data} metric={metric} />
case 'resource_type_vs_response_end':
return <ResourceLoadedVsResponseEnd data={data} />
return <ResourceLoadedVsResponseEnd data={data} metric={metric} />
case 'resources_loading_time':
return <ResourceLoadingTime data={data} />
// case 'slowest_resources':
return <ResourceLoadingTime data={data} metric={metric} />
case 'slowest_resources':
return <SlowestResources data={data} metric={metric} />
default:
return <div className="h-40 color-red">Widget not supported</div>

View file

@ -11,7 +11,8 @@ interface Props {
}
function WidgetPreview(props: Props) {
const { className = '' } = props;
const { metricStore } = useStore();
const { metricStore, dashboardStore } = useStore();
const period = useObserver(() => dashboardStore.period);
const metric: any = useObserver(() => metricStore.instance);
const isTimeSeries = metric.metricType === 'timeseries';
const isTable = metric.metricType === 'table';
@ -20,11 +21,6 @@ function WidgetPreview(props: Props) {
metric.update({ [ name ]: value });
}
const onDateChange = (changedDates) => {
// setPeriod({ ...changedDates, rangeName: changedDates.rangeValue })
metric.update({ ...changedDates, rangeName: changedDates.rangeValue });
}
return useObserver(() => (
<div className={cn(className)}>
<div className="flex items-center justify-between">
@ -68,10 +64,10 @@ function WidgetPreview(props: Props) {
<div className="mx-4" />
<span className="mr-1 color-gray-medium">Time Range</span>
<DateRange
rangeValue={metric.rangeName}
startDate={metric.startDate}
endDate={metric.endDate}
onDateChange={onDateChange}
rangeValue={period.rangeName}
startDate={period.startDate}
endDate={period.endDate}
onDateChange={(period) => dashboardStore.setPeriod(period)}
customRangeRight
direction="left"
/>

View file

@ -1,37 +1,106 @@
import React from 'react';
import { NoContent } from 'UI';
import React, { useEffect, useState } from 'react';
import { NoContent, Dropdown, Icon, Loader } from 'UI';
import cn from 'classnames';
import { useStore } from 'App/mstore';
import SessionItem from 'Shared/SessionItem';
import { observer, useObserver } from 'mobx-react-lite';
import { DateTime } from 'luxon';
interface Props {
className?: string;
}
function WidgetSessions(props: Props) {
const { className = '' } = props;
const { dashboardStore } = useStore();
const widget = dashboardStore.currentWidget;
const [data, setData] = useState<any>([]);
const [seriesOptions, setSeriesOptions] = useState([
{ text: 'All', value: 'all' },
]);
return (
const [activeSeries, setActiveSeries] = useState('all');
const writeOption = (e, { name, value }) => setActiveSeries(value);
useEffect(() => {
if (!data) return;
const seriesOptions = data.map(item => ({
text: item.seriesName,
value: item.seriesId,
}));
setSeriesOptions([
{ text: 'All', value: 'all' },
...seriesOptions,
]);
}, [data]);
const filteredSessions = getListSessionsBySeries(data, activeSeries);
const { dashboardStore, metricStore } = useStore();
const filter = useObserver(() => dashboardStore.drillDownFilter);
const widget: any = metricStore.instance;
const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat('LLL dd, yyyy HH:mm a');
const endTime = DateTime.fromMillis(filter.endTimestamp).toFormat('LLL dd, yyyy HH:mm a');
useEffect(() => {
widget.fetchSessions({ ...filter, filter: widget.toJsonDrilldown()}).then(res => {
console.log('res', res)
setData(res);
});
}, [filter.startTimestamp, filter.endTimestamp, filter.filters]);
return useObserver(() => (
<div className={cn(className)}>
<div>
<h2 className="text-2xl">Sessions</h2>
{/* <div className="mr-auto">Showing all sessions between <span className="font-medium">{startTime}</span> and <span className="font-medium">{endTime}</span> </div> */}
<div className="flex items-center justify-between">
<div className="flex items-baseline">
<h2 className="text-2xl">Sessions</h2>
<div className="ml-2 color-gray-medium">between <span className="font-medium color-gray-darkest">{startTime}</span> and <span className="font-medium color-gray-darkest">{endTime}</span> </div>
</div>
{ widget.metricType !== 'table' && (
<div className="flex items-center ml-6">
<span className="mr-2 color-gray-medium">Series</span>
<Dropdown
// className={stl.dropdown}
className="font-medium flex items-center hover:bg-gray-light rounded px-2 py-1"
direction="left"
options={ seriesOptions }
name="change"
value={ activeSeries }
onChange={ writeOption }
id="change-dropdown"
// icon={null}
icon={ <Icon name="chevron-down" color="gray-dark" size="14" className="ml-2" /> }
/>
</div>
)}
</div>
<div className="mt-3">
<NoContent
title="No recordings found"
show={widget.sessions.length === 0}
icon="exclamation-circle"
>
{widget.sessions.map((session: any) => (
<SessionItem key={ session.sessionId } session={ session } />
))}
</NoContent>
<Loader loading={widget.sessionsLoading}>
<NoContent
title="No recordings found"
show={filteredSessions.length === 0}
animatedIcon="no-results"
>
{filteredSessions.map((session: any) => (
<SessionItem key={ session.sessionId } session={ session } />
))}
</NoContent>
</Loader>
</div>
</div>
);
));
}
export default WidgetSessions;
const getListSessionsBySeries = (data, seriesId) => {
const arr: any = []
data.forEach(element => {
if (seriesId === 'all') {
const sessionIds = arr.map(i => i.sessionId);
arr.push(...element.sessions.filter(i => !sessionIds.includes(i.sessionId)));
} else {
if (element.seriesId === seriesId) {
arr.push(...element.sessions)
}
}
});
return arr;
}
export default observer(WidgetSessions);

View file

@ -38,7 +38,7 @@ function WidgetView(props: Props) {
return useObserver(() => (
<Loader loading={loading}>
<div className="relative">
<div className="relative pb-10">
<BackLink onClick={onBackHandler} vertical className="absolute" style={{ left: '-50px', top: '0px' }} />
<div className="bg-white rounded border">
<div className="p-4 flex justify-between items-center">
@ -54,7 +54,7 @@ function WidgetView(props: Props) {
onClick={() => setExpanded(!expanded)}
className="flex items-center cursor-pointer select-none"
>
<span className="mr-2 color-teal">{expanded ? 'Collapse' : 'Expand'}</span>
<span className="mr-2 color-teal">{expanded ? 'Close' : 'Edit'}</span>
<Icon name={expanded ? 'chevron-up' : 'chevron-down'} size="16" color="teal" />
</div>
</div>

View file

@ -0,0 +1,28 @@
import React from 'react';
import { connect } from 'react-redux';
import WidgetIcon from './WidgetIcon';
import { init as initAlert } from 'Duck/alerts';
import { useStore } from 'App/mstore';
interface Props {
seriesId: string;
initAlert: Function;
}
function AlertButton(props: Props) {
const { seriesId, initAlert } = props;
const { dashboardStore } = useStore();
const onClick = () => {
initAlert({ query: { left: seriesId }})
dashboardStore.updateKey('showAlertModal', true);
}
return (
<WidgetIcon
className="cursor-pointer"
icon="bell-plus"
tooltip="Set Alert"
onClick={onClick}
/>
);
}
export default connect(null, { initAlert })(AlertButton);

View file

@ -0,0 +1,19 @@
import React from 'react';
import { Tooltip } from 'react-tippy';
function TemplateOverlay() {
return (
<div>
<Tooltip
title="Click to select"
trigger="mouseenter"
hideOnClick={true}
delay={300}
>
<div className="absolute inset-0 cursor-pointer z-10" />
</Tooltip>
</div>
);
}
export default TemplateOverlay;

View file

@ -0,0 +1,27 @@
import React from 'react';
import { Icon } from 'UI';
import { Tooltip } from 'react-tippy';
interface Props {
className: string
onClick: () => void
icon: string
tooltip: string
}
function WidgetIcon(props: Props) {
const { className, onClick, icon, tooltip } = props;
return (
<Tooltip
arrow
size="small"
title={tooltip}
position="top"
>
<div className={className} onClick={onClick}>
<Icon name={icon} size="14" />
</div>
</Tooltip>
);
}
export default WidgetIcon;

View file

@ -4,11 +4,14 @@ import { ItemMenu } from 'UI';
import { useDrag, useDrop } from 'react-dnd';
import WidgetChart from '../WidgetChart';
import { useObserver } from 'mobx-react-lite';
import { confirm } from 'UI/Confirmation';
// import { confirm } from 'UI/Confirmation';
import { useStore } from 'App/mstore';
import LazyLoad from 'react-lazyload';
import { withRouter } from 'react-router-dom';
import { withSiteId, dashboardMetricDetails } from 'App/routes';
import TemplateOverlay from './TemplateOverlay';
import WidgetIcon from './WidgetIcon';
import AlertButton from './AlertButton';
interface Props {
className?: string;
@ -27,7 +30,8 @@ interface Props {
function WidgetWrapper(props: Props) {
const { dashboardStore } = useStore();
const { isWidget = false, active = false, index = 0, moveListItem = null, isPreview = false, isTemplate = false, dashboardId, siteId } = props;
const widget = useObserver(() => props.widget);
const widget: any = useObserver(() => props.widget);
const isPredefined = widget.metricType === 'predefined';
const [{ opacity, isDragging }, dragRef] = useDrag({
type: 'item',
@ -40,7 +44,6 @@ function WidgetWrapper(props: Props) {
const [{ isOver, canDrop }, dropRef] = useDrop({
accept: 'item',
drop: (item: any) => {
if (item.index === index) return;
moveListItem(item.index, index);
@ -52,21 +55,19 @@ function WidgetWrapper(props: Props) {
})
const onDelete = async () => {
if (await confirm({
header: 'Confirm',
confirmButton: 'Yes, delete',
confirmation: `Are you sure you want to permanently delete the widget from this dashboard?`
})) {
dashboardStore.deleteDashboardWidget(dashboardId!, widget.widgetId);
}
}
const editHandler = () => {
console.log('clicked', widget.metricId);
dashboardStore.deleteDashboardWidget(dashboardId!, widget.widgetId);
// if (await confirm({
// header: 'Confirm',
// confirmButton: 'Yes, delete',
// confirmation: `Are you sure you want to permanently delete the widget from this dashboard?`
// })) {
// dashboardStore.deleteDashboardWidget(dashboardId!, widget.widgetId);
// }
}
const onChartClick = () => {
if (!isWidget || widget.metricType === 'predefined') return;
if (!isWidget || isPredefined) return;
props.history.push(withSiteId(dashboardMetricDetails(dashboardId, widget.metricId),siteId));
}
@ -75,9 +76,8 @@ function WidgetWrapper(props: Props) {
return useObserver(() => (
<div
className={cn("rounded bg-white border", 'col-span-' + widget.config.col)}
className={cn("relative rounded bg-white border", 'col-span-' + widget.config.col, { "cursor-pointer" : isTemplate })}
style={{
// borderColor: 'transparent'
userSelect: 'none',
opacity: isDragging ? 0.5 : 1,
borderColor: (canDrop && isOver) || active ? '#394EFF' : (isPreview ? 'transparent' : '#EEEEEE'),
@ -85,20 +85,29 @@ function WidgetWrapper(props: Props) {
ref={dragDropRef}
onClick={props.onClick ? props.onClick : () => {}}
>
{isTemplate && <TemplateOverlay />}
<div
className="p-3 cursor-move flex items-center justify-between"
className={cn("p-3 flex items-center justify-between", { "cursor-move" : !isTemplate })}
>
<h3 className="capitalize">{widget.name}</h3>
{isWidget && (
<div>
<div className="flex items-center">
{!isPredefined && (
<>
<AlertButton seriesId={widget.series[0] && widget.series[0].seriesId} />
<div className='mx-2'/>
</>
)}
<ItemMenu
items={[
{
text: 'Edit', onClick: editHandler,
text: 'Edit', onClick: onChartClick,
disabled: widget.metricType === 'predefined',
disabledMessage: 'Cannot edit system generated metrics'
},
{
text: 'Hide from view',
text: 'Remove from view',
onClick: onDelete
},
]}
@ -107,7 +116,7 @@ function WidgetWrapper(props: Props) {
)}
</div>
<LazyLoad height={300} offset={320} >
<LazyLoad height={100} offset={120} >
<div className="px-4" onClick={onChartClick}>
<WidgetChart metric={widget} isWidget={isWidget} />
</div>

View file

@ -67,7 +67,7 @@ export default class ErrorInfo extends React.PureComponent {
<NoContent
title="No Error Found!"
subtext="Please try to find existing one."
icon="exclamation-circle"
animatedIcon="no-results"
show={ !loading && errorIdInStore == null }
>
<div className="w-9/12 mb-4 flex justify-between">

View file

@ -219,7 +219,7 @@ export default class List extends React.PureComponent {
<NoContent
title="No Errors Found!"
subtext="Please try to change your search parameters."
icon="exclamation-circle"
animatedIcon="empty-state"
show={ !loading && list.size === 0}
>
<Loader loading={ loading }>

View file

@ -143,7 +143,7 @@ export default connect((state, props) => {
funnelId: props.match.params.funnelId,
activeStages: state.getIn(['funnels', 'activeStages']),
funnelFilters: state.getIn(['funnels', 'funnelFilters']),
siteId: state.getIn([ 'user', 'siteId' ]),
siteId: state.getIn([ 'site', 'siteId' ]),
liveFilters: state.getIn(['funnelFilters', 'appliedFilter']),
}
}, {

View file

@ -33,5 +33,5 @@ function FunnelDropdown(props) {
export default connect((state, props) => ({
funnels: state.getIn(['funnels', 'list']),
funnel: state.getIn(['funnels', 'instance']),
siteId: state.getIn([ 'user', 'siteId' ]),
siteId: state.getIn([ 'site', 'siteId' ]),
}), { })(withRouter(FunnelDropdown))

View file

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { Icon, BackLink, IconButton, Dropdown, Popup, TextEllipsis, Button } from 'UI';
import { remove as deleteFunnel, fetch, fetchInsights, fetchIssuesFiltered, fetchSessionsFiltered } from 'Duck/funnels';
import { editFilter, refresh, addFilter } from 'Duck/funnels';
import { editFilter, editFunnelFilter, refresh, addFilter } from 'Duck/funnels';
import DateRange from 'Shared/DateRange';
import { connect } from 'react-redux';
import { confirm } from 'UI/Confirmation';
@ -18,7 +18,7 @@ const Info = ({ label = '', value = '', className = 'mx-4' }) => {
}
const FunnelHeader = (props) => {
const { funnel, insights, funnels, onBack, funnelId, showFilters = false, renameHandler } = props;
const { funnel, insights, funnels, onBack, funnelId, showFilters = false, funnelFilters, renameHandler } = props;
const [showSaveModal, setShowSaveModal] = useState(false)
const writeOption = (e, { name, value }) => {
@ -40,7 +40,7 @@ const FunnelHeader = (props) => {
}
const onDateChange = (e) => {
props.editFilter(e, funnelId);
props.editFunnelFilter(e, funnelId);
}
const options = funnels.map(({ funnelId, name }) => ({ text: name, value: funnelId })).toJS();
@ -55,7 +55,7 @@ const FunnelHeader = (props) => {
show={showSaveModal}
closeHandler={() => setShowSaveModal(false)}
/>
<div className="flex items-center mr-auto relative">
<div className="flex items-center mr-auto relative">
<Dropdown
scrolling
trigger={
@ -96,9 +96,9 @@ const FunnelHeader = (props) => {
/>
</div>
<DateRange
rangeValue={funnel.filter.rangeValue}
startDate={funnel.filter.startDate}
endDate={funnel.filter.endDate}
rangeValue={funnelFilters.rangeValue}
startDate={funnelFilters.startDate}
endDate={funnelFilters.endDate}
onDateChange={onDateChange}
customRangeRight
/>
@ -109,5 +109,6 @@ const FunnelHeader = (props) => {
}
export default connect(state => ({
funnelFilters: state.getIn([ 'funnels', 'funnelFilters' ]).toJS(),
funnel: state.getIn([ 'funnels', 'instance' ]),
}), { editFilter, deleteFunnel, fetch, fetchInsights, fetchIssuesFiltered, fetchSessionsFiltered, refresh })(FunnelHeader)
}), { editFilter, editFunnelFilter, deleteFunnel, fetch, fetchInsights, fetchIssuesFiltered, fetchSessionsFiltered, refresh })(FunnelHeader)

View file

@ -39,5 +39,5 @@ export default connect((state, props) => ({
issue: state.getIn(['funnels', 'issue']),
issueId: props.match.params.issueId,
funnelId: props.match.params.funnelId,
siteId: state.getIn([ 'user', 'siteId' ]),
siteId: state.getIn([ 'site', 'siteId' ]),
}), { fetchIssue, setNavRef, resetIssue })(withRouter(FunnelIssueDetails))

View file

@ -44,7 +44,7 @@ function FunnelIssues(props) {
<NoContent
title="No Issues Found!"
subtext="Please try changing your search parameters."
icon="exclamation-circle"
animatedIcon="no-results"
show={ !loading && filteredList.size === 0}
>
{ filteredList.take(displayedCount).map(issue => (
@ -73,7 +73,7 @@ export default connect(state => ({
list: state.getIn(['funnels', 'issues']),
criticalIssuesCount: state.getIn(['funnels', 'criticalIssuesCount']),
loading: state.getIn(['funnels', 'fetchIssuesRequest', 'loading']),
siteId: state.getIn([ 'user', 'siteId' ]),
siteId: state.getIn([ 'site', 'siteId' ]),
funnel: state.getIn(['funnels', 'instance']),
activeStages: state.getIn(['funnels', 'activeStages']),
funnelFilters: state.getIn(['funnels', 'funnelFilters']),

View file

@ -28,5 +28,5 @@ function FunnelList(props) {
export default connect(state => ({
list: state.getIn(['funnels', 'list']),
siteId: state.getIn([ 'user', 'siteId' ]),
siteId: state.getIn([ 'site', 'siteId' ]),
}))(withRouter(FunnelList))

View file

@ -30,7 +30,7 @@ function FunnelSessionList(props) {
<NoContent
title="No recordings found!"
subtext="Please try changing your search parameters."
icon="exclamation-circle"
animatedIcon="no-results"
show={ list.size === 0}
>
{ list.take(displayedCount).map(session => (

View file

@ -151,7 +151,7 @@ export default withRouter(connect(
state => ({
account: state.getIn([ 'user', 'account' ]),
appearance: state.getIn([ 'user', 'account', 'appearance' ]),
siteId: state.getIn([ 'user', 'siteId' ]),
siteId: state.getIn([ 'site', 'siteId' ]),
sites: state.getIn([ 'site', 'list' ]),
showAlerts: state.getIn([ 'dashboard', 'showAlerts' ]),
boardingCompletion: state.getIn([ 'dashboard', 'boardingCompletion' ])

View file

@ -37,7 +37,7 @@ const styles = {
};
@connect(state => ({
siteId: state.getIn([ 'user', 'siteId' ]),
siteId: state.getIn([ 'site', 'siteId' ]),
boarding: state.getIn([ 'dashboard', 'boarding' ]),
boardingCompletion: state.getIn([ 'dashboard', 'boardingCompletion' ]),
}), {

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { setSiteId } from 'Duck/user';
import { setSiteId } from 'Duck/site';
import { withRouter } from 'react-router-dom';
import { hasSiteId, siteChangeAvaliable } from 'App/routes';
import { STATUS_COLOR_MAP, GREEN } from 'Types/site';
@ -13,11 +13,13 @@ import { clearSearch } from 'Duck/search';
import { fetchList as fetchIntegrationVariables } from 'Duck/customField';
import { fetchList as fetchAlerts } from 'Duck/alerts';
import { fetchWatchdogStatus } from 'Duck/watchdogs';
import { withStore } from 'App/mstore'
@withStore
@withRouter
@connect(state => ({
sites: state.getIn([ 'site', 'list' ]),
siteId: state.getIn([ 'user', 'siteId' ]),
siteId: state.getIn([ 'site', 'siteId' ]),
account: state.getIn([ 'user', 'account' ]),
}), {
setSiteId,
@ -45,11 +47,16 @@ export default class SiteDropdown extends React.PureComponent {
}
switchSite = (siteId) => {
const { mstore } = this.props
this.props.setSiteId(siteId);
this.props.clearSearch();
this.props.fetchIntegrationVariables();
this.props.fetchAlerts();
this.props.fetchWatchdogStatus();
mstore.initClient();
}
render() {

View file

@ -1,5 +1,4 @@
import React from 'react';
import { ModalContext } from "App/components/Modal/modalContext";
import { useModal } from 'App/components/Modal';
import stl from './ModalOverlay.css'
@ -7,7 +6,7 @@ function ModalOverlay({ children }) {
let modal = useModal();
return (
<div className="fixed w-full h-screen" style={{ zIndex: '99999' }}>
<div className="fixed w-full h-screen" style={{ zIndex: '999' }}>
<div
onClick={() => modal.hideModal()}
className={stl.overlay}

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