Skip to content
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

Draft
wants to merge 25 commits into
base: refactor/onboarding_hub_new_architecture
Choose a base branch
from

Conversation

angorayc
Copy link
Collaborator

@angorayc angorayc commented Oct 2, 2024

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

@@ -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}>
Copy link
Owner

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.

Comment on lines +18 to +20
const addAgentLink = getAppUrl({ appId: FLEET_APP_ID, path: ADD_AGENT_PATH });
const onAddAgentClick = useCallback(() => {
navigateTo({ appId: FLEET_APP_ID, path: ADD_AGENT_PATH });
Copy link
Owner

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

Comment on lines +19 to +71
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} />;
Copy link
Owner

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[] = [
Copy link
Owner

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',
Copy link
Owner

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?

Copy link
Owner

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 = ({
Copy link
Owner

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) => {
Copy link
Owner

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;
Copy link
Owner

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.

Comment on lines +23 to +39
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 />
Copy link
Owner

@semd semd Oct 3, 2024

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>
  );
};

Comment on lines +53 to +54
integrationsInstalled: 0,
agentStillRequired: false,
Copy link
Owner

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?

Suggested change
integrationsInstalled: 0,
agentStillRequired: false,
installedIntegrationsCount: 0,
isAgentRequired: false,

Comment on lines +40 to +45
const [selectedTabId, setSelectedTabIdToStorage] = useStoredIntegrationTabId(
spaceId,
DEFAULT_TAB.id
);
const [searchTermFromStorage, setSearchTermToStorage] = useStoredIntegrationSearchTerm(spaceId);
const [toggleIdSelected, setToggleIdSelected] = useState<string>(selectedTabId);
Copy link
Owner

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(
Copy link
Owner

Choose a reason for hiding this comment

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

Suggested change
export const PackageInstalledCallout = React.memo(
export const InstalledIntegrationsCallout = React.memo(

Comment on lines +25 to +62
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" />,
}}
/>
}
/>
);
Copy link
Owner

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?

Copy link
Owner

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
Copy link
Owner

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?

Copy link
Owner

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?

Comment on lines +121 to +122
} else if (url.startsWith('http') || url.startsWith('https')) {
window.open(url, '_blank');
Copy link
Owner

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);
Copy link
Owner

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants