mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Fix silences preview (#66000)
* Use alertmanager /alerts endpoint to show preview of instances affected by silence * Fix debounce dependency, add no instances warning * Rename silences preview component * Fix the preview file name, use IsNulLDate to check the date * Fix valid matchers condition * Cleanup * Remove unused code
This commit is contained in:
parent
044d7f61c7
commit
c41c638b52
@ -1,10 +1,13 @@
|
||||
import {
|
||||
AlertmanagerAlert,
|
||||
AlertmanagerChoice,
|
||||
AlertManagerCortexConfig,
|
||||
ExternalAlertmanagerConfig,
|
||||
ExternalAlertmanagers,
|
||||
ExternalAlertmanagersResponse,
|
||||
Matcher,
|
||||
} from '../../../../plugins/datasource/alertmanager/types';
|
||||
import { matcherToOperator } from '../utils/alertmanager';
|
||||
import { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||
|
||||
import { alertingApi } from './alertingApi';
|
||||
@ -16,8 +19,34 @@ export interface AlertmanagersChoiceResponse {
|
||||
numExternalAlertmanagers: number;
|
||||
}
|
||||
|
||||
interface AlertmanagerAlertsFilter {
|
||||
active?: boolean;
|
||||
silenced?: boolean;
|
||||
inhibited?: boolean;
|
||||
unprocessed?: boolean;
|
||||
matchers?: Matcher[];
|
||||
}
|
||||
|
||||
// Based on https://github.com/prometheus/alertmanager/blob/main/api/v2/openapi.yaml
|
||||
export const alertmanagerApi = alertingApi.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
getAlertmanagerAlerts: build.query<
|
||||
AlertmanagerAlert[],
|
||||
{ amSourceName: string; filter?: AlertmanagerAlertsFilter }
|
||||
>({
|
||||
query: ({ amSourceName, filter }) => {
|
||||
// TODO Add support for active, silenced, inhibited, unprocessed filters
|
||||
const filterMatchers = filter?.matchers
|
||||
?.filter((matcher) => matcher.name && matcher.value)
|
||||
.map((matcher) => `${matcher.name}${matcherToOperator(matcher)}${matcher.value}`);
|
||||
|
||||
return {
|
||||
url: `/api/alertmanager/${getDatasourceAPIUid(amSourceName)}/api/v2/alerts`,
|
||||
params: { filter: filterMatchers },
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
getAlertmanagerChoiceStatus: build.query<AlertmanagersChoiceResponse, void>({
|
||||
query: () => ({ url: '/api/v1/ngalert' }),
|
||||
providesTags: ['AlertmanagerChoice'],
|
||||
|
@ -17,6 +17,7 @@ export interface DynamicTableColumnProps<T = unknown> {
|
||||
|
||||
renderCell: (item: DynamicTableItemProps<T>, index: number) => ReactNode;
|
||||
size?: number | string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface DynamicTableItemProps<T = unknown> {
|
||||
@ -134,7 +135,11 @@ export const DynamicTable = <T extends object>({
|
||||
</div>
|
||||
)}
|
||||
{cols.map((col) => (
|
||||
<div className={cx(styles.cell, styles.bodyCell)} data-column={col.label} key={`${item.id}-${col.id}`}>
|
||||
<div
|
||||
className={cx(styles.cell, styles.bodyCell, col.className)}
|
||||
data-column={col.label}
|
||||
key={`${item.id}-${col.id}`}
|
||||
>
|
||||
{col.renderCell(item, index)}
|
||||
</div>
|
||||
))}
|
||||
|
@ -1,128 +0,0 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useDebounce } from 'react-use';
|
||||
|
||||
import { dateTime, GrafanaTheme2 } from '@grafana/data';
|
||||
import { Badge, useStyles2 } from '@grafana/ui';
|
||||
import { useDispatch } from 'app/types';
|
||||
import { Alert, AlertingRule } from 'app/types/unified-alerting';
|
||||
|
||||
import { useCombinedRuleNamespaces } from '../../hooks/useCombinedRuleNamespaces';
|
||||
import { fetchAllPromAndRulerRulesAction } from '../../state/actions';
|
||||
import { MatcherFieldValue, SilenceFormFields } from '../../types/silence-form';
|
||||
import { findAlertInstancesWithMatchers } from '../../utils/matchers';
|
||||
import { isAlertingRule } from '../../utils/rules';
|
||||
import { AlertLabels } from '../AlertLabels';
|
||||
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
|
||||
import { AlertStateTag } from '../rules/AlertStateTag';
|
||||
|
||||
type MatchedRulesTableItemProps = DynamicTableItemProps<{
|
||||
matchedInstance: Alert;
|
||||
}>;
|
||||
type MatchedRulesTableColumnProps = DynamicTableColumnProps<{ matchedInstance: Alert }>;
|
||||
|
||||
export const MatchedSilencedRules = () => {
|
||||
const [matchedAlertRules, setMatchedAlertRules] = useState<MatchedRulesTableItemProps[]>([]);
|
||||
const formApi = useFormContext<SilenceFormFields>();
|
||||
const dispatch = useDispatch();
|
||||
const { watch } = formApi;
|
||||
const matchers: MatcherFieldValue[] = watch('matchers');
|
||||
const styles = useStyles2(getStyles);
|
||||
const columns = useColumns();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchAllPromAndRulerRulesAction());
|
||||
}, [dispatch]);
|
||||
|
||||
const combinedNamespaces = useCombinedRuleNamespaces();
|
||||
useDebounce(
|
||||
() => {
|
||||
const matchedInstances = combinedNamespaces.flatMap((namespace) => {
|
||||
return namespace.groups.flatMap((group) => {
|
||||
return group.rules
|
||||
.map((combinedRule) => combinedRule.promRule)
|
||||
.filter((rule): rule is AlertingRule => isAlertingRule(rule))
|
||||
.flatMap((rule) => findAlertInstancesWithMatchers(rule.alerts ?? [], matchers));
|
||||
});
|
||||
});
|
||||
setMatchedAlertRules(matchedInstances);
|
||||
},
|
||||
500,
|
||||
[combinedNamespaces, matchers]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className={styles.title}>
|
||||
Affected alert instances
|
||||
{matchedAlertRules.length > 0 ? (
|
||||
<Badge className={styles.badge} color="blue" text={matchedAlertRules.length} />
|
||||
) : null}
|
||||
</h4>
|
||||
<div className={styles.table}>
|
||||
{matchers.every((matcher) => !matcher.value && !matcher.name) ? (
|
||||
<span>Add a valid matcher to see affected alerts</span>
|
||||
) : (
|
||||
<DynamicTable
|
||||
items={matchedAlertRules}
|
||||
isExpandable={false}
|
||||
cols={columns}
|
||||
pagination={{ itemsPerPage: 5 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function useColumns(): MatchedRulesTableColumnProps[] {
|
||||
return [
|
||||
{
|
||||
id: 'state',
|
||||
label: 'State',
|
||||
renderCell: function renderStateTag({ data: { matchedInstance } }) {
|
||||
return <AlertStateTag state={matchedInstance.state} />;
|
||||
},
|
||||
size: '160px',
|
||||
},
|
||||
{
|
||||
id: 'labels',
|
||||
label: 'Labels',
|
||||
renderCell: function renderName({ data: { matchedInstance } }) {
|
||||
return <AlertLabels labels={matchedInstance.labels} />;
|
||||
},
|
||||
size: 'auto',
|
||||
},
|
||||
{
|
||||
id: 'created',
|
||||
label: 'Created',
|
||||
renderCell: function renderSummary({ data: { matchedInstance } }) {
|
||||
return (
|
||||
<>
|
||||
{matchedInstance.activeAt.startsWith('0001')
|
||||
? '-'
|
||||
: dateTime(matchedInstance.activeAt).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</>
|
||||
);
|
||||
},
|
||||
size: '180px',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
table: css`
|
||||
max-width: ${theme.breakpoints.values.lg}px;
|
||||
`,
|
||||
moreMatches: css`
|
||||
margin-top: ${theme.spacing(1)};
|
||||
`,
|
||||
title: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`,
|
||||
badge: css`
|
||||
margin-left: ${theme.spacing(1)};
|
||||
`,
|
||||
});
|
@ -0,0 +1,129 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { dateTime, GrafanaTheme2 } from '@grafana/data';
|
||||
import { Alert, Badge, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
|
||||
import { AlertmanagerAlert, Matcher } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { alertmanagerApi } from '../../api/alertmanagerApi';
|
||||
import { isNullDate } from '../../utils/time';
|
||||
import { AlertLabels } from '../AlertLabels';
|
||||
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
|
||||
|
||||
import { AmAlertStateTag } from './AmAlertStateTag';
|
||||
|
||||
interface Props {
|
||||
amSourceName: string;
|
||||
matchers: Matcher[];
|
||||
}
|
||||
|
||||
export const SilencedInstancesPreview = ({ amSourceName, matchers }: Props) => {
|
||||
const { useGetAlertmanagerAlertsQuery } = alertmanagerApi;
|
||||
const styles = useStyles2(getStyles);
|
||||
const columns = useColumns();
|
||||
|
||||
// By default the form contains an empty matcher - with empty name and value and = operator
|
||||
// We don't want to fetch previews for empty matchers as it results in all alerts returned
|
||||
const hasValidMatchers = matchers.some((matcher) => matcher.value && matcher.name);
|
||||
|
||||
const {
|
||||
currentData: alerts = [],
|
||||
isFetching,
|
||||
isError,
|
||||
} = useGetAlertmanagerAlertsQuery(
|
||||
{ amSourceName, filter: { matchers } },
|
||||
{ skip: !hasValidMatchers, refetchOnMountOrArgChange: true }
|
||||
);
|
||||
|
||||
const tableItemAlerts = alerts.map<DynamicTableItemProps<AlertmanagerAlert>>((alert) => ({
|
||||
id: alert.fingerprint,
|
||||
data: alert,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className={styles.title}>
|
||||
Affected alert instances
|
||||
{tableItemAlerts.length > 0 ? (
|
||||
<Badge className={styles.badge} color="blue" text={tableItemAlerts.length} />
|
||||
) : null}
|
||||
</h4>
|
||||
{!hasValidMatchers && <span>Add a valid matcher to see affected alerts</span>}
|
||||
{isError && (
|
||||
<Alert title="Preview not available" severity="error">
|
||||
Error occured when generating affected alerts preview. Are you matchers valid?
|
||||
</Alert>
|
||||
)}
|
||||
{isFetching && <LoadingPlaceholder text="Loading..." />}
|
||||
{!isFetching && !isError && hasValidMatchers && (
|
||||
<div className={styles.table}>
|
||||
{tableItemAlerts.length > 0 ? (
|
||||
<DynamicTable
|
||||
items={tableItemAlerts}
|
||||
isExpandable={false}
|
||||
cols={columns}
|
||||
pagination={{ itemsPerPage: 10 }}
|
||||
/>
|
||||
) : (
|
||||
<span>No matching alert instances found</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function useColumns(): Array<DynamicTableColumnProps<AlertmanagerAlert>> {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'state',
|
||||
label: 'State',
|
||||
renderCell: function renderStateTag({ data }) {
|
||||
return <AmAlertStateTag state={data.status.state} />;
|
||||
},
|
||||
size: '120px',
|
||||
className: styles.stateColumn,
|
||||
},
|
||||
{
|
||||
id: 'labels',
|
||||
label: 'Labels',
|
||||
renderCell: function renderName({ data }) {
|
||||
return <AlertLabels labels={data.labels} className={styles.alertLabels} />;
|
||||
},
|
||||
size: 'auto',
|
||||
},
|
||||
{
|
||||
id: 'created',
|
||||
label: 'Created',
|
||||
renderCell: function renderSummary({ data }) {
|
||||
return <>{isNullDate(data.startsAt) ? '-' : dateTime(data.startsAt).format('YYYY-MM-DD HH:mm:ss')}</>;
|
||||
},
|
||||
size: '180px',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
table: css`
|
||||
max-width: ${theme.breakpoints.values.lg}px;
|
||||
`,
|
||||
moreMatches: css`
|
||||
margin-top: ${theme.spacing(1)};
|
||||
`,
|
||||
title: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`,
|
||||
badge: css`
|
||||
margin-left: ${theme.spacing(1)};
|
||||
`,
|
||||
stateColumn: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`,
|
||||
alertLabels: css`
|
||||
justify-content: flex-start;
|
||||
`,
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { pickBy } from 'lodash';
|
||||
import { isEqual, pickBy } from 'lodash';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
import { useDebounce } from 'react-use';
|
||||
@ -16,7 +16,7 @@ import {
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Button, Field, FieldSet, Input, LinkButton, TextArea, useStyles2 } from '@grafana/ui';
|
||||
import { useCleanup } from 'app/core/hooks/useCleanup';
|
||||
import { MatcherOperator, Silence, SilenceCreatePayload } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { Matcher, MatcherOperator, Silence, SilenceCreatePayload } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { useDispatch } from 'app/types';
|
||||
|
||||
import { useURLSearchParams } from '../../hooks/useURLSearchParams';
|
||||
@ -28,9 +28,9 @@ import { parseQueryParamMatchers } from '../../utils/matchers';
|
||||
import { makeAMLink } from '../../utils/misc';
|
||||
import { initialAsyncRequestState } from '../../utils/redux';
|
||||
|
||||
import { MatchedSilencedRules } from './MatchedSilencedRules';
|
||||
import MatchersField from './MatchersField';
|
||||
import { SilencePeriod } from './SilencePeriod';
|
||||
import { SilencedInstancesPreview } from './SilencedInstancesPreview';
|
||||
|
||||
interface Props {
|
||||
silence?: Silence;
|
||||
@ -104,6 +104,9 @@ export const SilencesEditor = ({ silence, alertManagerSourceName }: Props) => {
|
||||
const formAPI = useForm({ defaultValues });
|
||||
const dispatch = useDispatch();
|
||||
const styles = useStyles2(getStyles);
|
||||
const [matchersForPreview, setMatchersForPreview] = useState<Matcher[]>(
|
||||
defaultValues.matchers.map(matcherFieldToMatcher)
|
||||
);
|
||||
|
||||
const { loading } = useUnifiedAlertingSelector((state) => state.updateSilence);
|
||||
|
||||
@ -138,6 +141,7 @@ export const SilencesEditor = ({ silence, alertManagerSourceName }: Props) => {
|
||||
const duration = watch('duration');
|
||||
const startsAt = watch('startsAt');
|
||||
const endsAt = watch('endsAt');
|
||||
const matcherFields = watch('matchers');
|
||||
|
||||
// Keep duration and endsAt in sync
|
||||
const [prevDuration, setPrevDuration] = useState(duration);
|
||||
@ -164,6 +168,19 @@ export const SilencesEditor = ({ silence, alertManagerSourceName }: Props) => {
|
||||
700,
|
||||
[clearErrors, duration, endsAt, prevDuration, setValue, startsAt]
|
||||
);
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
// React-hook-form watch does not return referentialy equal values so this trick is needed
|
||||
const newMatchers = matcherFields.filter((m) => m.name && m.value).map(matcherFieldToMatcher);
|
||||
if (!isEqual(matchersForPreview, newMatchers)) {
|
||||
setMatchersForPreview(newMatchers);
|
||||
}
|
||||
},
|
||||
700,
|
||||
[matcherFields]
|
||||
);
|
||||
|
||||
const userLogged = Boolean(config.bootData.user.isSignedIn && config.bootData.user.name);
|
||||
|
||||
return (
|
||||
@ -221,7 +238,7 @@ export const SilencesEditor = ({ silence, alertManagerSourceName }: Props) => {
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
<MatchedSilencedRules />
|
||||
<SilencedInstancesPreview amSourceName={alertManagerSourceName} matchers={matchersForPreview} />
|
||||
</FieldSet>
|
||||
<div className={styles.flexRow}>
|
||||
{loading && (
|
||||
|
@ -1,8 +1,4 @@
|
||||
import { MatcherOperator } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { mockPromAlert } from '../mocks';
|
||||
|
||||
import { getMatcherQueryParams, findAlertInstancesWithMatchers, parseQueryParamMatchers } from './matchers';
|
||||
import { getMatcherQueryParams, parseQueryParamMatchers } from './matchers';
|
||||
|
||||
describe('Unified Alerting matchers', () => {
|
||||
describe('getMatcherQueryParams tests', () => {
|
||||
@ -37,57 +33,4 @@ describe('Unified Alerting matchers', () => {
|
||||
expect(matchers[0].value).toBe('TestData 1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchLabelsToMatchers', () => {
|
||||
it('should match for equal', () => {
|
||||
const matchers = [{ name: 'foo', value: 'bar', operator: MatcherOperator.equal }];
|
||||
const alerts = [mockPromAlert({ labels: { foo: 'bar' } }), mockPromAlert({ labels: { foo: 'baz' } })];
|
||||
const matchedAlerts = findAlertInstancesWithMatchers(alerts, matchers);
|
||||
|
||||
expect(matchedAlerts).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should match for not equal', () => {
|
||||
const matchers = [{ name: 'foo', value: 'bar', operator: MatcherOperator.notEqual }];
|
||||
const alerts = [mockPromAlert({ labels: { foo: 'bar' } }), mockPromAlert({ labels: { foo: 'baz' } })];
|
||||
|
||||
const matchedAlerts = findAlertInstancesWithMatchers(alerts, matchers);
|
||||
expect(matchedAlerts).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should match for regex', () => {
|
||||
const matchers = [{ name: 'foo', value: 'b{1}a.*', operator: MatcherOperator.regex }];
|
||||
const alerts = [
|
||||
mockPromAlert({ labels: { foo: 'bbr' } }),
|
||||
mockPromAlert({ labels: { foo: 'aba' } }), // This does not match because the regex is implicitly anchored.
|
||||
mockPromAlert({ labels: { foo: 'ba' } }),
|
||||
mockPromAlert({ labels: { foo: 'bar' } }),
|
||||
mockPromAlert({ labels: { foo: 'baz' } }),
|
||||
mockPromAlert({ labels: { foo: 'bas' } }),
|
||||
];
|
||||
|
||||
const matchedAlerts = findAlertInstancesWithMatchers(alerts, matchers);
|
||||
expect(matchedAlerts).toHaveLength(4);
|
||||
expect(matchedAlerts.map((instance) => instance.data.matchedInstance.labels.foo)).toEqual([
|
||||
'ba',
|
||||
'bar',
|
||||
'baz',
|
||||
'bas',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not match regex', () => {
|
||||
const matchers = [{ name: 'foo', value: 'ba{3}', operator: MatcherOperator.notRegex }];
|
||||
const alerts = [
|
||||
mockPromAlert({ labels: { foo: 'bar' } }),
|
||||
mockPromAlert({ labels: { foo: 'baz' } }),
|
||||
mockPromAlert({ labels: { foo: 'baaa' } }),
|
||||
mockPromAlert({ labels: { foo: 'bas' } }),
|
||||
];
|
||||
|
||||
const matchedAlerts = findAlertInstancesWithMatchers(alerts, matchers);
|
||||
expect(matchedAlerts).toHaveLength(3);
|
||||
expect(matchedAlerts.map((instance) => instance.data.matchedInstance.labels.foo)).toEqual(['bar', 'baz', 'bas']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,10 +1,7 @@
|
||||
import { uniqBy } from 'lodash';
|
||||
|
||||
import { Labels } from '@grafana/data';
|
||||
import { Matcher, MatcherOperator } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { Alert } from 'app/types/unified-alerting';
|
||||
|
||||
import { MatcherFieldValue } from '../types/silence-form';
|
||||
import { Matcher } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { parseMatcher } from './alertmanager';
|
||||
|
||||
@ -29,61 +26,3 @@ export const getMatcherQueryParams = (labels: Labels) => {
|
||||
|
||||
return matcherUrlParams;
|
||||
};
|
||||
|
||||
interface MatchedInstance {
|
||||
id: string;
|
||||
data: {
|
||||
matchedInstance: Alert;
|
||||
};
|
||||
}
|
||||
|
||||
export const findAlertInstancesWithMatchers = (
|
||||
instances: Alert[],
|
||||
matchers: MatcherFieldValue[]
|
||||
): MatchedInstance[] => {
|
||||
const anchorRegex = (regexpString: string): RegExp => {
|
||||
// Silence matchers are always fully anchored in the Alertmanager: https://github.com/prometheus/alertmanager/pull/748
|
||||
if (!regexpString.startsWith('^')) {
|
||||
regexpString = '^' + regexpString;
|
||||
}
|
||||
if (!regexpString.endsWith('$')) {
|
||||
regexpString = regexpString + '$';
|
||||
}
|
||||
return new RegExp(regexpString);
|
||||
};
|
||||
|
||||
const matchesInstance = (instance: Alert, matcher: MatcherFieldValue) => {
|
||||
return Object.entries(instance.labels).some(([key, value]) => {
|
||||
if (!matcher.name || !matcher.value) {
|
||||
return false;
|
||||
}
|
||||
if (matcher.name !== key) {
|
||||
return false;
|
||||
}
|
||||
switch (matcher.operator) {
|
||||
case MatcherOperator.equal:
|
||||
return matcher.value === value;
|
||||
case MatcherOperator.notEqual:
|
||||
return matcher.value !== value;
|
||||
case MatcherOperator.regex:
|
||||
const regex = anchorRegex(matcher.value);
|
||||
return regex.test(value);
|
||||
case MatcherOperator.notRegex:
|
||||
const negregex = anchorRegex(matcher.value);
|
||||
return !negregex.test(value);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const filteredInstances = instances.filter((instance) => {
|
||||
return matchers.every((matcher) => matchesInstance(instance, matcher));
|
||||
});
|
||||
const mappedInstances = filteredInstances.map((instance) => ({
|
||||
id: `${instance.activeAt}-${instance.value}`,
|
||||
data: { matchedInstance: instance },
|
||||
}));
|
||||
|
||||
return mappedInstances;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user