diff --git a/frontend/app/Router.js b/frontend/app/Router.js index acbdd9cbb..e8fa64eab 100644 --- a/frontend/app/Router.js +++ b/frontend/app/Router.js @@ -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 && } {/* DASHBOARD and Metrics */} + + + diff --git a/frontend/app/components/Dashboard/NewDashboard.tsx b/frontend/app/components/Dashboard/NewDashboard.tsx index b8be58ad1..cf93618b0 100644 --- a/frontend/app/components/Dashboard/NewDashboard.tsx +++ b/frontend/app/components/Dashboard/NewDashboard.tsx @@ -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) { 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) { diff --git a/frontend/app/components/Dashboard/components/Alerts/AlertListItem.tsx b/frontend/app/components/Dashboard/components/Alerts/AlertListItem.tsx new file mode 100644 index 000000000..3bfa860f1 --- /dev/null +++ b/frontend/app/components/Dashboard/components/Alerts/AlertListItem.tsx @@ -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) => { + 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 ( +
+
+
+
+
+ +
+
{alert.name}
+
+
+
+
+ {alert.detectionMethod} +
+
+
+ {checkForRecent(DateTime.fromMillis(alert.createdAt), 'LLL dd, yyyy, hh:mm a')} +
+
+
+ {'When the '} + {alert.detectionMethod} + {' of '} + {alert.query.left} + {' is '} + + {alert.query.operator}{alert.query.right} {alert.metric.unit} + + {' over the past '} + {getThreshold(alert.currentPeriod)} + {alert.detectionMethod === 'change' ? ( + <> + {' compared to the previous '} + {getThreshold(alert.previousPeriod)} + + ) : null} + {', notify me on '} + {getNotifyChannel(alert)}. +
+ {alert.description ? ( +
{alert.description}
+ ) : null} +
+ ); +} + +export default withRouter(AlertListItem); diff --git a/frontend/app/components/Dashboard/components/Alerts/AlertsList.tsx b/frontend/app/components/Dashboard/components/Alerts/AlertsList.tsx new file mode 100644 index 000000000..ce14773c6 --- /dev/null +++ b/frontend/app/components/Dashboard/components/Alerts/AlertsList.tsx @@ -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 ( + + +
+ {alertsSearch !== '' ? 'No matching results' : "You haven't created any alerts yet"} +
+ + } + > +
+
+
Title
+
Type
+
Modified
+
+ + {sliceListPerPage(list, page - 1, pageSize).map((alert: any) => ( + + + + ))} +
+ +
+
+ Showing {Math.min(list.length, pageSize)} out of{' '} + {list.length} Alerts +
+ setPage(page)} + limit={pageSize} + debounceRequest={100} + /> +
+
+ ); +} + +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); diff --git a/frontend/app/components/Dashboard/components/Alerts/AlertsSearch.tsx b/frontend/app/components/Dashboard/components/Alerts/AlertsSearch.tsx new file mode 100644 index 000000000..547d5fc61 --- /dev/null +++ b/frontend/app/components/Dashboard/components/Alerts/AlertsSearch.tsx @@ -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) => { + setInputValue(value); + debounceUpdate(value); + }; + + return ( +
+ + +
+ ); +} + +export default connect( + (state) => ({ + // @ts-ignore + alertsSearch: state.getIn(['alerts', 'alertsSearch']), + }), + { changeSearch } +)(AlertsSearch); diff --git a/frontend/app/components/Dashboard/components/Alerts/AlertsView.tsx b/frontend/app/components/Dashboard/components/Alerts/AlertsView.tsx new file mode 100644 index 000000000..e20e5719e --- /dev/null +++ b/frontend/app/components/Dashboard/components/Alerts/AlertsView.tsx @@ -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; + remove: (alertId: string) => Promise; +} + +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 ( +
+
+
+ +
+ +
+ +
+
+
+ + A dashboard is a custom visualization using your OpenReplay data. +
+ +
+ ); +} + +// @ts-ignore +const Container = connect(null, { init, edit, save, remove })(AlertsView); + +export default withPageTitle('Alerts - OpenReplay')(Container); diff --git a/frontend/app/components/Dashboard/components/Alerts/DropdownChips/DropdownChips.js b/frontend/app/components/Dashboard/components/Alerts/DropdownChips/DropdownChips.js new file mode 100644 index 000000000..1f805057d --- /dev/null +++ b/frontend/app/components/Dashboard/components/Alerts/DropdownChips/DropdownChips.js @@ -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 onRemove(val)} outline={true} />; + }; + + return ( +
+ {textFiled ? ( + + ) : ( + +
+
+ props.edit({ [name]: value })} + value={{ value: instance.detectionMethod }} + list={[ + { name: 'Threshold', value: 'threshold' }, + { name: 'Change', value: 'change' }, + ]} + /> +
+ {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.'} +
+
+
+ } + /> + +
+ +
+ {!isThreshold && ( +
+ + i.value === instance.query.left)} + // onChange={ writeQueryOption } + onChange={({ value }) => + writeQueryOption(null, { name: 'left', value: value.value }) + } + /> +
+ +
+ +
+ + {'test'} + + )} + {!unit && ( + + )} +
+
+ +
+ + writeOption(null, { name: 'previousPeriod', value })} + /> +
+ )} +
+ } + /> + +
+ +
+
+ + + +
+ + {instance.slack && ( +
+ +
+ props.edit({ slackInput: selected })} + /> +
+
+ )} + + {instance.email && ( +
+ +
+ props.edit({ emailInput: selected })} + /> +
+
+ )} + + {instance.webhook && ( +
+ + props.edit({ webhookInput: selected })} + /> +
+ )} +
+ } + /> + + +
+
+ +
+ +
+
+ {instance.exists() && ( + + )} +
+
+ + ); +}; + +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); diff --git a/frontend/app/components/Dashboard/components/Alerts/alertForm.module.css b/frontend/app/components/Dashboard/components/Alerts/alertForm.module.css new file mode 100644 index 000000000..9e41ffd94 --- /dev/null +++ b/frontend/app/components/Dashboard/components/Alerts/alertForm.module.css @@ -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; + } + } +} diff --git a/frontend/app/components/Dashboard/components/Alerts/index.tsx b/frontend/app/components/Dashboard/components/Alerts/index.tsx new file mode 100644 index 000000000..793c47aaf --- /dev/null +++ b/frontend/app/components/Dashboard/components/Alerts/index.tsx @@ -0,0 +1 @@ +export { default } from './AlertsView' diff --git a/frontend/app/components/Dashboard/components/Alerts/type.d.ts b/frontend/app/components/Dashboard/components/Alerts/type.d.ts new file mode 100644 index 000000000..6ac1a8f34 --- /dev/null +++ b/frontend/app/components/Dashboard/components/Alerts/type.d.ts @@ -0,0 +1,2 @@ +// TODO burn the immutable and make typing this possible +type Alert = Record diff --git a/frontend/app/components/Dashboard/components/DashboardList/DashboardListItem.tsx b/frontend/app/components/Dashboard/components/DashboardList/DashboardListItem.tsx index ad532d21c..54f359041 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/DashboardListItem.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/DashboardListItem.tsx @@ -41,7 +41,7 @@ function DashboardListItem(props: Props) {
{checkForRecent(dashboard.createdAt, 'LLL dd, yyyy, hh:mm a')}
- {dashboard.description ?
{dashboard.description}
: null} + {dashboard.description ?
{dashboard.description}
: null} ); } diff --git a/frontend/app/components/Dashboard/components/DashboardRouter/DashboardRouter.tsx b/frontend/app/components/Dashboard/components/DashboardRouter/DashboardRouter.tsx index f18415f27..a7c2d62fd 100644 --- a/frontend/app/components/Dashboard/components/DashboardRouter/DashboardRouter.tsx +++ b/frontend/app/components/Dashboard/components/DashboardRouter/DashboardRouter.tsx @@ -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 ( - - ) +function DashboardViewSelected({ siteId, dashboardId }: { siteId: string; dashboardId: string }) { + return ; } 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 ( -
- - - - + return ( +
+ + + + - - - - - - - + + + - - - + + + - - - + + + - - - + + + - - - - -
- ); + + + + + + + + + + + + + + + + + + + +
+
+ ); } export default withRouter(DashboardRouter); diff --git a/frontend/app/components/Dashboard/components/DashboardSideMenu/DashboardSideMenu.tsx b/frontend/app/components/Dashboard/components/DashboardSideMenu/DashboardSideMenu.tsx index 2c4a4091e..b7748d1a7 100644 --- a/frontend/app/components/Dashboard/components/DashboardSideMenu/DashboardSideMenu.tsx +++ b/frontend/app/components/Dashboard/components/DashboardSideMenu/DashboardSideMenu.tsx @@ -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 ( -
- -
- redirect(withSiteId(dashboard(), siteId))} - /> -
-
-
- redirect(withSiteId(metrics(), siteId))} - /> -
-
-
- setShowAlerts(true)} - /> -
-
- ); + return ( +
+ +
+ redirect(withSiteId(dashboard(), siteId))} + /> +
+
+
+ redirect(withSiteId(metrics(), siteId))} + /> +
+
+
+ redirect(withSiteId(alerts(), siteId))} + /> +
+
+ ); } -export default compose( - withRouter, - connect(null, { setShowAlerts }), -)(DashboardSideMenu) +export default compose(withRouter, connect(null, { setShowAlerts }))(DashboardSideMenu); diff --git a/frontend/app/components/Session_/Issues/IssueForm.js b/frontend/app/components/Session_/Issues/IssueForm.js index 991a227ec..17cd0f07b 100644 --- a/frontend/app/components/Session_/Issues/IssueForm.js +++ b/frontend/app/components/Session_/Issues/IssueForm.js @@ -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 : '' }); } diff --git a/frontend/app/components/Session_/Player/Controls/Controls.js b/frontend/app/components/Session_/Player/Controls/Controls.js index b8e428d93..ead10433c 100644 --- a/frontend/app/components/Session_/Player/Controls/Controls.js +++ b/frontend/app/components/Session_/Player/Controls/Controls.js @@ -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 ( - -
- -
-
- ); - }; - - controlIcon = (icon, size, action, isBackwards, additionalClasses) => ( + return ( +
- +
+
); + }; - 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) => ( +
+ +
+ ); + + 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 ( -
- {!live || liveTimeTravel ? ( - - ) : null} - {!fullscreen && ( -
-
- {!live && ( - <> - - {/* */} -
- toggleBottomTools(OVERVIEW)} /> - - )} + return ( +
+ {!live || liveTimeTravel ? ( + + ) : null} + {!fullscreen && ( +
+
+ {!live && ( + <> + + {/* */} +
+ toggleBottomTools(OVERVIEW)} + /> + + )} - {live && !closedLive && ( -
- (livePlay ? null : jumpToLive())} /> -
- -
+ {live && !closedLive && ( +
+ (livePlay ? null : jumpToLive())} /> +
+ +
- {!liveTimeTravel && ( -
- See Past Activity -
- )} -
- )} -
+ {!liveTimeTravel && ( +
+ See Past Activity +
+ )} +
+ )} +
-
- {/* { !live &&
} */} - {/* ! TEMP DISABLED ! +
+ {/* { !live &&
} */} + {/* ! TEMP DISABLED ! {!live && ( )} */} - {/* toggleBottomTools(OVERVIEW) } active={ bottomBlock === OVERVIEW && !inspectorMode} @@ -376,131 +405,131 @@ export default class Controls extends React.Component { // hasErrors={ logRedCount > 0 } containerClassName="mx-2" /> */} - toggleBottomTools(CONSOLE)} - active={bottomBlock === CONSOLE && !inspectorMode} - label="CONSOLE" - noIcon - labelClassName="!text-base font-semibold" - count={logCount} - hasErrors={logRedCount > 0} - containerClassName="mx-2" - /> - {!live && ( - toggleBottomTools(NETWORK)} - active={bottomBlock === NETWORK && !inspectorMode} - label="NETWORK" - hasErrors={resourceRedCount > 0} - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" - /> - )} - {!live && ( - toggleBottomTools(PERFORMANCE)} - active={bottomBlock === PERFORMANCE && !inspectorMode} - label="PERFORMANCE" - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" - /> - )} - {showFetch && ( - toggleBottomTools(FETCH)} - active={bottomBlock === FETCH && !inspectorMode} - hasErrors={fetchRedCount > 0} - count={fetchCount} - label="FETCH" - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" - /> - )} - {!live && showGraphql && ( - toggleBottomTools(GRAPHQL)} - active={bottomBlock === GRAPHQL && !inspectorMode} - count={graphqlCount} - label="GRAPHQL" - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" - /> - )} - {!live && showStorage && ( - toggleBottomTools(STORAGE)} - active={bottomBlock === STORAGE && !inspectorMode} - count={storageCount} - label={getStorageName(storageType)} - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" - /> - )} - {showExceptions && ( - toggleBottomTools(EXCEPTIONS)} - active={bottomBlock === EXCEPTIONS && !inspectorMode} - label="EXCEPTIONS" - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" - count={exceptionsCount} - hasErrors={exceptionsCount > 0} - /> - )} - {!live && showStack && ( - toggleBottomTools(STACKEVENTS)} - active={bottomBlock === STACKEVENTS && !inspectorMode} - label="EVENTS" - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" - count={stackCount} - hasErrors={stackRedCount > 0} - /> - )} - {!live && showProfiler && ( - toggleBottomTools(PROFILER)} - active={bottomBlock === PROFILER && !inspectorMode} - count={profilesCount} - label="PROFILER" - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" - /> - )} - {!live &&
} - {!live && ( - - {this.controlIcon( - 'arrows-angle-extend', - 18, - this.props.fullscreenOn, - false, - 'rounded hover:bg-gray-light-shade color-gray-medium' - )} - - )} -
-
- )} + toggleBottomTools(CONSOLE)} + active={bottomBlock === CONSOLE && !inspectorMode} + label="CONSOLE" + noIcon + labelClassName="!text-base font-semibold" + count={logCount} + hasErrors={logRedCount > 0} + containerClassName="mx-2" + /> + {!live && ( + toggleBottomTools(NETWORK)} + active={bottomBlock === NETWORK && !inspectorMode} + label="NETWORK" + hasErrors={resourceRedCount > 0} + noIcon + labelClassName="!text-base font-semibold" + containerClassName="mx-2" + /> + )} + {!live && ( + toggleBottomTools(PERFORMANCE)} + active={bottomBlock === PERFORMANCE && !inspectorMode} + label="PERFORMANCE" + noIcon + labelClassName="!text-base font-semibold" + containerClassName="mx-2" + /> + )} + {showFetch && ( + toggleBottomTools(FETCH)} + active={bottomBlock === FETCH && !inspectorMode} + hasErrors={fetchRedCount > 0} + count={fetchCount} + label="FETCH" + noIcon + labelClassName="!text-base font-semibold" + containerClassName="mx-2" + /> + )} + {!live && showGraphql && ( + toggleBottomTools(GRAPHQL)} + active={bottomBlock === GRAPHQL && !inspectorMode} + count={graphqlCount} + label="GRAPHQL" + noIcon + labelClassName="!text-base font-semibold" + containerClassName="mx-2" + /> + )} + {!live && showStorage && ( + toggleBottomTools(STORAGE)} + active={bottomBlock === STORAGE && !inspectorMode} + count={storageCount} + label={getStorageName(storageType)} + noIcon + labelClassName="!text-base font-semibold" + containerClassName="mx-2" + /> + )} + {showExceptions && ( + toggleBottomTools(EXCEPTIONS)} + active={bottomBlock === EXCEPTIONS && !inspectorMode} + label="EXCEPTIONS" + noIcon + labelClassName="!text-base font-semibold" + containerClassName="mx-2" + count={exceptionsCount} + hasErrors={exceptionsCount > 0} + /> + )} + {!live && showStack && ( + toggleBottomTools(STACKEVENTS)} + active={bottomBlock === STACKEVENTS && !inspectorMode} + label="EVENTS" + noIcon + labelClassName="!text-base font-semibold" + containerClassName="mx-2" + count={stackCount} + hasErrors={stackRedCount > 0} + /> + )} + {!live && showProfiler && ( + toggleBottomTools(PROFILER)} + active={bottomBlock === PROFILER && !inspectorMode} + count={profilesCount} + label="PROFILER" + noIcon + labelClassName="!text-base font-semibold" + containerClassName="mx-2" + /> + )} + {!live &&
} + {!live && ( + + {this.controlIcon( + 'arrows-angle-extend', + 18, + this.props.fullscreenOn, + false, + 'rounded hover:bg-gray-light-shade color-gray-medium' + )} + + )}
- ); - } +
+ )} +
+ ); + } } diff --git a/frontend/app/components/Session_/Player/Controls/components/PlayerControls.tsx b/frontend/app/components/Session_/Player/Controls/components/PlayerControls.tsx index 76808fa76..a1d3dd87c 100644 --- a/frontend/app/components/Session_/Player/Controls/components/PlayerControls.tsx +++ b/frontend/app/components/Session_/Player/Controls/components/PlayerControls.tsx @@ -71,7 +71,7 @@ function PlayerControls(props: Props) { const toggleTooltip = () => { setShowTooltip(!showTooltip); - } + }; return (
{playButton} @@ -112,31 +112,35 @@ function PlayerControls(props: Props) { className="cursor-pointer select-none" distance={20} html={ -
-
- Jump (Secs) -
- {Object.keys(skipIntervals).map((interval) => ( -
{ - 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} - s + showTooltip ? toggleTooltip() : null}> +
+
+ Jump (Secs)
- ))} -
+ {Object.keys(skipIntervals).map((interval) => ( +
{ + 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} + s +
+ ))} +
+ } >
{/* @ts-ignore */} - {currentInterval}s + + {currentInterval}s +
diff --git a/frontend/app/components/shared/Select/Select.tsx b/frontend/app/components/shared/Select/Select.tsx index b58f99132..3ef9383ca 100644 --- a/frontend/app/components/shared/Select/Select.tsx +++ b/frontend/app/components/shared/Select/Select.tsx @@ -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 { diff --git a/frontend/app/duck/alerts.js b/frontend/app/duck/alerts.js index 623b34301..dcc3c1633 100644 --- a/frontend/app/duck/alerts.js +++ b/frontend/app/duck/alerts.js @@ -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(), diff --git a/frontend/app/routes.js b/frontend/app/routes.js index 627095f86..90de53012 100644 --- a/frontend/app/routes.js +++ b/frontend/app/routes.js @@ -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('') ]; diff --git a/frontend/app/utils.ts b/frontend/app/utils.ts index 2bc9a1451..03202fff0 100644 --- a/frontend/app/utils.ts +++ b/frontend/app/utils.ts @@ -67,13 +67,14 @@ export const filterList = >( 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'; -} \ No newline at end of file +}