Skip to content

[Cases] Add search in attachment tab#246265

Merged
christineweng merged 2 commits intoelastic:mainfrom
christineweng:cases-attachment-search
Dec 15, 2025
Merged

[Cases] Add search in attachment tab#246265
christineweng merged 2 commits intoelastic:mainfrom
christineweng:cases-attachment-search

Conversation

@christineweng
Copy link
Copy Markdown
Contributor

Summary

Adds a search bar in attachment tab. Counts and table will reflect results based on search term. Supported search fields are:

  • Alert id
  • Event id
  • Observables value
  • File name
Screen.Recording.2025-12-12.at.2.01.33.PM.mov

Checklist

  • Any text added follows EUI's writing guidelines, uses sentence case text and includes i18n support
  • Documentation was added for features that require explanation or tutorials
  • Unit or functional tests were updated or added to match the most common scenarios
  • If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the docker list
  • This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The release_note:breaking label should be applied in these situations.
  • Flaky Test Runner was used on any tests changed
  • The PR description includes the appropriate Release Notes section, and the correct release_note:* label is applied per the guidelines
  • Review the backport guidelines and apply applicable backport:* labels.
@christineweng christineweng self-assigned this Dec 12, 2025
@christineweng christineweng requested a review from a team as a code owner December 12, 2025 21:00
@christineweng christineweng added release_note:enhancement backport:skip This PR does not require backporting Team:Cases Security Solution Cases team v9.3.0 9.3 candidate labels Dec 12, 2025
@elasticmachine
Copy link
Copy Markdown
Contributor

Pinging @elastic/kibana-cases (Team:Cases)

Comment on lines +212 to +229
const totalAlerts = useMemo(() => {
return features.alerts.enabled
? caseData.comments
.filter((comment) => comment.type === AttachmentType.alert)
.map((comment) => comment.alertId)
.flat().length
: 0;
}, [features.alerts.enabled, caseData.comments]);

const totalEvents = useMemo(() => {
return features.events.enabled
? caseData.comments
.filter((comment) => comment.type === AttachmentType.event)
.map((comment) => comment.eventId)
.flat().length
: 0;
}, [features.events.enabled, caseData.comments]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a lot of iterations that could be done in a single run:

const stats = useMemo(() => {
    return caseData.comments.reduce(
      (acc, comment) => {
        if (comment.type === AttachmentType.alert && features.alerts.enabled) {
          acc.totalAlerts = Array.isArray(comment.alertId)
            ? acc.totalAlerts + comment.alertId.length
            : acc.totalAlerts + 1;
        } else if (comment.type === AttachmentType.event && features.events.enabled) {
          acc.totalEvents = Array.isArray(comment.eventId)
            ? acc.totalEvents + comment.eventId.length
            : acc.totalEvents + 1;
        }

        return acc;
      },
      { totalEvents: 0, totalAlerts: 0 }
    );
  }, [features, caseData]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, we might not want to run this logic, in case no searchterm was set:

const stats = useMemo(() => {
    if (!searchTerm) {
      return { totalAlerts: caseData.totalAlerts, totalEvents: caseData.totalEvents };
    }
    return caseData.comments.reduce(
      (acc, comment) => {
        if (comment.type === AttachmentType.alert && features.alerts.enabled) {
          acc.totalAlerts = Array.isArray(comment.alertId)
            ? acc.totalAlerts + comment.alertId.length
            : acc.totalAlerts + 1;
        } else if (comment.type === AttachmentType.event && features.events.enabled) {
          acc.totalEvents = Array.isArray(comment.eventId)
            ? acc.totalEvents + comment.eventId.length
            : acc.totalEvents + 1;
        }

        return acc;
      },
      { totalEvents: 0, totalAlerts: 0 }
    );
  }, [searchTerm, features, caseData]);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @janmonschke ! At first I thought the second approach would not work because of #245916, but the stats are recalculated every time the case is fetched 🙈

case: flattenCaseSavedObject({
savedObject: resolvedSavedObject,
comments: theComments.saved_objects,
totalComment: theComments.total,
totalEvents: countEventsForID({ comments: theComments }),
totalAlerts: countAlertsForID({ comments: theComments, id: resolvedSavedObject.id }),

const { urlParams } = useUrlParams();
const refreshCaseViewPage = useRefreshCaseViewPage();

const [searchTerm, setSearchTerm] = useState<string>('');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey, I have 2 questions related to scenarios where we have some text input from user:

  1. Do we have some input validation utils or rules to be used across Kibana?
  2. Do we have any UX guidelines on debouncing user input?
Copy link
Copy Markdown
Contributor Author

@christineweng christineweng Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Depends on what you want to validate, for specific terms we write our own validators (as in observable form x-pack/platform/plugins/shared/cases/common/observables/validators.ts)

  2. In this specific case, because I'm using EuiFieldSearch, onSearch is only called when user press Enter, but if you set incremental to true, then it will fire as user types. The EUI team might have an opinion on it (like intervals if that's what you are asking?)

@christineweng christineweng force-pushed the cases-attachment-search branch from 0cb4401 to 94c279a Compare December 15, 2025 21:36
@christineweng christineweng enabled auto-merge (squash) December 15, 2025 21:37
@elasticmachine
Copy link
Copy Markdown
Contributor

elasticmachine commented Dec 15, 2025

💚 Build Succeeded

Metrics [docs]

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
cases 1.4MB 1.4MB +1.3KB

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
cases 144.2KB 144.2KB -3.0B

History

cc @christineweng

@christineweng christineweng merged commit 36b7d39 into elastic:main Dec 15, 2025
13 checks passed
@christineweng christineweng removed ci:cloud-deploy Create or update a Cloud deployment ci:cloud-persist-deployment Persist cloud deployment indefinitely labels Dec 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

9.3 candidate backport:skip This PR does not require backporting release_note:enhancement Team:Cases Security Solution Cases team v9.3.0

5 participants