feat(ui): redesign alerts page
This commit is contained in:
parent
d4ad80f234
commit
6766d360dc
22 changed files with 1409 additions and 553 deletions
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './DropdownChips'
|
||||
394
frontend/app/components/Dashboard/components/Alerts/NewAlert.tsx
Normal file
394
frontend/app/components/Dashboard/components/Alerts/NewAlert.tsx
Normal 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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './AlertsView'
|
||||
2
frontend/app/components/Dashboard/components/Alerts/type.d.ts
vendored
Normal file
2
frontend/app/components/Dashboard/components/Alerts/type.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// TODO burn the immutable and make typing this possible
|
||||
type Alert = Record<string, any>
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 : ''
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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('')
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue