[RAM][SecuritySolution] Simplify rules list notify badge (#155500)

**Relates to:** https://github.com/elastic/security-team/issues/5308

## Summary

This PR make improvements and simplifications to `RulesListNotifyBadge` component exported by `triggers_actions_ui` plugin spotted during [adoption](https://github.com/elastic/security-team/issues/5308) of this component in Security Solution.

## Details

The list of the changes

- Mixed controlled and uncontrolled state has been resolved in favour of controlled state. It means an external code is responsible to fetch and provide rule snooze settings to the component.
- Loading state management has been moved inside the component. The only way to to set the component in loading state it to provide `undefined` for `snoozeSettings` input prop.
- Popover's open/close state management has been moved inside the component. I haven't noticed any problems in tables (Stack Management Rules and Security Solution Rules) if there are multiple rules are shown with `RulesListNotifyBadge` rendered. In attempt to open another snooze popover a previous one closes automatically.
- `rule` input prop has been renamed to `snoozeSettings`.
- `isEditable` field has been moved to a separate `disabled` prop. `disabled` can accept a string which is considered as a disabled reason and displayed in a tooltip. It's quite helpful to display for example fetching errors.
- `onRuleChanged` handler can return a promise so internal loading state persists until this promise resolved.


### Checklist

- [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials
- [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
- [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
This commit is contained in:
Maxim Palenov 2023-05-08 19:23:59 +02:00 committed by GitHub
parent 6591da49df
commit a61c63dc07
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 353 additions and 424 deletions

View file

@ -6,46 +6,15 @@
*/
import React from 'react';
import {
TriggersAndActionsUIPublicPluginStart,
RuleTableItem,
} from '@kbn/triggers-actions-ui-plugin/public';
import type { RuleSnoozeSettings } from '@kbn/triggers-actions-ui-plugin/public/types';
import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public';
interface SandboxProps {
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
}
const mockRule: RuleTableItem = {
id: '1',
enabled: true,
name: 'test rule',
tags: ['tag1'],
ruleTypeId: 'test_rule_type',
consumer: 'rules',
schedule: { interval: '5d' },
actions: [
{ id: 'test', actionTypeId: 'the_connector', group: 'rule', params: { message: 'test' } },
],
params: { name: 'test rule type name' },
createdBy: null,
updatedBy: null,
createdAt: new Date(),
updatedAt: new Date(),
apiKeyOwner: null,
throttle: '1m',
notifyWhen: 'onActiveAlert',
const mockSnoozeSettings: RuleSnoozeSettings = {
muteAll: true,
mutedInstanceIds: [],
executionStatus: {
status: 'active',
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
},
actionsCount: 1,
index: 0,
ruleType: 'Test Rule Type',
isEditable: true,
enabledInLicense: true,
revision: 0,
};
export const RulesListNotifyBadgeSandbox = ({ triggersActionsUi }: SandboxProps) => {
@ -53,8 +22,8 @@ export const RulesListNotifyBadgeSandbox = ({ triggersActionsUi }: SandboxProps)
return (
<div style={{ flex: 1 }}>
<RulesListNotifyBadge
rule={mockRule}
isLoading={false}
ruleId="1"
snoozeSettings={mockSnoozeSettings}
onRuleChanged={() => Promise.resolve()}
/>
</div>

View file

@ -848,7 +848,7 @@ describe('Detections Rules API', () => {
mute_all: false,
},
{
id: '1',
id: '2',
mute_all: false,
active_snoozes: [],
is_snoozed_until: '2023-04-24T19:31:46.765Z',
@ -856,21 +856,19 @@ describe('Detections Rules API', () => {
],
});
const result = await fetchRulesSnoozeSettings({ ids: ['id1'] });
const result = await fetchRulesSnoozeSettings({ ids: ['1', '2'] });
expect(result).toEqual([
{
id: '1',
expect(result).toEqual({
'1': {
muteAll: false,
activeSnoozes: [],
},
{
id: '1',
'2': {
muteAll: false,
activeSnoozes: [],
isSnoozedUntil: new Date('2023-04-24T19:31:46.765Z'),
},
]);
});
});
});
});

View file

@ -57,8 +57,8 @@ import type {
PrePackagedRulesStatusResponse,
PreviewRulesProps,
Rule,
RuleSnoozeSettings,
RulesSnoozeSettingsBatchResponse,
RulesSnoozeSettingsMap,
UpdateRulesProps,
} from '../logic/types';
import { convertRulesFilterToKQL } from '../logic/utils';
@ -198,7 +198,7 @@ export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise<Rul
export const fetchRulesSnoozeSettings = async ({
ids,
signal,
}: FetchRuleSnoozingProps): Promise<RuleSnoozeSettings[]> => {
}: FetchRuleSnoozingProps): Promise<RulesSnoozeSettingsMap> => {
const response = await KibanaServices.get().http.fetch<RulesSnoozeSettingsBatchResponse>(
INTERNAL_ALERTING_API_FIND_RULES_PATH,
{
@ -212,15 +212,18 @@ export const fetchRulesSnoozeSettings = async ({
}
);
return response.data?.map((snoozeSettings) => ({
id: snoozeSettings?.id ?? '',
muteAll: snoozeSettings?.mute_all ?? false,
activeSnoozes: snoozeSettings?.active_snoozes ?? [],
isSnoozedUntil: snoozeSettings?.is_snoozed_until
? new Date(snoozeSettings.is_snoozed_until)
: undefined,
snoozeSchedule: snoozeSettings?.snooze_schedule,
}));
return response.data?.reduce((result, { id, ...snoozeSettings }) => {
result[id] = {
muteAll: snoozeSettings.mute_all ?? false,
activeSnoozes: snoozeSettings.active_snoozes ?? [],
isSnoozedUntil: snoozeSettings.is_snoozed_until
? new Date(snoozeSettings.is_snoozed_until)
: undefined,
snoozeSchedule: snoozeSettings.snooze_schedule,
};
return result;
}, {} as RulesSnoozeSettingsMap);
};
export interface BulkActionSummary {

View file

@ -9,7 +9,7 @@ import { INTERNAL_ALERTING_API_FIND_RULES_PATH } from '@kbn/alerting-plugin/comm
import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback } from 'react';
import type { RuleSnoozeSettings } from '../../logic';
import type { RulesSnoozeSettingsMap } from '../../logic';
import { fetchRulesSnoozeSettings } from '../api';
import { DEFAULT_QUERY_OPTIONS } from './constants';
@ -25,7 +25,7 @@ const FETCH_RULE_SNOOZE_SETTINGS_QUERY_KEY = ['GET', INTERNAL_ALERTING_API_FIND_
*/
export const useFetchRulesSnoozeSettings = (
ids: string[],
queryOptions?: UseQueryOptions<RuleSnoozeSettings[], Error, RuleSnoozeSettings[], string[]>
queryOptions?: UseQueryOptions<RulesSnoozeSettingsMap, Error, RulesSnoozeSettingsMap, string[]>
) => {
return useQuery(
[...FETCH_RULE_SNOOZE_SETTINGS_QUERY_KEY, ...ids],
@ -51,7 +51,7 @@ export const useInvalidateFetchRulesSnoozeSettingsQuery = () => {
* Invalidate all queries that start with FIND_RULES_QUERY_KEY. This
* includes the in-memory query cache and paged query cache.
*/
queryClient.invalidateQueries(FETCH_RULE_SNOOZE_SETTINGS_QUERY_KEY, {
return queryClient.invalidateQueries(FETCH_RULE_SNOOZE_SETTINGS_QUERY_KEY, {
refetchType: 'active',
});
}, [queryClient]);

View file

@ -6,7 +6,7 @@
*/
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import React, { useMemo } from 'react';
import React from 'react';
import type { RuleObjectId } from '../../../../../common/detection_engine/rule_schema';
import { useUserData } from '../../../../detections/components/user_info';
import { hasUserCRUDPermission } from '../../../../common/utils/privileges';
@ -31,20 +31,6 @@ export function RuleSnoozeBadge({
const [{ canUserCRUD }] = useUserData();
const hasCRUDPermissions = hasUserCRUDPermission(canUserCRUD);
const invalidateFetchRuleSnoozeSettings = useInvalidateFetchRulesSnoozeSettingsQuery();
const isLoading = !snoozeSettings;
const rule = useMemo(
() => ({
id: snoozeSettings?.id ?? '',
muteAll: snoozeSettings?.muteAll ?? false,
activeSnoozes: snoozeSettings?.activeSnoozes ?? [],
isSnoozedUntil: snoozeSettings?.isSnoozedUntil
? new Date(snoozeSettings.isSnoozedUntil)
: undefined,
snoozeSchedule: snoozeSettings?.snoozeSchedule,
isEditable: hasCRUDPermissions,
}),
[snoozeSettings, hasCRUDPermissions]
);
if (error) {
return (
@ -56,8 +42,10 @@ export function RuleSnoozeBadge({
return (
<RulesListNotifyBadge
rule={rule}
isLoading={isLoading}
ruleId={ruleId}
snoozeSettings={snoozeSettings}
loading={!snoozeSettings}
disabled={!hasCRUDPermissions}
showTooltipInline={showTooltipInline}
onRuleChanged={invalidateFetchRuleSnoozeSettings}
/>

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { RuleSnoozeSettings } from '../../logic';
import type { RuleSnoozeSettings } from '@kbn/triggers-actions-ui-plugin/public/types';
import { useFetchRulesSnoozeSettings } from '../../api/hooks/use_fetch_rules_snooze_settings';
import { useRulesTableContextOptional } from '../../../rule_management_ui/components/rules_table/rules_table/rules_table_context';
import * as i18n from './translations';
@ -26,7 +26,7 @@ export function useRuleSnoozeSettings(id: string): UseRuleSnoozeSettingsResult {
} = useFetchRulesSnoozeSettings([id], {
enabled: !rulesTableSnoozeSettings?.data[id] && !rulesTableSnoozeSettings?.isFetching,
});
const snoozeSettings = rulesTableSnoozeSettings?.data[id] ?? rulesSnoozeSettings?.[0];
const snoozeSettings = rulesTableSnoozeSettings?.data[id] ?? rulesSnoozeSettings?.[id];
const isFetching = rulesTableSnoozeSettings?.isFetching || isSingleSnoozeSettingsFetching;
const isError = rulesTableSnoozeSettings?.isError || isSingleSnoozeSettingsError;

View file

@ -28,6 +28,7 @@ import {
type,
} from '@kbn/securitysolution-io-ts-alerting-types';
import type { NamespaceType } from '@kbn/securitysolution-io-ts-list-types';
import type { RuleSnoozeSettings } from '@kbn/triggers-actions-ui-plugin/public/types';
import { PositiveInteger } from '@kbn/securitysolution-io-ts-types';
import type { WarningSchema } from '../../../../common/detection_engine/schemas/response';
@ -218,15 +219,13 @@ export interface FetchRulesProps {
signal?: AbortSignal;
}
export interface RuleSnoozeSettings {
id: string;
muteAll: boolean;
snoozeSchedule?: RuleSnooze;
activeSnoozes?: string[];
isSnoozedUntil?: Date;
}
// Rule snooze settings map keyed by rule SO's id (not ruleId) and valued by rule snooze settings
export type RulesSnoozeSettingsMap = Record<string, RuleSnoozeSettings>;
interface RuleSnoozeSettingsResponse {
/**
* Rule's SO id
*/
id: string;
mute_all: boolean;
snooze_schedule?: RuleSnooze;

View file

@ -5,11 +5,11 @@
* 2.0.
*/
import { renderHook } from '@testing-library/react-hooks';
import type { PropsWithChildren } from 'react';
import React from 'react';
import type { PropsWithChildren } from 'react';
import { renderHook } from '@testing-library/react-hooks';
import { useUiSetting$ } from '../../../../../common/lib/kibana';
import type { Rule, RuleSnoozeSettings } from '../../../../rule_management/logic/types';
import type { Rule, RulesSnoozeSettingsMap } from '../../../../rule_management/logic';
import { useFindRules } from '../../../../rule_management/logic/use_find_rules';
import { useFetchRulesSnoozeSettings } from '../../../../rule_management/api/hooks/use_fetch_rules_snooze_settings';
import type { RulesTableState } from './rules_table_context';
@ -34,7 +34,7 @@ function renderUseRulesTableContext({
savedState,
}: {
rules?: Rule[] | Error;
rulesSnoozeSettings?: RuleSnoozeSettings[] | Error;
rulesSnoozeSettings?: RulesSnoozeSettingsMap | Error;
savedState?: ReturnType<typeof useRulesTableSavedState>;
}): RulesTableState {
(useFindRules as jest.Mock).mockReturnValue({
@ -189,10 +189,10 @@ describe('RulesTableContextProvider', () => {
{ id: '1', name: 'rule 1' },
{ id: '2', name: 'rule 2' },
] as Rule[],
rulesSnoozeSettings: [
{ id: '1', muteAll: true, snoozeSchedule: [] },
{ id: '2', muteAll: false, snoozeSchedule: [] },
],
rulesSnoozeSettings: {
'1': { muteAll: true, snoozeSchedule: [] },
'2': { muteAll: false, snoozeSchedule: [] },
},
});
expect(state.rules).toEqual([
@ -215,20 +215,18 @@ describe('RulesTableContextProvider', () => {
{ id: '1', name: 'rule 1' },
{ id: '2', name: 'rule 2' },
] as Rule[],
rulesSnoozeSettings: [
{ id: '1', muteAll: true, snoozeSchedule: [] },
{ id: '2', muteAll: false, snoozeSchedule: [] },
],
rulesSnoozeSettings: {
'1': { muteAll: true, snoozeSchedule: [] },
'2': { muteAll: false, snoozeSchedule: [] },
},
});
expect(state.rulesSnoozeSettings.data).toEqual({
'1': {
id: '1',
muteAll: true,
snoozeSchedule: [],
},
'2': {
id: '2',
muteAll: false,
snoozeSchedule: [],
},

View file

@ -25,7 +25,7 @@ import type {
FilterOptions,
PaginationOptions,
Rule,
RuleSnoozeSettings,
RulesSnoozeSettingsMap,
SortingOptions,
} from '../../../../rule_management/logic/types';
import { useFindRules } from '../../../../rule_management/logic/use_find_rules';
@ -39,11 +39,11 @@ import {
import { RuleSource } from './rules_table_saved_state';
import { useRulesTableSavedState } from './use_rules_table_saved_state';
interface RulesSnoozeSettings {
interface RulesSnoozeSettingsState {
/**
* A map object using rule SO's id (not ruleId) as keys and snooze settings as values
*/
data: Record<string, RuleSnoozeSettings>;
data: RulesSnoozeSettingsMap;
/**
* Sets to true during the first data loading
*/
@ -127,7 +127,7 @@ export interface RulesTableState {
/**
* Rules snooze settings for the current rules
*/
rulesSnoozeSettings: RulesSnoozeSettings;
rulesSnoozeSettings: RulesSnoozeSettingsState;
}
export type LoadingRuleAction =
@ -298,7 +298,7 @@ export const RulesTableContextProvider = ({ children }: RulesTableContextProvide
// Fetch rules snooze settings
const {
data: rulesSnoozeSettings,
data: rulesSnoozeSettingsMap,
isLoading: isSnoozeSettingsLoading,
isFetching: isSnoozeSettingsFetching,
isError: isSnoozeSettingsFetchError,
@ -346,19 +346,12 @@ export const RulesTableContextProvider = ({ children }: RulesTableContextProvide
]
);
const providerValue = useMemo(() => {
const rulesSnoozeSettingsMap =
rulesSnoozeSettings?.reduce((map, snoozeSettings) => {
map[snoozeSettings.id] = snoozeSettings;
return map;
}, {} as Record<string, RuleSnoozeSettings>) ?? {};
return {
const providerValue = useMemo(
() => ({
state: {
rules,
rulesSnoozeSettings: {
data: rulesSnoozeSettingsMap,
data: rulesSnoozeSettingsMap ?? {},
isLoading: isSnoozeSettingsLoading,
isFetching: isSnoozeSettingsFetching,
isError: isSnoozeSettingsFetchError,
@ -389,32 +382,33 @@ export const RulesTableContextProvider = ({ children }: RulesTableContextProvide
}),
},
actions,
};
}, [
rules,
rulesSnoozeSettings,
isSnoozeSettingsLoading,
isSnoozeSettingsFetching,
isSnoozeSettingsFetchError,
page,
perPage,
total,
filterOptions,
isPreflightInProgress,
isActionInProgress,
isAllSelected,
isFetched,
isFetching,
isLoading,
isRefetching,
isRefreshOn,
dataUpdatedAt,
loadingRules.ids,
loadingRules.action,
selectedRuleIds,
sortingOptions,
actions,
]);
}),
[
rules,
rulesSnoozeSettingsMap,
isSnoozeSettingsLoading,
isSnoozeSettingsFetching,
isSnoozeSettingsFetchError,
page,
perPage,
total,
filterOptions,
isPreflightInProgress,
isActionInProgress,
isAllSelected,
isFetched,
isFetching,
isLoading,
isRefetching,
isRefreshOn,
dataUpdatedAt,
loadingRules.ids,
loadingRules.action,
selectedRuleIds,
sortingOptions,
actions,
]
);
return <RulesTableContext.Provider value={providerValue}>{children}</RulesTableContext.Provider>;
};

View file

@ -24,7 +24,14 @@ export const LogsList = ({
'xl'
)({
ruleId: '*',
refreshToken: 0,
refreshToken: {
resolve: () => {
/* noop */
},
reject: () => {
/* noop */
},
},
initialPageSize: 50,
hasRuleNames: true,
hasAllSpaceSwitch: true,

View file

@ -17,7 +17,7 @@ import {
} from '../../common/components/with_bulk_rule_api_operations';
import './rule.scss';
import type { RuleEventLogListProps } from './rule_event_log_list';
import { AlertListItem } from './types';
import { AlertListItem, RefreshToken } from './types';
import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features';
import { suspendedComponentWithProps } from '../../../lib/suspended_component_with_props';
import {
@ -41,7 +41,7 @@ type RuleProps = {
readOnly: boolean;
ruleSummary: RuleSummary;
requestRefresh: () => Promise<void>;
refreshToken?: number;
refreshToken?: RefreshToken;
numberOfExecutions: number;
onChangeDuration: (length: number) => void;
durationEpoch?: number;

View file

@ -23,10 +23,11 @@ import {
import { IExecutionLog } from '@kbn/alerting-plugin/common';
import { RuleErrorLogWithApi } from './rule_error_log';
import { RuleActionErrorBadge } from './rule_action_error_badge';
import { RefreshToken } from './types';
export interface RuleActionErrorLogFlyoutProps {
runLog: IExecutionLog;
refreshToken?: number;
refreshToken?: RefreshToken;
onClose: () => void;
activeSpaceId?: string;
}

View file

@ -63,7 +63,7 @@ const mockRuleApis = {
muteRule: jest.fn(),
unmuteRule: jest.fn(),
requestRefresh: jest.fn(),
refreshToken: Date.now(),
refreshToken: { resolve: jest.fn(), reject: jest.fn() },
snoozeRule: jest.fn(),
unsnoozeRule: jest.fn(),
bulkEnableRules: jest.fn(),

View file

@ -68,13 +68,14 @@ import {
MULTIPLE_RULE_TITLE,
} from '../../rules_list/translations';
import { useBulkOperationToast } from '../../../hooks/use_bulk_operation_toast';
import { RefreshToken } from './types';
export type RuleDetailsProps = {
rule: Rule;
ruleType: RuleType;
actionTypes: ActionType[];
requestRefresh: () => Promise<void>;
refreshToken?: number;
refreshToken?: RefreshToken;
} & Pick<
BulkOperationsComponentOpts,
'bulkDisableRules' | 'bulkEnableRules' | 'bulkDeleteRules' | 'snoozeRule' | 'unsnoozeRule'

View file

@ -6,7 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { ToastsApi } from '@kbn/core/public';
import { EuiSpacer } from '@elastic/eui';
@ -49,18 +49,38 @@ export const RuleDetailsRoute: React.FunctionComponent<RuleDetailsRouteProps> =
const [rule, setRule] = useState<ResolvedRule | null>(null);
const [ruleType, setRuleType] = useState<RuleType | null>(null);
const [actionTypes, setActionTypes] = useState<ActionType[] | null>(null);
const [refreshToken, requestRefresh] = React.useState<number>();
const [refreshToken, setRefreshToken] = useState<{
resolve: () => void;
reject: () => void;
}>();
const requestRefresh = useCallback(
() =>
new Promise<void>((resolve, reject) => {
setRefreshToken({
resolve,
reject,
});
}),
[setRefreshToken]
);
useEffect(() => {
getRuleData(
ruleId,
loadRuleTypes,
resolveRule,
loadActionTypes,
setRule,
setRuleType,
setActionTypes,
toasts
);
const loadData = async () => {
await getRuleData(
ruleId,
loadRuleTypes,
resolveRule,
loadActionTypes,
setRule,
setRuleType,
setActionTypes,
toasts
);
refreshToken?.resolve();
};
loadData();
}, [ruleId, http, loadActionTypes, loadRuleTypes, resolveRule, toasts, refreshToken]);
useEffect(() => {
@ -117,7 +137,7 @@ export const RuleDetailsRoute: React.FunctionComponent<RuleDetailsRouteProps> =
rule={rule}
ruleType={ruleType}
actionTypes={actionTypes}
requestRefresh={async () => requestRefresh(Date.now())}
requestRefresh={requestRefresh}
refreshToken={refreshToken}
/>
</>

View file

@ -30,6 +30,7 @@ import {
withBulkRuleOperations,
} from '../../common/components/with_bulk_rule_api_operations';
import { EventLogListCellRenderer } from '../../common/components/event_log';
import { RefreshToken } from './types';
const getParsedDate = (date: string) => {
if (date.includes('now')) {
@ -62,7 +63,7 @@ const MAX_RESULTS = 1000;
export type RuleErrorLogProps = {
ruleId: string;
runId?: string;
refreshToken?: number;
refreshToken?: RefreshToken;
spaceId?: string;
logFromDifferentSpace?: boolean;
requestRefresh?: () => Promise<void>;

View file

@ -12,6 +12,7 @@ import { RuleExecutionSummaryAndChartWithApi } from './rule_execution_summary_an
import { RuleSummary, RuleType } from '../../../../types';
import { ComponentOpts as RuleApis } from '../../common/components/with_bulk_rule_api_operations';
import { RuleEventLogListTableWithApi } from './rule_event_log_list_table';
import { RefreshToken } from './types';
const RULE_EVENT_LOG_LIST_STORAGE_KEY = 'xpack.triggersActionsUI.ruleEventLogList.initialColumns';
@ -23,7 +24,7 @@ export interface RuleEventLogListCommonProps {
ruleId: string;
ruleType: RuleType;
localStorageKey?: string;
refreshToken?: number;
refreshToken?: RefreshToken;
requestRefresh?: () => Promise<void>;
loadExecutionLogAggregations?: RuleApis['loadExecutionLogAggregations'];
fetchRuleSummary?: boolean;

View file

@ -17,6 +17,7 @@ import {
import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features';
import { useKibana } from '../../../../common/lib/kibana';
import { EventLogListStatus, EventLogStat } from '../../common/components/event_log';
import { RefreshToken } from './types';
const getParsedDate = (date: string) => {
if (date.includes('now')) {
@ -59,7 +60,7 @@ export type RuleEventLogListKPIProps = {
dateEnd: string;
outcomeFilter?: string[];
message?: string;
refreshToken?: number;
refreshToken?: RefreshToken;
namespaces?: Array<string | undefined>;
} & Pick<RuleApis, 'loadExecutionKPIAggregations' | 'loadGlobalExecutionKPIAggregations'>;

View file

@ -52,6 +52,7 @@ import {
} from '../../common/components/with_bulk_rule_api_operations';
import { useMultipleSpaces } from '../../../hooks/use_multiple_spaces';
import { RulesSettingsLink } from '../../../components/rules_setting/rules_settings_link';
import { RefreshToken } from './types';
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
@ -102,7 +103,7 @@ export type RuleEventLogListOptions = 'stackManagement' | 'default';
export type RuleEventLogListCommonProps = {
ruleId: string;
localStorageKey?: string;
refreshToken?: number;
refreshToken?: RefreshToken;
initialPageSize?: number;
// Duplicating these properties is extremely silly but it's the only way to get Jest to cooperate with the way this component is structured
overrideLoadExecutionLogAggregations?: RuleApis['loadExecutionLogAggregations'];
@ -142,7 +143,7 @@ export const RuleEventLogListTable = <T extends RuleEventLogListOptions>(
const [search, setSearch] = useState<string>('');
const [isFlyoutOpen, setIsFlyoutOpen] = useState<boolean>(false);
const [selectedRunLog, setSelectedRunLog] = useState<IExecutionLog | undefined>();
const [internalRefreshToken, setInternalRefreshToken] = useState<number | undefined>(
const [internalRefreshToken, setInternalRefreshToken] = useState<RefreshToken | undefined>(
refreshToken
);
const [showFromAllSpaces, setShowFromAllSpaces] = useState(false);
@ -298,7 +299,14 @@ export const RuleEventLogListTable = <T extends RuleEventLogListOptions>(
);
const onRefresh = () => {
setInternalRefreshToken(Date.now());
setInternalRefreshToken({
resolve: () => {
/* noop */
},
reject: () => {
/* noop */
},
});
loadEventLogs();
};

View file

@ -20,6 +20,7 @@ import {
ComponentOpts as RuleApis,
withBulkRuleOperations,
} from '../../common/components/with_bulk_rule_api_operations';
import { RefreshToken } from './types';
export const DEFAULT_NUMBER_OF_EXECUTIONS = 60;
@ -29,7 +30,7 @@ type RuleExecutionSummaryAndChartProps = {
ruleSummary?: RuleSummary;
numberOfExecutions?: number;
isLoadingRuleSummary?: boolean;
refreshToken?: number;
refreshToken?: RefreshToken;
onChangeDuration?: (duration: number) => void;
requestRefresh?: () => Promise<void>;
fetchRuleSummary?: boolean;

View file

@ -16,13 +16,14 @@ import {
import { RuleWithApi as Rules } from './rule';
import { useKibana } from '../../../../common/lib/kibana';
import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner';
import { RefreshToken } from './types';
type WithRuleSummaryProps = {
rule: Rule;
ruleType: RuleType;
readOnly: boolean;
requestRefresh: () => Promise<void>;
refreshToken?: number;
refreshToken?: RefreshToken;
} & Pick<RuleApis, 'loadRuleSummary'>;
export const RuleRoute: React.FunctionComponent<WithRuleSummaryProps> = ({

View file

@ -58,12 +58,8 @@ export const RuleStatusPanel: React.FC<ComponentOpts> = ({
statusMessage,
loadExecutionLogAggregations,
}) => {
const [isSnoozeLoading, setIsSnoozeLoading] = useState(false);
const [isSnoozeOpen, setIsSnoozeOpen] = useState(false);
const [lastNumberOfExecutions, setLastNumberOfExecutions] = useState<number | null>(null);
const openSnooze = useCallback(() => setIsSnoozeOpen(true), [setIsSnoozeOpen]);
const closeSnooze = useCallback(() => setIsSnoozeOpen(false), [setIsSnoozeOpen]);
const onSnoozeRule = useCallback(
(snoozeSchedule) => snoozeRule(rule, snoozeSchedule),
[rule, snoozeRule]
@ -187,12 +183,9 @@ export const RuleStatusPanel: React.FC<ComponentOpts> = ({
<EuiHorizontalRule margin="none" />
<EuiPanel hasShadow={false}>
<RulesListNotifyBadge
rule={{ ...rule, isEditable }}
isOpen={isSnoozeOpen}
isLoading={isSnoozeLoading}
onLoading={setIsSnoozeLoading}
onClick={openSnooze}
onClose={closeSnooze}
snoozeSettings={rule}
loading={!rule}
disabled={!isEditable}
onRuleChanged={requestRefresh}
snoozeRule={onSnoozeRule}
unsnoozeRule={onUnsnoozeRule}

View file

@ -16,3 +16,8 @@ export interface AlertListItem {
flapping: boolean;
maintenanceWindowIds?: string[];
}
export interface RefreshToken {
resolve: () => void;
reject: () => void;
}

View file

@ -83,7 +83,7 @@ export const CollapsedItemActions: React.FunctionComponent<ComponentOpts> = ({
try {
onLoading(true);
await snoozeRule(item, snoozeSchedule);
onRuleChanged();
await onRuleChanged();
toasts.addSuccess(SNOOZE_SUCCESS_MESSAGE);
} catch (e) {
toasts.addDanger(SNOOZE_FAILED_MESSAGE);
@ -101,7 +101,7 @@ export const CollapsedItemActions: React.FunctionComponent<ComponentOpts> = ({
try {
onLoading(true);
await unsnoozeRule(item, scheduleIds);
onRuleChanged();
await onRuleChanged();
toasts.addSuccess(UNSNOOZE_SUCCESS_MESSAGE);
} catch (e) {
toasts.addDanger(SNOOZE_FAILED_MESSAGE);

View file

@ -9,73 +9,27 @@ import { EuiButtonIcon, EuiButton } from '@elastic/eui';
import React from 'react';
import { act } from 'react-dom/test-utils';
import moment from 'moment';
import { RuleTableItem } from '../../../../../types';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { RulesListNotifyBadge } from './notify_badge';
jest.mock('../../../../../common/lib/kibana');
const onClick = jest.fn();
const onClose = jest.fn();
const onLoading = jest.fn();
const onRuleChanged = jest.fn();
const snoozeRule = jest.fn();
const unsnoozeRule = jest.fn();
const getRule = (overrides = {}): RuleTableItem => ({
id: '1',
enabled: true,
name: 'test rule',
tags: ['tag1'],
ruleTypeId: 'test_rule_type',
consumer: 'rules',
schedule: { interval: '5d' },
actions: [
{ id: 'test', actionTypeId: 'the_connector', group: 'rule', params: { message: 'test' } },
],
params: { name: 'test rule type name' },
createdBy: null,
updatedBy: null,
createdAt: new Date(),
updatedAt: new Date(),
apiKeyOwner: null,
throttle: '1m',
notifyWhen: 'onActiveAlert',
muteAll: false,
mutedInstanceIds: [],
executionStatus: {
status: 'active',
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
},
actionsCount: 1,
index: 0,
ruleType: 'Test Rule Type',
isEditable: true,
enabledInLicense: true,
revision: 0,
...overrides,
});
describe('RulesListNotifyBadge', () => {
const onRuleChanged = jest.fn();
const snoozeRule = jest.fn();
const unsnoozeRule = jest.fn();
afterEach(() => {
jest.clearAllMocks();
});
it('renders the notify badge correctly', async () => {
jest.useFakeTimers().setSystemTime(moment('1990-01-01').toDate());
it('renders an unsnoozed badge', () => {
const wrapper = mountWithIntl(
<RulesListNotifyBadge
rule={getRule({
snoozeSettings={{
isSnoozedUntil: null,
muteAll: false,
})}
isLoading={false}
isOpen={false}
onLoading={onLoading}
onClick={onClick}
onClose={onClose}
}}
onRuleChanged={onRuleChanged}
snoozeRule={snoozeRule}
unsnoozeRule={unsnoozeRule}
@ -85,26 +39,46 @@ describe('RulesListNotifyBadge', () => {
// Rule without snooze
const badge = wrapper.find(EuiButtonIcon);
expect(badge.first().props().iconType).toEqual('bell');
});
it('renders a snoozed badge', () => {
jest.useFakeTimers().setSystemTime(moment('1990-01-01').toDate());
const wrapper = mountWithIntl(
<RulesListNotifyBadge
snoozeSettings={{
muteAll: false,
isSnoozedUntil: moment('1990-02-01').toDate(),
}}
onRuleChanged={onRuleChanged}
snoozeRule={snoozeRule}
unsnoozeRule={unsnoozeRule}
/>
);
// Rule with snooze
wrapper.setProps({
rule: getRule({
isSnoozedUntil: moment('1990-02-01').format(),
}),
});
const snoozeBadge = wrapper.find(EuiButton);
expect(snoozeBadge.first().props().iconType).toEqual('bellSlash');
expect(snoozeBadge.text()).toEqual('Feb 1');
});
// Rule with indefinite snooze
wrapper.setProps({
rule: getRule({
isSnoozedUntil: moment('1990-02-01').format(),
muteAll: true,
}),
});
it('renders an indefinitely snoozed badge', () => {
jest.useFakeTimers().setSystemTime(moment('1990-01-01').toDate());
const wrapper = mountWithIntl(
<RulesListNotifyBadge
snoozeSettings={{
muteAll: true,
isSnoozedUntil: moment('1990-02-01').toDate(),
}}
onRuleChanged={onRuleChanged}
snoozeRule={snoozeRule}
unsnoozeRule={unsnoozeRule}
/>
);
const indefiniteSnoozeBadge = wrapper.find(EuiButtonIcon);
expect(indefiniteSnoozeBadge.first().props().iconType).toEqual('bellSlash');
expect(indefiniteSnoozeBadge.text()).toEqual('');
});
@ -113,24 +87,21 @@ describe('RulesListNotifyBadge', () => {
jest.useFakeTimers().setSystemTime(moment('1990-01-01').toDate());
const wrapper = mountWithIntl(
<RulesListNotifyBadge
rule={getRule({
isSnoozedUntil: null,
snoozeSettings={{
muteAll: false,
})}
isLoading={false}
isOpen={true}
onLoading={onLoading}
onClick={onClick}
onClose={onClose}
isSnoozedUntil: null,
}}
onRuleChanged={onRuleChanged}
snoozeRule={snoozeRule}
unsnoozeRule={unsnoozeRule}
/>
);
// Open the popover
wrapper.find(EuiButtonIcon).first().simulate('click');
// Snooze for 1 hour
wrapper.find('button[data-test-subj="linkSnooze1h"]').first().simulate('click');
expect(onLoading).toHaveBeenCalledWith(true);
expect(snoozeRule).toHaveBeenCalledWith({
duration: 3600000,
id: null,
@ -146,38 +117,31 @@ describe('RulesListNotifyBadge', () => {
});
expect(onRuleChanged).toHaveBeenCalled();
expect(onLoading).toHaveBeenCalledWith(false);
expect(onClose).toHaveBeenCalled();
});
it('should allow the user to unsnooze rules', async () => {
jest.useFakeTimers().setSystemTime(moment('1990-01-01').toDate());
const wrapper = mountWithIntl(
<RulesListNotifyBadge
rule={getRule({
snoozeSettings={{
muteAll: true,
})}
isLoading={false}
isOpen={true}
onLoading={onLoading}
onClick={onClick}
onClose={onClose}
}}
onRuleChanged={onRuleChanged}
snoozeRule={snoozeRule}
unsnoozeRule={unsnoozeRule}
/>
);
// Open the popover
wrapper.find(EuiButtonIcon).first().simulate('click');
// Unsnooze
wrapper.find('[data-test-subj="ruleSnoozeCancel"] button').simulate('click');
expect(onLoading).toHaveBeenCalledWith(true);
await act(async () => {
jest.runOnlyPendingTimers();
});
expect(unsnoozeRule).toHaveBeenCalled();
expect(onLoading).toHaveBeenCalledWith(false);
expect(onClose).toHaveBeenCalled();
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import moment from 'moment';
import {
EuiButton,
@ -30,35 +30,38 @@ import {
} from './translations';
import { RulesListNotifyBadgeProps } from './types';
export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeProps> = (props) => {
const {
isLoading = false,
rule,
isOpen,
onClick,
onClose,
onLoading,
onRuleChanged,
snoozeRule,
unsnoozeRule,
showOnHover = false,
showTooltipInline = false,
} = props;
const { isSnoozedUntil, muteAll, isEditable } = rule;
export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeProps> = ({
snoozeSettings,
loading = false,
disabled = false,
onRuleChanged,
snoozeRule,
unsnoozeRule,
showOnHover = false,
showTooltipInline = false,
}) => {
const [requestInFlight, setRequestInFlightLoading] = useState(false);
const isLoading = loading || requestInFlight;
const isDisabled = Boolean(disabled) || !snoozeSettings;
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const openPopover = useCallback(() => setIsPopoverOpen(true), [setIsPopoverOpen]);
const closePopover = useCallback(() => setIsPopoverOpen(false), [setIsPopoverOpen]);
const isSnoozedUntil = snoozeSettings?.isSnoozedUntil;
const muteAll = snoozeSettings?.muteAll ?? false;
const isSnoozedIndefinitely = muteAll;
const isSnoozed = useMemo(
() => (snoozeSettings ? isRuleSnoozed(snoozeSettings) : false),
[snoozeSettings]
);
const nextScheduledSnooze = useMemo(
() => (snoozeSettings ? getNextRuleSnoozeSchedule(snoozeSettings) : null),
[snoozeSettings]
);
const {
notifications: { toasts },
} = useKibana().services;
const isSnoozed = useMemo(() => {
return isRuleSnoozed(rule);
}, [rule]);
const nextScheduledSnooze = useMemo(() => getNextRuleSnoozeSchedule(rule), [rule]);
const isScheduled = useMemo(() => {
return !isSnoozed && Boolean(nextScheduledSnooze);
}, [nextScheduledSnooze, isSnoozed]);
@ -124,18 +127,18 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
<EuiButton
size="s"
isLoading={isLoading}
disabled={isLoading || !isEditable}
disabled={isLoading || isDisabled}
data-test-subj="rulesListNotifyBadge-snoozed"
aria-label={OPEN_SNOOZE_PANEL_ARIA_LABEL}
minWidth={85}
iconType="bellSlash"
color="accent"
onClick={onClick}
onClick={openPopover}
>
<EuiText size="xs">{formattedSnoozeText}</EuiText>
</EuiButton>
);
}, [formattedSnoozeText, isLoading, isEditable, onClick]);
}, [formattedSnoozeText, isLoading, isDisabled, openPopover]);
const scheduledSnoozeButton = useMemo(() => {
// TODO: Implement scheduled snooze button
@ -143,18 +146,18 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
<EuiButton
size="s"
isLoading={isLoading}
disabled={isLoading || !isEditable}
disabled={isLoading || isDisabled}
data-test-subj="rulesListNotifyBadge-scheduled"
minWidth={85}
iconType="calendar"
color="text"
aria-label={OPEN_SNOOZE_PANEL_ARIA_LABEL}
onClick={onClick}
onClick={openPopover}
>
<EuiText size="xs">{formattedSnoozeText}</EuiText>
</EuiButton>
);
}, [formattedSnoozeText, isLoading, isEditable, onClick]);
}, [formattedSnoozeText, isLoading, isDisabled, openPopover]);
const unsnoozedButton = useMemo(() => {
// This show on hover is needed because we need style sheets to achieve the
@ -165,32 +168,32 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
<EuiButtonIcon
size="s"
isLoading={isLoading}
disabled={isLoading || !isEditable}
disabled={isLoading || isDisabled}
display={isLoading ? 'base' : 'empty'}
data-test-subj="rulesListNotifyBadge-unsnoozed"
aria-label={OPEN_SNOOZE_PANEL_ARIA_LABEL}
className={isOpen || isLoading ? '' : showOnHoverClass}
className={isPopoverOpen || isLoading ? '' : showOnHoverClass}
iconType="bell"
onClick={onClick}
onClick={openPopover}
/>
);
}, [isOpen, isLoading, isEditable, showOnHover, onClick]);
}, [isPopoverOpen, isLoading, isDisabled, showOnHover, openPopover]);
const indefiniteSnoozeButton = useMemo(() => {
return (
<EuiButtonIcon
size="s"
isLoading={isLoading}
disabled={isLoading || !isEditable}
disabled={isLoading || isDisabled}
display="base"
data-test-subj="rulesListNotifyBadge-snoozedIndefinitely"
aria-label={OPEN_SNOOZE_PANEL_ARIA_LABEL}
iconType="bellSlash"
color="accent"
onClick={onClick}
onClick={openPopover}
/>
);
}, [isLoading, isEditable, onClick]);
}, [isLoading, isDisabled, openPopover]);
const button = useMemo(() => {
if (isScheduled) {
@ -214,57 +217,55 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
]);
const buttonWithToolTip = useMemo(() => {
if (isOpen || showTooltipInline) {
return button;
}
return <EuiToolTip content={snoozeTooltipText}>{button}</EuiToolTip>;
}, [isOpen, button, snoozeTooltipText, showTooltipInline]);
const tooltipContent =
typeof disabled === 'string'
? disabled
: isPopoverOpen || showTooltipInline
? undefined
: snoozeTooltipText;
const onClosePopover = useCallback(() => {
onClose();
// Set a timeout on closing the scheduler to avoid flicker
// setTimeout(onCloseScheduler, 1000);
}, [onClose]);
return <EuiToolTip content={tooltipContent}>{button}</EuiToolTip>;
}, [disabled, isPopoverOpen, button, snoozeTooltipText, showTooltipInline]);
const onApplySnooze = useCallback(
async (schedule: SnoozeSchedule) => {
try {
onLoading(true);
onClosePopover();
setRequestInFlightLoading(true);
closePopover();
await snoozeRule(schedule);
onRuleChanged();
await onRuleChanged();
toasts.addSuccess(SNOOZE_SUCCESS_MESSAGE);
} catch (e) {
toasts.addDanger(SNOOZE_FAILED_MESSAGE);
} finally {
onLoading(false);
setRequestInFlightLoading(false);
}
},
[onLoading, snoozeRule, onRuleChanged, toasts, onClosePopover]
[setRequestInFlightLoading, snoozeRule, onRuleChanged, toasts, closePopover]
);
const onApplyUnsnooze = useCallback(
async (scheduleIds?: string[]) => {
try {
onLoading(true);
onClosePopover();
setRequestInFlightLoading(true);
closePopover();
await unsnoozeRule(scheduleIds);
onRuleChanged();
await onRuleChanged();
toasts.addSuccess(UNSNOOZE_SUCCESS_MESSAGE);
} catch (e) {
toasts.addDanger(SNOOZE_FAILED_MESSAGE);
} finally {
onLoading(false);
setRequestInFlightLoading(false);
}
},
[onLoading, unsnoozeRule, onRuleChanged, toasts, onClosePopover]
[setRequestInFlightLoading, unsnoozeRule, onRuleChanged, toasts, closePopover]
);
const popover = (
<EuiPopover
data-test-subj="rulesListNotifyBadge"
isOpen={isOpen}
closePopover={onClosePopover}
isOpen={isPopoverOpen && !isDisabled}
closePopover={closePopover}
button={buttonWithToolTip}
anchorPosition="rightCenter"
panelStyle={{ maxHeight: '100vh', overflowY: 'auto' }}
@ -274,8 +275,8 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
unsnoozeRule={onApplyUnsnooze}
interval={futureTimeToInterval(isSnoozedUntil)}
showCancel={isSnoozed}
scheduledSnoozes={rule.snoozeSchedule ?? []}
activeSnoozes={rule.activeSnoozes ?? []}
scheduledSnoozes={snoozeSettings?.snoozeSchedule ?? []}
activeSnoozes={snoozeSettings?.activeSnoozes ?? []}
inPopover
/>
</EuiPopover>

View file

@ -27,7 +27,7 @@ export default {
title: 'app/RulesListNotifyBadgeWithApi',
component: RulesListNotifyBadgeWithApi,
argTypes: {
rule: {
snoozeSettings: {
defaultValue: rule,
control: {
type: 'object',
@ -54,7 +54,7 @@ export default {
onRuleChanged: {},
},
args: {
rule,
snoozeSettings: rule,
onRuleChanged: (...args: any) => action('onRuleChanged')(args),
},
} as Meta<RulesListNotifyBadgePropsWithApi>;
@ -69,7 +69,7 @@ const IndefinitelyDate = new Date();
IndefinitelyDate.setDate(IndefinitelyDate.getDate() + 1);
export const IndefinitelyRuleNotifyBadgeWithApi = Template.bind({});
IndefinitelyRuleNotifyBadgeWithApi.args = {
rule: {
snoozeSettings: {
...rule,
muteAll: true,
isSnoozedUntil: IndefinitelyDate,
@ -80,7 +80,7 @@ export const ActiveSnoozesRuleNotifyBadgeWithApi = Template.bind({});
const ActiveSnoozeDate = new Date();
ActiveSnoozeDate.setDate(ActiveSnoozeDate.getDate() + 2);
ActiveSnoozesRuleNotifyBadgeWithApi.args = {
rule: {
snoozeSettings: {
...rule,
activeSnoozes: ['24da3b26-bfa5-4317-b72f-4063dbea618e'],
isSnoozedUntil: ActiveSnoozeDate,
@ -111,7 +111,7 @@ export const ScheduleSnoozesRuleNotifyBadgeWithApi: Story<RulesListNotifyBadgePr
};
ScheduleSnoozesRuleNotifyBadgeWithApi.args = {
rule: {
snoozeSettings: {
...rule,
snoozeSchedule: [
{

View file

@ -5,10 +5,9 @@
* 2.0.
*/
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback } from 'react';
import { useKibana } from '../../../../../common/lib/kibana';
import { SnoozeSchedule } from '../../../../../types';
import { loadRule } from '../../../../lib/rule_api/get_rule';
import { unsnoozeRule as unsnoozeRuleApi } from '../../../../lib/rule_api/unsnooze';
import { snoozeRule as snoozeRuleApi } from '../../../../lib/rule_api/snooze';
import { RulesListNotifyBadge } from './notify_badge';
@ -16,74 +15,35 @@ import { RulesListNotifyBadgePropsWithApi } from './types';
export const RulesListNotifyBadgeWithApi: React.FunctionComponent<
RulesListNotifyBadgePropsWithApi
> = (props) => {
const { onRuleChanged, rule, isLoading, showTooltipInline, showOnHover } = props;
> = ({
ruleId,
snoozeSettings,
loading,
disabled,
showTooltipInline,
showOnHover,
onRuleChanged,
}) => {
const { http } = useKibana().services;
const [currentlyOpenNotify, setCurrentlyOpenNotify] = useState<string>();
const [loadingSnoozeAction, setLoadingSnoozeAction] = useState<boolean>(false);
const [ruleSnoozeInfo, setRuleSnoozeInfo] =
useState<RulesListNotifyBadgePropsWithApi['rule']>(rule);
// This helps to fix problems related to rule prop updates. As component handles the loading state via isLoading prop
// rule prop is obviously not ready atm so when it's ready ruleSnoozeInfo won't be updated without useEffect so
// incorrect state will be shown.
useEffect(() => {
setRuleSnoozeInfo(rule);
}, [rule]);
const onSnoozeRule = useCallback(
(snoozeSchedule: SnoozeSchedule) => {
return snoozeRuleApi({ http, id: ruleSnoozeInfo.id, snoozeSchedule });
},
[http, ruleSnoozeInfo.id]
(snoozeSchedule: SnoozeSchedule) =>
ruleId ? snoozeRuleApi({ http, id: ruleId, snoozeSchedule }) : Promise.resolve(),
[http, ruleId]
);
const onUnsnoozeRule = useCallback(
(scheduleIds?: string[]) => {
return unsnoozeRuleApi({ http, id: ruleSnoozeInfo.id, scheduleIds });
},
[http, ruleSnoozeInfo.id]
(scheduleIds?: string[]) =>
ruleId ? unsnoozeRuleApi({ http, id: ruleId, scheduleIds }) : Promise.resolve(),
[http, ruleId]
);
const onRuleChangedCallback = useCallback(async () => {
const updatedRule = await loadRule({
http,
ruleId: ruleSnoozeInfo.id,
});
setLoadingSnoozeAction(false);
setRuleSnoozeInfo((prevRule) => ({
...prevRule,
activeSnoozes: updatedRule.activeSnoozes,
isSnoozedUntil: updatedRule.isSnoozedUntil,
muteAll: updatedRule.muteAll,
snoozeSchedule: updatedRule.snoozeSchedule,
}));
onRuleChanged();
}, [http, ruleSnoozeInfo.id, onRuleChanged]);
const openSnooze = useCallback(() => {
setCurrentlyOpenNotify(props.rule.id);
}, [props.rule.id]);
const closeSnooze = useCallback(() => {
setCurrentlyOpenNotify('');
}, []);
const onLoading = useCallback((value: boolean) => {
if (value) {
setLoadingSnoozeAction(value);
}
}, []);
return (
<RulesListNotifyBadge
rule={ruleSnoozeInfo}
isOpen={currentlyOpenNotify === ruleSnoozeInfo.id}
isLoading={isLoading || loadingSnoozeAction}
onClick={openSnooze}
onClose={closeSnooze}
onLoading={onLoading}
onRuleChanged={onRuleChangedCallback}
snoozeSettings={snoozeSettings}
loading={loading}
disabled={disabled}
onRuleChanged={onRuleChanged}
snoozeRule={onSnoozeRule}
unsnoozeRule={onUnsnoozeRule}
showTooltipInline={showTooltipInline}

View file

@ -5,20 +5,23 @@
* 2.0.
*/
import { RuleTableItem, SnoozeSchedule } from '../../../../../types';
import { RuleSnoozeSettings, SnoozeSchedule } from '../../../../../types';
export interface RulesListNotifyBadgeProps {
rule: Pick<
RuleTableItem,
'id' | 'activeSnoozes' | 'isSnoozedUntil' | 'muteAll' | 'isEditable' | 'snoozeSchedule'
>;
isOpen: boolean;
isLoading: boolean;
previousSnoozeInterval?: string | null;
onClick: React.MouseEventHandler<HTMLButtonElement>;
onClose: () => void;
onLoading: (isLoading: boolean) => void;
onRuleChanged: () => void;
/**
* Rule's snooze settings
*/
snoozeSettings: RuleSnoozeSettings | undefined;
/**
* Displays the component in the loading state. If isLoading = false and snoozeSettings aren't set
* and the component is shown in disabled state.
*/
loading?: boolean;
/**
* Whether the component is disabled or not, string give a disabled reason displayed as a tooltip
*/
disabled?: boolean | string;
onRuleChanged: () => void | Promise<void>;
snoozeRule: (schedule: SnoozeSchedule, muteAll?: boolean) => Promise<void>;
unsnoozeRule: (scheduleIds?: string[]) => Promise<void>;
showTooltipInline?: boolean;
@ -27,5 +30,10 @@ export interface RulesListNotifyBadgeProps {
export type RulesListNotifyBadgePropsWithApi = Pick<
RulesListNotifyBadgeProps,
'rule' | 'isLoading' | 'onRuleChanged' | 'showOnHover' | 'showTooltipInline'
>;
'snoozeSettings' | 'loading' | 'disabled' | 'onRuleChanged' | 'showOnHover' | 'showTooltipInline'
> & {
/**
* Rule's SO id
*/
ruleId: string;
};

View file

@ -46,10 +46,12 @@ export const SnoozePanel: React.FC<SnoozePanelProps> = ({
try {
await snoozeRule(schedule);
} finally {
setIsLoading(false);
if (!inPopover) {
setIsLoading(false);
}
}
},
[setIsLoading, snoozeRule]
[inPopover, setIsLoading, snoozeRule]
);
const onUnsnoozeRule = useCallback(
@ -58,10 +60,12 @@ export const SnoozePanel: React.FC<SnoozePanelProps> = ({
try {
await unsnoozeRule(scheduleIds);
} finally {
setIsLoading(false);
if (!inPopover) {
setIsLoading(false);
}
}
},
[setIsLoading, unsnoozeRule]
[inPopover, setIsLoading, unsnoozeRule]
);
const saveSnoozeSchedule = useCallback(
@ -70,10 +74,12 @@ export const SnoozePanel: React.FC<SnoozePanelProps> = ({
try {
await snoozeRule(schedule);
} finally {
setIsLoading(false);
if (!inPopover) {
setIsLoading(false);
}
}
},
[snoozeRule, setIsLoading]
[inPopover, snoozeRule, setIsLoading]
);
const cancelSnoozeSchedules = useCallback(
@ -82,10 +88,12 @@ export const SnoozePanel: React.FC<SnoozePanelProps> = ({
try {
await unsnoozeRule(scheduleIds);
} finally {
setIsLoading(false);
if (!inPopover) {
setIsLoading(false);
}
}
},
[unsnoozeRule, setIsLoading]
[inPopover, unsnoozeRule, setIsLoading]
);
const onOpenScheduler = useCallback(

View file

@ -847,7 +847,7 @@ export const RulesList = ({
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
onSort={setSort}
onPage={setPage}
onRuleChanged={() => refreshRules()}
onRuleChanged={refreshRules}
onRuleClick={(rule) => {
const detailsRoute = ruleDetailsRoute ? ruleDetailsRoute : commonRuleDetailsRoute;
history.push(detailsRoute.replace(`:ruleId`, rule.id));
@ -883,7 +883,7 @@ export const RulesList = ({
key={rule.id}
item={rule}
onLoading={onLoading}
onRuleChanged={() => refreshRules()}
onRuleChanged={refreshRules}
onDeleteRule={() =>
updateRulesToBulkEdit({
action: 'delete',

View file

@ -208,7 +208,6 @@ export const RulesListTable = (props: RulesListTableProps) => {
} = props;
const [tagPopoverOpenIndex, setTagPopoverOpenIndex] = useState<number>(-1);
const [currentlyOpenNotify, setCurrentlyOpenNotify] = useState<string>();
const [isLoadingMap, setIsLoadingMap] = useState<Record<string, boolean>>({});
const isRuleUsingExecutionStatus = getIsExperimentalFeatureEnabled('ruleUseExecutionStatus');
@ -485,12 +484,9 @@ export const RulesListTable = (props: RulesListTableProps) => {
return (
<RulesListNotifyBadge
showOnHover
rule={rule}
isLoading={!!isLoadingMap[rule.id]}
onLoading={(newIsLoading) => onLoading(rule.id, newIsLoading)}
isOpen={currentlyOpenNotify === rule.id}
onClick={() => setCurrentlyOpenNotify(rule.id)}
onClose={() => setCurrentlyOpenNotify('')}
snoozeSettings={rule}
loading={!!isLoadingMap[rule.id]}
disabled={!rule.isEditable}
onRuleChanged={onRuleChanged}
snoozeRule={async (snoozeSchedule) => {
await onSnoozeRule(rule, snoozeSchedule);
@ -769,7 +765,6 @@ export const RulesListTable = (props: RulesListTableProps) => {
];
}, [
config.minimumScheduleInterval,
currentlyOpenNotify,
isLoadingMap,
isRuleTypeEditableInContext,
onRuleChanged,

View file

@ -343,6 +343,11 @@ export type SanitizedRuleType = Omit<RuleType, 'apiKey'>;
export type RuleUpdates = Omit<Rule, 'id' | 'executionStatus' | 'lastRun' | 'nextRun'>;
export type RuleSnoozeSettings = Pick<
Rule,
'activeSnoozes' | 'isSnoozedUntil' | 'muteAll' | 'snoozeSchedule'
>;
export interface RuleTableItem extends Rule {
ruleType: RuleType['name'];
index: number;
@ -350,7 +355,6 @@ export interface RuleTableItem extends Rule {
isEditable: boolean;
enabledInLicense: boolean;
showIntervalWarning?: boolean;
activeSnoozes?: string[];
}
export interface RuleTypeParamsExpressionProps<