commit
d69b6220b3
160 changed files with 2373 additions and 720 deletions
2
.github/workflows/frontend.yaml
vendored
2
.github/workflows/frontend.yaml
vendored
|
|
@ -3,7 +3,7 @@ on:
|
|||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- api-v1.5.5
|
||||
paths:
|
||||
- frontend/**
|
||||
|
||||
|
|
|
|||
|
|
@ -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 } />
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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']),
|
||||
}), {
|
||||
|
|
|
|||
|
|
@ -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 }>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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']),
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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 => (
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
& .tabContent {
|
||||
background-color: white;
|
||||
padding: 25px;
|
||||
margin-top: -30px;
|
||||
/* margin-top: -30px; */
|
||||
margin-right: -20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ function CustomMetricWidget(props: Props) {
|
|||
<CustomMetriLineChart
|
||||
data={ data }
|
||||
params={ params }
|
||||
seriesMap={ seriesMap }
|
||||
// seriesMap={ seriesMap }
|
||||
colors={ colors }
|
||||
onClick={ clickHandler }
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ function CustomMetricWidget(props: Props) {
|
|||
{ metric.viewType === 'lineChart' && (
|
||||
<CustomMetriLineChart
|
||||
data={data}
|
||||
seriesMap={seriesMap}
|
||||
// seriesMap={seriesMap}
|
||||
colors={colors}
|
||||
params={params}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
16
frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors/Chart.js
vendored
Normal file
16
frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors/Chart.js
vendored
Normal 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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CallWithErrors'
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ResponseTimeDistribution'
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SessionsPerBrowser'
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
15
frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestResources/Chart.js
vendored
Normal file
15
frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestResources/Chart.js
vendored
Normal 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;
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SlowestResources'
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SpeedIndexByLocation'
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
));
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
@ -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));
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 }>
|
||||
|
|
|
|||
|
|
@ -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']),
|
||||
}
|
||||
}, {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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']),
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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 => (
|
||||
|
|
|
|||
|
|
@ -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' ])
|
||||
|
|
|
|||
|
|
@ -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' ]),
|
||||
}), {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue