Merge branch 'dashboard' into api-v1.5.5
This commit is contained in:
commit
6c4664caa7
171 changed files with 32412 additions and 1203 deletions
|
|
@ -69,7 +69,7 @@ For those who want to simply use OpenReplay as a service, [sign up](https://app.
|
|||
|
||||
Please refer to the [official OpenReplay documentation](https://docs.openreplay.com/). That should help you troubleshoot common issues. For additional help, you can reach out to us on one of these channels:
|
||||
|
||||
- [Slack](https://slack.openreplay.com) (Connect with our engineers and community)
|
||||
- [Discord](https://discord.openreplay.com) (Connect with our engineers and community)
|
||||
- [GitHub](https://github.com/openreplay/openreplay/issues) (Bug and issue reports)
|
||||
- [Twitter](https://twitter.com/OpenReplayHQ) (Product updates, Great content)
|
||||
- [Website chat](https://openreplay.com) (Talk to us)
|
||||
|
|
@ -80,7 +80,7 @@ We're always on the lookout for contributions to OpenReplay, and we're glad you'
|
|||
|
||||
See our [Contributing Guide](CONTRIBUTING.md) for more details.
|
||||
|
||||
Also, feel free to join our [Slack](https://slack.openreplay.com) to ask questions, discuss ideas or connect with our contributors.
|
||||
Also, feel free to join our [Discord](https://discord.openreplay.com) to ask questions, discuss ideas or connect with our contributors.
|
||||
|
||||
## Roadmap
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React, { lazy, Suspense } from 'react';
|
||||
import { Switch, Route, Redirect } from 'react-router';
|
||||
import { BrowserRouter, withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
|
|
@ -5,26 +6,29 @@ import { Notification } from 'UI';
|
|||
import { Loader } from 'UI';
|
||||
import { fetchUserInfo } from 'Duck/user';
|
||||
import withSiteIdUpdater from 'HOCs/withSiteIdUpdater';
|
||||
import Login from 'Components/Login/Login';
|
||||
import ForgotPassword from 'Components/ForgotPassword/ForgotPassword';
|
||||
import UpdatePassword from 'Components/UpdatePassword/UpdatePassword';
|
||||
import ClientPure from 'Components/Client/Client';
|
||||
import OnboardingPure from 'Components/Onboarding/Onboarding';
|
||||
import SessionPure from 'Components/Session/Session';
|
||||
import LiveSessionPure from 'Components/Session/LiveSession';
|
||||
import AssistPure from 'Components/Assist';
|
||||
import BugFinderPure from 'Components/BugFinder/BugFinder';
|
||||
import DashboardPure from 'Components/Dashboard/Dashboard';
|
||||
import ErrorsPure from 'Components/Errors/Errors';
|
||||
const Login = lazy(() => import('Components/Login/Login'));
|
||||
const ForgotPassword = lazy(() => import('Components/ForgotPassword/ForgotPassword'));
|
||||
const UpdatePassword = lazy(() => import('Components/UpdatePassword/UpdatePassword'));
|
||||
const SessionPure = lazy(() => import('Components/Session/Session'));
|
||||
const LiveSessionPure = lazy(() => import('Components/Session/LiveSession'));
|
||||
const OnboardingPure = lazy(() => import('Components/Onboarding/Onboarding'));
|
||||
const ClientPure = lazy(() => import('Components/Client/Client'));
|
||||
const AssistPure = lazy(() => import('Components/Assist'));
|
||||
const BugFinderPure = lazy(() => import('Components/BugFinder/BugFinder'));
|
||||
const DashboardPure = lazy(() => import('Components/Dashboard/NewDashboard'));
|
||||
const ErrorsPure = lazy(() => import('Components/Errors/Errors'));
|
||||
const FunnelDetails = lazy(() => import('Components/Funnels/FunnelDetails'));
|
||||
const FunnelIssueDetails = lazy(() => import('Components/Funnels/FunnelIssueDetails'));
|
||||
import WidgetViewPure from 'Components/Dashboard/components/WidgetView';
|
||||
import Header from 'Components/Header/Header';
|
||||
// import ResultsModal from 'Shared/Results/ResultsModal';
|
||||
import FunnelDetails from 'Components/Funnels/FunnelDetails';
|
||||
import FunnelIssueDetails from 'Components/Funnels/FunnelIssueDetails';
|
||||
import { fetchList as fetchIntegrationVariables } from 'Duck/customField';
|
||||
import { fetchList as fetchSiteList } from 'Duck/site';
|
||||
import { fetchList as fetchAnnouncements } from 'Duck/announcements';
|
||||
import { fetchList as fetchAlerts } from 'Duck/alerts';
|
||||
import { fetchWatchdogStatus } from 'Duck/watchdogs';
|
||||
import { dashboardService } from "App/services";
|
||||
import { withStore } from 'App/mstore'
|
||||
|
||||
import APIClient from './api_client';
|
||||
import * as routes from './routes';
|
||||
|
|
@ -32,9 +36,12 @@ import { OB_DEFAULT_TAB } from 'App/routes';
|
|||
import Signup from './components/Signup/Signup';
|
||||
import { fetchTenants } from 'Duck/user';
|
||||
import { setSessionPath } from 'Duck/sessions';
|
||||
import { ModalProvider } from './components/Modal';
|
||||
import ModalRoot from './components/Modal/ModalRoot';
|
||||
|
||||
const BugFinder = withSiteIdUpdater(BugFinderPure);
|
||||
const Dashboard = withSiteIdUpdater(DashboardPure);
|
||||
const WidgetView = withSiteIdUpdater(WidgetViewPure);
|
||||
const Session = withSiteIdUpdater(SessionPure);
|
||||
const LiveSession = withSiteIdUpdater(LiveSessionPure);
|
||||
const Assist = withSiteIdUpdater(AssistPure);
|
||||
|
|
@ -46,7 +53,15 @@ const FunnelIssue = withSiteIdUpdater(FunnelIssueDetails);
|
|||
const withSiteId = routes.withSiteId;
|
||||
const withObTab = routes.withObTab;
|
||||
|
||||
const METRICS_PATH = routes.metrics();
|
||||
const METRICS_DETAILS = routes.metricDetails();
|
||||
|
||||
const DASHBOARD_PATH = routes.dashboard();
|
||||
const DASHBOARD_SELECT_PATH = routes.dashboardSelected();
|
||||
const DASHBOARD_METRIC_CREATE_PATH = routes.dashboardMetricCreate();
|
||||
const DASHBOARD_METRIC_DETAILS_PATH = routes.dashboardMetricDetails();
|
||||
|
||||
// const WIDGET_PATAH = routes.dashboardMetric();
|
||||
const SESSIONS_PATH = routes.sessions();
|
||||
const ASSIST_PATH = routes.assist();
|
||||
const ERRORS_PATH = routes.errors();
|
||||
|
|
@ -62,6 +77,7 @@ const CLIENT_PATH = routes.client();
|
|||
const ONBOARDING_PATH = routes.onboarding();
|
||||
const ONBOARDING_REDIRECT_PATH = routes.onboarding(OB_DEFAULT_TAB);
|
||||
|
||||
@withStore
|
||||
@withRouter
|
||||
@connect((state) => {
|
||||
const siteId = state.getIn([ 'user', 'siteId' ]);
|
||||
|
|
@ -108,6 +124,8 @@ class Router extends React.Component {
|
|||
fetchInitialData = () => {
|
||||
Promise.all([
|
||||
this.props.fetchUserInfo().then(() => {
|
||||
const { mstore } = this.props
|
||||
mstore.initClient();
|
||||
this.props.fetchIntegrationVariables()
|
||||
}),
|
||||
this.props.fetchSiteList().then(() => {
|
||||
|
|
@ -153,54 +171,78 @@ class Router extends React.Component {
|
|||
{!hideHeader && <Header key="header"/>}
|
||||
<Notification />
|
||||
|
||||
<Switch key="content" >
|
||||
<Route path={ CLIENT_PATH } component={ Client } />
|
||||
<Route path={ withSiteId(ONBOARDING_PATH, siteIdList)} component={ Onboarding } />
|
||||
<Route
|
||||
path="/integrations/"
|
||||
render={
|
||||
({ location }) => {
|
||||
const client = new APIClient(jwt);
|
||||
switch (location.pathname) {
|
||||
case '/integrations/slack':
|
||||
client.post('integrations/slack/add', {
|
||||
code: location.search.split('=')[ 1 ],
|
||||
state: tenantId,
|
||||
});
|
||||
break;
|
||||
<Suspense fallback={<Loader loading={true} className="flex-1" />}>
|
||||
<ModalProvider>
|
||||
<ModalRoot />
|
||||
<Switch key="content" >
|
||||
<Route path={ CLIENT_PATH } component={ Client } />
|
||||
<Route path={ withSiteId(ONBOARDING_PATH, siteIdList)} component={ Onboarding } />
|
||||
<Route
|
||||
path="/integrations/"
|
||||
render={
|
||||
({ location }) => {
|
||||
const client = new APIClient(jwt);
|
||||
switch (location.pathname) {
|
||||
case '/integrations/slack':
|
||||
client.post('integrations/slack/add', {
|
||||
code: location.search.split('=')[ 1 ],
|
||||
state: tenantId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
return <Redirect to={ CLIENT_PATH } />;
|
||||
}
|
||||
return <Redirect to={ CLIENT_PATH } />;
|
||||
}
|
||||
}
|
||||
/>
|
||||
{ onboarding &&
|
||||
<Redirect to={ withSiteId(ONBOARDING_REDIRECT_PATH, siteId)} />
|
||||
}
|
||||
{ siteIdList.length === 0 &&
|
||||
<Redirect to={ routes.client(routes.CLIENT_TABS.SITES) } />
|
||||
}
|
||||
<Route exact strict path={ withSiteId(DASHBOARD_PATH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(ASSIST_PATH, siteIdList) } component={ Assist } />
|
||||
<Route exact strict path={ withSiteId(ERRORS_PATH, siteIdList) } component={ Errors } />
|
||||
<Route exact strict path={ withSiteId(ERROR_PATH, siteIdList) } component={ Errors } />
|
||||
<Route exact strict path={ withSiteId(FUNNEL_PATH, siteIdList) } component={ Funnels } />
|
||||
<Route exact strict path={ withSiteId(FUNNEL_ISSUE_PATH, siteIdList) } component={ FunnelIssue } />
|
||||
<Route exact strict path={ withSiteId(SESSIONS_PATH, siteIdList) } component={ BugFinder } />
|
||||
<Route exact strict path={ withSiteId(SESSION_PATH, siteIdList) } component={ Session } />
|
||||
<Route exact strict path={ withSiteId(LIVE_SESSION_PATH, siteIdList) } component={ LiveSession } />
|
||||
<Route exact strict path={ withSiteId(LIVE_SESSION_PATH, siteIdList) } render={ (props) => <Session { ...props } live /> } />
|
||||
{ routes.redirects.map(([ fr, to ]) => (
|
||||
<Redirect key={ fr } exact strict from={ fr } to={ to } />
|
||||
)) }
|
||||
<Redirect to={ withSiteId(SESSIONS_PATH, siteId) } />
|
||||
/>
|
||||
{ onboarding &&
|
||||
<Redirect to={ withSiteId(ONBOARDING_REDIRECT_PATH, siteId)} />
|
||||
}
|
||||
{ siteIdList.length === 0 &&
|
||||
<Redirect to={ routes.client(routes.CLIENT_TABS.SITES) } />
|
||||
}
|
||||
|
||||
<Route exact strict path={ withSiteId(METRICS_PATH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(METRICS_DETAILS, siteIdList) } component={ Dashboard } />
|
||||
|
||||
<Route exact strict path={ withSiteId(DASHBOARD_PATH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(DASHBOARD_SELECT_PATH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(DASHBOARD_METRIC_CREATE_PATH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(DASHBOARD_METRIC_DETAILS_PATH, siteIdList) } component={ Dashboard } />
|
||||
|
||||
|
||||
|
||||
{/* <Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } /> */}
|
||||
|
||||
<Route exact strict path={ withSiteId(ASSIST_PATH, siteIdList) } component={ Assist } />
|
||||
<Route exact strict path={ withSiteId(ERRORS_PATH, siteIdList) } component={ Errors } />
|
||||
<Route exact strict path={ withSiteId(ERROR_PATH, siteIdList) } component={ Errors } />
|
||||
<Route exact strict path={ withSiteId(FUNNEL_PATH, siteIdList) } component={ Funnels } />
|
||||
<Route exact strict path={ withSiteId(FUNNEL_ISSUE_PATH, siteIdList) } component={ FunnelIssue } />
|
||||
<Route exact strict path={ withSiteId(SESSIONS_PATH, siteIdList) } component={ BugFinder } />
|
||||
<Route exact strict path={ withSiteId(SESSION_PATH, siteIdList) } component={ Session } />
|
||||
<Route exact strict path={ withSiteId(LIVE_SESSION_PATH, siteIdList) } component={ LiveSession } />
|
||||
<Route exact strict path={ withSiteId(LIVE_SESSION_PATH, siteIdList) } render={ (props) => <Session { ...props } live /> } />
|
||||
{ routes.redirects.map(([ fr, to ]) => (
|
||||
<Redirect key={ fr } exact strict from={ fr } to={ to } />
|
||||
)) }
|
||||
<Redirect to={ withSiteId(SESSIONS_PATH, siteId) } />
|
||||
</Switch>
|
||||
</ModalProvider>
|
||||
</Suspense>
|
||||
</Loader>
|
||||
:
|
||||
<Suspense fallback={<Loader loading={true} className="flex-1" />}>
|
||||
<Switch>
|
||||
<Route exact strict path={ FORGOT_PASSWORD } component={ ForgotPassword } />
|
||||
<Route exact strict path={ LOGIN_PATH } component={ changePassword ? UpdatePassword : Login } />
|
||||
{ !existingTenant && <Route exact strict path={ SIGNUP_PATH } component={ Signup } /> }
|
||||
<Redirect to={ LOGIN_PATH } />
|
||||
</Switch>
|
||||
</Loader> :
|
||||
<Switch>
|
||||
<Route exact strict path={ FORGOT_PASSWORD } component={ ForgotPassword } />
|
||||
<Route exact strict path={ LOGIN_PATH } component={ changePassword ? UpdatePassword : Login } />
|
||||
{ !existingTenant && <Route exact strict path={ SIGNUP_PATH } component={ Signup } /> }
|
||||
<Redirect to={ LOGIN_PATH } />
|
||||
</Switch>;
|
||||
</Suspense>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import store from 'App/store';
|
||||
|
||||
import { queried } from './routes';
|
||||
|
||||
const siteIdRequiredPaths = [
|
||||
|
|
@ -24,6 +23,8 @@ const siteIdRequiredPaths = [
|
|||
'/assist',
|
||||
'/heatmaps',
|
||||
'/custom_metrics',
|
||||
'/dashboards',
|
||||
'/metrics'
|
||||
// '/custom_metrics/sessions',
|
||||
];
|
||||
|
||||
|
|
@ -68,12 +69,16 @@ export default class APIClient {
|
|||
this.siteId = siteId;
|
||||
}
|
||||
|
||||
fetch(path, params, options = { clean: true }) {
|
||||
fetch(path, params, options = { clean: true }) {
|
||||
if (params !== undefined) {
|
||||
const cleanedParams = options.clean ? clean(params) : params;
|
||||
this.init.body = JSON.stringify(cleanedParams);
|
||||
}
|
||||
|
||||
if (this.init.method === 'GET') {
|
||||
delete this.init.body;
|
||||
}
|
||||
|
||||
|
||||
let fetch = window.fetch;
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="modal-root"></div>
|
||||
<div id="app"><p style="color: #eee;text-align: center;height: 100%;padding: 25%;">Loading...</p></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ function AlertFormModal(props: Props) {
|
|||
const onDelete = async (instance) => {
|
||||
if (await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, Delete',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this alert?`
|
||||
})) {
|
||||
props.remove(instance.alertId).then(() => {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ const Alerts = props => {
|
|||
const onDelete = async (instance) => {
|
||||
if (await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, Delete',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this alert?`
|
||||
})) {
|
||||
props.remove(instance.alertId).then(() => {
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ class AutoComplete extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
const { ddOpen, query, loading, values } = this.state;
|
||||
const {
|
||||
const {
|
||||
optionMapping = defaultOptionMapping,
|
||||
valueToText = defaultValueToText,
|
||||
placeholder = 'Type to search...',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { DNDSource, DNDTarget } from 'Components/hocs/dnd';
|
||||
// import { DNDSource, DNDTarget } from 'Components/hocs/dnd';
|
||||
import Event, { TYPES } from 'Types/filter/event';
|
||||
import { operatorOptions } from 'Types/filter';
|
||||
import { editEvent, removeEvent, clearEvents, applyFilter } from 'Duck/filters';
|
||||
|
|
@ -25,8 +25,8 @@ const getLabel = ({ type }) => {
|
|||
return getPlaceholder({ type });
|
||||
};
|
||||
|
||||
@DNDTarget('event')
|
||||
@DNDSource('event')
|
||||
// @DNDTarget('event')
|
||||
// @DNDSource('event')
|
||||
@connect(state => ({
|
||||
isLastEvent: state.getIn([ 'filters', 'appliedFilter', 'events' ]).size === 1,
|
||||
}), { editEvent, removeEvent, clearEvents, applyFilter })
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { Input } from 'semantic-ui-react';
|
||||
import { DNDContext } from 'Components/hocs/dnd';
|
||||
// import { DNDContext } from 'Components/hocs/dnd';
|
||||
import {
|
||||
addEvent, applyFilter, moveEvent, clearEvents, edit,
|
||||
addCustomFilter, addAttribute, setSearchQuery, setActiveFlow, setFilterOption
|
||||
|
|
@ -45,7 +45,7 @@ import SaveFilterButton from 'Shared/SaveFilterButton';
|
|||
setBlink,
|
||||
edit,
|
||||
})
|
||||
@DNDContext
|
||||
// @DNDContext
|
||||
export default class EventFilter extends React.PureComponent {
|
||||
state = { search: '', showFilterModal: false, showPlacehoder: true }
|
||||
fetchEventList = debounce(this.props.fetchEventList, 500)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class SlackAddForm extends React.PureComponent {
|
|||
remove = async (id) => {
|
||||
if (await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, Delete',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this channel?`
|
||||
})) {
|
||||
this.props.remove(id);
|
||||
|
|
|
|||
49
frontend/app/components/Dashboard/NewDashboard.tsx
Normal file
49
frontend/app/components/Dashboard/NewDashboard.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import { observer, useObserver } from "mobx-react-lite";
|
||||
import { useStore } from 'App/mstore';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import {
|
||||
dashboardSelected,
|
||||
withSiteId,
|
||||
} from 'App/routes';
|
||||
import DashboardSideMenu from './components/DashboardSideMenu';
|
||||
import { Loader } from 'UI';
|
||||
import DashboardRouter from './components/DashboardRouter';
|
||||
|
||||
function NewDashboard(props) {
|
||||
const { history, match: { params: { siteId, dashboardId, metricId } } } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const loading = useObserver(() => dashboardStore.isLoading);
|
||||
|
||||
useEffect(() => {
|
||||
dashboardStore.fetchList().then((resp) => {
|
||||
if (parseInt(dashboardId) > 0) {
|
||||
dashboardStore.selectDashboardById(dashboardId);
|
||||
} else {
|
||||
dashboardStore.selectDefaultDashboard().then(({ dashboardId }) => {
|
||||
if (!history.location.pathname.includes('/metrics')) {
|
||||
history.push(withSiteId(dashboardSelected(dashboardId), siteId));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Loader loading={loading}>
|
||||
<div className="page-margin container-90">
|
||||
<div className="side-menu">
|
||||
<DashboardSideMenu siteId={siteId} />
|
||||
</div>
|
||||
<div className="side-menu-margined">
|
||||
<DashboardRouter siteId={siteId} />
|
||||
</div>
|
||||
</div>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
export default withPageTitle('New Dashboard')(
|
||||
withRouter(observer(NewDashboard))
|
||||
);
|
||||
|
|
@ -22,7 +22,7 @@ function SideMenuSection({ title, items, onItemClick, setShowAlerts, siteId }) {
|
|||
)}
|
||||
|
||||
<div className={stl.divider} />
|
||||
<div className="my-3">
|
||||
<div className="my-3">
|
||||
<SideMenuitem
|
||||
id="menu-manage-alerts"
|
||||
title="Manage Alerts"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ interface Props {
|
|||
onClick?: (event, index) => void;
|
||||
}
|
||||
function CustomMetriLineChart(props: Props) {
|
||||
const { data, params, seriesMap, colors, onClick = () => null } = props;
|
||||
const { data, params, seriesMap = [], colors, onClick = () => null } = props;
|
||||
return (
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<LineChart
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
import React from 'react'
|
||||
import { Styles } from '../../common';
|
||||
import { AreaChart, ResponsiveContainer, XAxis, YAxis, CartesianGrid, Area, Tooltip } from 'recharts';
|
||||
import { LineChart, Line, Legend } from 'recharts';
|
||||
import cn from 'classnames';
|
||||
import CountBadge from '../../common/CountBadge';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
// onClick?: (event, index) => void;
|
||||
}
|
||||
function CustomMetricOverviewChart(props: Props) {
|
||||
const { data } = props;
|
||||
console.log('data', data)
|
||||
const gradientDef = Styles.gradientDef();
|
||||
return (
|
||||
<div className="relative -mx-4">
|
||||
<div className="absolute flex items-start flex-col justify-center inset-0 p-3">
|
||||
<div className="mb-2 flex items-center" >
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<CountBadge
|
||||
// title={subtext}
|
||||
count={ countView(Math.round(data.value), data.unit) }
|
||||
change={ data.progress || 0 }
|
||||
unit={ data.unit }
|
||||
// className={textClass}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ResponsiveContainer height={ 100 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ {
|
||||
top: 85, right: 0, left: 0, bottom: 5,
|
||||
} }
|
||||
>
|
||||
{gradientDef}
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<XAxis hide {...Styles.xaxis} interval={4} dataKey="time" />
|
||||
<YAxis hide interval={ 0 } />
|
||||
<Area
|
||||
name={''}
|
||||
// unit={unit && ' ' + unit}
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomMetricOverviewChart
|
||||
|
||||
|
||||
const countView = (avg, unit) => {
|
||||
if (unit === 'mb') {
|
||||
if (!avg) return 0;
|
||||
const count = Math.trunc(avg / 1024 / 1024);
|
||||
return numberWithCommas(count);
|
||||
}
|
||||
if (unit === 'min') {
|
||||
if (!avg) return 0;
|
||||
const count = Math.trunc(avg);
|
||||
return numberWithCommas(count > 1000 ? count +'k' : count);
|
||||
}
|
||||
return avg ? numberWithCommas(avg): 0;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CustomMetricOverviewChart';
|
||||
|
|
@ -35,8 +35,7 @@ function CustomMetricPieChart(props: Props) {
|
|||
}
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<NoContent size="small" show={data.values && data.values.length === 0} >
|
||||
<NoContent size="small" show={!data.values || data.values.length === 0} style={{ minHeight: '240px'}}>
|
||||
<ResponsiveContainer height={ 220 } width="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
|
|
@ -52,105 +51,77 @@ function CustomMetricPieChart(props: Props) {
|
|||
activeIndex={1}
|
||||
onClick={onClickHandler}
|
||||
labelLine={({
|
||||
cx,
|
||||
cy,
|
||||
midAngle,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
value,
|
||||
index
|
||||
}) => {
|
||||
const RADIAN = Math.PI / 180;
|
||||
let radius1 = 15 + innerRadius + (outerRadius - innerRadius);
|
||||
let radius2 = innerRadius + (outerRadius - innerRadius);
|
||||
let x2 = cx + radius1 * Math.cos(-midAngle * RADIAN);
|
||||
let y2 = cy + radius1 * Math.sin(-midAngle * RADIAN);
|
||||
let x1 = cx + radius2 * Math.cos(-midAngle * RADIAN);
|
||||
let y1 = cy + radius2 * Math.sin(-midAngle * RADIAN);
|
||||
cx,
|
||||
cy,
|
||||
midAngle,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
value,
|
||||
index
|
||||
}) => {
|
||||
const RADIAN = Math.PI / 180;
|
||||
let radius1 = 15 + innerRadius + (outerRadius - innerRadius);
|
||||
let radius2 = innerRadius + (outerRadius - innerRadius);
|
||||
let x2 = cx + radius1 * Math.cos(-midAngle * RADIAN);
|
||||
let y2 = cy + radius1 * Math.sin(-midAngle * RADIAN);
|
||||
let x1 = cx + radius2 * Math.cos(-midAngle * RADIAN);
|
||||
let y1 = cy + radius2 * Math.sin(-midAngle * RADIAN);
|
||||
|
||||
const percentage = value * 100 / data.values.reduce((a, b) => a + b.sessionCount, 0);
|
||||
|
||||
if (percentage<3){
|
||||
return null;
|
||||
}
|
||||
|
||||
return(
|
||||
<line x1={x1} y1={y1} x2={x2} y2={y2} stroke="#3EAAAF" strokeWidth={1} />
|
||||
)
|
||||
}}
|
||||
label={({
|
||||
cx,
|
||||
cy,
|
||||
midAngle,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
value,
|
||||
index
|
||||
}) => {
|
||||
const RADIAN = Math.PI / 180;
|
||||
let radius = 20 + innerRadius + (outerRadius - innerRadius);
|
||||
let x = cx + radius * Math.cos(-midAngle * RADIAN);
|
||||
let y = cy + radius * Math.sin(-midAngle * RADIAN);
|
||||
const percentage = (value / data.values.reduce((a, b) => a + b.sessionCount, 0)) * 100;
|
||||
let name = data.values[index].name || 'Unidentified';
|
||||
name = name.length > 20 ? name.substring(0, 20) + '...' : name;
|
||||
if (percentage<3){
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<text
|
||||
x={x}
|
||||
y={y}
|
||||
fontWeight="400"
|
||||
fontSize="12px"
|
||||
// fontFamily="'Source Sans Pro', 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'"
|
||||
textAnchor={x > cx ? "start" : "end"}
|
||||
dominantBaseline="central"
|
||||
fill='#666'
|
||||
>
|
||||
{name || 'Unidentified'} {numberWithCommas(value)}
|
||||
</text>
|
||||
);
|
||||
}}
|
||||
// label={({
|
||||
// cx,
|
||||
// cy,
|
||||
// midAngle,
|
||||
// innerRadius,
|
||||
// outerRadius,
|
||||
// value,
|
||||
// index
|
||||
// }) => {
|
||||
// const RADIAN = Math.PI / 180;
|
||||
// const radius = 30 + innerRadius + (outerRadius - innerRadius);
|
||||
// const x = cx + radius * Math.cos(-midAngle * RADIAN);
|
||||
// const y = cy + radius * Math.sin(-midAngle * RADIAN);
|
||||
|
||||
// return (
|
||||
// <text
|
||||
// x={x}
|
||||
// y={y}
|
||||
// fill="#3EAAAF"
|
||||
// textAnchor={x > cx ? "start" : "end"}
|
||||
// dominantBaseline="top"
|
||||
// fontSize={10}
|
||||
// >
|
||||
// {data.values[index].name} ({value})
|
||||
// </text>
|
||||
// );
|
||||
// }}
|
||||
const percentage = value * 100 / data.values.reduce((a, b) => a + b.sessionCount, 0);
|
||||
|
||||
if (percentage<3){
|
||||
return null;
|
||||
}
|
||||
|
||||
return(
|
||||
<line x1={x1} y1={y1} x2={x2} y2={y2} stroke="#3EAAAF" strokeWidth={1} />
|
||||
)
|
||||
}}
|
||||
label={({
|
||||
cx,
|
||||
cy,
|
||||
midAngle,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
value,
|
||||
index
|
||||
}) => {
|
||||
const RADIAN = Math.PI / 180;
|
||||
let radius = 20 + innerRadius + (outerRadius - innerRadius);
|
||||
let x = cx + radius * Math.cos(-midAngle * RADIAN);
|
||||
let y = cy + radius * Math.sin(-midAngle * RADIAN);
|
||||
const percentage = (value / data.values.reduce((a, b) => a + b.sessionCount, 0)) * 100;
|
||||
let name = data.values[index].name || 'Unidentified';
|
||||
name = name.length > 20 ? name.substring(0, 20) + '...' : name;
|
||||
if (percentage<3){
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<text
|
||||
x={x}
|
||||
y={y}
|
||||
fontWeight="400"
|
||||
fontSize="12px"
|
||||
// fontFamily="'Source Sans Pro', 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'"
|
||||
textAnchor={x > cx ? "start" : "end"}
|
||||
dominantBaseline="central"
|
||||
fill='#666'
|
||||
>
|
||||
{name || 'Unidentified'} {numberWithCommas(value)}
|
||||
</text>
|
||||
);
|
||||
}}
|
||||
>
|
||||
{data.values.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={Styles.colorsPie[index % Styles.colorsPie.length]} />
|
||||
))}
|
||||
{data && data.values && data.values.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={Styles.colorsPie[index % Styles.colorsPie.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
</PieChart>
|
||||
|
||||
|
||||
</ResponsiveContainer>
|
||||
<div className="text-sm color-gray-medium">Top 5 </div>
|
||||
</NoContent>
|
||||
</div>
|
||||
</NoContent>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,29 +56,29 @@ function CustomMetricWidget(props: Props) {
|
|||
const isTable = metric.viewType === 'table';
|
||||
const isPieChart = metric.viewType === 'pieChart';
|
||||
|
||||
useEffect(() => {
|
||||
new APIClient()['post'](`/custom_metrics/${metricParams.metricId}/chart`, { ...metricParams, q: metric.name })
|
||||
.then(response => response.json())
|
||||
.then(({ errors, data }) => {
|
||||
if (errors) {
|
||||
console.log('err', errors)
|
||||
} else {
|
||||
const namesMap = data
|
||||
.map(i => Object.keys(i))
|
||||
.flat()
|
||||
.filter(i => i !== 'time' && i !== 'timestamp')
|
||||
.reduce((unique: any, item: any) => {
|
||||
if (!unique.includes(item)) {
|
||||
unique.push(item);
|
||||
}
|
||||
return unique;
|
||||
}, []);
|
||||
// useEffect(() => {
|
||||
// new APIClient()['post'](`/custom_metrics/${metricParams.metricId}/chart`, { ...metricParams, q: metric.name })
|
||||
// .then(response => response.json())
|
||||
// .then(({ errors, data }) => {
|
||||
// if (errors) {
|
||||
// console.log('err', errors)
|
||||
// } else {
|
||||
// const namesMap = data
|
||||
// .map(i => Object.keys(i))
|
||||
// .flat()
|
||||
// .filter(i => i !== 'time' && i !== 'timestamp')
|
||||
// .reduce((unique: any, item: any) => {
|
||||
// if (!unique.includes(item)) {
|
||||
// unique.push(item);
|
||||
// }
|
||||
// return unique;
|
||||
// }, []);
|
||||
|
||||
setSeriesMap(namesMap);
|
||||
setData(getChartFormatter(period)(data));
|
||||
}
|
||||
}).finally(() => setLoading(false));
|
||||
}, [period])
|
||||
// setSeriesMap(namesMap);
|
||||
// setData(getChartFormatter(period)(data));
|
||||
// }
|
||||
// }).finally(() => setLoading(false));
|
||||
// }, [period])
|
||||
|
||||
const clickHandlerTable = (filters) => {
|
||||
const activeWidget = {
|
||||
|
|
|
|||
|
|
@ -61,27 +61,27 @@ function CustomMetricWidget(props: Props) {
|
|||
setLoading(true);
|
||||
|
||||
// fetch new data for the widget preview
|
||||
new APIClient()['post']('/custom_metrics/try', { ...metricParams, ...metric.toSaveData() })
|
||||
.then(response => response.json())
|
||||
.then(({ errors, data }) => {
|
||||
if (errors) {
|
||||
console.log('err', errors)
|
||||
} else {
|
||||
const namesMap = data
|
||||
.map(i => Object.keys(i))
|
||||
.flat()
|
||||
.filter(i => i !== 'time' && i !== 'timestamp')
|
||||
.reduce((unique: any, item: any) => {
|
||||
if (!unique.includes(item)) {
|
||||
unique.push(item);
|
||||
}
|
||||
return unique;
|
||||
}, []);
|
||||
// new APIClient()['post']('/custom_metrics/try', { ...metricParams, ...metric.toSaveData() })
|
||||
// .then(response => response.json())
|
||||
// .then(({ errors, data }) => {
|
||||
// if (errors) {
|
||||
// console.log('err', errors)
|
||||
// } else {
|
||||
// const namesMap = data
|
||||
// .map(i => Object.keys(i))
|
||||
// .flat()
|
||||
// .filter(i => i !== 'time' && i !== 'timestamp')
|
||||
// .reduce((unique: any, item: any) => {
|
||||
// if (!unique.includes(item)) {
|
||||
// unique.push(item);
|
||||
// }
|
||||
// return unique;
|
||||
// }, []);
|
||||
|
||||
setSeriesMap(namesMap);
|
||||
setData(getChartFormatter(period)(data));
|
||||
}
|
||||
}).finally(() => setLoading(false));
|
||||
// setSeriesMap(namesMap);
|
||||
// setData(getChartFormatter(period)(data));
|
||||
// }
|
||||
// }).finally(() => setLoading(false));
|
||||
}, [metric])
|
||||
|
||||
const onDateChange = (changedDates) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
.bar {
|
||||
height: 10px;
|
||||
height: 5px;
|
||||
background-color: red;
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const Bar = ({ className = '', width = 0, avg, domain, color }) => {
|
|||
<span className="font-medium">{`${avg}`}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm leading-3">{domain}</div>
|
||||
<div className="text-sm leading-3 color-gray-medium">{domain}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function BreakdownOfLoadedResources(props: Props) {
|
||||
const { data } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 28 }
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<BarChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={params.density/7} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Number of Resources" }}
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Bar minPointSize={1} name="CSS" dataKey="stylesheet" stackId="a" fill={Styles.colors[0]} />
|
||||
<Bar name="Images" dataKey="img" stackId="a" fill={Styles.colors[2]} />
|
||||
<Bar name="Scripts" dataKey="script" stackId="a" fill={Styles.colors[3]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default BreakdownOfLoadedResources;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './BreakdownOfLoadedResources'
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function CPULoad(props: Props) {
|
||||
const { data } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Avg"
|
||||
type="monotone"
|
||||
unit="%"
|
||||
dataKey="avgCpu"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default CPULoad;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CPULoad'
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function CallsErrors4xx(props: Props) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<BarChart
|
||||
data={data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
syncId="errorsPerType"
|
||||
// syncId={ showSync ? "errorsPerType" : undefined }
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
// interval={params.density/7}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Number of Errors" }}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
{/* { data.namesMap.map((key, index) => (
|
||||
<Line key={key} name={key} type="monotone" dataKey={key} stroke={Styles.colors[index]} fillOpacity={ 1 } strokeWidth={ 2 } strokeOpacity={ 0.8 } fill="url(#colorCount)" dot={false} />
|
||||
))} */}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default CallsErrors4xx;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CallsErrors4xx'
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function CallsErrors5xx(props: Props) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<BarChart
|
||||
data={data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
syncId="errorsPerType"
|
||||
// syncId={ showSync ? "errorsPerType" : undefined }
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
// interval={params.density/7}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Number of Errors" }}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
{/* { data.namesMap.map((key, index) => (
|
||||
<Line key={key} name={key} type="monotone" dataKey={key} stroke={Styles.colors[index]} fillOpacity={ 1 } strokeWidth={ 2 } strokeOpacity={ 0.8 } fill="url(#colorCount)" dot={false} />
|
||||
))} */}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default CallsErrors5xx;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CallsErrors5xx'
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function Crashes(props: Props) {
|
||||
const { data } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Crashes"
|
||||
type="monotone"
|
||||
unit="%"
|
||||
dataKey="avgCpu"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default Crashes;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './Crashes'
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles, AvgLabel } from '../../common';
|
||||
import { withRequest } from 'HOCs'
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
import WidgetAutoComplete from 'Shared/WidgetAutoComplete';
|
||||
import { toUnderscore } from 'App/utils';
|
||||
|
||||
const WIDGET_KEY = 'pagesDomBuildtime';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
optionsLoading: any
|
||||
fetchOptions: any
|
||||
options: any
|
||||
}
|
||||
function DomBuildingTime(props: Props) {
|
||||
const { data, optionsLoading } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
|
||||
|
||||
const onSelect = (params) => {
|
||||
const _params = { density: 70 }
|
||||
console.log('params', params) // TODO reload the data with new params;
|
||||
// this.props.fetchWidget(WIDGET_KEY, dashbaordStore.period, props.platform, { ..._params, url: params.value })
|
||||
}
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<>
|
||||
<div className="flex items-center mb-3">
|
||||
<WidgetAutoComplete
|
||||
loading={optionsLoading}
|
||||
fetchOptions={props.fetchOptions}
|
||||
options={props.options}
|
||||
onSelect={onSelect}
|
||||
placeholder="Search for Page"
|
||||
/>
|
||||
<AvgLabel className="ml-auto" text="Avg" count={Math.round(data.avg)} unit="ms" />
|
||||
</div>
|
||||
<ResponsiveContainer height={ 207 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Avg"
|
||||
type="monotone"
|
||||
unit="%"
|
||||
dataKey="avgCpu"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRequest({
|
||||
dataName: "options",
|
||||
initialData: [],
|
||||
dataWrapper: data => data,
|
||||
loadingName: 'optionsLoading',
|
||||
requestName: "fetchOptions",
|
||||
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
|
||||
method: 'GET'
|
||||
})(DomBuildingTime)
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './DomBuildingTime'
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function ErrorsByOrigin(props: Props) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<BarChart
|
||||
data={data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
syncId="errorsPerType"
|
||||
// syncId={ showSync ? "errorsPerType" : undefined }
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
// interval={params.density/7}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Number of Errors" }}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Bar minPointSize={1} name={<span className="float">1<sup>st</sup> Party</span>} dataKey="firstParty" stackId="a" fill={Styles.colors[0]} />
|
||||
<Bar name={<span className="float">3<sup>rd</sup> Party</span>} dataKey="thirdParty" stackId="a" fill={Styles.colors[2]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorsByOrigin;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ErrorsByOrigin'
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function ErrorsByType(props: Props) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<BarChart
|
||||
data={data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
syncId="errorsPerType"
|
||||
// syncId={ showSync ? "errorsPerType" : undefined }
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
// interval={params.density/7}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Number of Errors" }}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Bar minPointSize={1} name="Integrations" dataKey="integrations" stackId="a" fill={Styles.colors[0]}/>
|
||||
<Bar name="4xx" dataKey="4xx" stackId="a" fill={Styles.colors[1]} />
|
||||
<Bar name="5xx" dataKey="5xx" stackId="a" fill={Styles.colors[2]} />
|
||||
<Bar name="Javascript" dataKey="js" stackId="a" fill={Styles.colors[3]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorsByType;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ErrorsByType'
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
import Bar from 'App/components/Dashboard/Widgets/ErrorsPerDomain/Bar';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function ErrorsPerDomain(props: Props) {
|
||||
const { data } = props;
|
||||
console.log('ErrorsPerDomain', data);
|
||||
// const firstAvg = 10;
|
||||
const firstAvg = data.chart[0] && data.chart[0].errorsCount;
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<div className="w-full" style={{ height: '240px' }}>
|
||||
{data.chart.map((item, i) =>
|
||||
<Bar
|
||||
key={i}
|
||||
className="mb-2"
|
||||
avg={numberWithCommas(Math.round(item.errorsCount))}
|
||||
width={Math.round((item.errorsCount * 100) / firstAvg) - 10}
|
||||
domain={item.domain}
|
||||
color={Styles.colors[i]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorsPerDomain;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ErrorsPerDomain'
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles, AvgLabel } from '../../common';
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function FPS(props: Props) {
|
||||
const { data } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<>
|
||||
<div className="flex items-center justify-end mb-3">
|
||||
<AvgLabel text="Avg" className="ml-3" count={data.avgFps} />
|
||||
</div>
|
||||
<ResponsiveContainer height={ 207 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Avg"
|
||||
type="monotone"
|
||||
dataKey="avgFps"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default FPS;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './FPS'
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles, AvgLabel } from '../../common';
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function MemoryConsumption(props: Props) {
|
||||
const { data } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<>
|
||||
<div className="flex items-center justify-end mb-3">
|
||||
<AvgLabel text="Avg" unit="mb" className="ml-3" count={data.avgUsedJsHeapSize} />
|
||||
</div>
|
||||
<ResponsiveContainer height={ 207 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "JS Heap Size (mb)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Avg"
|
||||
unit=" mb"
|
||||
type="monotone"
|
||||
dataKey="avgFps"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default MemoryConsumption;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './MemoryConsumption'
|
||||
16
frontend/app/components/Dashboard/Widgets/PredefinedWidgets/MissingResources/Chart.js
vendored
Normal file
16
frontend/app/components/Dashboard/Widgets/PredefinedWidgets/MissingResources/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,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,62 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles, Table } from '../../common';
|
||||
import { List } from 'immutable';
|
||||
|
||||
import Chart from './Chart';
|
||||
import ResourceInfo from './ResourceInfo';
|
||||
import CopyPath from './CopyPath';
|
||||
|
||||
const cols = [
|
||||
{
|
||||
key: 'resource',
|
||||
title: 'Resource',
|
||||
Component: ResourceInfo,
|
||||
width: '40%',
|
||||
},
|
||||
{
|
||||
key: 'sessions',
|
||||
title: 'Sessions',
|
||||
toText: count => `${ count > 1000 ? Math.trunc(count / 1000) : count }${ count > 1000 ? 'k' : '' }`,
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
key: 'trend',
|
||||
title: 'Trend',
|
||||
Component: Chart,
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
key: 'copy-path',
|
||||
title: '',
|
||||
Component: CopyPath,
|
||||
cellClass: 'invisible group-hover:visible text-right',
|
||||
width: '20%',
|
||||
}
|
||||
];
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function MissingResources(props: Props) {
|
||||
const { data } = props;
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
title="No resources missing."
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<div style={{ height: '240px'}}>
|
||||
<Table
|
||||
small
|
||||
cols={ cols }
|
||||
rows={ List(data.chart) }
|
||||
rowClass="group"
|
||||
/>
|
||||
</div>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default MissingResources;
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { diffFromNowString } from 'App/date';
|
||||
import { TextEllipsis } from 'UI';
|
||||
|
||||
import styles from './resourceInfo.css';
|
||||
|
||||
export default class ResourceInfo extends React.PureComponent {
|
||||
render() {
|
||||
const { data } = this.props;
|
||||
return (
|
||||
<div className="flex flex-col" >
|
||||
<TextEllipsis className={ styles.name } text={ data.name } hintText={ data.url } />
|
||||
<div className={ styles.timings }>
|
||||
{ data.endedAt && data.startedAt && `${ diffFromNowString(data.endedAt) } ago - ${ diffFromNowString(data.startedAt) } old` }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './MissingResources'
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
.name {
|
||||
letter-spacing: -.04em;
|
||||
font-size: .9rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.timings {
|
||||
color: $gray-medium;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
ComposedChart, Bar, CartesianGrid, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis, Tooltip
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function ResourceLoadedVsResponseEnd(props: Props) {
|
||||
const { data } = props;
|
||||
const params = { density: 70 }
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<ComposedChart
|
||||
data={data.chart}
|
||||
margin={ Styles.chartMargins}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
// interval={3}
|
||||
interval={(params.density / 7)}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Number of Resources" }}
|
||||
yAxisId="left"
|
||||
tickFormatter={val => Styles.tickFormatter(val, 'ms')}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{
|
||||
...Styles.axisLabelLeft,
|
||||
value: "Response End (ms)",
|
||||
position: "insideRight",
|
||||
offset: 0
|
||||
}}
|
||||
yAxisId="right"
|
||||
orientation="right"
|
||||
tickFormatter={val => Styles.tickFormatter(val, 'ms')}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Legend />
|
||||
<Bar minPointSize={1} yAxisId="left" name="XHR" dataKey="xhr" stackId="a" fill={Styles.colors[0]} />
|
||||
<Bar yAxisId="left" name="Other" dataKey="total" stackId="a" fill={Styles.colors[2]} />
|
||||
<Line
|
||||
yAxisId="right"
|
||||
strokeWidth={2}
|
||||
name="Response End"
|
||||
type="monotone"
|
||||
dataKey="avgResponseEnd"
|
||||
stroke={Styles.lineColor}
|
||||
dot={false}
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResourceLoadedVsResponseEnd;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ResourceLoadedVsResponseEnd'
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
ComposedChart, Bar, CartesianGrid, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis, Tooltip
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function ResourceLoadedVsVisuallyComplete(props: Props) {
|
||||
const { data } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<ComposedChart
|
||||
data={data.chart}
|
||||
margin={ Styles.chartMargins}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
// interval={3}
|
||||
interval={(params.density / 7)}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Visually Complete (ms)" }}
|
||||
yAxisId="left"
|
||||
tickFormatter={val => Styles.tickFormatter(val, 'ms')}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{
|
||||
...Styles.axisLabelLeft,
|
||||
value: "Number of Resources",
|
||||
position: "insideRight",
|
||||
offset: 0
|
||||
}}
|
||||
yAxisId="right"
|
||||
orientation="right"
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Legend />
|
||||
<Bar minPointSize={1} yAxisId="right" name="Images" type="monotone" dataKey="types.img" stackId="a" fill={Styles.colors[0]} />
|
||||
<Bar yAxisId="right" name="Scripts" type="monotone" dataKey="types.script" stackId="a" fill={Styles.colors[2]} />
|
||||
<Bar yAxisId="right" name="CSS" type="monotone" dataKey="types.stylesheet" stackId="a" fill={Styles.colors[4]} />
|
||||
<Line
|
||||
yAxisId="left"
|
||||
name="Visually Complete"
|
||||
type="monotone"
|
||||
dataKey="avgTimeToRender"
|
||||
stroke={Styles.lineColor }
|
||||
dot={false}
|
||||
unit=" ms"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResourceLoadedVsVisuallyComplete;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ResourceLoadedVsVisuallyComplete'
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
import React from 'react';
|
||||
import { NoContent, DropdownPlain } from 'UI';
|
||||
import { Styles, AvgLabel } from '../../common';
|
||||
import { withRequest } from 'HOCs'
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
import WidgetAutoComplete from 'Shared/WidgetAutoComplete';
|
||||
import { toUnderscore } from 'App/utils';
|
||||
|
||||
const WIDGET_KEY = 'resourcesLoadingTime';
|
||||
export const RESOURCE_OPTIONS = [
|
||||
{ text: 'All', value: 'all', },
|
||||
{ text: 'JS', value: "SCRIPT", },
|
||||
{ text: 'CSS', value: "STYLESHEET", },
|
||||
{ text: 'Fetch', value: "REQUEST", },
|
||||
{ text: 'Image', value: "IMG", },
|
||||
{ text: 'Media', value: "MEDIA", },
|
||||
{ text: 'Other', value: "OTHER", },
|
||||
];
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
optionsLoading: any
|
||||
fetchOptions: any
|
||||
options: any
|
||||
}
|
||||
function ResourceLoadingTime(props: Props) {
|
||||
const { data, optionsLoading } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
const [autoCompleteSelected, setSutoCompleteSelected] = React.useState('');
|
||||
const [type, setType] = React.useState('');
|
||||
|
||||
const onSelect = (params) => {
|
||||
const _params = { density: 70 }
|
||||
setSutoCompleteSelected(params.value);
|
||||
console.log('params', params) // TODO reload the data with new params;
|
||||
// this.props.fetchWidget(WIDGET_KEY, dashbaordStore.period, props.platform, { ..._params, url: params.value })
|
||||
}
|
||||
|
||||
const writeOption = (e, { name, value }) => {
|
||||
// this.setState({ [name]: value })
|
||||
setType(value);
|
||||
const _params = { density: 70 } // TODO reload the data with new params;
|
||||
// this.props.fetchWidget(WIDGET_KEY, this.props.period, this.props.platform, { ..._params, [ name ]: value === 'all' ? null : value })
|
||||
}
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<>
|
||||
<div className="flex items-center mb-3">
|
||||
<WidgetAutoComplete
|
||||
loading={optionsLoading}
|
||||
fetchOptions={props.fetchOptions}
|
||||
options={props.options}
|
||||
onSelect={onSelect}
|
||||
placeholder="Search for Page"
|
||||
/>
|
||||
<DropdownPlain
|
||||
disabled={!!autoCompleteSelected}
|
||||
name="type"
|
||||
label="Resource"
|
||||
options={ RESOURCE_OPTIONS }
|
||||
onChange={ writeOption }
|
||||
defaultValue={'all'}
|
||||
wrapperStyle={{
|
||||
position: 'absolute',
|
||||
top: '12px',
|
||||
left: '170px',
|
||||
}}
|
||||
/>
|
||||
<AvgLabel className="ml-auto" text="Avg" count={Math.round(data.avg)} unit="ms" />
|
||||
</div>
|
||||
<ResponsiveContainer height={ 200 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Avg"
|
||||
unit=" ms"
|
||||
type="monotone"
|
||||
dataKey="avg"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRequest({
|
||||
dataName: "options",
|
||||
initialData: [],
|
||||
dataWrapper: data => data,
|
||||
loadingName: 'optionsLoading',
|
||||
requestName: "fetchOptions",
|
||||
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
|
||||
method: 'GET'
|
||||
})(ResourceLoadingTime)
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ResourceLoadingTime'
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles, AvgLabel } from '../../common';
|
||||
import { withRequest } from 'HOCs'
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
import WidgetAutoComplete from 'Shared/WidgetAutoComplete';
|
||||
import { toUnderscore } from 'App/utils';
|
||||
|
||||
const WIDGET_KEY = 'pagesResponseTime';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
optionsLoading: any
|
||||
fetchOptions: any
|
||||
options: any
|
||||
}
|
||||
function ResponseTime(props: Props) {
|
||||
const { data, optionsLoading } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
|
||||
|
||||
const onSelect = (params) => {
|
||||
const _params = { density: 70 }
|
||||
console.log('params', params) // TODO reload the data with new params;
|
||||
// this.props.fetchWidget(WIDGET_KEY, dashbaordStore.period, props.platform, { ..._params, url: params.value })
|
||||
}
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<>
|
||||
<div className="flex items-center mb-3">
|
||||
<WidgetAutoComplete
|
||||
loading={optionsLoading}
|
||||
fetchOptions={props.fetchOptions}
|
||||
options={props.options}
|
||||
onSelect={onSelect}
|
||||
placeholder="Search for Page"
|
||||
/>
|
||||
<AvgLabel className="ml-auto" text="Avg" count={Math.round(data.avg)} unit="ms" />
|
||||
</div>
|
||||
<ResponsiveContainer height={ 207 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Page Response Time (ms)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Avg"
|
||||
type="monotone"
|
||||
unit=" ms"
|
||||
dataKey="avgCpu"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRequest({
|
||||
dataName: "options",
|
||||
initialData: [],
|
||||
dataWrapper: data => data,
|
||||
loadingName: 'optionsLoading',
|
||||
requestName: "fetchOptions",
|
||||
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
|
||||
method: 'GET'
|
||||
})(ResponseTime)
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ResponseTime'
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function SessionsAffectedByJSErrors(props: Props) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<BarChart
|
||||
data={data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
syncId="errorsPerType"
|
||||
// syncId={ showSync ? "errorsPerType" : undefined }
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
// interval={params.density/7}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Number of Errors" }}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Bar minPointSize={1} name="Sessions" dataKey="sessionsCount" stackId="a" fill={Styles.colors[0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default SessionsAffectedByJSErrors;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SessionsAffectedByJSErrors'
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function SessionsImpactedBySlowRequests(props: Props) {
|
||||
const { data } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Sessions"
|
||||
type="monotone"
|
||||
dataKey="count"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default SessionsImpactedBySlowRequests;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SessionsImpactedBySlowRequests'
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
import Bar from 'App/components/Dashboard/Widgets/SlowestDomains/Bar';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function SlowestDomains(props: Props) {
|
||||
const { data } = props;
|
||||
const firstAvg = data.chart[0] && data.chart[0].errorsCount;
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<div className="w-full" style={{ height: '240px' }}>
|
||||
{data.chart.map((item, i) =>
|
||||
<Bar
|
||||
key={i}
|
||||
className="mb-2"
|
||||
avg={numberWithCommas(Math.round(item.errorsCount))}
|
||||
width={Math.round((item.errorsCount * 100) / firstAvg) - 10}
|
||||
domain={item.domain}
|
||||
color={Styles.colors[i]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default SlowestDomains;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SlowestDomains'
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles, AvgLabel } from '../../common';
|
||||
import { withRequest } from 'HOCs'
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
import WidgetAutoComplete from 'Shared/WidgetAutoComplete';
|
||||
import { toUnderscore } from 'App/utils';
|
||||
|
||||
const WIDGET_KEY = 'timeToRender';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
optionsLoading: any
|
||||
fetchOptions: any
|
||||
options: any
|
||||
}
|
||||
function TimeToRender(props: Props) {
|
||||
const { data, optionsLoading } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
|
||||
|
||||
const onSelect = (params) => {
|
||||
const _params = { density: 70 }
|
||||
console.log('params', params) // TODO reload the data with new params;
|
||||
// this.props.fetchWidget(WIDGET_KEY, dashbaordStore.period, props.platform, { ..._params, url: params.value })
|
||||
}
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<>
|
||||
<div className="flex items-center mb-3">
|
||||
<WidgetAutoComplete
|
||||
loading={optionsLoading}
|
||||
fetchOptions={props.fetchOptions}
|
||||
options={props.options}
|
||||
onSelect={onSelect}
|
||||
placeholder="Search for Page"
|
||||
/>
|
||||
<AvgLabel className="ml-auto" text="Avg" count={Math.round(data.avg)} unit="ms" />
|
||||
</div>
|
||||
<ResponsiveContainer height={ 200 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
// allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Time to Render (ms)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Avg"
|
||||
type="monotone"
|
||||
unit=" ms"
|
||||
dataKey="avgCpu"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRequest({
|
||||
dataName: "options",
|
||||
initialData: [],
|
||||
dataWrapper: data => data,
|
||||
loadingName: 'optionsLoading',
|
||||
requestName: "fetchOptions",
|
||||
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
|
||||
method: 'GET'
|
||||
})(TimeToRender)
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './TimeToRender'
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import React from 'react';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { SideMenuitem, SideMenuHeader, Icon, Button } from 'UI';
|
||||
import { withSiteId, dashboardSelected, metrics } from 'App/routes';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
|
||||
interface Props {
|
||||
siteId: string
|
||||
history: any
|
||||
}
|
||||
function DashbaordListModal(props: Props) {
|
||||
const { dashboardStore } = useStore();
|
||||
const { hideModal } = useModal();
|
||||
const dashboards = dashboardStore.dashboards;
|
||||
const activeDashboardId = dashboardStore.selectedDashboard?.dashboardId;
|
||||
|
||||
const onItemClick = (dashboard) => {
|
||||
dashboardStore.selectDashboardById(dashboard.dashboardId);
|
||||
const path = withSiteId(dashboardSelected(dashboard.dashboardId), parseInt(props.siteId));
|
||||
props.history.push(path);
|
||||
hideModal();
|
||||
};
|
||||
return (
|
||||
<div className="bg-white h-screen" style={{ width: '300px'}}>
|
||||
<div className="color-gray-medium uppercase p-4 text-lg">Dashboards</div>
|
||||
<div>
|
||||
{dashboards.map((item: any) => (
|
||||
<div key={ item.dashboardId } className="px-4">
|
||||
<SideMenuitem
|
||||
key={ item.dashboardId }
|
||||
active={item.dashboardId === activeDashboardId}
|
||||
title={ item.name }
|
||||
iconName={ item.icon }
|
||||
onClick={() => onItemClick(item)} // TODO add click handler
|
||||
leading = {(
|
||||
<div className="ml-2 flex items-center">
|
||||
{item.isPublic && <div className="p-1"><Icon name="user-friends" color="gray-light" size="16" /></div>}
|
||||
{item.isPinned && <div className="p-1"><Icon name="pin-fill" size="16" /></div>}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(DashbaordListModal);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './DashbaordListModal'
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import { useObserver } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import { Button, Modal, Form, Icon, Checkbox } from 'UI';
|
||||
import { useStore } from 'App/mstore'
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
// dashboard: any;
|
||||
closeHandler?: () => void;
|
||||
}
|
||||
function DashboardEditModal(props: Props) {
|
||||
const { show, closeHandler } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const dashboard = useObserver(() => dashboardStore.dashboardInstance);
|
||||
|
||||
const onSave = () => {
|
||||
dashboardStore.save(dashboard).then(closeHandler);
|
||||
}
|
||||
|
||||
const write = ({ target: { value, name } }) => dashboard.update({ [ name ]: value })
|
||||
const writeOption = (e, { checked, name }) => {
|
||||
dashboard.update({ [name]: checked });
|
||||
}
|
||||
|
||||
return useObserver(() => (
|
||||
<Modal size="tiny" open={ show }>
|
||||
<Modal.Header className="flex items-center justify-between">
|
||||
<div>{ 'Edit Dashboard' }</div>
|
||||
<Icon
|
||||
role="button"
|
||||
tabIndex="-1"
|
||||
color="gray-dark"
|
||||
size="14"
|
||||
name="close"
|
||||
onClick={ closeHandler }
|
||||
/>
|
||||
</Modal.Header>
|
||||
|
||||
<Modal.Content>
|
||||
<Form onSubmit={onSave}>
|
||||
<Form.Field>
|
||||
<label>{'Title:'}</label>
|
||||
<input
|
||||
autoFocus={ true }
|
||||
className=""
|
||||
name="name"
|
||||
value={ dashboard.name }
|
||||
onChange={write}
|
||||
placeholder="Title"
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
<Form.Field>
|
||||
<div className="flex items-center">
|
||||
<Checkbox
|
||||
name="isPublic"
|
||||
className="font-medium mr-3"
|
||||
type="checkbox"
|
||||
checked={ dashboard.isPublic }
|
||||
onClick={ writeOption }
|
||||
/>
|
||||
<div className="flex items-center cursor-pointer" onClick={ () => dashboard.update({ 'isPublic': !dashboard.isPublic }) }>
|
||||
<Icon name="user-friends" size="16" />
|
||||
<span className="ml-2"> Team can see and edit the dashboard.</span>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Field>
|
||||
</Form>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<div className="-mx-2 px-2">
|
||||
<Button
|
||||
primary
|
||||
onClick={ onSave }
|
||||
// loading={ loading }
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button className="" marginRight onClick={ closeHandler }>{ 'Cancel' }</Button>
|
||||
</div>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
));
|
||||
}
|
||||
|
||||
export default DashboardEditModal;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './DashboardEditModal'
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import { useObserver } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import { Input } from 'UI';
|
||||
import { useDashboardStore } from '../../store/store';
|
||||
import cn from 'classnames';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
||||
interface Props {
|
||||
}
|
||||
|
||||
function DashboardForm(props: Props) {
|
||||
const { dashboardStore } = useStore();
|
||||
const dashboard = dashboardStore.dashboardInstance;
|
||||
|
||||
const write = ({ target: { value, name } }) => dashboard.update({ [ name ]: value })
|
||||
const writeRadio = ({ target: { value, name } }) => {
|
||||
dashboard.update({ [name]: value === 'team' });
|
||||
}
|
||||
|
||||
return useObserver(() => (
|
||||
<div className="mb-8 grid grid-cols-2 gap-8">
|
||||
<div className="form-field flex flex-col">
|
||||
<label htmlFor="name" className="font-medium mb-2">Title</label>
|
||||
<Input type="text" name="name" onChange={write} value={dashboard.name} />
|
||||
</div>
|
||||
|
||||
<div className="form-field flex flex-col">
|
||||
<label htmlFor="name" className="font-medium mb-2">Visibility and Editing</label>
|
||||
|
||||
<div className="flex items-center py-2">
|
||||
<label className="inline-flex items-center mr-6">
|
||||
<input
|
||||
type="radio"
|
||||
className="form-radio h-5 w-5"
|
||||
name="isPublic"
|
||||
value="team"
|
||||
checked={dashboard.isPublic}
|
||||
onChange={writeRadio}
|
||||
/>
|
||||
<span className={cn("ml-2", { 'color-teal' : dashboard.isPublic})}>Team</span>
|
||||
</label>
|
||||
|
||||
<label className="inline-flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
className="form-radio h-5 w-5"
|
||||
name="isPublic"
|
||||
value="personal"
|
||||
checked={!dashboard.isPublic}
|
||||
onChange={writeRadio}
|
||||
/>
|
||||
<span className={cn("ml-2", { 'color-teal' : !dashboard.isPublic})}>Personal</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
export default DashboardForm;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './DashboardForm';
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import WidgetWrapper from '../WidgetWrapper';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import cn from 'classnames';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
||||
function WidgetCategoryItem({ category, isSelected, onClick, selectedWidgetIds, unSelectCategory }) {
|
||||
const selectedCategoryWidgetsCount = useObserver(() => {
|
||||
return category.widgets.filter(widget => selectedWidgetIds.includes(widget.metricId)).length;
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={cn("rounded p-4 shadow border cursor-pointer", { 'bg-active-blue border-color-teal':isSelected, 'bg-white': !isSelected })}
|
||||
onClick={() => onClick(category)}
|
||||
>
|
||||
<div className="font-medium text-lg mb-2 capitalize">{category.name}</div>
|
||||
<div className="mb-2">{category.description}</div>
|
||||
{selectedCategoryWidgetsCount > 0 && (
|
||||
<div className="flex items-center">
|
||||
<input type="checkbox" checked={true} onChange={() => unSelectCategory(category)} />
|
||||
<span className="color-gray-medium ml-2">{`Selected ${selectedCategoryWidgetsCount} of ${category.widgets.length}`}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DashboardMetricSelection(props) {
|
||||
const { dashboardStore } = useStore();
|
||||
let widgetCategories: any[] = useObserver(() => dashboardStore.widgetCategories);
|
||||
const [activeCategory, setActiveCategory] = React.useState<any>();
|
||||
const selectedWidgetIds = useObserver(() => dashboardStore.selectedWidgets.map((widget: any) => widget.metricId));
|
||||
|
||||
useEffect(() => {
|
||||
dashboardStore?.fetchTemplates().then(templates => {
|
||||
setActiveCategory(dashboardStore.widgetCategories[0]);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleWidgetCategoryClick = (category: any) => {
|
||||
setActiveCategory(category);
|
||||
};
|
||||
|
||||
const toggleAllWidgets = ({ target: { checked }}) => {
|
||||
dashboardStore.toggleAllSelectedWidgets(checked);
|
||||
}
|
||||
|
||||
return useObserver(() => (
|
||||
<div >
|
||||
<div className="grid grid-cols-12 gap-4 my-3 items-end">
|
||||
<div className="col-span-3">
|
||||
<div className="uppercase color-gray-medium text-lg">Categories</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-9 flex items-center">
|
||||
{activeCategory && (
|
||||
<>
|
||||
<div className="flex items-baseline">
|
||||
<h2 className="text-2xl capitalize">{activeCategory.name}</h2>
|
||||
<span className="text-2xl color-gray-medium ml-2">{activeCategory.widgets.length}</span>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex items-center">
|
||||
<span className="color-gray-medium">Showing past 7 days data for visual clue</span>
|
||||
<div className="flex items-center ml-3">
|
||||
<input type="checkbox" onChange={toggleAllWidgets} />
|
||||
<span className="ml-2">Select All</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-3">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{activeCategory && widgetCategories.map((category, index) =>
|
||||
<WidgetCategoryItem
|
||||
key={category.name}
|
||||
onClick={handleWidgetCategoryClick}
|
||||
category={category}
|
||||
isSelected={activeCategory.name === category.name}
|
||||
selectedWidgetIds={selectedWidgetIds}
|
||||
unSelectCategory={dashboardStore.removeSelectedWidgetByCategory}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-9">
|
||||
<div
|
||||
className="grid grid-cols-4 gap-4 -mx-4 px-4 pb-40 items-start"
|
||||
style={{ height: "calc(100vh - 165px)", overflowY: 'auto' }}
|
||||
>
|
||||
{activeCategory && activeCategory.widgets.map((widget: any) => (
|
||||
<WidgetWrapper
|
||||
key={widget.metricId}
|
||||
widget={widget}
|
||||
active={selectedWidgetIds.includes(widget.metricId)}
|
||||
isTemplate={true}
|
||||
isWidget={widget.metricType === 'predefined'}
|
||||
onClick={() => dashboardStore.toggleWidgetSelection(widget)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
export default DashboardMetricSelection;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './DashboardMetricSelection';
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import React from 'react';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import DashboardMetricSelection from '../DashboardMetricSelection';
|
||||
import DashboardForm from '../DashboardForm';
|
||||
import { Button } from 'UI';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { dashboardMetricCreate, withSiteId } from 'App/routes';
|
||||
|
||||
interface Props {
|
||||
history: any
|
||||
siteId?: string
|
||||
dashboardId?: string
|
||||
}
|
||||
function DashboardModal(props) {
|
||||
const { history, siteId, dashboardId } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const { hideModal } = useModal();
|
||||
const dashboard = useObserver(() => dashboardStore.dashboardInstance);
|
||||
const loading = useObserver(() => dashboardStore.isSaving);
|
||||
|
||||
const onSave = () => {
|
||||
dashboardStore.save(dashboard).then(hideModal)
|
||||
}
|
||||
|
||||
const handleCreateNew = () => {
|
||||
const path = withSiteId(dashboardMetricCreate(dashboardId), siteId);
|
||||
history.push(path);
|
||||
hideModal();
|
||||
}
|
||||
|
||||
return useObserver(() => (
|
||||
<div
|
||||
className="fixed border-r shadow p-4 h-screen"
|
||||
style={{ backgroundColor: '#FAFAFA', zIndex: '9999', width: '85%'}}
|
||||
>
|
||||
<div className="mb-6 flex items-end justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl">
|
||||
{ dashboard.exists() ? "Add metric(s) to dashboard" : "Create Dashboard" }
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
{dashboard.exists() && <Button outline size="small" onClick={handleCreateNew}>Create New</Button>}
|
||||
</div>
|
||||
</div>
|
||||
{ !dashboard.exists() && (
|
||||
<>
|
||||
<DashboardForm />
|
||||
<p>Create new dashboard by choosing from the range of predefined metrics that you care about. You can always add your custom metrics later.</p>
|
||||
</>
|
||||
)}
|
||||
<DashboardMetricSelection />
|
||||
|
||||
<div className="flex absolute bottom-0 left-0 right-0 bg-white border-t p-3">
|
||||
<Button
|
||||
primary
|
||||
className=""
|
||||
disabled={!dashboard.isValid || loading}
|
||||
onClick={onSave}
|
||||
>
|
||||
{ dashboard.exists() ? "Add Selected to Dashboard" : "Create and Add to Dashboard" }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
export default withRouter(DashboardModal);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './DashboardModal'
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import React from 'react';
|
||||
import { Switch, Route } from 'react-router';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
metrics,
|
||||
metricDetails,
|
||||
dashboardSelected,
|
||||
dashboardMetricCreate,
|
||||
dashboardMetricDetails,
|
||||
withSiteId,
|
||||
dashboard,
|
||||
} from 'App/routes';
|
||||
import DashboardView from '../DashboardView';
|
||||
import MetricsView from '../MetricsView';
|
||||
import WidgetView from '../WidgetView';
|
||||
|
||||
interface Props {
|
||||
history: any
|
||||
match: any
|
||||
}
|
||||
function DashboardRouter(props: Props) {
|
||||
const { match: { params: { siteId, dashboardId, metricId } } } = props;
|
||||
return (
|
||||
<div>
|
||||
<Switch>
|
||||
<Route exact strict path={withSiteId(metrics(), siteId)}>
|
||||
<MetricsView siteId={siteId} />
|
||||
</Route>
|
||||
|
||||
<Route exact strict path={withSiteId(metricDetails(), siteId)}>
|
||||
<WidgetView siteId={siteId} {...props} />
|
||||
</Route>
|
||||
|
||||
<Route exact strict path={withSiteId(dashboardMetricDetails(dashboardId), siteId)}>
|
||||
<WidgetView siteId={siteId} {...props} />
|
||||
</Route>
|
||||
|
||||
<Route exact strict path={withSiteId(dashboardMetricCreate(dashboardId), siteId)}>
|
||||
<WidgetView siteId={siteId} {...props} />
|
||||
</Route>
|
||||
|
||||
<Route exact strict path={withSiteId(dashboard(''), siteId)}>
|
||||
<>Nothing...</>
|
||||
</Route>
|
||||
|
||||
<Route exact strict path={withSiteId(dashboardSelected(dashboardId), siteId)}>
|
||||
<DashboardView siteId={siteId} dashboardId={dashboardId} />
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(DashboardRouter);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './DashboardRouter';
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import { useObserver } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import { Button, Modal, Form, Icon } from 'UI';
|
||||
import { useStore } from 'App/mstore'
|
||||
import DropdownPlain from 'Shared/DropdownPlain';
|
||||
|
||||
interface Props {
|
||||
metricId: string,
|
||||
show: boolean;
|
||||
closeHandler?: () => void;
|
||||
}
|
||||
function DashboardSelectionModal(props: Props) {
|
||||
const { show, metricId, closeHandler } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const dashboardOptions = dashboardStore.dashboards.map((i: any) => ({
|
||||
key: i.id,
|
||||
text: i.name,
|
||||
value: i.dashboardId,
|
||||
}));
|
||||
const [selectedId, setSelectedId] = React.useState(dashboardOptions[0].value);
|
||||
|
||||
const onSave = () => {
|
||||
const dashboard = dashboardStore.getDashboard(selectedId)
|
||||
if (dashboard) {
|
||||
dashboardStore.addWidgetToDashboard(dashboard, [metricId]).then(closeHandler)
|
||||
}
|
||||
}
|
||||
|
||||
return useObserver(() => (
|
||||
<Modal size="tiny" open={ show }>
|
||||
<Modal.Header className="flex items-center justify-between">
|
||||
<div>{ 'Add to selected Dashboard' }</div>
|
||||
<Icon
|
||||
role="button"
|
||||
tabIndex="-1"
|
||||
color="gray-dark"
|
||||
size="14"
|
||||
name="close"
|
||||
onClick={ closeHandler }
|
||||
/>
|
||||
</Modal.Header>
|
||||
|
||||
<Modal.Content>
|
||||
<div className="py-4">
|
||||
<Form onSubmit={onSave}>
|
||||
<Form.Field>
|
||||
<label>{'Dashbaord:'}</label>
|
||||
<DropdownPlain
|
||||
options={dashboardOptions}
|
||||
value={selectedId}
|
||||
onChange={(e, { value }) => setSelectedId(value)}
|
||||
/>
|
||||
</Form.Field>
|
||||
</Form>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<div className="-mx-2 px-2">
|
||||
<Button
|
||||
primary
|
||||
onClick={ onSave }
|
||||
// loading={ loading }
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
<Button className="" marginRight onClick={ closeHandler }>{ 'Cancel' }</Button>
|
||||
</div>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
));
|
||||
}
|
||||
|
||||
export default DashboardSelectionModal;
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
import { useObserver } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import { SideMenuitem, SideMenuHeader, Icon, Button } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withSiteId, dashboardSelected, metrics } from 'App/routes';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import DashbaordListModal from '../DashbaordListModal';
|
||||
import DashboardModal from '../DashboardModal';
|
||||
import cn from 'classnames';
|
||||
|
||||
const SHOW_COUNT = 5;
|
||||
interface Props {
|
||||
siteId: string
|
||||
history: any
|
||||
}
|
||||
function DashboardSideMenu(props: Props) {
|
||||
const { history, siteId } = props;
|
||||
const { hideModal, showModal } = useModal();
|
||||
const { dashboardStore } = useStore();
|
||||
const dashboardId = dashboardStore.selectedDashboard?.dashboardId;
|
||||
const dashboardsPicked = useObserver(() => dashboardStore.dashboards.slice(0, SHOW_COUNT));
|
||||
const remainingDashboardsCount = dashboardStore.dashboards.length - SHOW_COUNT;
|
||||
|
||||
const redirect = (path) => {
|
||||
history.push(path);
|
||||
}
|
||||
|
||||
const onItemClick = (dashboard) => {
|
||||
dashboardStore.selectDashboardById(dashboard.dashboardId);
|
||||
const path = withSiteId(dashboardSelected(dashboard.dashboardId), parseInt(siteId));
|
||||
history.push(path);
|
||||
};
|
||||
|
||||
const onAddDashboardClick = (e) => {
|
||||
dashboardStore.initDashboard();
|
||||
showModal(<DashboardModal />, {})
|
||||
}
|
||||
|
||||
const togglePinned = (dashboard) => {
|
||||
dashboardStore.updatePinned(dashboard.dashboardId);
|
||||
}
|
||||
|
||||
return useObserver(() => (
|
||||
<div>
|
||||
<SideMenuHeader className="mb-4" text="Dashboards" />
|
||||
{dashboardsPicked.sort((a: any, b: any) => a.isPinned === b.isPinned ? 0 : a.isPinned ? -1 : 1 ).map((item: any) => (
|
||||
<SideMenuitem
|
||||
key={ item.dashboardId }
|
||||
active={item.dashboardId === dashboardId}
|
||||
title={ item.name }
|
||||
iconName={ item.icon }
|
||||
onClick={() => onItemClick(item)}
|
||||
className="group"
|
||||
leading = {(
|
||||
<div className="ml-2 flex items-center">
|
||||
{item.isPublic && <div className="p-1"><Icon name="user-friends" color="gray-light" size="16" /></div>}
|
||||
{<div className={cn("p-1 group-hover:visible", { 'invisible' : !item.isPinned })} onClick={() => togglePinned(item)}><Icon name="pin-fill" size="16" /></div>}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
<div>
|
||||
{remainingDashboardsCount > 0 && (
|
||||
<div
|
||||
className="my-2 py-2 color-teal cursor-pointer"
|
||||
onClick={() => showModal(<DashbaordListModal siteId={siteId} />, {})}
|
||||
>
|
||||
{remainingDashboardsCount} More
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="border-t w-full my-2" />
|
||||
<div className="w-full">
|
||||
<SideMenuitem
|
||||
id="menu-manage-alerts"
|
||||
title="Create Dashboard"
|
||||
iconName="plus"
|
||||
onClick={onAddDashboardClick}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t w-full my-2" />
|
||||
<div className="w-full">
|
||||
<SideMenuitem
|
||||
id="menu-manage-alerts"
|
||||
title="Metrics"
|
||||
iconName="bar-chart-line"
|
||||
onClick={() => redirect(withSiteId(metrics(), siteId))}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t w-full my-2" />
|
||||
<div className="my-3 w-full">
|
||||
<SideMenuitem
|
||||
id="menu-manage-alerts"
|
||||
title="Alerts"
|
||||
iconName="bell-plus"
|
||||
// onClick={() => setShowAlerts(true)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
export default withRouter(DashboardSideMenu);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './DashboardSideMenu';
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { observer, useObserver } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { Button, PageTitle, Link, Loader, NoContent, ItemMenu } from 'UI';
|
||||
import { withSiteId, dashboardMetricCreate, dashboardSelected, dashboard } from 'App/routes';
|
||||
import withModal from 'App/components/Modal/withModal';
|
||||
import DashboardWidgetGrid from '../DashboardWidgetGrid';
|
||||
import { confirm } from 'UI/Confirmation';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import DashboardModal from '../DashboardModal';
|
||||
import DashboardEditModal from '../DashboardEditModal';
|
||||
import DateRange from 'Shared/DateRange';
|
||||
|
||||
interface Props {
|
||||
siteId: number;
|
||||
history: any
|
||||
match: any
|
||||
dashboardId: any
|
||||
}
|
||||
function DashboardView(props: Props) {
|
||||
const { siteId, dashboardId } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const { hideModal, showModal } = useModal();
|
||||
const loading = useObserver(() => dashboardStore.fetchingDashboard);
|
||||
const dashboard: any = dashboardStore.selectedDashboard
|
||||
const period = useObserver(() => dashboardStore.period);
|
||||
const [showEditModal, setShowEditModal] = React.useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
dashboardStore.fetch(dashboardId)
|
||||
}, []);
|
||||
|
||||
const onAddWidgets = () => {
|
||||
dashboardStore.initDashboard(dashboard)
|
||||
showModal(<DashboardModal siteId={siteId} dashboardId={dashboardId} />, {})
|
||||
}
|
||||
|
||||
const onEdit = () => {
|
||||
dashboardStore.initDashboard(dashboard)
|
||||
setShowEditModal(true)
|
||||
}
|
||||
|
||||
const onDelete = async () => {
|
||||
if (await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this Dashboard?`
|
||||
})) {
|
||||
dashboardStore.deleteDashboard(dashboard).then(() => {
|
||||
dashboardStore.selectDefaultDashboard().then(({ dashboardId }) => {
|
||||
props.history.push(withSiteId(dashboard(), siteId));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return useObserver(() => (
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
show={!dashboard || !dashboard.dashboardId}
|
||||
title="No data available."
|
||||
size="small"
|
||||
>
|
||||
<div>
|
||||
<DashboardEditModal
|
||||
show={showEditModal}
|
||||
closeHandler={() => setShowEditModal(false)}
|
||||
/>
|
||||
<div className="flex items-center mb-4 justify-between">
|
||||
<div className="flex items-center">
|
||||
<PageTitle title={dashboard?.name} className="mr-3" />
|
||||
<Button primary size="small" onClick={onAddWidgets}>Add Metric</Button>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2 color-gray-medium">Time Range</span>
|
||||
<DateRange
|
||||
rangeValue={period.rangeName}
|
||||
startDate={period.start}
|
||||
endDate={period.end}
|
||||
onDateChange={(period) => dashboardStore.setPeriod(period)}
|
||||
customRangeRight
|
||||
direction="left"
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-4" />
|
||||
<ItemMenu
|
||||
items={[
|
||||
{
|
||||
text: 'Edit',
|
||||
onClick: onEdit
|
||||
},
|
||||
{
|
||||
text: 'Delete Dashboard',
|
||||
onClick: onDelete
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DashboardWidgetGrid
|
||||
siteId={siteId}
|
||||
dashboardId={dashboardId}
|
||||
onEditHandler={onAddWidgets}
|
||||
/>
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
));
|
||||
}
|
||||
|
||||
export default withRouter(withModal(observer(DashboardView)));
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './DashboardView'
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import React from 'react';
|
||||
import { useStore } from 'App/mstore';
|
||||
import WidgetWrapper from '../WidgetWrapper';
|
||||
import { NoContent, Button, Loader } from 'UI';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
|
||||
interface Props {
|
||||
siteId: string,
|
||||
dashboardId: string;
|
||||
onEditHandler: () => void;
|
||||
}
|
||||
function DashboardWidgetGrid(props) {
|
||||
const { dashboardId, siteId } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const loading = useObserver(() => dashboardStore.isLoading);
|
||||
const dashbaord: any = dashboardStore.selectedDashboard;
|
||||
const list: any = useObserver(() => dashbaord?.widgets);
|
||||
|
||||
return useObserver(() => (
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
show={list.length === 0}
|
||||
icon="exclamation-circle"
|
||||
title="No metrics added to this dashboard"
|
||||
subtext={
|
||||
<div>
|
||||
<p>Metrics helps you visualize trends from sessions captured by OpenReplay</p>
|
||||
<Button size="small" primary onClick={props.onEditHandler}>Add Metric</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="grid gap-4 grid-cols-4 items-start pb-10">
|
||||
{list && list.map((item, index) => (
|
||||
<WidgetWrapper
|
||||
index={index}
|
||||
widget={item}
|
||||
key={item.widgetId}
|
||||
moveListItem={(dragIndex, hoverIndex) => dashbaord.swapWidgetPosition(dragIndex, hoverIndex)}
|
||||
dashboardId={dashboardId}
|
||||
siteId={siteId}
|
||||
isWidget={true}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
));
|
||||
}
|
||||
|
||||
export default DashboardWidgetGrid;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './DashboardWidgetGrid';
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import FilterList from 'Shared/Filters/FilterList';
|
||||
import {
|
||||
edit,
|
||||
updateSeries,
|
||||
addSeriesFilterFilter,
|
||||
removeSeriesFilterFilter,
|
||||
editSeriesFilterFilter,
|
||||
editSeriesFilter,
|
||||
} from 'Duck/customMetrics';
|
||||
import { connect } from 'react-redux';
|
||||
import { IconButton, Icon } from 'UI';
|
||||
import FilterSelection from 'Shared/Filters/FilterSelection';
|
||||
import SeriesName from './SeriesName';
|
||||
import cn from 'classnames';
|
||||
import { useDashboardStore } from '../../store/store';
|
||||
import { observer, useObserver } from 'mobx-react-lite';
|
||||
|
||||
interface Props {
|
||||
seriesIndex: number;
|
||||
series: any;
|
||||
edit: typeof edit;
|
||||
updateSeries: typeof updateSeries;
|
||||
onRemoveSeries: (seriesIndex) => void;
|
||||
canDelete?: boolean;
|
||||
addSeriesFilterFilter: typeof addSeriesFilterFilter;
|
||||
editSeriesFilterFilter: typeof editSeriesFilterFilter;
|
||||
editSeriesFilter: typeof editSeriesFilter;
|
||||
removeSeriesFilterFilter: typeof removeSeriesFilterFilter;
|
||||
hideHeader?: boolean;
|
||||
emptyMessage?: any;
|
||||
observeChanges?: () => void;
|
||||
}
|
||||
|
||||
function FilterSeries(props: Props) {
|
||||
const { observeChanges = () => {}, canDelete, hideHeader = false, emptyMessage = 'Add user event or filter to define the series by clicking Add Step.' } = props;
|
||||
const [expanded, setExpanded] = useState(true)
|
||||
const { series, seriesIndex } = props;
|
||||
|
||||
useEffect(observeChanges, [series])
|
||||
|
||||
const onAddFilter = (filter) => {
|
||||
series.filter.addFilter(filter)
|
||||
}
|
||||
|
||||
const onUpdateFilter = (filterIndex, filter) => {
|
||||
series.filter.updateFilter(filterIndex, filter)
|
||||
}
|
||||
|
||||
const onChangeEventsOrder = (e, { name, value }) => {
|
||||
series.filter.updateKey(name, value)
|
||||
// props.editSeriesFilter(seriesIndex, { eventsOrder: value });
|
||||
}
|
||||
|
||||
const onRemoveFilter = (filterIndex) => {
|
||||
series.filter.removeFilter(filterIndex)
|
||||
// props.removeSeriesFilterFilter(seriesIndex, filterIndex);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border rounded bg-white">
|
||||
<div className={cn("border-b px-5 h-12 flex items-center relative", { 'hidden': hideHeader })}>
|
||||
<div className="mr-auto">
|
||||
<SeriesName seriesIndex={seriesIndex} name={series.name} onUpdate={(name) => series.update('name', name) } />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center cursor-pointer">
|
||||
<div onClick={props.onRemoveSeries} className={cn("ml-3", {'disabled': !canDelete})}>
|
||||
<Icon name="trash" size="16" />
|
||||
</div>
|
||||
|
||||
<div onClick={() => setExpanded(!expanded)} className="ml-3">
|
||||
<Icon name="chevron-down" size="16" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{ expanded && (
|
||||
<>
|
||||
<div className="p-5">
|
||||
{ series.filter.filters.length > 0 ? (
|
||||
<FilterList
|
||||
filter={series.filter}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
onRemoveFilter={onRemoveFilter}
|
||||
onChangeEventsOrder={onChangeEventsOrder}
|
||||
observeChanges={observeChanges}
|
||||
/>
|
||||
): (
|
||||
<div className="color-gray-medium">{emptyMessage}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="border-t h-12 flex items-center">
|
||||
<div className="-mx-4 px-6">
|
||||
<FilterSelection
|
||||
filter={undefined}
|
||||
onFilterClick={onAddFilter}
|
||||
>
|
||||
<IconButton primaryText label="ADD STEP" icon="plus" />
|
||||
</FilterSelection>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(null, {
|
||||
edit,
|
||||
updateSeries,
|
||||
addSeriesFilterFilter,
|
||||
editSeriesFilterFilter,
|
||||
editSeriesFilter,
|
||||
removeSeriesFilterFilter,
|
||||
})(observer(FilterSeries));
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Icon } from 'UI';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
onUpdate: (name) => void;
|
||||
seriesIndex?: number;
|
||||
}
|
||||
function SeriesName(props: Props) {
|
||||
const { seriesIndex = 1 } = props;
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [name, setName] = useState(props.name)
|
||||
const ref = useRef<any>(null)
|
||||
|
||||
const write = ({ target: { value, name } }) => {
|
||||
setName(value)
|
||||
}
|
||||
|
||||
const onBlur = () => {
|
||||
setEditing(false)
|
||||
props.onUpdate(name)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (editing) {
|
||||
ref.current.focus()
|
||||
}
|
||||
}, [editing])
|
||||
|
||||
useEffect(() => {
|
||||
setName(props.name)
|
||||
}, [props.name])
|
||||
|
||||
// const { name } = props;
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
{ editing ? (
|
||||
<input
|
||||
ref={ ref }
|
||||
name="name"
|
||||
className="fluid border-0 -mx-2 px-2 h-8"
|
||||
value={name}
|
||||
// readOnly={!editing}
|
||||
onChange={write}
|
||||
onBlur={onBlur}
|
||||
onFocus={() => setEditing(true)}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-base h-8 flex items-center border-transparent">{name.trim() === '' ? 'Seriess ' + (seriesIndex + 1) : name }</div>
|
||||
)}
|
||||
|
||||
<div className="ml-3 cursor-pointer" onClick={() => setEditing(true)}><Icon name="pencil" size="14" /></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SeriesName;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SeriesName';
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './FilterSeries'
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import React from 'react';
|
||||
import { Icon, NoContent, Label, Link, Pagination } from 'UI';
|
||||
import { checkForRecent, formatDateTimeDefault, convertTimestampToUtcTimestamp } from 'App/date';
|
||||
|
||||
interface Props {
|
||||
metric: any;
|
||||
}
|
||||
|
||||
function DashboardLink({ dashboards}) {
|
||||
return (
|
||||
dashboards.map(dashboard => (
|
||||
<Link to={`/dashboard/${dashboard.dashboardId}`} className="">
|
||||
<div className="flex items-center mb-1">
|
||||
<div className="mr-2 text-4xl no-underline" style={{ textDecoration: 'none'}}>·</div>
|
||||
<span className="link leading-4">{dashboard.name}</span>
|
||||
</div>
|
||||
</Link>
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
function MetricListItem(props: Props) {
|
||||
const { metric } = props;
|
||||
return (
|
||||
<div className="grid grid-cols-12 p-3 border-t select-none">
|
||||
<div className="col-span-3">
|
||||
<Link to={`/metrics/${metric.metricId}`} className="link">
|
||||
{metric.name}
|
||||
</Link>
|
||||
</div>
|
||||
<div><Label className="capitalize">{metric.metricType}</Label></div>
|
||||
<div className="col-span-2">
|
||||
<DashboardLink dashboards={metric.dashboards} />
|
||||
</div>
|
||||
<div className="col-span-3">{metric.owner}</div>
|
||||
<div>
|
||||
<div className="flex items-center">
|
||||
<Icon name={metric.isPublic ? "user-friends" : "person-fill"} className="mr-2" />
|
||||
<span>{metric.isPublic ? 'Team' : 'Private'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2">{metric.lastModified && checkForRecent(metric.lastModified, 'LLL dd, yyyy, hh:mm a')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricListItem;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './MetricListItem';
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { useObserver } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import { NoContent, Pagination } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { getRE } from 'App/utils';
|
||||
import MetricListItem from '../MetricListItem';
|
||||
import { sliceListPerPage } from 'App/utils';
|
||||
|
||||
interface Props { }
|
||||
function MetricsList(props: Props) {
|
||||
const { metricStore } = useStore();
|
||||
const metrics = useObserver(() => metricStore.metrics);
|
||||
const metricsSearch = useObserver(() => metricStore.metricsSearch);
|
||||
const filterList = (list) => {
|
||||
const filterRE = getRE(metricsSearch, 'i');
|
||||
let _list = list.filter(w => {
|
||||
const dashbaordNames = w.dashboards.map(d => d.name).join(' ');
|
||||
return filterRE.test(w.name) || filterRE.test(w.metricType) || filterRE.test(w.owner) || filterRE.test(dashbaordNames);
|
||||
});
|
||||
return _list
|
||||
}
|
||||
const list: any = metricsSearch !== '' ? filterList(metrics) : metrics;
|
||||
const lenth = list.length;
|
||||
|
||||
return useObserver(() => (
|
||||
<NoContent show={lenth === 0} icon="exclamation-circle">
|
||||
<div className="mt-3 border rounded bg-white">
|
||||
<div className="grid grid-cols-12 p-3 font-medium">
|
||||
<div className="col-span-3">Title</div>
|
||||
<div>Type</div>
|
||||
<div className="col-span-2">Dashboards</div>
|
||||
<div className="col-span-3">Owner</div>
|
||||
<div>Visibility</div>
|
||||
<div className="col-span-2">Last Modified</div>
|
||||
</div>
|
||||
|
||||
{sliceListPerPage(list, metricStore.page - 1, metricStore.pageSize).map((metric: any) => (
|
||||
<MetricListItem metric={metric} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="w-full flex items-center justify-center py-6">
|
||||
<Pagination
|
||||
page={metricStore.page}
|
||||
totalPages={Math.ceil(lenth / metricStore.pageSize)}
|
||||
onPageChange={(page) => metricStore.updateKey('page', page)}
|
||||
limit={metricStore.pageSize}
|
||||
debounceRequest={100}
|
||||
/>
|
||||
</div>
|
||||
</NoContent>
|
||||
));
|
||||
}
|
||||
|
||||
export default MetricsList;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './MetricsList';
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { useObserver } from 'mobx-react-lite';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { Icon } from 'UI';
|
||||
import { debounce } from 'App/utils';
|
||||
|
||||
let debounceUpdate: any = () => {}
|
||||
function MetricsSearch(props) {
|
||||
const { metricStore } = useStore();
|
||||
const [query, setQuery] = useState(metricStore.metricsSearch);
|
||||
useEffect(() => {
|
||||
debounceUpdate = debounce((key, value) => metricStore.updateKey(key, value), 500);
|
||||
}, [])
|
||||
|
||||
const write = ({ target: { name, value } }) => {
|
||||
setQuery(value);
|
||||
debounceUpdate('metricsSearch', value);
|
||||
}
|
||||
|
||||
return useObserver(() => (
|
||||
<div className="relative">
|
||||
<Icon name="search" className="absolute top-0 bottom-0 ml-2 m-auto" size="16" />
|
||||
<input
|
||||
value={query}
|
||||
name="metricsSearch"
|
||||
className="bg-white p-2 border rounded w-full pl-10"
|
||||
placeholder="Filter by title, type, dashboard and owner"
|
||||
onChange={write}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
export default MetricsSearch;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './MetricsSearch';
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import React from 'react';
|
||||
import { Button, PageTitle, Icon, Link } from 'UI';
|
||||
import { withSiteId, metricCreate } from 'App/routes';
|
||||
import MetricsList from '../MetricsList';
|
||||
import MetricsSearch from '../MetricsSearch';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
|
||||
interface Props{
|
||||
siteId: number;
|
||||
}
|
||||
function MetricsView(props: Props) {
|
||||
const { siteId } = props;
|
||||
const { metricStore } = useStore();
|
||||
const metricsCount = useObserver(() => metricStore.metrics.length);
|
||||
|
||||
React.useEffect(() => {
|
||||
metricStore.fetchList();
|
||||
}, []);
|
||||
return useObserver(() => (
|
||||
<div>
|
||||
<div className="flex items-center mb-4 justify-between">
|
||||
<div className="flex items-baseline mr-3">
|
||||
<PageTitle title="Metrics" className="" />
|
||||
<span className="text-2xl color-gray-medium ml-2">{metricsCount}</span>
|
||||
</div>
|
||||
<Link to={'/metrics/create'}><Button primary size="small">Add Metric</Button></Link>
|
||||
<div className="ml-auto w-1/3">
|
||||
<MetricsSearch />
|
||||
</div>
|
||||
</div>
|
||||
<MetricsList />
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
export default MetricsView;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './MetricsView';
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import CustomMetriLineChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetriLineChart';
|
||||
import CustomMetricPercentage from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage';
|
||||
import CustomMetricTable from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTable';
|
||||
import CustomMetricPieChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart';
|
||||
import { Styles } from 'App/components/Dashboard/Widgets/common';
|
||||
import { observer, useObserver, useLocalObservable } from 'mobx-react-lite';
|
||||
import { Loader } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import WidgetPredefinedChart from '../WidgetPredefinedChart';
|
||||
import CustomMetricOverviewChart from '../../Widgets/CustomMetricsWidgets/CustomMetricOverviewChart';
|
||||
interface Props {
|
||||
metric: any;
|
||||
isWidget?: boolean
|
||||
}
|
||||
function WidgetChart(props: Props) {
|
||||
const { isWidget = false, metric } = props;
|
||||
// const metric = useObserver(() => props.metric);
|
||||
const { dashboardStore } = useStore();
|
||||
const period = useObserver(() => dashboardStore.period);
|
||||
const colors = Styles.customMetricColors;
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [seriesMap, setSeriesMap] = useState<any>([]);
|
||||
const params = { density: 70 }
|
||||
const metricParams = { ...params, metricId: metric.metricId, viewType: 'lineChart' }
|
||||
const prevMetricRef = useRef<any>();
|
||||
const [data, setData] = useState<any>(metric.data);
|
||||
|
||||
useEffect(() => {
|
||||
if (prevMetricRef.current && prevMetricRef.current.name !== metric.name) {
|
||||
prevMetricRef.current = metric;
|
||||
return
|
||||
};
|
||||
prevMetricRef.current = metric;
|
||||
|
||||
setLoading(true);
|
||||
const data = isWidget ? {} : { ...metricParams, ...metric.toJson() };
|
||||
dashboardStore.fetchMetricChartData(metric, data, isWidget).then((res: any) => {
|
||||
setData(res);
|
||||
}).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [period]);
|
||||
|
||||
const renderChart = () => {
|
||||
const { metricType, viewType, predefinedKey } = metric;
|
||||
|
||||
if (metricType === 'predefined') {
|
||||
if (viewType === 'overview') {
|
||||
return <CustomMetricOverviewChart data={data} />
|
||||
}
|
||||
return <WidgetPredefinedChart data={data} predefinedKey={metric.predefinedKey} />
|
||||
}
|
||||
|
||||
if (metricType === 'timeseries') {
|
||||
if (viewType === 'lineChart') {
|
||||
return (
|
||||
<CustomMetriLineChart
|
||||
data={metric.data}
|
||||
seriesMap={seriesMap}
|
||||
colors={colors}
|
||||
params={params}
|
||||
/>
|
||||
)
|
||||
} else if (viewType === 'progress') {
|
||||
return (
|
||||
<CustomMetricPercentage
|
||||
data={metric.data[0]}
|
||||
colors={colors}
|
||||
params={params}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (metricType === 'table') {
|
||||
if (viewType === 'table') {
|
||||
return <CustomMetricTable metric={metric} data={metric.data[0]} />;
|
||||
} else if (viewType === 'pieChart') {
|
||||
return (
|
||||
<CustomMetricPieChart
|
||||
metric={metric}
|
||||
data={metric.data[0]}
|
||||
colors={colors}
|
||||
params={params}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return <div>Unknown</div>;
|
||||
}
|
||||
return useObserver(() => (
|
||||
<Loader loading={loading}>
|
||||
{renderChart()}
|
||||
</Loader>
|
||||
));
|
||||
}
|
||||
|
||||
export default WidgetChart;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './WidgetChart'
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
import React, { useState } from 'react';
|
||||
import DropdownPlain from 'Shared/DropdownPlain';
|
||||
import { metricTypes, metricOf, issueOptions } from 'App/constants/filterOptions';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import { HelpText, Button, Icon } from 'UI'
|
||||
import FilterSeries from '../FilterSeries';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { confirm } from 'UI/Confirmation';
|
||||
import { withSiteId, dashboardMetricDetails, metricDetails } from 'App/routes'
|
||||
import DashboardSelectionModal from '../DashboardSelectionModal/DashboardSelectionModal';
|
||||
|
||||
interface Props {
|
||||
history: any;
|
||||
match: any;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
function WidgetForm(props: Props) {
|
||||
const [showDashboardSelectionModal, setShowDashboardSelectionModal] = useState(false);
|
||||
const { history, match: { params: { siteId, dashboardId, metricId } } } = props;
|
||||
const { metricStore } = useStore();
|
||||
const isSaving = useObserver(() => metricStore.isSaving);
|
||||
const metric: any = useObserver(() => metricStore.instance);
|
||||
|
||||
const timeseriesOptions = metricOf.filter(i => i.type === 'timeseries');
|
||||
const tableOptions = metricOf.filter(i => i.type === 'table');
|
||||
const isTable = metric.metricType === 'table';
|
||||
const isTimeSeries = metric.metricType === 'timeseries';
|
||||
const _issueOptions = [{ text: 'All', value: 'all' }].concat(issueOptions);
|
||||
|
||||
const write = ({ target: { value, name } }) => metricStore.merge({ [ name ]: value });
|
||||
const writeOption = (e, { value, name }) => {
|
||||
metricStore.merge({ [ name ]: value });
|
||||
|
||||
if (name === 'metricValue') {
|
||||
metricStore.merge({ metricValue: [value] });
|
||||
}
|
||||
|
||||
if (name === 'metricOf') {
|
||||
if (value === FilterKey.ISSUE) {
|
||||
metricStore.merge({ metricValue: ['all'] });
|
||||
}
|
||||
}
|
||||
|
||||
if (name === 'metricType') {
|
||||
if (value === 'timeseries') {
|
||||
metricStore.merge({ metricOf: timeseriesOptions[0].value, viewType: 'lineChart' });
|
||||
} else if (value === 'table') {
|
||||
metricStore.merge({ metricOf: tableOptions[0].value, viewType: 'table' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onSave = () => {
|
||||
const wasCreating = !metric.exists()
|
||||
metricStore.save(metric, dashboardId).then((metric) => {
|
||||
if (wasCreating) {
|
||||
if (parseInt(dashboardId) > 0) {
|
||||
history.push(withSiteId(dashboardMetricDetails(parseInt(dashboardId), metric.metricId), siteId));
|
||||
} else {
|
||||
history.push(withSiteId(metricDetails(metric.metricId), siteId));
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const onDelete = async () => {
|
||||
if (await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this metric?`
|
||||
})) {
|
||||
metricStore.delete(metric).then(props.onDelete);
|
||||
}
|
||||
}
|
||||
|
||||
const onObserveChanges = () => {
|
||||
console.log('observe changes');
|
||||
// metricStore.fetchMetricChartData(metric);
|
||||
}
|
||||
|
||||
return useObserver(() => (
|
||||
<div className="p-4">
|
||||
<div className="form-group">
|
||||
<label className="font-medium">Metric Type</label>
|
||||
<div className="flex items-center">
|
||||
<DropdownPlain
|
||||
name="metricType"
|
||||
options={metricTypes}
|
||||
value={ metric.metricType }
|
||||
onChange={ writeOption }
|
||||
/>
|
||||
|
||||
{metric.metricType === 'timeseries' && (
|
||||
<>
|
||||
<span className="mx-3">of</span>
|
||||
<DropdownPlain
|
||||
name="metricOf"
|
||||
options={timeseriesOptions}
|
||||
value={ metric.metricOf }
|
||||
onChange={ writeOption }
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{metric.metricType === 'table' && (
|
||||
<>
|
||||
<span className="mx-3">of</span>
|
||||
<DropdownPlain
|
||||
name="metricOf"
|
||||
options={tableOptions}
|
||||
value={ metric.metricOf }
|
||||
onChange={ writeOption }
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{metric.metricOf === FilterKey.ISSUE && (
|
||||
<>
|
||||
<span className="mx-3">issue type</span>
|
||||
<DropdownPlain
|
||||
name="metricValue"
|
||||
options={_issueOptions}
|
||||
value={ metric.metricValue[0] }
|
||||
onChange={ writeOption }
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{metric.metricType === 'table' && (
|
||||
<>
|
||||
<span className="mx-3">showing</span>
|
||||
<DropdownPlain
|
||||
name="metricFormat"
|
||||
options={[
|
||||
{ value: 'sessionCount', text: 'Session Count' },
|
||||
]}
|
||||
value={ metric.metricFormat }
|
||||
onChange={ writeOption }
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="font-medium items-center">
|
||||
{`${isTable ? 'Filter by' : 'Chart Series'}`}
|
||||
{!isTable && (
|
||||
<Button
|
||||
className="ml-2"
|
||||
primary plain size="small"
|
||||
onClick={() => metric.addSeries()}
|
||||
>Add Series</Button>
|
||||
)}
|
||||
</label>
|
||||
|
||||
{metric.series.length > 0 && metric.series.slice(0, isTable ? 1 : metric.series.length).map((series: any, index: number) => (
|
||||
<div className="mb-2">
|
||||
<FilterSeries
|
||||
hideHeader={ isTable }
|
||||
seriesIndex={index}
|
||||
series={series}
|
||||
// onRemoveSeries={() => removeSeries(index)}
|
||||
onRemoveSeries={() => metric.removeSeries(index)}
|
||||
canDelete={metric.series.length > 1}
|
||||
emptyMessage={isTable ?
|
||||
'Filter data using any event or attribute. Use Add Step button below to do so.' :
|
||||
'Add user event or filter to define the series by clicking Add Step.'
|
||||
}
|
||||
observeChanges={onObserveChanges}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="form-groups flex items-center justify-between">
|
||||
<Button
|
||||
primary
|
||||
size="small"
|
||||
onClick={onSave}
|
||||
disabled={isSaving}
|
||||
>
|
||||
{metric.exists() ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
<div className="flex items-center">
|
||||
{metric.exists() && (
|
||||
<>
|
||||
<Button plain size="small" onClick={onDelete} className="flex items-center">
|
||||
<Icon name="trash" size="14" className="mr-2" color="teal"/>
|
||||
Delete
|
||||
</Button>
|
||||
<Button plain size="small" className="flex items-center ml-2" onClick={() => setShowDashboardSelectionModal(true)}>
|
||||
<Icon name="columns-gap" size="14" className="mr-2" color="teal"/>
|
||||
Add to Dashboard
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<DashboardSelectionModal
|
||||
metricId={metric.metricId}
|
||||
show={showDashboardSelectionModal}
|
||||
closeHandler={() => setShowDashboardSelectionModal(false)}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
export default WidgetForm;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './WidgetForm';
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue