feat(ui): redesign alerts creation page
This commit is contained in:
parent
6766d360dc
commit
db635ba97b
9 changed files with 581 additions and 381 deletions
|
|
@ -0,0 +1,44 @@
|
|||
import React from 'react'
|
||||
import { Button, Icon } from 'UI'
|
||||
|
||||
interface IBottomButtons {
|
||||
loading: boolean
|
||||
deleting: boolean
|
||||
instance: Alert
|
||||
onDelete: (instance: Alert) => void
|
||||
}
|
||||
|
||||
function BottomButtons({ loading, instance, deleting, onDelete }: IBottomButtons) {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
<div>
|
||||
{instance.exists() && (
|
||||
<Button
|
||||
hover
|
||||
variant="text"
|
||||
loading={deleting}
|
||||
type="button"
|
||||
onClick={() => onDelete(instance)}
|
||||
id="trash-button"
|
||||
className="!text-teal !fill-teal"
|
||||
>
|
||||
<Icon name="trash" color="inherit" className="mr-2" size="18" /> Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default BottomButtons
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
import React from 'react';
|
||||
import { Input } from 'UI';
|
||||
import Select from 'Shared/Select';
|
||||
import { alertConditions as conditions } from 'App/constants';
|
||||
|
||||
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' },
|
||||
];
|
||||
|
||||
interface ICondition {
|
||||
isThreshold: boolean;
|
||||
writeOption: (e: any, data: any) => void;
|
||||
instance: Alert;
|
||||
triggerOptions: any[];
|
||||
writeQuery: (data: any) => void;
|
||||
writeQueryOption: (e: any, data: any) => void;
|
||||
unit: any;
|
||||
}
|
||||
|
||||
function Condition({
|
||||
isThreshold,
|
||||
writeOption,
|
||||
instance,
|
||||
triggerOptions,
|
||||
writeQueryOption,
|
||||
writeQuery,
|
||||
unit,
|
||||
}: ICondition) {
|
||||
return (
|
||||
<div>
|
||||
{!isThreshold && (
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-1/6 flex-shrink-0 font-normal">{'Trigger when'}</label>
|
||||
<Select
|
||||
className="w-2/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-1/6 flex-shrink-0 font-normal">
|
||||
{isThreshold ? 'Trigger when' : 'of'}
|
||||
</label>
|
||||
<Select
|
||||
className="w-2/6"
|
||||
placeholder="Select Metric"
|
||||
isSearchable={true}
|
||||
options={triggerOptions}
|
||||
name="left"
|
||||
value={triggerOptions.find((i) => i.value === instance.query.left)}
|
||||
onChange={({ value }) => writeQueryOption(null, { name: 'left', value: value.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-1/6 flex-shrink-0 font-normal">{'is'}</label>
|
||||
<div className="w-2/6 flex items-center">
|
||||
<Select
|
||||
placeholder="Select Condition"
|
||||
options={conditions}
|
||||
name="operator"
|
||||
defaultValue={instance.query.operator}
|
||||
onChange={({ value }) =>
|
||||
writeQueryOption(null, { name: 'operator', value: value.value })
|
||||
}
|
||||
/>
|
||||
{unit && (
|
||||
<>
|
||||
<Input
|
||||
className="px-4"
|
||||
style={{ marginRight: '31px' }}
|
||||
name="right"
|
||||
value={instance.query.right}
|
||||
onChange={writeQuery}
|
||||
placeholder="E.g. 3"
|
||||
/>
|
||||
<span className="ml-2">{'test'}</span>
|
||||
</>
|
||||
)}
|
||||
{!unit && (
|
||||
<Input
|
||||
wrapperClassName="ml-2"
|
||||
name="right"
|
||||
value={instance.query.right}
|
||||
onChange={writeQuery}
|
||||
placeholder="Specify Value"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-1/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={({ 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={({ value }) => writeOption(null, { name: 'previousPeriod', value })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Condition;
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
import React from 'react';
|
||||
import { Checkbox } from 'UI';
|
||||
import DropdownChips from '../DropdownChips';
|
||||
|
||||
interface INotifyHooks {
|
||||
instance: Alert;
|
||||
onChangeCheck: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
slackChannels: Array<any>;
|
||||
validateEmail: (value: string) => boolean;
|
||||
edit: (data: any) => void;
|
||||
hooks: Array<any>;
|
||||
}
|
||||
|
||||
function NotifyHooks({
|
||||
instance,
|
||||
onChangeCheck,
|
||||
slackChannels,
|
||||
validateEmail,
|
||||
hooks,
|
||||
edit,
|
||||
}: INotifyHooks) {
|
||||
return (
|
||||
<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) => 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) => 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={hooks}
|
||||
placeholder="Select Webhook"
|
||||
// @ts-ignore
|
||||
onChange={(selected) => edit({ webhookInput: selected })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NotifyHooks;
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import { checkForRecent } from 'App/date';
|
||||
import { withSiteId, alertCreate } from 'App/routes';
|
||||
import { withSiteId, alertEdit } from 'App/routes';
|
||||
// @ts-ignore
|
||||
import { DateTime } from 'luxon';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import cn from 'classnames';
|
||||
|
||||
const getThreshold = (threshold: number) => {
|
||||
if (threshold === 15) return '15 Minutes';
|
||||
|
|
@ -15,11 +16,31 @@ const getThreshold = (threshold: number) => {
|
|||
if (threshold === 1440) return '1 Day';
|
||||
};
|
||||
|
||||
const getNotifyChannel = (alert: Record<string, string>) => {
|
||||
const getNotifyChannel = (alert: Record<string, any>, webhooks: Array<any>) => {
|
||||
const getSlackChannels = () => {
|
||||
return (
|
||||
' (' +
|
||||
alert.slackInput
|
||||
.map((channelId: number) => {
|
||||
return (
|
||||
'#' +
|
||||
webhooks.find((hook) => hook.webhookId === channelId && hook.type === 'slack').name
|
||||
);
|
||||
})
|
||||
.join(', ') +
|
||||
')'
|
||||
);
|
||||
};
|
||||
let str = '';
|
||||
if (alert.slack) str = 'Slack';
|
||||
if (alert.email) str += (str === '' ? '' : ' and ') + 'Email';
|
||||
if (alert.webhool) str += (str === '' ? '' : ' and ') + 'Webhook';
|
||||
if (alert.slack) {
|
||||
str = 'Slack';
|
||||
str += alert.slackInput.length > 0 ? getSlackChannels() : '';
|
||||
}
|
||||
if (alert.email) {
|
||||
str += (str === '' ? '' : ' and ') + (alert.emailInput.length > 1 ? 'Emails' : 'Email');
|
||||
str += alert.emailInput.length > 0 ? ' (' + alert.emailInput.join(', ') + ')' : '';
|
||||
}
|
||||
if (alert.webhook) str += (str === '' ? '' : ' and ') + 'Webhook';
|
||||
if (str === '') return 'OpenReplay';
|
||||
|
||||
return str;
|
||||
|
|
@ -29,18 +50,25 @@ interface Props extends RouteComponentProps {
|
|||
alert: Alert;
|
||||
siteId: string;
|
||||
init: (alert?: Alert) => void;
|
||||
demo?: boolean;
|
||||
webhooks: Array<any>;
|
||||
}
|
||||
|
||||
function AlertListItem(props: Props) {
|
||||
const { alert, siteId, history, init } = props;
|
||||
const { alert, siteId, history, init, demo, webhooks } = props;
|
||||
|
||||
const onItemClick = () => {
|
||||
const path = withSiteId(alertCreate(), siteId);
|
||||
init(alert)
|
||||
if (demo) return;
|
||||
const path = withSiteId(alertEdit(alert.alertId), siteId);
|
||||
init(alert);
|
||||
history.push(path);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="hover:bg-active-blue cursor-pointer border-t px-3" onClick={onItemClick}>
|
||||
<div
|
||||
className={cn('border-t px-3', !demo ? 'hover:bg-active-blue cursor-pointer' : '')}
|
||||
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">
|
||||
|
|
@ -56,7 +84,12 @@ function AlertListItem(props: Props) {
|
|||
</div>
|
||||
</div>
|
||||
<div className="col-span-5 text-right">
|
||||
{checkForRecent(DateTime.fromMillis(alert.createdAt), 'LLL dd, yyyy, hh:mm a')}
|
||||
{demo
|
||||
? DateTime.fromMillis(+new Date()).toFormat('LLL dd, yyyy, hh:mm a')
|
||||
: checkForRecent(
|
||||
DateTime.fromMillis(alert.createdAt || +new Date()),
|
||||
'LLL dd, yyyy, hh:mm a'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-disabled-text px-2 pb-2">
|
||||
|
|
@ -66,7 +99,8 @@ function AlertListItem(props: Props) {
|
|||
<span className="font-medium">{alert.query.left}</span>
|
||||
{' is '}
|
||||
<span className="font-medium">
|
||||
{alert.query.operator}{alert.query.right} {alert.metric.unit}
|
||||
{alert.query.operator}
|
||||
{alert.query.right} {alert.metric.unit}
|
||||
</span>
|
||||
{' over the past '}
|
||||
<span className="font-medium">{getThreshold(alert.currentPeriod)}</span>
|
||||
|
|
@ -77,7 +111,7 @@ function AlertListItem(props: Props) {
|
|||
</>
|
||||
) : null}
|
||||
{', notify me on '}
|
||||
<span>{getNotifyChannel(alert)}</span>.
|
||||
<span>{getNotifyChannel(alert, webhooks)}</span>.
|
||||
</div>
|
||||
{alert.description ? (
|
||||
<div className="text-disabled-text px-2 pb-2">{alert.description}</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { filterList } from 'App/utils';
|
|||
import { sliceListPerPage } from 'App/utils';
|
||||
import { fetchList } from 'Duck/alerts';
|
||||
import { connect } from 'react-redux';
|
||||
import { fetchList as fetchWebhooks } from 'Duck/webhook';
|
||||
|
||||
import AlertListItem from './AlertListItem'
|
||||
|
||||
|
|
@ -14,13 +15,13 @@ interface Props {
|
|||
list: any;
|
||||
alertsSearch: any;
|
||||
siteId: string;
|
||||
onDelete: (instance: Alert) => void;
|
||||
onSave: (instance: Alert) => void;
|
||||
webhooks: Array<any>;
|
||||
init: (instance?: Alert) => void
|
||||
fetchWebhooks: () => void;
|
||||
}
|
||||
|
||||
function AlertsList({ fetchList, list: alertsList, alertsSearch, siteId, init }: Props) {
|
||||
React.useEffect(() => { fetchList() }, []);
|
||||
function AlertsList({ fetchList, list: alertsList, alertsSearch, siteId, init, fetchWebhooks, webhooks }: Props) {
|
||||
React.useEffect(() => { fetchList(); fetchWebhooks() }, []);
|
||||
|
||||
const alertsArray = alertsList.toJS();
|
||||
const [page, setPage] = React.useState(1);
|
||||
|
|
@ -50,7 +51,7 @@ function AlertsList({ fetchList, list: alertsList, alertsSearch, siteId, init }:
|
|||
|
||||
{sliceListPerPage(list, page - 1, pageSize).map((alert: any) => (
|
||||
<React.Fragment key={alert.alertId}>
|
||||
<AlertListItem alert={alert} siteId={siteId} init={init} />
|
||||
<AlertListItem alert={alert} siteId={siteId} init={init} webhooks={webhooks} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -78,6 +79,8 @@ export default connect(
|
|||
list: state.getIn(['alerts', 'list']).sort((a, b) => b.createdAt - a.createdAt),
|
||||
// @ts-ignore
|
||||
alertsSearch: state.getIn(['alerts', 'alertsSearch']),
|
||||
// @ts-ignore
|
||||
webhooks: state.getIn(['webhooks', 'list']),
|
||||
}),
|
||||
{ fetchList }
|
||||
{ fetchList, fetchWebhooks }
|
||||
)(AlertsList);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import React from 'react';
|
||||
import { Button, PageTitle, Icon } from 'UI';
|
||||
import { Button, PageTitle, Icon, Link } 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 { init } from 'Duck/alerts';
|
||||
import { withSiteId, alertCreate } from 'App/routes';
|
||||
|
||||
import AlertsList from './AlertsList';
|
||||
import AlertsSearch from './AlertsSearch';
|
||||
|
|
@ -12,58 +11,30 @@ 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');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function AlertsView({ siteId, init }: IAlertsView) {
|
||||
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" />
|
||||
<PageTitle title="Alerts" />
|
||||
</div>
|
||||
<Button variant="primary" onClick={null}>Create</Button>
|
||||
<Link to={withSiteId(alertCreate(), siteId)}><Button variant="primary" onClick={null}>Create</Button></Link>
|
||||
<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.
|
||||
Alerts helps your team stay up to date with the activity on your app.
|
||||
</div>
|
||||
<AlertsList siteId={siteId} onSave={onSave} onDelete={onDelete} init={init} />
|
||||
<AlertsList siteId={siteId} init={init} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const Container = connect(null, { init, edit, save, remove })(AlertsView);
|
||||
const Container = connect(null, { init })(AlertsView);
|
||||
|
||||
export default withPageTitle('Alerts - OpenReplay')(Container);
|
||||
|
|
|
|||
|
|
@ -1,92 +1,159 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { Button, Form, Input, SegmentSelection, Checkbox, Icon } from 'UI';
|
||||
import { alertConditions as conditions } from 'App/constants';
|
||||
import { Form, SegmentSelection, Icon } from 'UI';
|
||||
import { connect } from 'react-redux';
|
||||
// @ts-ignore
|
||||
import stl from './alertForm.module.css';
|
||||
import DropdownChips from './DropdownChips';
|
||||
import { validateEmail } from 'App/validate';
|
||||
import { fetchTriggerOptions, init, edit, save, remove, fetchList } from 'Duck/alerts';
|
||||
import { confirm } from 'UI';
|
||||
import { toast } from 'react-toastify';
|
||||
import { SLACK, WEBHOOK } from 'App/constants/schedule';
|
||||
import { fetchList as fetchWebhooks } from 'Duck/webhook';
|
||||
import Breadcrumb from 'Shared/Breadcrumb';
|
||||
import { withSiteId, alerts } from 'App/routes';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import cn from 'classnames';
|
||||
import { fetchTriggerOptions } from 'Duck/alerts';
|
||||
import Select from 'Shared/Select';
|
||||
import WidgetName from '../WidgetName';
|
||||
import BottomButtons from './AlertForm/BottomButtons';
|
||||
import NotifyHooks from './AlertForm/NotifyHooks';
|
||||
import AlertListItem from './AlertListItem';
|
||||
import Condition from './AlertForm/Condition';
|
||||
|
||||
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">
|
||||
const Circle = ({ text }: { text: string }) => (
|
||||
<div style={{ left: -14, height: 26, width: 26 }} className="circle rounded-full bg-gray-light flex items-center justify-center absolute top-0">
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
|
||||
interface ISection {
|
||||
index: string
|
||||
title: string
|
||||
description?: string
|
||||
content: React.ReactNode
|
||||
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">
|
||||
<div className="w-full border-l-2 last:border-l-borderColor-transparent">
|
||||
<div className="flex items-start relative">
|
||||
<Circle text={index} />
|
||||
<div>
|
||||
<div className="ml-6">
|
||||
<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 className="ml-6">{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
|
||||
interface IProps extends RouteComponentProps {
|
||||
siteId: string;
|
||||
instance: Alert;
|
||||
slackChannels: any[];
|
||||
webhooks: any[];
|
||||
loading: boolean;
|
||||
deleting: boolean;
|
||||
triggerOptions: any[];
|
||||
list: any,
|
||||
fetchTriggerOptions: () => void;
|
||||
edit: (query: any) => void;
|
||||
init: (alert?: Alert) => any;
|
||||
save: (alert: Alert) => Promise<any>;
|
||||
remove: (alertId: string) => Promise<any>;
|
||||
onSubmit: (instance: Alert) => void;
|
||||
fetchWebhooks: () => void;
|
||||
fetchList: () => void;
|
||||
}
|
||||
|
||||
const NewAlert = (props: IProps) => {
|
||||
const {
|
||||
instance,
|
||||
slackChannels,
|
||||
siteId,
|
||||
webhooks,
|
||||
loading,
|
||||
onDelete,
|
||||
deleting,
|
||||
triggerOptions,
|
||||
style,
|
||||
init,
|
||||
edit,
|
||||
save,
|
||||
remove,
|
||||
fetchWebhooks,
|
||||
fetchList,
|
||||
list,
|
||||
} = 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 });
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (list.size === 0) fetchList();
|
||||
props.fetchTriggerOptions();
|
||||
fetchWebhooks();
|
||||
}, []);
|
||||
|
||||
const writeQueryOption = (e: React.ChangeEvent, { name, value }: { name: string, value: string }) => {
|
||||
useEffect(() => {
|
||||
if (list.size > 0) {
|
||||
const alertId = location.pathname.split('/').pop()
|
||||
const currentAlert = list.toJS().find((alert: Alert) => alert.alertId === parseInt(alertId, 10));
|
||||
init(currentAlert);
|
||||
}
|
||||
}, [list])
|
||||
|
||||
|
||||
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 });
|
||||
|
||||
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(() => {
|
||||
props.history.push(withSiteId(alerts(), siteId))
|
||||
});
|
||||
}
|
||||
};
|
||||
const onSave = (instance: Alert) => {
|
||||
const wasUpdating = instance.exists();
|
||||
save(instance).then(() => {
|
||||
if (!wasUpdating) {
|
||||
toast.success('New alert saved');
|
||||
props.history.push(withSiteId(alerts(), siteId))
|
||||
} else {
|
||||
toast.success('Alert updated');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
props.history.push(withSiteId(alerts(), siteId))
|
||||
}
|
||||
|
||||
const slackChannels = webhooks
|
||||
.filter((hook) => hook.type === SLACK)
|
||||
.map(({ webhookId, name }) => ({ value: webhookId, label: name }))
|
||||
// @ts-ignore
|
||||
.toJS();
|
||||
const hooks = webhooks
|
||||
.filter((hook) => hook.type === WEBHOOK)
|
||||
.map(({ webhookId, name }) => ({ value: webhookId, label: name }))
|
||||
// @ts-ignore
|
||||
.toJS();
|
||||
|
||||
|
||||
|
||||
const writeQueryOption = (
|
||||
e: React.ChangeEvent,
|
||||
{ name, value }: { name: string; value: string }
|
||||
) => {
|
||||
const { query } = instance;
|
||||
props.edit({ query: { ...query, [name]: value } });
|
||||
};
|
||||
|
|
@ -104,291 +171,137 @@ const NewAlert = (props: IProps) => {
|
|||
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"
|
||||
<>
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{
|
||||
label: 'Alerts',
|
||||
to: withSiteId('/alerts', siteId),
|
||||
},
|
||||
{ label: (instance && instance.name) || 'Alert' },
|
||||
]}
|
||||
/>
|
||||
<Form
|
||||
className="relative bg-white rounded border"
|
||||
onSubmit={() => onSave(instance)}
|
||||
id="alert-form"
|
||||
>
|
||||
<div
|
||||
onClick={() => expanded ? null : setExpanded(!expanded)}
|
||||
className={cn('px-6 py-4 flex justify-between items-center', {
|
||||
'cursor-pointer hover:bg-active-blue hover:shadow-border-blue rounded': !expanded,
|
||||
})}
|
||||
>
|
||||
<h1 className="mb-0 text-2xl mr-4 min-w-fit">
|
||||
<WidgetName name={instance.name} onUpdate={(name) => write({ target: { value: name, name: 'name' }} as any)} canEdit={expanded} />
|
||||
</h1>
|
||||
<div
|
||||
className="text-gray-600 w-full cursor-pointer"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{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 className="flex items-center select-none w-fit ml-auto">
|
||||
<span className="mr-2 color-teal">{expanded ? 'Close' : 'Edit'}</span>
|
||||
<Icon name={expanded ? 'chevron-up' : 'chevron-down'} size="16" color="teal" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{expanded ? (
|
||||
<>
|
||||
<div className="px-6 pb-3 flex flex-col">
|
||||
<Section
|
||||
index="1"
|
||||
title={'Alert based on'}
|
||||
content={
|
||||
<div className="">
|
||||
<SegmentSelection
|
||||
outline
|
||||
name="detectionMethod"
|
||||
className="my-3 w-1/4"
|
||||
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. When Threshold is above 1ms over the past 15mins, notify me through Slack #foss-notifications.'}
|
||||
{!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>
|
||||
}
|
||||
/>
|
||||
<Section
|
||||
index="2"
|
||||
title="Condition"
|
||||
content={
|
||||
<Condition
|
||||
isThreshold={isThreshold}
|
||||
writeOption={writeOption}
|
||||
instance={instance}
|
||||
triggerOptions={triggerOptions}
|
||||
writeQueryOption={writeQueryOption}
|
||||
writeQuery={writeQuery}
|
||||
unit={unit}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Section
|
||||
index="3"
|
||||
title="Notify Through"
|
||||
description="You'll be noticed in app notifications. Additionally opt in to receive alerts on:"
|
||||
content={
|
||||
<NotifyHooks
|
||||
instance={instance}
|
||||
onChangeCheck={onChangeCheck}
|
||||
slackChannels={slackChannels}
|
||||
validateEmail={validateEmail}
|
||||
hooks={hooks}
|
||||
edit={edit}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-6 border-t bg-white">
|
||||
<BottomButtons
|
||||
loading={loading}
|
||||
instance={instance}
|
||||
deleting={deleting}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</Form>
|
||||
|
||||
<div className="bg-white mt-4 border rounded">
|
||||
{instance && (
|
||||
<AlertListItem alert={instance} demo siteId="" init={() => null} webhooks={webhooks} />
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(
|
||||
export default withRouter(connect(
|
||||
(state) => ({
|
||||
// @ts-ignore
|
||||
instance: state.getIn(['alerts', 'instance']),
|
||||
//@ts-ignore
|
||||
list: state.getIn(['alerts', 'list']),
|
||||
// @ts-ignore
|
||||
triggerOptions: state.getIn(['alerts', 'triggerOptions']),
|
||||
// @ts-ignore
|
||||
loading: state.getIn(['alerts', 'saveRequest', 'loading']),
|
||||
// @ts-ignore
|
||||
deleting: state.getIn(['alerts', 'removeRequest', 'loading']),
|
||||
// @ts-ignore
|
||||
webhooks: state.getIn(['webhooks', 'list']),
|
||||
}),
|
||||
{ fetchTriggerOptions }
|
||||
)(NewAlert);
|
||||
{ fetchTriggerOptions, init, edit, save, remove, fetchWebhooks, fetchList }
|
||||
// @ts-ignore
|
||||
)(NewAlert));
|
||||
|
|
|
|||
|
|
@ -180,5 +180,5 @@ function DashboardView(props: Props) {
|
|||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
export default withPageTitle('Dashboards - OpenReplay')(withReport(withRouter(withModal(observer(DashboardView)))));
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import MetricsSearch from '../MetricsSearch';
|
|||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
|
||||
interface Props{
|
||||
siteId: number;
|
||||
interface Props {
|
||||
siteId: string;
|
||||
}
|
||||
function MetricsView({ siteId }: Props) {
|
||||
const { metricStore } = useStore();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue