Skip to content

Commit

Permalink
[Security Solution] ThreeWayDiff UI: Add remaining field components f…
Browse files Browse the repository at this point in the history
…or `FieldReadOnly` (elastic#193261)

**Partially addresses: elastic#171520
**Is a follow-up PR to: elastic#192342

This is the 3rd of the 3 PRs for `FieldReadOnly`.
- The 1st [PR](elastic#191499) added the
`FieldReadOnly` and a bunch of field components.
- The 2nd [PR](elastic#192342) moved away
from using `DiffableAllFields` type in favour of `DiffableRule` and
split the large `FieldReadOnly` component into smaller ones for
readability.
 - This (3rd) PR adds the remaining field components.

## Summary

This PR adds field components for `FieldReadOnly`. Field components
display a read-only view of a particular `DiffableRule` field, similar
to how fields are shown on the Rule Details page.

`FieldReadOnly` and field components will be displayed in the right side
of the new Diff tab of the Upgrade flyout (see it on the [Miro
board](https://miro.com/app/board/uXjVK0gqjjQ=/?moveToWidget=3458764594148126123&cot=14)).
They will let the user see how an upgraded version of a rule will look
like in a user-friendly way.

### Running
`FinalReadOnly` and its field components are not yet integrated into the
flyout, but you can view components in Storybook.
1. Run Storybook: `yarn storybook security_solution`
2. Go to `http://localhost:9001` in browser.

<img width="1062" alt="Scherm­afbeelding 2024-09-03 om 13 05 11"
src="https://github.com/user-attachments/assets/13b227d4-1321-47d9-a0a7-93868c9f4a15">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Maxim Palenov <maxim.palenov@elastic.co>
(cherry picked from commit b78b633)
  • Loading branch information
nikitaindik committed Sep 24, 2024
1 parent 6b9fbf8 commit 1d17d2f
Show file tree
Hide file tree
Showing 71 changed files with 2,079 additions and 88 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { COLORS } from './constants';

/*
Finds an element with a text content that exactly matches the passed argument.
Handly because React Testing Library's doesn't provide an easy way to search by
Handy because React Testing Library's doesn't provide an easy way to search by
text if the text is split into multiple DOM elements.
*/
function findChildByTextContent(parent: Element, textContent: string): HTMLElement {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,11 @@ interface AuthorProps {
author: string[];
}

const Author = ({ author }: AuthorProps) => (
export const Author = ({ author }: AuthorProps) => (
<BadgeList badges={author} data-test-subj="authorPropertyValue" />
);

const BuildingBlock = () => (
export const BuildingBlock = () => (
<EuiText size="s" data-test-subj="buildingBlockPropertyValue">
{i18n.BUILDING_BLOCK_FIELD_DESCRIPTION}
</EuiText>
Expand Down Expand Up @@ -124,7 +124,7 @@ interface RiskScoreProps {
riskScore: number;
}

const RiskScore = ({ riskScore }: RiskScoreProps) => (
export const RiskScore = ({ riskScore }: RiskScoreProps) => (
<EuiText size="s" data-test-subj="riskScorePropertyValue">
{riskScore}
</EuiText>
Expand Down Expand Up @@ -157,7 +157,7 @@ interface ReferencesProps {
references: string[];
}

const References = ({ references }: ReferencesProps) => (
export const References = ({ references }: ReferencesProps) => (
<EuiText size="s">
<ul>
{references
Expand All @@ -173,7 +173,7 @@ const References = ({ references }: ReferencesProps) => (
</EuiText>
);

const FalsePositives = ({ falsePositives }: { falsePositives: string[] }) => (
export const FalsePositives = ({ falsePositives }: { falsePositives: string[] }) => (
<EuiText size="s">
<ul>
{falsePositives.map((falsePositivesItem) => (
Expand All @@ -192,15 +192,15 @@ interface InvestigationFieldsProps {
investigationFields: string[];
}

const InvestigationFields = ({ investigationFields }: InvestigationFieldsProps) => (
export const InvestigationFields = ({ investigationFields }: InvestigationFieldsProps) => (
<BadgeList badges={investigationFields} data-test-subj="investigationFieldsPropertyValue" />
);

interface LicenseProps {
license: string;
}

const License = ({ license }: LicenseProps) => (
export const License = ({ license }: LicenseProps) => (
<EuiText size="s" data-test-subj="licensePropertyValue">
{license}
</EuiText>
Expand All @@ -210,7 +210,7 @@ interface RuleNameOverrideProps {
ruleNameOverride: string;
}

const RuleNameOverride = ({ ruleNameOverride }: RuleNameOverrideProps) => (
export const RuleNameOverride = ({ ruleNameOverride }: RuleNameOverrideProps) => (
<EuiText size="s" data-test-subj="ruleNameOverridePropertyValue">
{ruleNameOverride}
</EuiText>
Expand All @@ -236,7 +236,7 @@ interface TimestampOverrideProps {
timestampOverride: string;
}

const TimestampOverride = ({ timestampOverride }: TimestampOverrideProps) => (
export const TimestampOverride = ({ timestampOverride }: TimestampOverrideProps) => (
<EuiText size="s" data-test-subj="timestampOverridePropertyValue">
{timestampOverride}
</EuiText>
Expand All @@ -246,7 +246,7 @@ interface MaxSignalsProps {
maxSignals: number;
}

const MaxSignals = ({ maxSignals }: MaxSignalsProps) => (
export const MaxSignals = ({ maxSignals }: MaxSignalsProps) => (
<EuiText size="s" data-test-subj="maxSignalsPropertyValue">
{maxSignals}
</EuiText>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ interface ThresholdProps {
threshold: ThresholdType;
}

const Threshold = ({ threshold }: ThresholdProps) => (
export const Threshold = ({ threshold }: ThresholdProps) => (
<div data-test-subj="thresholdPropertyValue">
{isEmpty(threshold.field[0])
? `${descriptionStepI18n.THRESHOLD_RESULTS_ALL} >= ${threshold.value}`
Expand All @@ -193,7 +193,7 @@ interface AnomalyThresholdProps {
anomalyThreshold: number;
}

const AnomalyThreshold = ({ anomalyThreshold }: AnomalyThresholdProps) => (
export const AnomalyThreshold = ({ anomalyThreshold }: AnomalyThresholdProps) => (
<EuiText size="s" data-test-subj="anomalyThresholdPropertyValue">
{anomalyThreshold}
</EuiText>
Expand Down Expand Up @@ -258,7 +258,7 @@ interface RuleTypeProps {
type: Type;
}

const RuleType = ({ type }: RuleTypeProps) => (
export const RuleType = ({ type }: RuleTypeProps) => (
<EuiText size="s">{getRuleTypeDescription(type)}</EuiText>
);

Expand Down Expand Up @@ -298,7 +298,7 @@ interface TimelineTitleProps {
timelineTitle: string;
}

const TimelineTitle = ({ timelineTitle }: TimelineTitleProps) => (
export const TimelineTitle = ({ timelineTitle }: TimelineTitleProps) => (
<EuiText size="s" data-test-subj="timelineTemplatePropertyValue">
{timelineTitle}
</EuiText>
Expand Down Expand Up @@ -354,15 +354,15 @@ interface SuppressAlertsByFieldProps {
fields: string[];
}

const SuppressAlertsByField = ({ fields }: SuppressAlertsByFieldProps) => (
export const SuppressAlertsByField = ({ fields }: SuppressAlertsByFieldProps) => (
<BadgeList badges={fields} data-test-subj="alertSuppressionGroupByPropertyValue" />
);

interface SuppressAlertsDurationProps {
duration?: Duration;
}

const SuppressAlertsDuration = ({ duration }: SuppressAlertsDurationProps) => {
export const SuppressAlertsDuration = ({ duration }: SuppressAlertsDurationProps) => {
const durationDescription = duration
? `${duration.value}${duration.unit}`
: descriptionStepI18n.ALERT_SUPPRESSION_PER_RULE_EXECUTION;
Expand All @@ -378,7 +378,7 @@ interface MissingFieldsStrategyProps {
missingFieldsStrategy?: AlertSuppressionMissingFieldsStrategy;
}

const MissingFieldsStrategy = ({ missingFieldsStrategy }: MissingFieldsStrategyProps) => {
export const MissingFieldsStrategy = ({ missingFieldsStrategy }: MissingFieldsStrategyProps) => {
const missingFieldsDescription =
missingFieldsStrategy === AlertSuppressionMissingFieldsStrategyEnum.suppress
? descriptionStepI18n.ALERT_SUPPRESSION_SUPPRESS_ON_MISSING_FIELDS
Expand All @@ -395,15 +395,15 @@ interface NewTermsFieldsProps {
newTermsFields: string[];
}

const NewTermsFields = ({ newTermsFields }: NewTermsFieldsProps) => (
export const NewTermsFields = ({ newTermsFields }: NewTermsFieldsProps) => (
<BadgeList badges={newTermsFields} data-test-subj="newTermsFieldsPropertyValue" />
);

interface HistoryWindowSizeProps {
historyWindowStart?: string;
}

const HistoryWindowSize = ({ historyWindowStart }: HistoryWindowSizeProps) => {
export const HistoryWindowSize = ({ historyWindowStart }: HistoryWindowSizeProps) => {
const size = historyWindowStart ? convertHistoryStartToSize(historyWindowStart) : '7d';

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,26 @@ import { getHumanizedDuration } from '../../../../detections/pages/detection_eng
import { DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS } from './constants';
import * as i18n from './translations';

interface AccessibleTimeValueProps {
timeValue: string;
'data-test-subj'?: string;
}

export const AccessibleTimeValue = ({
timeValue,
'data-test-subj': dataTestSubj,
}: AccessibleTimeValueProps) => (
<EuiText size="s" data-test-subj={dataTestSubj}>
<IntervalAbbrScreenReader interval={timeValue} />
</EuiText>
);

interface IntervalProps {
interval: string;
}

const Interval = ({ interval }: IntervalProps) => (
<EuiText size="s" data-test-subj="intervalPropertyValue">
<IntervalAbbrScreenReader interval={interval} />
</EuiText>
<AccessibleTimeValue timeValue={interval} data-test-subj="intervalPropertyValue" />
);

interface FromProps {
Expand All @@ -30,9 +42,10 @@ interface FromProps {
}

const From = ({ from, interval }: FromProps) => (
<EuiText size="s" data-test-subj={`fromPropertyValue-${from}`}>
<IntervalAbbrScreenReader interval={getHumanizedDuration(from, interval)} />
</EuiText>
<AccessibleTimeValue
timeValue={getHumanizedDuration(from, interval)}
data-test-subj={`fromPropertyValue-${from}`}
/>
);

export interface RuleScheduleSectionProps extends React.ComponentProps<typeof EuiDescriptionList> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ import { NameReadOnly } from './fields/name/name';
import { TagsReadOnly } from './fields/tags/tags';
import { DescriptionReadOnly } from './fields/description/description';
import { assertUnreachable } from '../../../../../../../common/utility_types';
import { AuthorReadOnly } from './fields/author/author';
import { BuildingBlockReadOnly } from './fields/building_block/building_block';
import { InvestigationFieldsReadOnly } from './fields/investigation_fields/investigation_fields';
import { FalsePositivesReadOnly } from './fields/false_positives/false_positives';
import { LicenseReadOnly } from './fields/license/license';
import { MaxSignalsReadOnly } from './fields/max_signals/max_signals';
import { NoteReadOnly } from './fields/note/note';
import { RuleScheduleReadOnly } from './fields/rule_schedule/rule_schedule';
import { ReferencesReadOnly } from './fields/references/references';
import { RiskScoreReadOnly } from './fields/risk_score/risk_score';
import { RuleNameOverrideReadOnly } from './fields/rule_name_override/rule_name_override';
import { SetupReadOnly } from './fields/setup/setup';
import { SeverityReadOnly } from './fields/severity/severity';
import { TimestampOverrideReadOnly } from './fields/timestamp_override/timestamp_override';
import { TimelineTemplateReadOnly } from './fields/timeline_template/timeline_template';

interface CommonRuleFieldReadOnlyProps {
fieldName: keyof DiffableCommonFields;
Expand All @@ -32,25 +47,28 @@ export function CommonRuleFieldReadOnly({
}: CommonRuleFieldReadOnlyProps) {
switch (fieldName) {
case 'author':
return null;
return <AuthorReadOnly author={finalDiffableRule.author} />;
case 'building_block':
return null;
return <BuildingBlockReadOnly />;
case 'description':
return <DescriptionReadOnly description={finalDiffableRule.description} />;
case 'exceptions_list':
/* Exceptions are not used in prebuilt rules */
return null;
case 'investigation_fields':
return null;
return (
<InvestigationFieldsReadOnly investigationFields={finalDiffableRule.investigation_fields} />
);
case 'false_positives':
return null;
return <FalsePositivesReadOnly falsePositives={finalDiffableRule.false_positives} />;
case 'license':
return null;
return <LicenseReadOnly license={finalDiffableRule.license} />;
case 'max_signals':
return null;
return <MaxSignalsReadOnly maxSignals={finalDiffableRule.max_signals} />;
case 'name':
return <NameReadOnly name={finalDiffableRule.name} />;
case 'note':
return null;
return <NoteReadOnly note={finalDiffableRule.note} />;
case 'related_integrations':
return (
<RelatedIntegrationsReadOnly relatedIntegrations={finalDiffableRule.related_integrations} />
Expand All @@ -60,30 +78,32 @@ export function CommonRuleFieldReadOnly({
case 'risk_score_mapping':
return <RiskScoreMappingReadOnly riskScoreMapping={finalDiffableRule.risk_score_mapping} />;
case 'rule_schedule':
return null;
return <RuleScheduleReadOnly ruleSchedule={finalDiffableRule.rule_schedule} />;
case 'severity_mapping':
return <SeverityMappingReadOnly severityMapping={finalDiffableRule.severity_mapping} />;
case 'tags':
return <TagsReadOnly tags={finalDiffableRule.tags} />;
case 'threat':
return <ThreatReadOnly threat={finalDiffableRule.threat} />;
case 'references':
return null;
return <ReferencesReadOnly references={finalDiffableRule.references} />;
case 'risk_score':
return null;
return <RiskScoreReadOnly riskScore={finalDiffableRule.risk_score} />;
case 'rule_id':
/* Rule ID is not displayed in the UI */
return null;
case 'rule_name_override':
return null;
return <RuleNameOverrideReadOnly ruleNameOverride={finalDiffableRule.rule_name_override} />;
case 'setup':
return null;
return <SetupReadOnly setup={finalDiffableRule.setup} />;
case 'severity':
return null;
return <SeverityReadOnly severity={finalDiffableRule.severity} />;
case 'timestamp_override':
return null;
return <TimestampOverrideReadOnly timestampOverride={finalDiffableRule.timestamp_override} />;
case 'timeline_template':
return null;
return <TimelineTemplateReadOnly timelineTemplate={finalDiffableRule.timeline_template} />;
case 'version':
/* Version is not displayed in the UI */
return null;
default:
return assertUnreachable(fieldName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import React from 'react';
import type { DiffableCustomQueryFields } from '../../../../../../../common/api/detection_engine';
import { DataSourceReadOnly } from './fields/data_source/data_source';
import { KqlQueryReadOnly } from './fields/kql_query';
import { assertUnreachable } from '../../../../../../../common/utility_types';
import { TypeReadOnly } from './fields/type/type';
import { AlertSuppressionReadOnly } from './fields/alert_suppression/alert_suppression';

interface CustomQueryRuleFieldReadOnlyProps {
fieldName: keyof DiffableCustomQueryFields;
Expand All @@ -20,6 +23,13 @@ export function CustomQueryRuleFieldReadOnly({
finalDiffableRule,
}: CustomQueryRuleFieldReadOnlyProps) {
switch (fieldName) {
case 'alert_suppression':
return (
<AlertSuppressionReadOnly
alertSuppression={finalDiffableRule.alert_suppression}
ruleType={finalDiffableRule.type}
/>
);
case 'data_source':
return <DataSourceReadOnly dataSource={finalDiffableRule.data_source} />;
case 'kql_query':
Expand All @@ -30,7 +40,9 @@ export function CustomQueryRuleFieldReadOnly({
ruleType={finalDiffableRule.type}
/>
);
case 'type':
return <TypeReadOnly type={finalDiffableRule.type} />;
default:
return null; // Will replace with `assertUnreachable(fieldName)` once all fields are implemented
return assertUnreachable(fieldName);
}
}
Loading

0 comments on commit 1d17d2f

Please sign in to comment.