-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Actionable Observability] Expose ObservabilityAlertSearchBar from Observability plugin #146401
[Actionable Observability] Expose ObservabilityAlertSearchBar from Observability plugin #146401
Conversation
@@ -27,17 +27,17 @@ const getAlertStatusQuery = (status: string): Query[] => { | |||
|
|||
export function ObservabilityAlertSearchBar({ | |||
appName, | |||
defaultSearchQueries = DEFAULT_QUERIES, | |||
onEsQueryChange, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed for readability
dateRange: { from: string; to: string; mode?: 'absolute' | 'relative' }; | ||
query?: string; | ||
query: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the implementation, we always pass a string to this function and in the observability plugin, we rely on getting a string for the query (if it is undefined, '' will be returned).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some initial feedback. Happy to chat more any time!
x-pack/plugins/observability/public/components/shared/alert_search_bar/alert_search_bar.tsx
Show resolved
Hide resolved
x-pack/plugins/observability/public/components/shared/alert_search_bar/alert_search_bar.tsx
Show resolved
Hide resolved
onChange={(id) => { | ||
setStatus(id as AlertStatus); | ||
}} | ||
onChange={(id) => onStatusChange(id as AlertStatus)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
always avoid inline creation of functions and objects... they will trigger a re-render due to reference inequality.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've read different articles about this topic and there are opposite views about it, such as this one:
https://reacttraining.com/blog/react-inline-functions-and-performance/
How do we check if re-rendering actually has a negative performance impact? (I assume the concern is performance)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is indeed a widely contested point.
AFAIK the performance hit is negligible (defining functions is considered cheap)
@maryam-saeidi if you want to know for sure, you could add performance.now()
's to the component and compare the different implementations. Testing in dev mode might give a skewed result, you want to test this in a production build.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even if the hit is "negligible", I haven't encountered an argument that defining inline is preferable, other than that's how it was written when the PR was created.
I personally pull them out into const
for readability and to avoid waffling/considering it every time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@maryam-saeidi I just looked at the article you referenced... it's from 2017, and reflects class components, (e.g. PureComponent
.
AFAIK, the strict equality check and re-render is always an issue with inline objects and props. @CoenWarmer it's not about defining functions, it's about the function causing a sub-component to re-render... which you can't really know how a re-render will affect children.
The rule of thumb still applies here. I don't see an argument where defining inline gains anything, where I see plenty of arguments why not to-- along with the cheapness of it, namely creating a const
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just looked at the article you referenced... it's from 2017, and reflects class components, (e.g. PureComponent.
Yes, since we were talking about the concepts (the article that you shared was from 2018 and also related to PureComponents) I didn't look for a newer article.
When I read react documents, I don't see a focus on avoiding re-rendering as much as possible but rather on using tools to improve performance when it is needed. Actually, I saw more focus on making sure our components work as expected even if it is slow and it re-render unnecessarily and then optimizing it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@clintandrewhall Using inline functions is considered an acceptable practice both in the old and the new React docs. In this case, the component that was passed the inline function is not memoized, so unsure as to what value not passing an arrow function would bring apart from perhaps aesthetic reasons.
I'm however not against defining functions in a const
as a rule of thumb. Perhaps the component which receives the function could become a memoized component in the future, in which case you don't have to touch this component anymore which saves time and effort.
|
||
return <ObservabilityAlertSearchBar {...stateProps} {...searchBarProps} />; | ||
return ( | ||
<ObservabilityAlertSearchBarProvider {...services}> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would move this up to your React root.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the benefit of that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see: #146401 (comment)
|
||
function AlertSearchbarWithUrlSync(props: AlertSearchBarWithUrlSyncProps) { | ||
const { urlStorageKey, ...searchBarProps } = props; | ||
const stateProps = useAlertSearchBarStateContainer(urlStorageKey); | ||
const services = useKibana<ObservabilityAppServices>().services; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Take every opportunity to eliminate useKibana
. If you move the Provider to your React root, these lines get deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wondering about the reasoning for it, AlertSearchbarWithUrlSync
is an internal component (only used within the Observability plugin) and I put the provider here to make AlertSearchbarWithUrlSync
stand alone without a need to always wrap the component with this provider on every usage. Will it cause an issue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To make a component shareable and portable, we need to prioritize decoupling it from Kibana. The useKibana
hook is incredibly troublesome, as it literally allows a plugin to slam entire start contracts into context, all without any checks except at runtime.
In short: you have no idea if ObservabilityAppServices
is actually in context when this component is consumed elsewhere. It's an implementation detail a consumer must know, and due to the nature of useKibana
, they won't know until runtime. It's a constant problem.
This is why creating a provider for the shared component and putting it in the root is so helpful-- it allows us to provide and enforce dependency injection.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you have no idea if ObservabilityAppServices is actually in context when this component is consumed elsewhere.
Since this component is meant to be used inside of the Observability
plugin, it can assume that ObservabilityAppServices
is provided through useKibana
. I understand using these shared components between plugins can cause an issue, hence the creation of the related provider, but I don't see the issue that you mentioned in sharing components inside of a plugin. Am I missing something?
This is why creating a provider for the shared component and putting it in the root is so helpful-- it allows us to provide and enforce dependency injection.
How do we do that at the root level?
...ublic/components/shared/alert_search_bar/containers/use_alert_search_bar_state_container.tsx
Show resolved
Hide resolved
value={{ | ||
timeFilterService: services.data.query.timefilter.timefilter, | ||
errorToast: services.notifications.toasts.addError, | ||
AlertsSearchBar: services.triggersActionsUi.getAlertsSearchBar, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be the result of getAlertsSearchBar
...?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can be, but we can also use it as a functional component without calling it first.
return ( | ||
<ObservabilityAlertSearchBarContext.Provider | ||
value={{ | ||
timeFilterService: services.data.query.timefilter.timefilter, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I would suggest destructuring your incoming services and make this a bit easier to read, and avoid referential inequality:
const { data, notifications, triggersActionsUi } = services;
// You could avoid this part, I just did this to make it clearer what is being pulled from where.
const timeFilterService = data.query.timefilter.timefilter;
const errorToast = notifications.toasts.addError;
const AlertsSearchBar = triggersActionsUi.getAlertsSearchBar();
// You can optionally `useMemo` this, depending on the volatility of the services you consume.
const services = {
timeFilterService,
errorToast,
AlertsSearchBar,
};
/*
const services = useMemo({
timeFilterService,
errorToast,
AlertsSearchBar,
}), [timeFilterService, errorToast, AlertsSearchBar]);
*/
return (
<ObservabilityAlertSearchBarContext.Provider value={services}>
...
|
||
export interface Services { | ||
timeFilterService: TimefilterContract; | ||
AlertsSearchBar: (props: AlertsSearchBarProps) => ReactElement<AlertsSearchBarProps>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this level of specificity is necessary for your use case. Unless you're interacting directly with those props, you can just say, "this is a React component I'm rendering":
AlertsSearchBar: () => ReactNode;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since I am passing the related props to this component, I expect the component to get AlertsSearchBarProps
.
x-pack/plugins/observability/public/components/shared/alert_search_bar/types.ts
Outdated
Show resolved
Hide resolved
Pinging @elastic/actionable-observability (Team: Actionable Observability) |
timefilter: { timefilter: timeFilterService }, | ||
}, | ||
}, | ||
useToasts, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've tried to pass toast directly similar to other dependencies but it will cause an issue regarding this
in the toasts API. So I passed useToasts
to call useKibana
when we need to show the toast. I saw useToasts
in other plugins such as cases
too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useToasts, | ||
triggersActionsUi: { getAlertsSearchBar: AlertsSearchBar }, | ||
}) => { | ||
const services = useMemo<Services>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed we use useMemo
/ useCallback
almost everywhere in kibana.
I wonder if we really need a useMemo here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, in this case, it made sense to me conceptually but maybe in reality it is not needed. I will try to gather more information about this topic in order to have a better process in making those decisions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a really well written article about useMemo
and useCallback
. https://www.developerway.com/posts/how-to-use-memo-use-callback
tl;dr: there is a high chance it is not leading to performance improvements, and it certainly leads to less readable components.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The React team has been experimenting with a compiler that can optimize JSX, applying useMemo
/ useCallback
where it can really improve performance called React Forget: https://www.youtube.com/watch?v=lGEMwh32soc.
This doesn't mean that this particular compiler will ever make it. It does serve to illustrate that the React team is aware that useCallback
/ useMemo
APIs don't provide easy enough tools to decide whether or not it actually makes sense to use them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@CoenWarmer Thanks for the links, I will check them and apply the result accordingly 👍🏻
const mockServices = () => { | ||
useServicesMock.mockReturnValue({ | ||
timeFilterService: timefilterServiceMock, | ||
AlertsSearchBar: getAlertsSearchBarMock.mockReturnValue( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added getAlertsSearchBar
to the TriggerActionsUI mock over here. So you might not need to reintroduce this here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But this one is mocking useServices
not useKibana
, and I also want to check the related props, so I defined the mock here.
...ck/plugins/observability/public/components/shared/alert_search_bar/alert_search_bar.test.tsx
Outdated
Show resolved
Hide resolved
import { useKibana } from '../../../utils/kibana_react'; | ||
import { observabilityAlertFeatureIds } from '../../../config'; | ||
import { ObservabilityAppServices } from '../../../application/types'; | ||
import { useServices } from './services'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What benefit does it have to introduce a new useServices
which just exports useKibana().services
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this component will be used in another plugin, we don't know if they have all the related dependencies defined in their plugin as this shared component needs.
So I used a similar pattern as mentioned in Clint's presentation to make these dependencies explicit and the consumer should make sure to pass the related services to the ObservabilityAlertSearchBarProvider
that I defined here.
useServices
is a hook that will access the services passed to the provider.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the whole looks good. I have some concerns about introducing a new context
and useServices
hook making things more complex for which I've left some comments.
I can't fully judge why this component needs to live in the observability
and not triggerActionsUi
plugin. Do you know?
Yes, I've decided on that based on the discussion with @XavierM. The extra filtering for active/recovered alerts is the design we want to use in Observability-related apps and not in other solutions such as Security, so I implemented it in the Observability plugin to be used everywhere. (In future, we will change this filter to use controls instead) |
💚 Build Succeeded
Metrics [docs]Module Count
Public APIs missing comments
Async chunks
Public APIs missing exports
Page load bundle
Unknown metric groupsAPI count
async chunk count
ESLint disabled in files
ESLint disabled line counts
Total ESLint disabled count
History
To update your PR or re-run it, just comment with: |
## 📝 Summary In the previous [PR](#146401), I've added a provider to pass Kibana dependencies to the observability alert search bar component. Since then, I had a lot of discussions about whether it makes sense to have a context for the dependencies of this component or not. Since we haven't decided yet whether this pattern should be adopted or not and there are suggestions about having one provider for the alert-related components, I refactored it to pass the dependency via props instead. ## 🧪 How to test Add this component to [APM](https://github.com/elastic/kibana/blob/main/x-pack/plugins/apm/public/components/app/alerts_overview/index.tsx): ``` // Add import { ObservabilityAlertSearchBar } from '@kbn/observability-plugin/public'; export const useToasts = () => useKibana<ApmPluginStartDeps>().services.notifications!.toasts; // Update const { triggersActionsUi: { getAlertsStateTable: AlertsStateTable, getAlertsSearchBar: AlertsSearchBar, alertsTableConfigurationRegistry, }, data: { query: { timefilter: { timefilter: timeFilterService }, }, }, } = services; // Replace // <AlertsTableStatusFilter // status={alertStatusFilter} // onChange={setAlertStatusFilter} // /> <ObservabilityAlertSearchBar appName={'apmApp'} kuery={''} onRangeFromChange={(input) => console.log(input)} onRangeToChange={(input) => console.log(input)} onKueryChange={(input) => console.log(input)} onStatusChange={(input) => console.log(input)} onEsQueryChange={(input) => console.log(input)} rangeTo={'now'} rangeFrom={'now-15m'} status={'all'} services={{ timeFilterService, AlertsSearchBar, useToasts }} /> ```
Resolves #146286
📝 Summary
In this PR, I exposed ObservabilityAlertSearchBar from the Observability plugin to be used in other plugins such as APM.
I've added
ObservabilityAlertSearchBarProvider
in order for other plugins to provide Kibana dependencies to the shared component.🧪 How to test
For testing the implementation, I imported this component in the APM plugin and used it in the alerts tab, you can do the same locally by following these steps:
ObservabilityAlertSearchBar
in APM and define related hook:ObservabilityAlertSearchBar
component:You should see the new search bar in APM alerts tab: