feat(ui): redesign alerts page

This commit is contained in:
sylenien 2022-08-18 11:43:48 +02:00 committed by Delirium
parent d4ad80f234
commit 6766d360dc
22 changed files with 1409 additions and 553 deletions

View file

@ -55,6 +55,10 @@ const METRICS_PATH = routes.metrics();
const METRICS_DETAILS = routes.metricDetails();
const METRICS_DETAILS_SUB = routes.metricDetailsSub();
const ALERTS_PATH = routes.alerts();
const ALERT_CREATE_PATH = routes.alertCreate();
const ALERT_EDIT_PATH = routes.alertEdit();
const DASHBOARD_PATH = routes.dashboard();
const DASHBOARD_SELECT_PATH = routes.dashboardSelected();
const DASHBOARD_METRIC_CREATE_PATH = routes.dashboardMetricCreate();
@ -198,6 +202,9 @@ class Router extends React.Component {
{onboarding && <Redirect to={withSiteId(ONBOARDING_REDIRECT_PATH, siteId)} />}
{/* DASHBOARD and Metrics */}
<Route exact strict path={withSiteId(ALERTS_PATH, siteIdList)} component={Dashboard} />
<Route exact strict path={withSiteId(ALERT_EDIT_PATH, siteIdList)} component={Dashboard} />
<Route exact strict path={withSiteId(ALERT_CREATE_PATH, siteIdList)} component={Dashboard} />
<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(METRICS_DETAILS_SUB, siteIdList)} component={Dashboard} />

View file

@ -6,7 +6,6 @@ import DashboardSideMenu from './components/DashboardSideMenu';
import { Loader } from 'UI';
import DashboardRouter from './components/DashboardRouter';
import cn from 'classnames';
import { withSiteId } from 'App/routes';
import withPermissions from 'HOCs/withPermissions'
interface RouterProps {
@ -21,8 +20,9 @@ function NewDashboard(props: RouteComponentProps<RouterProps>) {
const loading = useObserver(() => dashboardStore.isLoading);
const isMetricDetails = history.location.pathname.includes('/metrics/') || history.location.pathname.includes('/metric/');
const isDashboardDetails = history.location.pathname.includes('/dashboard/')
const isAlertsDetails = history.location.pathname.includes('/alert/')
const shouldHideMenu = isMetricDetails || isDashboardDetails;
const shouldHideMenu = isMetricDetails || isDashboardDetails || isAlertsDetails;
useEffect(() => {
dashboardStore.fetchList().then((resp) => {
if (parseInt(dashboardId) > 0) {

View file

@ -0,0 +1,89 @@
import React from 'react';
import { Icon } from 'UI';
import { checkForRecent } from 'App/date';
import { withSiteId, alertCreate } from 'App/routes';
// @ts-ignore
import { DateTime } from 'luxon';
import { withRouter, RouteComponentProps } from 'react-router-dom';
const getThreshold = (threshold: number) => {
if (threshold === 15) return '15 Minutes';
if (threshold === 30) return '30 Minutes';
if (threshold === 60) return '1 Hour';
if (threshold === 120) return '2 Hours';
if (threshold === 240) return '4 Hours';
if (threshold === 1440) return '1 Day';
};
const getNotifyChannel = (alert: Record<string, string>) => {
let str = '';
if (alert.slack) str = 'Slack';
if (alert.email) str += (str === '' ? '' : ' and ') + 'Email';
if (alert.webhool) str += (str === '' ? '' : ' and ') + 'Webhook';
if (str === '') return 'OpenReplay';
return str;
};
interface Props extends RouteComponentProps {
alert: Alert;
siteId: string;
init: (alert?: Alert) => void;
}
function AlertListItem(props: Props) {
const { alert, siteId, history, init } = props;
const onItemClick = () => {
const path = withSiteId(alertCreate(), siteId);
init(alert)
history.push(path);
};
return (
<div className="hover:bg-active-blue cursor-pointer border-t px-3" onClick={onItemClick}>
<div className="grid grid-cols-12 py-4 select-none">
<div className="col-span-5 flex items-start">
<div className="flex items-center capitalize-first">
<div className="w-9 h-9 rounded-full bg-tealx-lightest flex items-center justify-center mr-2">
<Icon name="bell" size="16" color="tealx" />
</div>
<div className="link capitalize-first">{alert.name}</div>
</div>
</div>
<div className="col-span-2">
<div className="flex items-center uppercase">
<span>{alert.detectionMethod}</span>
</div>
</div>
<div className="col-span-5 text-right">
{checkForRecent(DateTime.fromMillis(alert.createdAt), 'LLL dd, yyyy, hh:mm a')}
</div>
</div>
<div className="text-disabled-text px-2 pb-2">
{'When the '}
<span className="font-medium">{alert.detectionMethod}</span>
{' of '}
<span className="font-medium">{alert.query.left}</span>
{' is '}
<span className="font-medium">
{alert.query.operator}{alert.query.right} {alert.metric.unit}
</span>
{' over the past '}
<span className="font-medium">{getThreshold(alert.currentPeriod)}</span>
{alert.detectionMethod === 'change' ? (
<>
{' compared to the previous '}
<span className="font-medium">{getThreshold(alert.previousPeriod)}</span>
</>
) : null}
{', notify me on '}
<span>{getNotifyChannel(alert)}</span>.
</div>
{alert.description ? (
<div className="text-disabled-text px-2 pb-2">{alert.description}</div>
) : null}
</div>
);
}
export default withRouter(AlertListItem);

View file

@ -0,0 +1,83 @@
import React from 'react';
import { NoContent, Pagination, Icon } from 'UI';
import { filterList } from 'App/utils';
import { sliceListPerPage } from 'App/utils';
import { fetchList } from 'Duck/alerts';
import { connect } from 'react-redux';
import AlertListItem from './AlertListItem'
const pageSize = 20;
interface Props {
fetchList: () => void;
list: any;
alertsSearch: any;
siteId: string;
onDelete: (instance: Alert) => void;
onSave: (instance: Alert) => void;
init: (instance?: Alert) => void
}
function AlertsList({ fetchList, list: alertsList, alertsSearch, siteId, init }: Props) {
React.useEffect(() => { fetchList() }, []);
const alertsArray = alertsList.toJS();
const [page, setPage] = React.useState(1);
const filteredAlerts = filterList(alertsArray, alertsSearch, ['name'], (item, query) => query.test(item.query.left))
const list = alertsSearch !== '' ? filteredAlerts : alertsArray;
const lenth = list.length;
return (
<NoContent
show={lenth === 0}
title={
<div className="flex flex-col items-center justify-center">
<Icon name="bell" size={80} color="figmaColors-accent-secondary" />
<div className="text-center text-gray-600 my-4">
{alertsSearch !== '' ? 'No matching results' : "You haven't created any alerts yet"}
</div>
</div>
}
>
<div className="mt-3 border-b">
<div className="grid grid-cols-12 py-2 font-medium px-3">
<div className="col-span-5">Title</div>
<div className="col-span-2">Type</div>
<div className="col-span-5 text-right">Modified</div>
</div>
{sliceListPerPage(list, page - 1, pageSize).map((alert: any) => (
<React.Fragment key={alert.alertId}>
<AlertListItem alert={alert} siteId={siteId} init={init} />
</React.Fragment>
))}
</div>
<div className="w-full flex items-center justify-between pt-4">
<div className="text-disabled-text">
Showing <span className="font-semibold">{Math.min(list.length, pageSize)}</span> out of{' '}
<span className="font-semibold">{list.length}</span> Alerts
</div>
<Pagination
page={page}
totalPages={Math.ceil(lenth / pageSize)}
onPageChange={(page) => setPage(page)}
limit={pageSize}
debounceRequest={100}
/>
</div>
</NoContent>
);
}
export default connect(
(state) => ({
// @ts-ignore
list: state.getIn(['alerts', 'list']).sort((a, b) => b.createdAt - a.createdAt),
// @ts-ignore
alertsSearch: state.getIn(['alerts', 'alertsSearch']),
}),
{ fetchList }
)(AlertsList);

View file

@ -0,0 +1,45 @@
import React, { useEffect, useState } from 'react';
import { Icon } from 'UI';
import { debounce } from 'App/utils';
import { changeSearch } from 'Duck/alerts';
import { connect } from 'react-redux';
let debounceUpdate: any = () => {};
interface Props {
changeSearch: (value: string) => void;
}
function AlertsSearch({ changeSearch }: Props) {
const [inputValue, setInputValue] = useState('');
useEffect(() => {
debounceUpdate = debounce((value: string) => changeSearch(value), 500);
}, []);
const write = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(value);
debounceUpdate(value);
};
return (
<div className="relative">
<Icon name="search" className="absolute top-0 bottom-0 ml-2 m-auto" size="16" />
<input
value={inputValue}
name="alertsSearch"
className="bg-white p-2 border border-borderColor-gray-light-shade rounded w-full pl-10"
placeholder="Filter by title or description"
onChange={write}
/>
</div>
);
}
export default connect(
(state) => ({
// @ts-ignore
alertsSearch: state.getIn(['alerts', 'alertsSearch']),
}),
{ changeSearch }
)(AlertsSearch);

View file

@ -0,0 +1,69 @@
import React from 'react';
import { Button, PageTitle, Icon } from 'UI';
import withPageTitle from 'HOCs/withPageTitle';
import { connect } from 'react-redux';
import { init, edit, save, remove } from 'Duck/alerts';
import { confirm } from 'UI';
import { toast } from 'react-toastify';
import AlertsList from './AlertsList';
import AlertsSearch from './AlertsSearch';
interface IAlertsView {
siteId: string;
init: (instance?: Alert) => any;
save: (instance: Alert) => Promise<any>;
remove: (alertId: string) => Promise<any>;
}
function AlertsView({ siteId, remove, save, init }: IAlertsView) {
const onDelete = async (instance: Alert) => {
if (
await confirm({
header: 'Confirm',
confirmButton: 'Yes, delete',
confirmation: `Are you sure you want to permanently delete this alert?`,
})
) {
remove(instance.alertId).then(() => {
// toggleForm(null, false);
});
}
};
const onSave = (instance: Alert) => {
const wasUpdating = instance.exists();
save(instance).then(() => {
if (!wasUpdating) {
toast.success('New alert saved');
// toggleForm(null, false);
} else {
toast.success('Alert updated');
}
});
};
return (
<div style={{ maxWidth: '1300px', margin: 'auto'}} className="bg-white rounded py-4 px-6 border">
<div className="flex items-center mb-4 justify-between">
<div className="flex items-baseline mr-3">
<PageTitle title="Dashboards" />
</div>
<Button variant="primary" onClick={null}>Create</Button>
<div className="ml-auto w-1/4" style={{ minWidth: 300 }}>
<AlertsSearch />
</div>
</div>
<div className="text-base text-disabled-text flex items-center">
<Icon name="info-circle-fill" className="mr-2" size={16} />
A dashboard is a custom visualization using your OpenReplay data.
</div>
<AlertsList siteId={siteId} onSave={onSave} onDelete={onDelete} init={init} />
</div>
);
}
// @ts-ignore
const Container = connect(null, { init, edit, save, remove })(AlertsView);
export default withPageTitle('Alerts - OpenReplay')(Container);

View file

@ -0,0 +1,66 @@
import React from 'react';
import { Input, TagBadge } from 'UI';
import Select from 'Shared/Select';
const DropdownChips = ({
textFiled = false,
validate = null,
placeholder = '',
selected = [],
options = [],
badgeClassName = 'lowercase',
onChange = () => null,
...props
}) => {
const onRemove = (id) => {
onChange(selected.filter((i) => i !== id));
};
const onSelect = ({ value }) => {
const newSlected = selected.concat(value.value);
onChange(newSlected);
};
const onKeyPress = (e) => {
const val = e.target.value;
if (e.key !== 'Enter' || selected.includes(val)) return;
e.preventDefault();
e.stopPropagation();
if (validate && !validate(val)) return;
const newSlected = selected.concat(val);
e.target.value = '';
onChange(newSlected);
};
const _options = options.filter((item) => !selected.includes(item.value));
const renderBadge = (item) => {
const val = typeof item === 'string' ? item : item.value;
const text = typeof item === 'string' ? item : item.label;
return <TagBadge className={badgeClassName} key={text} text={text} hashed={false} onRemove={() => onRemove(val)} outline={true} />;
};
return (
<div className="w-full">
{textFiled ? (
<Input type="text" onKeyPress={onKeyPress} placeholder={placeholder} />
) : (
<Select
placeholder={placeholder}
isSearchable={true}
options={_options}
name="webhookInput"
value={null}
onChange={onSelect}
{...props}
/>
)}
<div className="flex flex-wrap mt-3">
{textFiled ? selected.map(renderBadge) : options.filter((i) => selected.includes(i.value)).map(renderBadge)}
</div>
</div>
);
};
export default DropdownChips;

View file

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

View file

@ -0,0 +1,394 @@
import React, { useEffect } from 'react';
import { Button, Form, Input, SegmentSelection, Checkbox, Icon } from 'UI';
import { alertConditions as conditions } from 'App/constants';
import { connect } from 'react-redux';
// @ts-ignore
import stl from './alertForm.module.css';
import DropdownChips from './DropdownChips';
import { validateEmail } from 'App/validate';
import cn from 'classnames';
import { fetchTriggerOptions } from 'Duck/alerts';
import Select from 'Shared/Select';
const thresholdOptions = [
{ label: '15 minutes', value: 15 },
{ label: '30 minutes', value: 30 },
{ label: '1 hour', value: 60 },
{ label: '2 hours', value: 120 },
{ label: '4 hours', value: 240 },
{ label: '1 day', value: 1440 },
];
const changeOptions = [
{ label: 'change', value: 'change' },
{ label: '% change', value: 'percent' },
];
const Circle = ({ text }: { text: string}) => (
<div className="circle mr-4 w-6 h-6 rounded-full bg-gray-light flex items-center justify-center">
{text}
</div>
);
interface ISection {
index: string
title: string
description?: string
content: React.ReactNode
}
const Section = ({ index, title, description, content }: ISection) => (
<div className="w-full">
<div className="flex items-start">
<Circle text={index} />
<div>
<span className="font-medium">{title}</span>
{description && <div className="text-sm color-gray-medium">{description}</div>}
</div>
</div>
<div className="ml-10">{content}</div>
</div>
);
interface IProps {
instance: Alert
style: Record<string, string | number>
slackChannels: any[]
webhooks: any[]
loading: boolean
deleting: boolean
triggerOptions: any[]
onDelete: (instance: Alert) => void
onClose: () => void
fetchTriggerOptions: () => void
edit: (query: any) => void
onSubmit: (instance: Alert) => void
}
const NewAlert = (props: IProps) => {
const {
instance,
slackChannels,
webhooks,
loading,
onDelete,
deleting,
triggerOptions,
style,
} = props;
const write = ({ target: { value, name } }: React.ChangeEvent<HTMLInputElement>) => props.edit({ [name]: value });
const writeOption = (_: React.ChangeEvent, { name, value }: { name: string, value: Record<string, any>}) => props.edit({ [name]: value.value });
const onChangeCheck = ({ target: { checked, name } }: React.ChangeEvent<HTMLInputElement>) => props.edit({ [name]: checked });
useEffect(() => {
props.fetchTriggerOptions();
}, []);
const writeQueryOption = (e: React.ChangeEvent, { name, value }: { name: string, value: string }) => {
const { query } = instance;
props.edit({ query: { ...query, [name]: value } });
};
const writeQuery = ({ target: { value, name } }: React.ChangeEvent<HTMLInputElement>) => {
const { query } = instance;
props.edit({ query: { ...query, [name]: value } });
};
const metric =
instance && instance.query.left
? triggerOptions.find((i) => i.value === instance.query.left)
: null;
const unit = metric ? metric.unit : '';
const isThreshold = instance.detectionMethod === 'threshold';
return (
<Form
className={cn('p-6 pb-10', stl.wrapper)}
style={style}
onSubmit={() => props.onSubmit(instance)}
id="alert-form"
>
<div className={cn(stl.content, '-mx-6 px-6 pb-12')}>
<input
autoFocus={true}
className="text-lg border border-gray-light rounded w-full"
name="name"
style={{ fontSize: '18px', padding: '10px', fontWeight: '600' }}
value={instance && instance.name}
onChange={write}
placeholder="Untiltled Alert"
id="name-field"
/>
<div className="mb-8" />
<Section
index="1"
title={'What kind of alert do you want to set?'}
content={
<div>
<SegmentSelection
primary
name="detectionMethod"
className="my-3"
onSelect={(e: any, { name, value }: any) => props.edit({ [name]: value })}
value={{ value: instance.detectionMethod }}
list={[
{ name: 'Threshold', value: 'threshold' },
{ name: 'Change', value: 'change' },
]}
/>
<div className="text-sm color-gray-medium">
{isThreshold &&
'Eg. Alert me if memory.avg is greater than 500mb over the past 4 hours.'}
{!isThreshold &&
'Eg. Alert me if % change of memory.avg is greater than 10% over the past 4 hours compared to the previous 4 hours.'}
</div>
<div className="my-4" />
</div>
}
/>
<hr className="my-8" />
<Section
index="2"
title="Condition"
content={
<div>
{!isThreshold && (
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">{'Trigger when'}</label>
<Select
className="w-4/6"
placeholder="change"
options={changeOptions}
name="change"
defaultValue={instance.change}
onChange={({ value }) => writeOption(null, { name: 'change', value })}
id="change-dropdown"
/>
</div>
)}
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">
{isThreshold ? 'Trigger when' : 'of'}
</label>
<Select
className="w-4/6"
placeholder="Select Metric"
isSearchable={true}
options={triggerOptions}
name="left"
value={triggerOptions.find((i) => i.value === instance.query.left)}
// onChange={ writeQueryOption }
onChange={({ value }) =>
writeQueryOption(null, { name: 'left', value: value.value })
}
/>
</div>
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">{'is'}</label>
<div className="w-4/6 flex items-center">
<Select
placeholder="Select Condition"
options={conditions}
name="operator"
defaultValue={instance.query.operator}
// onChange={ writeQueryOption }
onChange={({ value }) =>
writeQueryOption(null, { name: 'operator', value: value.value })
}
/>
{unit && (
<>
<Input
className="px-4"
style={{ marginRight: '31px' }}
// label={{ basic: true, content: unit }}
// labelPosition='right'
name="right"
value={instance.query.right}
onChange={writeQuery}
placeholder="E.g. 3"
/>
<span className="ml-2">{'test'}</span>
</>
)}
{!unit && (
<Input
wrapperClassName="ml-2"
// className="pl-4"
name="right"
value={instance.query.right}
onChange={writeQuery}
placeholder="Specify Value"
/>
)}
</div>
</div>
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">{'over the past'}</label>
<Select
className="w-2/6"
placeholder="Select timeframe"
options={thresholdOptions}
name="currentPeriod"
defaultValue={instance.currentPeriod}
// onChange={ writeOption }
onChange={({ value }) => writeOption(null, { name: 'currentPeriod', value })}
/>
</div>
{!isThreshold && (
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">
{'compared to previous'}
</label>
<Select
className="w-2/6"
placeholder="Select timeframe"
options={thresholdOptions}
name="previousPeriod"
defaultValue={instance.previousPeriod}
// onChange={ writeOption }
onChange={({ value }) => writeOption(null, { name: 'previousPeriod', value })}
/>
</div>
)}
</div>
}
/>
<hr className="my-8" />
<Section
index="3"
title="Notify Through"
description="You'll be noticed in app notifications. Additionally opt in to receive alerts on:"
content={
<div className="flex flex-col">
<div className="flex items-center my-4">
<Checkbox
name="slack"
className="mr-8"
type="checkbox"
checked={instance.slack}
onClick={onChangeCheck}
label="Slack"
/>
<Checkbox
name="email"
type="checkbox"
checked={instance.email}
onClick={onChangeCheck}
className="mr-8"
label="Email"
/>
<Checkbox
name="webhook"
type="checkbox"
checked={instance.webhook}
onClick={onChangeCheck}
label="Webhook"
/>
</div>
{instance.slack && (
<div className="flex items-start my-4">
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'Slack'}</label>
<div className="w-4/6">
<DropdownChips
fluid
selected={instance.slackInput}
options={slackChannels}
placeholder="Select Channel"
// @ts-ignore
onChange={(selected) => props.edit({ slackInput: selected })}
/>
</div>
</div>
)}
{instance.email && (
<div className="flex items-start my-4">
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'Email'}</label>
<div className="w-4/6">
<DropdownChips
textFiled
validate={validateEmail}
selected={instance.emailInput}
placeholder="Type and press Enter key"
// @ts-ignore
onChange={(selected) => props.edit({ emailInput: selected })}
/>
</div>
</div>
)}
{instance.webhook && (
<div className="flex items-start my-4">
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'Webhook'}</label>
<DropdownChips
fluid
selected={instance.webhookInput}
options={webhooks}
placeholder="Select Webhook"
// @ts-ignore
onChange={(selected) => props.edit({ webhookInput: selected })}
/>
</div>
)}
</div>
}
/>
</div>
<div className="flex items-center justify-between absolute bottom-0 left-0 right-0 p-6 border-t z-10 bg-white">
<div className="flex items-center">
<Button
loading={loading}
variant="primary"
type="submit"
disabled={loading || !instance.validate()}
id="submit-button"
>
{instance.exists() ? 'Update' : 'Create'}
</Button>
<div className="mx-1" />
<Button onClick={props.onClose}>Cancel</Button>
</div>
<div>
{instance.exists() && (
<Button
hover
variant="text"
loading={deleting}
type="button"
onClick={() => onDelete(instance)}
id="trash-button"
>
<Icon name="trash" color="gray-medium" size="18" />
</Button>
)}
</div>
</div>
</Form>
);
};
export default connect(
(state) => ({
// @ts-ignore
instance: state.getIn(['alerts', 'instance']),
// @ts-ignore
triggerOptions: state.getIn(['alerts', 'triggerOptions']),
// @ts-ignore
loading: state.getIn(['alerts', 'saveRequest', 'loading']),
// @ts-ignore
deleting: state.getIn(['alerts', 'removeRequest', 'loading']),
}),
{ fetchTriggerOptions }
)(NewAlert);

View file

@ -0,0 +1,27 @@
.wrapper {
position: relative;
}
.content {
height: calc(100vh - 102px);
overflow-y: auto;
&::-webkit-scrollbar {
width: 2px;
}
&::-webkit-scrollbar-thumb {
background: transparent;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&:hover {
&::-webkit-scrollbar-track {
background: #f3f3f3;
}
&::-webkit-scrollbar-thumb {
background: $gray-medium;
}
}
}

View file

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

View file

@ -0,0 +1,2 @@
// TODO burn the immutable and make typing this possible
type Alert = Record<string, any>

View file

@ -41,7 +41,7 @@ function DashboardListItem(props: Props) {
</div>
<div className="col-span-2 text-right">{checkForRecent(dashboard.createdAt, 'LLL dd, yyyy, hh:mm a')}</div>
</div>
{dashboard.description ? <div className="text-disabled-text px-4 pb-2">{dashboard.description}</div> : null}
{dashboard.description ? <div className="text-disabled-text px-2 pb-2">{dashboard.description}</div> : null}
</div>
);
}

View file

@ -3,66 +3,87 @@ import { Switch, Route } from 'react-router';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import {
metrics,
metricDetails,
metricDetailsSub,
dashboardSelected,
dashboardMetricCreate,
dashboardMetricDetails,
withSiteId,
dashboard,
metrics,
metricDetails,
metricDetailsSub,
dashboardSelected,
dashboardMetricCreate,
dashboardMetricDetails,
withSiteId,
dashboard,
alerts,
alertCreate,
alertEdit,
} from 'App/routes';
import DashboardView from '../DashboardView';
import MetricsView from '../MetricsView';
import WidgetView from '../WidgetView';
import WidgetSubDetailsView from '../WidgetSubDetailsView';
import DashboardsView from '../DashboardList';
import Alerts from '../Alerts';
import CreateAlert from '../Alerts/NewAlert'
function DashboardViewSelected({ siteId, dashboardId }: { siteId: string, dashboardId: string }) {
return (
<DashboardView siteId={siteId} dashboardId={dashboardId} />
)
function DashboardViewSelected({ siteId, dashboardId }: { siteId: string; dashboardId: string }) {
return <DashboardView siteId={siteId} dashboardId={dashboardId} />;
}
interface Props extends RouteComponentProps {
match: any
match: any;
}
function DashboardRouter(props: Props) {
const { match: { params: { siteId, dashboardId } }, history } = props;
const {
match: {
params: { siteId, dashboardId },
},
history,
} = props;
return (
<div>
<Switch>
<Route exact strict path={withSiteId(metrics(), siteId)}>
<MetricsView siteId={siteId} />
</Route>
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(metricDetailsSub(), siteId)}>
<WidgetSubDetailsView siteId={siteId} {...props} />
</Route>
<Route exact strict path={withSiteId(metricDetails(), siteId)}>
<WidgetView siteId={siteId} {...props} />
</Route>
<Route exact path={withSiteId(dashboard(), siteId)}>
<DashboardsView siteId={siteId} history={history} />
</Route>
<Route exact strict path={withSiteId(metricDetailsSub(), siteId)}>
<WidgetSubDetailsView siteId={siteId} {...props} />
</Route>
<Route exact strict path={withSiteId(dashboardMetricDetails(dashboardId), siteId)}>
<WidgetView siteId={siteId} {...props} />
</Route>
<Route exact path={withSiteId(dashboard(), siteId)}>
<DashboardsView siteId={siteId} history={history} />
</Route>
<Route exact strict path={withSiteId(dashboardMetricCreate(dashboardId), siteId)}>
<WidgetView siteId={siteId} {...props} />
</Route>
<Route exact strict path={withSiteId(dashboardMetricDetails(dashboardId), siteId)}>
<WidgetView siteId={siteId} {...props} />
</Route>
<Route exact strict path={withSiteId(dashboardSelected(dashboardId), siteId)}>
<DashboardViewSelected siteId={siteId} dashboardId={dashboardId} />
</Route>
</Switch>
</div>
);
<Route exact strict path={withSiteId(dashboardMetricCreate(dashboardId), siteId)}>
<WidgetView siteId={siteId} {...props} />
</Route>
<Route exact strict path={withSiteId(dashboardSelected(dashboardId), siteId)}>
<DashboardViewSelected siteId={siteId} dashboardId={dashboardId} />
</Route>
<Route exact strict path={withSiteId(alerts(), siteId)}>
<Alerts siteId={siteId} />
</Route>
<Route exact strict path={withSiteId(alertCreate(), siteId)}>
<CreateAlert siteId={siteId} />
</Route>
<Route exact strict path={withSiteId(alertEdit(), siteId)}>
<CreateAlert siteId={siteId} {...props} />
</Route>
</Switch>
</div>
);
}
export default withRouter(DashboardRouter);

View file

@ -1,64 +1,60 @@
import React from 'react';
import { SideMenuitem, SideMenuHeader } from 'UI';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { withSiteId, metrics, dashboard } from 'App/routes';
import { withSiteId, metrics, dashboard, alerts } from 'App/routes';
import { connect } from 'react-redux';
import { compose } from 'redux'
import { compose } from 'redux';
import { setShowAlerts } from 'Duck/dashboard';
interface Props extends RouteComponentProps {
siteId: string
history: any
setShowAlerts: (show: boolean) => void
siteId: string;
history: any;
setShowAlerts: (show: boolean) => void;
}
function DashboardSideMenu(props: Props) {
const { history, siteId, setShowAlerts } = props;
const isMetric = history.location.pathname.includes('metrics');
const isDashboards = history.location.pathname.includes('dashboard');
const { history, siteId, setShowAlerts } = props;
const isMetric = history.location.pathname.includes('metrics');
const isDashboards = history.location.pathname.includes('dashboard');
const isAlerts = history.location.pathname.includes('alerts');
const redirect = (path: string) => {
history.push(path);
}
const redirect = (path: string) => {
history.push(path);
};
return (
<div>
<SideMenuHeader
className="mb-4 flex items-center"
text="Preferences"
/>
<div className="w-full">
<SideMenuitem
active={isDashboards}
id="menu-manage-alerts"
title="Dashboards"
iconName="columns-gap"
onClick={() => redirect(withSiteId(dashboard(), siteId))}
/>
</div>
<div className="border-t w-full my-2" />
<div className="w-full">
<SideMenuitem
active={isMetric}
id="menu-manage-alerts"
title="Metrics"
iconName="bar-chart-line"
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>
);
return (
<div>
<SideMenuHeader className="mb-4 flex items-center" text="Preferences" />
<div className="w-full">
<SideMenuitem
active={isDashboards}
id="menu-manage-alerts"
title="Dashboards"
iconName="columns-gap"
onClick={() => redirect(withSiteId(dashboard(), siteId))}
/>
</div>
<div className="border-t w-full my-2" />
<div className="w-full">
<SideMenuitem
active={isMetric}
id="menu-manage-alerts"
title="Metrics"
iconName="bar-chart-line"
onClick={() => redirect(withSiteId(metrics(), siteId))}
/>
</div>
<div className="border-t w-full my-2" />
<div className="w-full">
<SideMenuitem
active={isAlerts}
id="menu-manage-alerts"
title="Alerts"
iconName="bell-plus"
onClick={() => redirect(withSiteId(alerts(), siteId))}
/>
</div>
</div>
);
}
export default compose(
withRouter,
connect(null, { setShowAlerts }),
)(DashboardSideMenu)
export default compose(withRouter, connect(null, { setShowAlerts }))(DashboardSideMenu);

View file

@ -18,9 +18,10 @@ const SelectedValue = ({ icon, text }) => {
class IssueForm extends React.PureComponent {
componentDidMount() {
const { projects, issueTypes } = this.props;
this.props.init({
projectId: projects.first() ? projects.first().id : '',
issueType: issueTypes.first() ? issueTypes.first().id : ''
projectId: projects[0] ? projects[0].id : '',
issueType: issueTypes[0] ? issueTypes[0].id : ''
});
}

View file

@ -1,28 +1,33 @@
import React from 'react';
import cn from 'classnames';
import { connect } from 'react-redux';
import { connectPlayer, STORAGE_TYPES, selectStorageType, selectStorageListNow } from 'Player/store';
import {
connectPlayer,
STORAGE_TYPES,
selectStorageType,
selectStorageListNow,
} from 'Player/store';
import LiveTag from 'Shared/LiveTag';
import { toggleTimetravel, jumpToLive } from 'Player';
import { Icon, Button } from 'UI';
import { toggleInspectorMode } from 'Player';
import {
fullscreenOn,
fullscreenOff,
toggleBottomBlock,
changeSkipInterval,
OVERVIEW,
CONSOLE,
NETWORK,
STACKEVENTS,
STORAGE,
PROFILER,
PERFORMANCE,
GRAPHQL,
FETCH,
EXCEPTIONS,
INSPECTOR,
fullscreenOn,
fullscreenOff,
toggleBottomBlock,
changeSkipInterval,
OVERVIEW,
CONSOLE,
NETWORK,
STACKEVENTS,
STORAGE,
PROFILER,
PERFORMANCE,
GRAPHQL,
FETCH,
EXCEPTIONS,
INSPECTOR,
} from 'Duck/components/player';
import { AssistDuration } from './Time';
import Timeline from './Timeline';
@ -34,18 +39,18 @@ import { Tooltip } from 'react-tippy';
import XRayButton from 'Shared/XRayButton';
function getStorageIconName(type) {
switch (type) {
case STORAGE_TYPES.REDUX:
return 'vendors/redux';
case STORAGE_TYPES.MOBX:
return 'vendors/mobx';
case STORAGE_TYPES.VUEX:
return 'vendors/vuex';
case STORAGE_TYPES.NGRX:
return 'vendors/ngrx';
case STORAGE_TYPES.NONE:
return 'store';
}
switch (type) {
case STORAGE_TYPES.REDUX:
return 'vendors/redux';
case STORAGE_TYPES.MOBX:
return 'vendors/mobx';
case STORAGE_TYPES.VUEX:
return 'vendors/vuex';
case STORAGE_TYPES.NGRX:
return 'vendors/ngrx';
case STORAGE_TYPES.NONE:
return 'store';
}
}
const SKIP_INTERVALS = {
@ -59,301 +64,325 @@ const SKIP_INTERVALS = {
};
function getStorageName(type) {
switch (type) {
case STORAGE_TYPES.REDUX:
return 'REDUX';
case STORAGE_TYPES.MOBX:
return 'MOBX';
case STORAGE_TYPES.VUEX:
return 'VUEX';
case STORAGE_TYPES.NGRX:
return 'NGRX';
case STORAGE_TYPES.NONE:
return 'STATE';
}
switch (type) {
case STORAGE_TYPES.REDUX:
return 'REDUX';
case STORAGE_TYPES.MOBX:
return 'MOBX';
case STORAGE_TYPES.VUEX:
return 'VUEX';
case STORAGE_TYPES.NGRX:
return 'NGRX';
case STORAGE_TYPES.NONE:
return 'STATE';
}
}
@connectPlayer((state) => ({
time: state.time,
endTime: state.endTime,
live: state.live,
livePlay: state.livePlay,
playing: state.playing,
completed: state.completed,
skip: state.skip,
skipToIssue: state.skipToIssue,
speed: state.speed,
disabled: state.cssLoading || state.messagesLoading || state.inspectorMode || state.markedTargets,
inspectorMode: state.inspectorMode,
fullscreenDisabled: state.messagesLoading,
logCount: state.logListNow.length,
logRedCount: state.logRedCountNow,
resourceRedCount: state.resourceRedCountNow,
fetchRedCount: state.fetchRedCountNow,
showStack: state.stackList.length > 0,
stackCount: state.stackListNow.length,
stackRedCount: state.stackRedCountNow,
profilesCount: state.profilesListNow.length,
storageCount: selectStorageListNow(state).length,
storageType: selectStorageType(state),
showStorage: selectStorageType(state) !== STORAGE_TYPES.NONE,
showProfiler: state.profilesList.length > 0,
showGraphql: state.graphqlList.length > 0,
showFetch: state.fetchCount > 0,
fetchCount: state.fetchCountNow,
graphqlCount: state.graphqlListNow.length,
exceptionsCount: state.exceptionsListNow.length,
showExceptions: state.exceptionsList.length > 0,
showLongtasks: state.longtasksList.length > 0,
liveTimeTravel: state.liveTimeTravel,
time: state.time,
endTime: state.endTime,
live: state.live,
livePlay: state.livePlay,
playing: state.playing,
completed: state.completed,
skip: state.skip,
skipToIssue: state.skipToIssue,
speed: state.speed,
disabled: state.cssLoading || state.messagesLoading || state.inspectorMode || state.markedTargets,
inspectorMode: state.inspectorMode,
fullscreenDisabled: state.messagesLoading,
logCount: state.logListNow.length,
logRedCount: state.logRedCountNow,
resourceRedCount: state.resourceRedCountNow,
fetchRedCount: state.fetchRedCountNow,
showStack: state.stackList.length > 0,
stackCount: state.stackListNow.length,
stackRedCount: state.stackRedCountNow,
profilesCount: state.profilesListNow.length,
storageCount: selectStorageListNow(state).length,
storageType: selectStorageType(state),
showStorage: selectStorageType(state) !== STORAGE_TYPES.NONE,
showProfiler: state.profilesList.length > 0,
showGraphql: state.graphqlList.length > 0,
showFetch: state.fetchCount > 0,
fetchCount: state.fetchCountNow,
graphqlCount: state.graphqlListNow.length,
exceptionsCount: state.exceptionsListNow.length,
showExceptions: state.exceptionsList.length > 0,
showLongtasks: state.longtasksList.length > 0,
liveTimeTravel: state.liveTimeTravel,
}))
@connect(
(state, props) => {
const permissions = state.getIn(['user', 'account', 'permissions']) || [];
const isEnterprise = state.getIn(['user', 'account', 'edition']) === 'ee';
return {
disabled: props.disabled || (isEnterprise && !permissions.includes('DEV_TOOLS')),
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
bottomBlock: state.getIn(['components', 'player', 'bottomBlock']),
showStorage: props.showStorage || !state.getIn(['components', 'player', 'hiddenHints', 'storage']),
showStack: props.showStack || !state.getIn(['components', 'player', 'hiddenHints', 'stack']),
closedLive: !!state.getIn(['sessions', 'errors']) || !state.getIn(['sessions', 'current', 'live']),
skipInterval: state.getIn(['components', 'player', 'skipInterval']),
};
},
{
fullscreenOn,
fullscreenOff,
toggleBottomBlock,
changeSkipInterval,}
(state, props) => {
const permissions = state.getIn(['user', 'account', 'permissions']) || [];
const isEnterprise = state.getIn(['user', 'account', 'edition']) === 'ee';
return {
disabled: props.disabled || (isEnterprise && !permissions.includes('DEV_TOOLS')),
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
bottomBlock: state.getIn(['components', 'player', 'bottomBlock']),
showStorage:
props.showStorage || !state.getIn(['components', 'player', 'hiddenHints', 'storage']),
showStack: props.showStack || !state.getIn(['components', 'player', 'hiddenHints', 'stack']),
closedLive:
!!state.getIn(['sessions', 'errors']) || !state.getIn(['sessions', 'current', 'live']),
skipInterval: state.getIn(['components', 'player', 'skipInterval']),
};
},
{
fullscreenOn,
fullscreenOff,
toggleBottomBlock,
changeSkipInterval,
}
)
export default class Controls extends React.Component {
componentDidMount() {
document.addEventListener('keydown', this.onKeyDown);
componentDidMount() {
document.addEventListener('keydown', this.onKeyDown);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.onKeyDown);
//this.props.toggleInspectorMode(false);
}
shouldComponentUpdate(nextProps) {
if (
nextProps.fullscreen !== this.props.fullscreen ||
nextProps.bottomBlock !== this.props.bottomBlock ||
nextProps.live !== this.props.live ||
nextProps.livePlay !== this.props.livePlay ||
nextProps.playing !== this.props.playing ||
nextProps.completed !== this.props.completed ||
nextProps.skip !== this.props.skip ||
nextProps.skipToIssue !== this.props.skipToIssue ||
nextProps.speed !== this.props.speed ||
nextProps.disabled !== this.props.disabled ||
nextProps.fullscreenDisabled !== this.props.fullscreenDisabled ||
// nextProps.inspectorMode !== this.props.inspectorMode ||
nextProps.logCount !== this.props.logCount ||
nextProps.logRedCount !== this.props.logRedCount ||
nextProps.resourceRedCount !== this.props.resourceRedCount ||
nextProps.fetchRedCount !== this.props.fetchRedCount ||
nextProps.showStack !== this.props.showStack ||
nextProps.stackCount !== this.props.stackCount ||
nextProps.stackRedCount !== this.props.stackRedCount ||
nextProps.profilesCount !== this.props.profilesCount ||
nextProps.storageCount !== this.props.storageCount ||
nextProps.storageType !== this.props.storageType ||
nextProps.showStorage !== this.props.showStorage ||
nextProps.showProfiler !== this.props.showProfiler ||
nextProps.showGraphql !== this.props.showGraphql ||
nextProps.showFetch !== this.props.showFetch ||
nextProps.fetchCount !== this.props.fetchCount ||
nextProps.graphqlCount !== this.props.graphqlCount ||
nextProps.showExceptions !== this.props.showExceptions ||
nextProps.exceptionsCount !== this.props.exceptionsCount ||
nextProps.showLongtasks !== this.props.showLongtasks ||
nextProps.liveTimeTravel !== this.props.liveTimeTravel ||
nextProps.skipInterval !== this.props.skipInterval
)
return true;
return false;
}
onKeyDown = (e) => {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return;
}
if (this.props.inspectorMode) {
if (e.key === 'Esc' || e.key === 'Escape') {
toggleInspectorMode(false);
}
}
// if (e.key === ' ') {
// document.activeElement.blur();
// this.props.togglePlay();
// }
if (e.key === 'Esc' || e.key === 'Escape') {
this.props.fullscreenOff();
}
if (e.key === 'ArrowRight') {
this.forthTenSeconds();
}
if (e.key === 'ArrowLeft') {
this.backTenSeconds();
}
if (e.key === 'ArrowDown') {
this.props.speedDown();
}
if (e.key === 'ArrowUp') {
this.props.speedUp();
}
};
forthTenSeconds = () => {
const { time, endTime, jump, skipInterval } = this.props;
jump(Math.min(endTime, time + SKIP_INTERVALS[skipInterval]));
};
backTenSeconds = () => {
//shouldComponentUpdate
const { time, jump, skipInterval } = this.props;
jump(Math.max(0, time - SKIP_INTERVALS[skipInterval]));
};
goLive = () => this.props.jump(this.props.endTime);
renderPlayBtn = () => {
const { completed, playing } = this.props;
let label;
let icon;
if (completed) {
icon = 'arrow-clockwise';
label = 'Replay this session';
} else if (playing) {
icon = 'pause-fill';
label = 'Pause';
} else {
icon = 'play-fill-new';
label = 'Pause';
label = 'Play';
}
componentWillUnmount() {
document.removeEventListener('keydown', this.onKeyDown);
//this.props.toggleInspectorMode(false);
}
shouldComponentUpdate(nextProps) {
if (
nextProps.fullscreen !== this.props.fullscreen ||
nextProps.bottomBlock !== this.props.bottomBlock ||
nextProps.live !== this.props.live ||
nextProps.livePlay !== this.props.livePlay ||
nextProps.playing !== this.props.playing ||
nextProps.completed !== this.props.completed ||
nextProps.skip !== this.props.skip ||
nextProps.skipToIssue !== this.props.skipToIssue ||
nextProps.speed !== this.props.speed ||
nextProps.disabled !== this.props.disabled ||
nextProps.fullscreenDisabled !== this.props.fullscreenDisabled ||
// nextProps.inspectorMode !== this.props.inspectorMode ||
nextProps.logCount !== this.props.logCount ||
nextProps.logRedCount !== this.props.logRedCount ||
nextProps.resourceRedCount !== this.props.resourceRedCount ||
nextProps.fetchRedCount !== this.props.fetchRedCount ||
nextProps.showStack !== this.props.showStack ||
nextProps.stackCount !== this.props.stackCount ||
nextProps.stackRedCount !== this.props.stackRedCount ||
nextProps.profilesCount !== this.props.profilesCount ||
nextProps.storageCount !== this.props.storageCount ||
nextProps.storageType !== this.props.storageType ||
nextProps.showStorage !== this.props.showStorage ||
nextProps.showProfiler !== this.props.showProfiler ||
nextProps.showGraphql !== this.props.showGraphql ||
nextProps.showFetch !== this.props.showFetch ||
nextProps.fetchCount !== this.props.fetchCount ||
nextProps.graphqlCount !== this.props.graphqlCount ||
nextProps.showExceptions !== this.props.showExceptions ||
nextProps.exceptionsCount !== this.props.exceptionsCount ||
nextProps.showLongtasks !== this.props.showLongtasks ||
nextProps.liveTimeTravel !== this.props.liveTimeTravel||
nextProps.skipInterval !== this.props.skipInterval)
return true;
return false;
}
onKeyDown = (e) => {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return;
}
if (this.props.inspectorMode) {
if (e.key === 'Esc' || e.key === 'Escape') {
toggleInspectorMode(false);
}
}
// if (e.key === ' ') {
// document.activeElement.blur();
// this.props.togglePlay();
// }
if (e.key === 'Esc' || e.key === 'Escape') {
this.props.fullscreenOff();
}
if (e.key === 'ArrowRight') {
this.forthTenSeconds();
}
if (e.key === 'ArrowLeft') {
this.backTenSeconds();
}
if (e.key === 'ArrowDown') {
this.props.speedDown();
}
if (e.key === 'ArrowUp') {
this.props.speedUp();
}
};
forthTenSeconds = () => {
const { time, endTime, jump, skipInterval } = this.props;
jump(Math.min(endTime, time + SKIP_INTERVALS[skipInterval]));
};
backTenSeconds = () => {
//shouldComponentUpdate
const { time, jump, skipInterval } = this.props;
jump(Math.max(0, time - SKIP_INTERVALS[skipInterval]));
};
goLive = () => this.props.jump(this.props.endTime);
renderPlayBtn = () => {
const { completed, playing } = this.props;
let label;
let icon;
if (completed) {
icon = 'arrow-clockwise';
label = 'Replay this session';
} else if (playing) {
icon = 'pause-fill';
label = 'Pause';
} else {
icon = 'play-fill-new';
label = 'Pause';
label = 'Play';
}
return (
<Tooltip delay={0} position="top" title={label} interactive hideOnClick="persistent" className="mr-4">
<div onClick={this.props.togglePlay} className="hover-main color-main cursor-pointer rounded hover:bg-gray-light-shade">
<Icon name={icon} size="36" color="inherit" />
</div>
</Tooltip>
);
};
controlIcon = (icon, size, action, isBackwards, additionalClasses) => (
return (
<Tooltip
delay={0}
position="top"
title={label}
interactive
hideOnClick="persistent"
className="mr-4"
>
<div
onClick={action}
className={cn('py-1 px-2 hover-main cursor-pointer', additionalClasses)}
style={{ transform: isBackwards ? 'rotate(180deg)' : '' }}
onClick={this.props.togglePlay}
className="hover-main color-main cursor-pointer rounded hover:bg-gray-light-shade"
>
<Icon name={icon} size={size} color="inherit" />
<Icon name={icon} size="36" color="inherit" />
</div>
</Tooltip>
);
};
render() {
const {
bottomBlock,
toggleBottomBlock,
live,
livePlay,
skip,
speed,
disabled,
logCount,
logRedCount,
resourceRedCount,
fetchRedCount,
showStack,
stackCount,
stackRedCount,
profilesCount,
storageCount,
showStorage,
storageType,
showProfiler,
showGraphql,
showFetch,
fetchCount,
graphqlCount,
exceptionsCount,
showExceptions,
fullscreen,
inspectorMode,
closedLive,
toggleSpeed,
toggleSkip,
liveTimeTravel,
changeSkipInterval,
controlIcon = (icon, size, action, isBackwards, additionalClasses) => (
<div
onClick={action}
className={cn('py-1 px-2 hover-main cursor-pointer', additionalClasses)}
style={{ transform: isBackwards ? 'rotate(180deg)' : '' }}
>
<Icon name={icon} size={size} color="inherit" />
</div>
);
render() {
const {
bottomBlock,
toggleBottomBlock,
live,
livePlay,
skip,
speed,
disabled,
logCount,
logRedCount,
resourceRedCount,
fetchRedCount,
showStack,
stackCount,
stackRedCount,
profilesCount,
storageCount,
showStorage,
storageType,
showProfiler,
showGraphql,
showFetch,
fetchCount,
graphqlCount,
exceptionsCount,
showExceptions,
fullscreen,
inspectorMode,
closedLive,
toggleSpeed,
toggleSkip,
liveTimeTravel,
changeSkipInterval,
skipInterval,
} = this.props;
const toggleBottomTools = (blockName) => {
if (blockName === INSPECTOR) {
toggleInspectorMode();
bottomBlock && toggleBottomBlock();
} else {
toggleInspectorMode(false);
toggleBottomBlock(blockName);
}
};
const toggleBottomTools = (blockName) => {
if (blockName === INSPECTOR) {
toggleInspectorMode();
bottomBlock && toggleBottomBlock();
} else {
toggleInspectorMode(false);
toggleBottomBlock(blockName);
}
};
return (
<div className={styles.controls}>
{!live || liveTimeTravel ? (
<Timeline live={live} jump={this.props.jump} liveTimeTravel={liveTimeTravel} pause={this.props.pause} togglePlay={this.props.togglePlay} />
) : null}
{!fullscreen && (
<div className={cn(styles.buttons, { '!px-5 !pt-0': live })} data-is-live={live}>
<div className="flex items-center">
{!live && (
<>
<PlayerControls
live={live}
skip={skip}
speed={speed}
disabled={disabled}
backTenSeconds={this.backTenSeconds}
forthTenSeconds={this.forthTenSeconds}
toggleSpeed={toggleSpeed}
toggleSkip={toggleSkip}
playButton={this.renderPlayBtn()}
controlIcon={this.controlIcon}
ref={this.speedRef}
skipIntervals={SKIP_INTERVALS}
setSkipInterval={changeSkipInterval}
currentInterval={skipInterval}/>
{/* <Button variant="text" onClick={() => toggleBottomTools(OVERVIEW)}>X-RAY</Button> */}
<div className={cn('h-14 border-r bg-gray-light mx-6')} />
<XRayButton isActive={bottomBlock === OVERVIEW && !inspectorMode} onClick={() => toggleBottomTools(OVERVIEW)} />
</>
)}
return (
<div className={styles.controls}>
{!live || liveTimeTravel ? (
<Timeline
live={live}
jump={this.props.jump}
liveTimeTravel={liveTimeTravel}
pause={this.props.pause}
togglePlay={this.props.togglePlay}
/>
) : null}
{!fullscreen && (
<div className={cn(styles.buttons, { '!px-5 !pt-0': live })} data-is-live={live}>
<div className="flex items-center">
{!live && (
<>
<PlayerControls
live={live}
skip={skip}
speed={speed}
disabled={disabled}
backTenSeconds={this.backTenSeconds}
forthTenSeconds={this.forthTenSeconds}
toggleSpeed={toggleSpeed}
toggleSkip={toggleSkip}
playButton={this.renderPlayBtn()}
controlIcon={this.controlIcon}
ref={this.speedRef}
skipIntervals={SKIP_INTERVALS}
setSkipInterval={changeSkipInterval}
currentInterval={skipInterval}
/>
{/* <Button variant="text" onClick={() => toggleBottomTools(OVERVIEW)}>X-RAY</Button> */}
<div className={cn('h-14 border-r bg-gray-light mx-6')} />
<XRayButton
isActive={bottomBlock === OVERVIEW && !inspectorMode}
onClick={() => toggleBottomTools(OVERVIEW)}
/>
</>
)}
{live && !closedLive && (
<div className={styles.buttonsLeft}>
<LiveTag isLive={livePlay} onClick={() => (livePlay ? null : jumpToLive())} />
<div className="font-semibold px-2">
<AssistDuration isLivePlay={livePlay} />
</div>
{live && !closedLive && (
<div className={styles.buttonsLeft}>
<LiveTag isLive={livePlay} onClick={() => (livePlay ? null : jumpToLive())} />
<div className="font-semibold px-2">
<AssistDuration isLivePlay={livePlay} />
</div>
{!liveTimeTravel && (
<div
onClick={toggleTimetravel}
className="p-2 ml-2 rounded hover:bg-teal-light bg-gray-lightest cursor-pointer"
>
See Past Activity
</div>
)}
</div>
)}
</div>
{!liveTimeTravel && (
<div
onClick={toggleTimetravel}
className="p-2 ml-2 rounded hover:bg-teal-light bg-gray-lightest cursor-pointer"
>
See Past Activity
</div>
)}
</div>
)}
</div>
<div className="flex items-center h-full">
{/* { !live && <div className={cn(styles.divider, 'h-full')} /> } */}
{/* ! TEMP DISABLED !
<div className="flex items-center h-full">
{/* { !live && <div className={cn(styles.divider, 'h-full')} /> } */}
{/* ! TEMP DISABLED !
{!live && (
<ControlButton
disabled={ disabled && !inspectorMode }
@ -365,7 +394,7 @@ export default class Controls extends React.Component {
containerClassName="mx-2"
/>
)} */}
{/* <ControlButton
{/* <ControlButton
// disabled={ disabled && !inspectorMode }
onClick={ () => toggleBottomTools(OVERVIEW) }
active={ bottomBlock === OVERVIEW && !inspectorMode}
@ -376,131 +405,131 @@ export default class Controls extends React.Component {
// hasErrors={ logRedCount > 0 }
containerClassName="mx-2"
/> */}
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(CONSOLE)}
active={bottomBlock === CONSOLE && !inspectorMode}
label="CONSOLE"
noIcon
labelClassName="!text-base font-semibold"
count={logCount}
hasErrors={logRedCount > 0}
containerClassName="mx-2"
/>
{!live && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(NETWORK)}
active={bottomBlock === NETWORK && !inspectorMode}
label="NETWORK"
hasErrors={resourceRedCount > 0}
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(PERFORMANCE)}
active={bottomBlock === PERFORMANCE && !inspectorMode}
label="PERFORMANCE"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{showFetch && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(FETCH)}
active={bottomBlock === FETCH && !inspectorMode}
hasErrors={fetchRedCount > 0}
count={fetchCount}
label="FETCH"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && showGraphql && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(GRAPHQL)}
active={bottomBlock === GRAPHQL && !inspectorMode}
count={graphqlCount}
label="GRAPHQL"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && showStorage && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(STORAGE)}
active={bottomBlock === STORAGE && !inspectorMode}
count={storageCount}
label={getStorageName(storageType)}
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{showExceptions && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(EXCEPTIONS)}
active={bottomBlock === EXCEPTIONS && !inspectorMode}
label="EXCEPTIONS"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
count={exceptionsCount}
hasErrors={exceptionsCount > 0}
/>
)}
{!live && showStack && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(STACKEVENTS)}
active={bottomBlock === STACKEVENTS && !inspectorMode}
label="EVENTS"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
count={stackCount}
hasErrors={stackRedCount > 0}
/>
)}
{!live && showProfiler && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(PROFILER)}
active={bottomBlock === PROFILER && !inspectorMode}
count={profilesCount}
label="PROFILER"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && <div className={cn('h-14 border-r bg-gray-light ml-6')} />}
{!live && (
<Tooltip title="Fullscreen" delay={0} position="top-end" className="mx-4">
{this.controlIcon(
'arrows-angle-extend',
18,
this.props.fullscreenOn,
false,
'rounded hover:bg-gray-light-shade color-gray-medium'
)}
</Tooltip>
)}
</div>
</div>
)}
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(CONSOLE)}
active={bottomBlock === CONSOLE && !inspectorMode}
label="CONSOLE"
noIcon
labelClassName="!text-base font-semibold"
count={logCount}
hasErrors={logRedCount > 0}
containerClassName="mx-2"
/>
{!live && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(NETWORK)}
active={bottomBlock === NETWORK && !inspectorMode}
label="NETWORK"
hasErrors={resourceRedCount > 0}
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(PERFORMANCE)}
active={bottomBlock === PERFORMANCE && !inspectorMode}
label="PERFORMANCE"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{showFetch && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(FETCH)}
active={bottomBlock === FETCH && !inspectorMode}
hasErrors={fetchRedCount > 0}
count={fetchCount}
label="FETCH"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && showGraphql && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(GRAPHQL)}
active={bottomBlock === GRAPHQL && !inspectorMode}
count={graphqlCount}
label="GRAPHQL"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && showStorage && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(STORAGE)}
active={bottomBlock === STORAGE && !inspectorMode}
count={storageCount}
label={getStorageName(storageType)}
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{showExceptions && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(EXCEPTIONS)}
active={bottomBlock === EXCEPTIONS && !inspectorMode}
label="EXCEPTIONS"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
count={exceptionsCount}
hasErrors={exceptionsCount > 0}
/>
)}
{!live && showStack && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(STACKEVENTS)}
active={bottomBlock === STACKEVENTS && !inspectorMode}
label="EVENTS"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
count={stackCount}
hasErrors={stackRedCount > 0}
/>
)}
{!live && showProfiler && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(PROFILER)}
active={bottomBlock === PROFILER && !inspectorMode}
count={profilesCount}
label="PROFILER"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && <div className={cn('h-14 border-r bg-gray-light ml-6')} />}
{!live && (
<Tooltip title="Fullscreen" delay={0} position="top-end" className="mx-4">
{this.controlIcon(
'arrows-angle-extend',
18,
this.props.fullscreenOn,
false,
'rounded hover:bg-gray-light-shade color-gray-medium'
)}
</Tooltip>
)}
</div>
);
}
</div>
)}
</div>
);
}
}

View file

@ -71,7 +71,7 @@ function PlayerControls(props: Props) {
const toggleTooltip = () => {
setShowTooltip(!showTooltip);
}
};
return (
<div className="flex items-center">
{playButton}
@ -112,31 +112,35 @@ function PlayerControls(props: Props) {
className="cursor-pointer select-none"
distance={20}
html={
<div className="flex flex-col bg-white border border-borderColor-gray-light-shade text-figmaColors-text-primary rounded">
<div className="font-semibold py-2 px-4 w-full text-left">
Jump <span className="text-disabled-text">(Secs)</span>
</div>
{Object.keys(skipIntervals).map((interval) => (
<div
onClick={() => {
toggleTooltip();
setSkipInterval(parseInt(interval, 10))
}}
className={cn(
"py-2 px-4 cursor-pointer w-full text-left font-semibold",
"hover:bg-active-blue border-t border-borderColor-gray-light-shade",
)}
>
{interval}
<span className="text-disabled-text">s</span>
<OutsideClickDetectingDiv onClickOutside={() => showTooltip ? toggleTooltip() : null}>
<div className="flex flex-col bg-white border border-borderColor-gray-light-shade text-figmaColors-text-primary rounded">
<div className="font-semibold py-2 px-4 w-full text-left">
Jump <span className="text-disabled-text">(Secs)</span>
</div>
))}
</div>
{Object.keys(skipIntervals).map((interval) => (
<div
onClick={() => {
toggleTooltip();
setSkipInterval(parseInt(interval, 10));
}}
className={cn(
'py-2 px-4 cursor-pointer w-full text-left font-semibold',
'hover:bg-active-blue border-t border-borderColor-gray-light-shade'
)}
>
{interval}
<span className="text-disabled-text">s</span>
</div>
))}
</div>
</OutsideClickDetectingDiv>
}
>
<div onClick={toggleTooltip}>
{/* @ts-ignore */}
<Tooltip disabled={showTooltip} title="Set default skip duration">{currentInterval}s</Tooltip>
<Tooltip disabled={showTooltip} title="Set default skip duration">
{currentInterval}s
</Tooltip>
</div>
</Tooltip>
</div>

View file

@ -5,8 +5,8 @@ import colors from 'App/theme/colors';
const { ValueContainer } = components;
type ValueObject = {
value: string,
label: string
value: string | number,
label: string,
}
interface Props<Value extends ValueObject> {

View file

@ -9,10 +9,12 @@ const idKey = 'alertId';
const crudDuck = crudDuckGenerator(name, Alert, { idKey: idKey });
export const { fetchList, init, edit, remove } = crudDuck.actions;
const FETCH_TRIGGER_OPTIONS = new RequestTypes(`${name}/FETCH_TRIGGER_OPTIONS`);
const CHANGE_SEARCH = `${name}/CHANGE_SEARCH`
const initialState = Map({
definedPercent: 0,
triggerOptions: [],
alertsSearch: '',
});
const reducer = (state = initialState, action = {}) => {
@ -28,6 +30,8 @@ const reducer = (state = initialState, action = {}) => {
// return member
// })
// );
case CHANGE_SEARCH:
return state.set('alertsSearch', action.search);
case FETCH_TRIGGER_OPTIONS.SUCCESS:
return state.set('triggerOptions', action.data.map(({ name, value }) => ({ label: name, value })));
}
@ -41,6 +45,13 @@ export function save(instance) {
};
}
export function changeSearch(search) {
return {
type: CHANGE_SEARCH,
search,
};
}
export function fetchTriggerOptions() {
return {
types: FETCH_TRIGGER_OPTIONS.toArray(),

View file

@ -114,6 +114,10 @@ export const metricCreate = () => `/metrics/create`;
export const metricDetails = (id = ':metricId', hash) => hashed(`/metrics/${ id }`, hash);
export const metricDetailsSub = (id = ':metricId', subId = ':subId', hash) => hashed(`/metrics/${ id }/details/${subId}`, hash);
export const alerts = () => '/alerts';
export const alertCreate = () => '/alert/create';
export const alertEdit = (id = ':alertId', hash) => hashed(`/alert/${id}`, hash);
const REQUIRED_SITE_ID_ROUTES = [
liveSession(''),
session(''),
@ -130,6 +134,10 @@ const REQUIRED_SITE_ID_ROUTES = [
dashboardMetricCreate(''),
dashboardMetricDetails(''),
alerts(),
alertCreate(),
alertEdit(''),
error(''),
errors(),
onboarding(''),
@ -167,6 +175,7 @@ const SITE_CHANGE_AVALIABLE_ROUTES = [
dashboard(),
dashboardSelected(),
metrics(),
alerts(),
errors(),
onboarding('')
];

View file

@ -67,13 +67,14 @@ export const filterList = <T extends Record<string, any>>(
list: T[],
searchQuery: string,
testKeys: string[],
searchCb?: (listItem: T, query: string | RegExp
searchCb?: (listItem: T, query: RegExp
) => boolean): T[] => {
if (searchQuery === '') return list;
const filterRE = getRE(searchQuery, 'i');
let _list = list.filter((listItem: T) => {
return testKeys.some((key) => filterRE.test(listItem[key]) || searchCb?.(listItem, filterRE));
});
return _list
return _list;
}
export const getStateColor = (state) => {
@ -374,4 +375,4 @@ export function millisToMinutesAndSeconds(millis: any) {
const minutes = Math.floor(millis / 60000);
const seconds: any = ((millis % 60000) / 1000).toFixed(0);
return minutes + 'm' + (seconds < 10 ? '0' : '') + seconds + 's';
}
}