-
Notifications
You must be signed in to change notification settings - Fork 0
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
Onboarding fleet integration #8
base: refactor/onboarding_hub_new_architecture
Are you sure you want to change the base?
Onboarding fleet integration #8
Conversation
@@ -11,7 +11,7 @@ export const OnboardingCardContentPanel = React.memo<PropsWithChildren<EuiPanelP | |||
({ children, ...panelProps }) => { | |||
return ( | |||
<EuiPanel hasShadow={false} hasBorder={false} paddingSize="m"> | |||
<EuiPanel {...panelProps} hasShadow={false} paddingSize="l"> | |||
<EuiPanel {...panelProps} hasShadow={false}> |
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 think that what we want here is style={{ paddingTop: 0 }}
instead of removing paddingSize="l"
the rest of the paddings are fine.
const addAgentLink = getAppUrl({ appId: FLEET_APP_ID, path: ADD_AGENT_PATH }); | ||
const onAddAgentClick = useCallback(() => { | ||
navigateTo({ appId: FLEET_APP_ID, path: ADD_AGENT_PATH }); |
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: we could extract:
const fleetAgentLinkProps = { appId: FLEET_APP_ID, path: ADD_AGENT_PATH };
outside the component
const [fetchAvailablePackages, setFetchAvailablePackages] = useState<AvailablePackagesHookType>(); | ||
|
||
const { error, retry, loading } = useAsyncRetry(async () => { | ||
if (fetchAvailablePackages) { | ||
return; | ||
} | ||
const loadedHook = await fetchAvailablePackagesHook(); | ||
setFetchAvailablePackages(() => { | ||
return loadedHook; | ||
}); | ||
}); | ||
|
||
const onRetry = useCallback(() => { | ||
if (!loading) { | ||
retry(); | ||
} | ||
}, [loading, retry]); | ||
|
||
if (error) { | ||
return ( | ||
<EuiCallOut | ||
title={i18n.translate('xpack.securitySolution.onboarding.asyncLoadFailureCallout.title', { | ||
defaultMessage: 'Loading failure', | ||
})} | ||
color="warning" | ||
iconType="cross" | ||
size="m" | ||
> | ||
<p> | ||
<FormattedMessage | ||
id="xpack.securitySolution.onboarding.asyncLoadFailureCallout.copy" | ||
defaultMessage="Some required elements failed to load." | ||
/> | ||
</p> | ||
<EuiButton color="warning" data-test-subj="retryButton" onClick={onRetry}> | ||
<FormattedMessage | ||
id="xpack.securitySolution.onboarding.asyncLoadFailureCallout.buttonContent" | ||
defaultMessage="Retry" | ||
/> | ||
</EuiButton> | ||
</EuiCallOut> | ||
); | ||
} | ||
if (loading || !fetchAvailablePackages) { | ||
return ( | ||
<EuiSkeletonText | ||
data-test-subj="loadingPackages" | ||
isLoading={true} | ||
lines={LOADING_SKELETON_HEIGHT} | ||
/> | ||
); | ||
} | ||
return <PackageListGrid useAvailablePackages={fetchAvailablePackages} />; |
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 requires a lot of custom logic to load a hook from Fleet. What if we create a HOC to do that, so we can clean this component from that hook lazy loading logic, this way we can also reuse it if we ever need to do this somewhere else.
We can create a new file at x-pack/plugins/security_solution/public/common/components/with_lazy_hook/index.tsx
with:
export const withLazyHook = <D extends {}, P extends {}>(
Component: React.ComponentType<PropsWithChildren<D & P>>,
moduleImport: () => Promise<P>,
fallback?: React.ReactNode
) => {
return React.memo(function WithLazy(props: D) {
const [lazyModuleProp, setLazyModuleProp] = useState<P>();
useEffect(() => {
moduleImport().then((module) => {
setLazyModuleProp(() => module);
});
}, []);
return lazyModuleProp ? <Component {...lazyModuleProp} {...props} /> : <>{fallback}</> ?? null;
});
};
Then we could just do:
export const AvailablePackages = withLazyHook(
PackageListGrid,
() => import('@kbn/fleet-plugin/public').then((module) => module.AvailablePackagesHook()),
<EuiSkeletonText
data-test-subj="loadingPackages"
isLoading={true}
lines={LOADING_SKELETON_HEIGHT}
/>
);
But I don't think we need to create a file for that, we can just place it next to the component definition and export the wrapped component.
We don't actually need error catching for lazy loading, we never do that, we assume all our libraries exist if the bundle compiles.
export const SEARCH_FILTER_CATEGORIES: CategoryFacet[] = []; | ||
export const WITH_SEARCH_BOX_HEIGHT = '517px'; | ||
export const WITHOUT_SEARCH_BOX_HEIGHT = '462px'; | ||
export const INTEGRATION_TABS: Tab[] = [ |
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 like the idea of the INTEGRATION_TABS
config, it could be in its own file if you want
{ | ||
category: 'security', | ||
iconType: 'starFilled', | ||
id: 'recommended', |
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 would be good to create an enum for the tab IDs?
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.
Can we name this file constants.ts
?
import { getFilteredCards } from './utils'; | ||
import { INTEGRATION_TABS } from './const'; | ||
|
||
export const useIntegrationCardList = ({ |
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.
Can we put this hook in its own file use_integration_card_list.ts
along with all the related logic? Including the getFilteredCards
function that currently is in the utils file, but is only used by this hook.
return integrationCards ?? []; | ||
}; | ||
|
||
export const useTabMetaData = (toggleIdSelected: 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.
Do we really need to extract this hook? We know the tab will always exist. Assuming we create the IntegrationTabId
enum, we can map all tabs in the constants file with:
export const INTEGRATION_TABS_BY_ID = Object.fromEntries(
INTEGRATION_TABS.map((tab) => [tab.id, tab])
) as Record<IntegrationTabId, Tab>;
And then we can do this in-place:
const selectedTab = useMemo(
() => INTEGRATION_TABS_BY_ID[selectedTabId],
[selectedTabId]
);
So selectedTab
can't be undefined
.
checkCompleteMetadata, // this is undefined before the first checkComplete call finishes | ||
}) => { | ||
// TODO: implement. This is just for demo purposes | ||
const integrationsInstalled: number = checkCompleteMetadata?.integrationsInstalled as number; |
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 can be undefined this casting to number
is error-prone.
const { isAgentlessAvailable$ } = useOnboardingService(); | ||
const isAgentlessAvailable = useObservable(isAgentlessAvailable$, undefined); | ||
const showAgentlessCallout = | ||
isAgentlessAvailable && AGENTLESS_LEARN_MORE_LINK && integrationsInstalled === 0; | ||
const showInstalledCallout = | ||
integrationsInstalled > 0 || checkCompleteMetadata?.agentStillRequired; | ||
|
||
return ( | ||
<OnboardingCardContentPanel> | ||
<EuiFlexGroup gutterSize="m" direction="column" alignItems="flexStart"> | ||
<EuiFlexItem grow={false}> | ||
{checkCompleteMetadata ? ( | ||
<CardCallOut | ||
text={`${checkCompleteMetadata.integrationsInstalled} integrations installed`} | ||
/> | ||
) : ( | ||
<EuiLoadingSpinner size="s" /> | ||
)} | ||
</EuiFlexItem> | ||
<EuiFlexItem grow={false}> | ||
<EuiButton onClick={() => setComplete(false)}>{'Set not complete'}</EuiButton> | ||
</EuiFlexItem> | ||
</EuiFlexGroup> | ||
<> | ||
{showAgentlessCallout && <AgentlessAvailableCallout />} | ||
{showInstalledCallout && ( | ||
<PackageInstalledCallout checkCompleteMetadata={checkCompleteMetadata} /> | ||
)} | ||
{(showAgentlessCallout || showInstalledCallout) && <EuiSpacer size="m" />} | ||
</> | ||
<AvailablePackages /> |
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.
Can we refactor this a bit? We should first check if checkCompleteMetadata
is defined, while it's undefined it means the check is running. We can check if it's finished with a type guard, so it's also cast automatically.
It would also be good to move the callouts logic outside of this component in a IntegrationCardTopCallout
(since we only show one at a time).
We can rename the AvailablePackages
component to something easier to understand like IntegrationsCardGridTabs
. I know a "package" is another name for "integration" in Fleet, but it will make it easier for future developers if we stick to only one of these names across the code of this card.
// put this type in a new "cards/integrations/types.ts"
interface IntegrationCardMetadata {
integrationsInstalled: number;
agentStillRequired: boolean;
}
const isCheckCompleteMetadata = (metadata?: unknown): metadata is IntegrationCardMetadata => {
return metadata !== undefined;
};
export const IntegrationsCard: OnboardingCardComponent = ({
checkCompleteMetadata, // this is undefined before the first checkComplete call finishes
}) => {
if (!isCheckCompleteMetadata(checkCompleteMetadata)) {
return <CenteredLoadingSpinner />;
}
const { integrationsInstalled, agentStillRequired } = checkCompleteMetadata;
return (
<OnboardingCardContentPanel>
<IntegrationCardTopCallout
agentRequired={agentStillRequired}
integrationsInstalled={integrationsInstalled}
/>
<IntegrationsCardGridTabs />
</OnboardingCardContentPanel>
);
};
integrationsInstalled: 0, | ||
agentStillRequired: false, |
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.
These names are a bit odd, can we use more conventional names?
integrationsInstalled: 0, | |
agentStillRequired: false, | |
installedIntegrationsCount: 0, | |
isAgentRequired: false, |
const [selectedTabId, setSelectedTabIdToStorage] = useStoredIntegrationTabId( | ||
spaceId, | ||
DEFAULT_TAB.id | ||
); | ||
const [searchTermFromStorage, setSearchTermToStorage] = useStoredIntegrationSearchTerm(spaceId); | ||
const [toggleIdSelected, setToggleIdSelected] = useState<string>(selectedTabId); |
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.
Why do we need 2 states for the selectedTabId
?
import { CardCallOut } from '../common/card_callout'; | ||
import { AgentRequiredCallout } from './agent_required_callout'; | ||
|
||
export const PackageInstalledCallout = React.memo( |
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.
export const PackageInstalledCallout = React.memo( | |
export const InstalledIntegrationsCallout = React.memo( |
return checkCompleteMetadata?.agentStillRequired ? ( | ||
<AgentRequiredCallout /> | ||
) : ( | ||
<CardCallOut | ||
color="primary" | ||
text={ | ||
<FormattedMessage | ||
id="xpack.securitySolution.onboarding.integrationsCard.callout.completeLabel" | ||
defaultMessage={` | ||
{desc} {link} {icon} | ||
`} | ||
values={{ | ||
desc: ( | ||
<FormattedMessage | ||
data-test-subj="integrationsCompleteText" | ||
id="xpack.securitySolution.onboarding.integrationsCard.callout.completeText" | ||
defaultMessage="{count} {count, plural, one {integration has} other {integrations have}} been added" | ||
values={{ count: integrationsInstalled }} | ||
/> | ||
), | ||
link: ( | ||
<LinkAnchor | ||
onClick={onAddIntegrationClicked} | ||
href={integrationUrl} | ||
data-test-subj="manageIntegrationsLink" | ||
> | ||
<FormattedMessage | ||
id="xpack.securitySolution.onboarding.integrationsCard.button.completeLink" | ||
defaultMessage="Manage integrations" | ||
/> | ||
</LinkAnchor> | ||
), | ||
icon: <EuiIcon type="arrowRight" size="s" />, | ||
}} | ||
/> | ||
} | ||
/> | ||
); |
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.
Can we extract the second callout in its own component like the AgentRequiredCallout
?
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 we have a lot of callouts, it would be good to put all of them in a /cards/integrations/callouts
directory, to keep things a bit more organized
onSaveNavigateTo: [APP_UI_ID, { path: ONBOARDING_PATH }], | ||
}; | ||
const url = | ||
card.url.indexOf(APP_INTEGRATIONS_PATH) >= 0 && onboardingLink |
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's the difference between APP_INTEGRATIONS_PATH
and integrationRootUrl
?
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 utils.ts is not necessary, nothing here is ever reused. Can we put everything in its correct place?
} else if (url.startsWith('http') || url.startsWith('https')) { | ||
window.open(url, '_blank'); |
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.
Why do we assume that absolute URLs need to open in a new tab? Just curiosity
spaceId, | ||
DEFAULT_TAB.id | ||
); | ||
const [searchTermFromStorage, setSearchTermToStorage] = useStoredIntegrationSearchTerm(spaceId); |
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.
A user may store the search term in a specific tab and when the page is reloaded be used in a different tab, which may no longer make sense. Is this a requirement?
Summary
Please use this branch to see the complete version: elastic#193131
Fleet changes: elastic#194028
It shows 9 cards without the search box on the recommended tab; and shows all the cards and a search box on other tabs:
Screen.Recording.2024-09-24.at.18.36.20.mov
When clicking on back button, cancellation button, and save button, it should go back to the security onboarding hub:
Screen.Recording.2024-09-24.at.17.48.50.mov
Screen.Recording.2024-09-30.at.15.49.12.mov