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

[Endpoint] Add Endpoint empty states for onboarding #69626

Merged
merged 25 commits into from
Jun 26, 2020

Conversation

kevinlog
Copy link
Contributor

@kevinlog kevinlog commented Jun 19, 2020

Summary

This PR adds an onboarding flow for the empty Host list.

On Endpoint:

  • Adds an empty state which gets users to add a policy
  • Following a policy, it will prompt users to deploy an Agent with Endpoint Security enabled if not already

On Ingest:

Testing:

  • Mostly manual with some unit tests added to verify the states load when appropriate
  • Our functional tests for this page are disabled right now, when they are re-enabled, I'll add functional tests in a later PR

No Policy prompt
image

Add Policy in Ingest
image

No Endpoint prompt
image

Enroll Agent in Ingest
image

Endpoint
image

Checklist

Delete any items that are not applicable to this PR.

For maintainers

if (location.pathname === intraAppState.forRoute && !wasHandled.has(intraAppState)) {

if (
location.pathname + location.search === intraAppState.forRoute &&
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looking for feedback on this, I'm thinking when we need search params, we pass them in separately and then keep this comparison to be by pathname only

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree that this should not trigger based off of .search string - only pathname. The page component is the one that should handle retrieving any URL .search params and process on them.

@kevinlog kevinlog changed the title add policy empty state to Host list, placeholder for add endpoint prompt [Endpoint] Add Endpoint empty states for onboarding Jun 24, 2020
@@ -252,6 +325,54 @@ export const HostList = () => {
];
}, [formatUrl, queryParams, search]);

const renderTableOrEmptyState = useMemo(() => {
if (listData && listData.length > 0) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO, the flow works, but some oddities in loading cause "pop ins", gonna introduce some additional loading states

Copy link
Contributor

@paul-tavares paul-tavares left a comment

Choose a reason for hiding this comment

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

Some feedback :)

if (location.pathname === intraAppState.forRoute && !wasHandled.has(intraAppState)) {

if (
location.pathname + location.search === intraAppState.forRoute &&
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree that this should not trigger based off of .search string - only pathname. The page component is the one that should handle retrieving any URL .search params and process on them.

*/
export interface AgentConfigDetailsDeployAgentAction {
/** On cancel, navigate to the given app */
onCancelNavigateTo?: Parameters<ApplicationStart['navigateToApp']>;
Copy link
Contributor

Choose a reason for hiding this comment

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

There are a few buttons/action on the flyout, but you only have onCancel defined here. Is this right? they probably would (in our case) do the same thing, but what about the "proceed" button?

If there is really no need for more than one param, then I would rename it to something else - maybe onDoneNavigateTo??

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yea, right now, everything is just controlled by "Cancel", including the "Continue" option. That name makes sense, will change.

const handleCreatePolicyClick = useNavigateToAppEventHandler<CreateDatasourceRouteState>(
'ingestManager',
{
path: `#/integrations${packageInfo ? `/endpoint-${packageInfo.version}/add-datasource` : ''}`,
Copy link
Contributor

Choose a reason for hiding this comment

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

Just FYI - I'm changing some of this in my PR that is up, so you may end up with some conflicts when the time comes

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I saw your PR, will take care of it 👍

@kevinlog kevinlog marked this pull request as ready for review June 25, 2020 13:00
@kevinlog kevinlog requested a review from a team as a code owner June 25, 2020 13:00
@kevinlog kevinlog requested a review from a team June 25, 2020 13:00
@kevinlog kevinlog requested a review from a team as a code owner June 25, 2020 13:00
@botelastic botelastic bot added the Team:Fleet Team label for Observability Data Collection Fleet team label Jun 25, 2020
@elasticmachine
Copy link
Contributor

Pinging @elastic/ingest-management (Team:Ingest Management)

@kevinlog kevinlog added release_note:skip Skip the PR/issue when compiling release notes v7.9.0 v8.0.0 labels Jun 25, 2020
@elasticmachine
Copy link
Contributor

Pinging @elastic/endpoint-management (Team:Endpoint Management)

@kevinlog
Copy link
Contributor Author

@elasticmachine merge upstream

* Supported routing state for the agent config details page routes with deploy agents action
*/
export interface AgentConfigDetailsDeployAgentAction {
/** On cancel, navigate to the given app */
Copy link
Contributor

Choose a reason for hiding this comment

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

Update comment (remove "cancel")

() => [
{
title: i18n.translate('xpack.securitySolution.endpoint.policyList.stepOneTitle', {
defaultMessage: 'Head over to Ingest Manager.',
Copy link
Contributor

Choose a reason for hiding this comment

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

I know we're still nailing down the labels, but remove period at the end? ( 😬 ) (some for labels below)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll update all of the labels after we finalize the content for the empty states

return (
<div data-test-subj={dataTestSubj}>
{loading ? (
<EuiProgress size="xs" color="accent" className="essentialAnimation" />
Copy link
Contributor

Choose a reason for hiding this comment

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

I take back my prior comment about using the Progress bar - I thought you had content inside of it, so that it would just show a small animation a top of the content that scrolled across the width of the area (similar to the animation on tables while loading)

For this use case, where all the user sees is a "busy animation", I would suggest going with the Loading component


// No hosts, so we should check to see if there are policies for onboarding
if (hostResponse && hostResponse.hosts.length === 0) {
const http = coreStart.http;
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe this should be defined at top - below line 17 - and used trough out this module

@@ -132,6 +165,8 @@ export const hostListReducer: ImmutableReducer<HostState, AppAction> = (
error: undefined,
detailsError: undefined,
policyResponseError: undefined,
loading: true,
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm actually confused by this return here (unrelated to your change), which may be causing the state to be updated on every URL change.

I think the return here should be to reset the state for hosts if the user is leaving the Endpoints page (list and details) and if not leaving, then do nothing (just return state).

so maybe something like:

if (
  (wasPreviouslyOnListPage || wasPreviouslyOnDetailsPage) &&
  !isCurrentlyOnListPage && 
  !iscurrentlyOnDetailsPage
) {
  return initialHostListState();
}

return state;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm actually removing this as it's what's causing the test failures

if (location.pathname === intraAppState.forRoute && !wasHandled.has(intraAppState)) {

// Compare the routes only, strip any query params
const forBaseRoute = intraAppState.forRoute.split('?')[0];
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe move the parsing of the hash to line 31 since it would mean it only gets parsed once

@@ -28,7 +28,7 @@ export const IntraAppStateProvider = memo<{
}>(({ kibanaScopedHistory, children }) => {
const internalAppToAppState = useMemo<IntraAppState>(() => {
return {
forRoute: kibanaScopedHistory.location.hash.substr(1),
forRoute: kibanaScopedHistory.location.hash.substr(1).split('?')[0],
Copy link
Contributor

Choose a reason for hiding this comment

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

It's not clear to me what's happening here, and I'm a bit uneasy with the chain of string operations on the url.

After playing around a bit I think I see the intent and, IIUC, I'd prefer something more explicit/safe like URL which is available in the browser and node

const route = kibanaScopedHistory.location.hash.substr(1);
const url = new URL(route, kibanaScopedHistory.location);
...
    forRoute: url.pathname

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jfsiii @paul-tavares I solved this by just passing in the url base path as well from the state so that we can control it from there. It saves us from having to parse potential query params and is more exact. let me know your thoughts

Comment on lines 58 to 59
const openEnrollmentFlyoutOpenByDefault =
queryString.parse(useLocation().search).openEnrollmentFlyout === 'true';
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we use URLSearchParams here, instead?

Suggested change
const openEnrollmentFlyoutOpenByDefault =
queryString.parse(useLocation().search).openEnrollmentFlyout === 'true';
const queryParams = new URLSearchParams(useLocation().search);
const openEnrollmentFlyoutOpenByDefault = queryParams.get('openEnrollmentFlyout') === 'true';

Copy link
Contributor

Choose a reason for hiding this comment

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

FYI - If I remember correctly, the last time I tried to use URLSearchParams I had issues in the Jest UT environment because I think jsdom did support it. I don't believe we have UT for these UIR components, so it maybe find to use it for now

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just pulled in the URLSearchParams as opposed to query-string and yes this doesn't run in Jest UT right now afaik. I plan to use functional tests when our host tests are re-enabled, so we'll see what happens.

Copy link
Contributor

@jfsiii jfsiii left a comment

Choose a reason for hiding this comment

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

Did an initial scan and left some notes about how we deal with URLs. I can come back later and do a deeper review.

@kevinlog
Copy link
Contributor Author

@jfsiii addressed your initial comments, let me know what else you find. Thanks for helping out!

return {
forRoute: kibanaScopedHistory.location.hash.substr(1),
routeState: kibanaScopedHistory.location.state as AnyIntraAppRouteState,
forRoute: intraAppState && intraAppState.baseRoute ? intraAppState.baseRoute : '',
Copy link
Contributor

Choose a reason for hiding this comment

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

I'll defer to @jfsiii here, but personally I think we should parse the hash and remove the search params from it (if any), for a few reason:

  1. This "bridge" solution here is temporary and should be looked at as a work-around to the fact that Hash router is still being used. So when this is removed (when Ingest moves to using BrowserRouter and the ScoppedHistory provided by Kibana's mount params), everything should just work normally (™️ famous words 😬 )
  2. It feels wrong to have the Route interface state params define what basePath the state applies to, being that we're already redirecting to a specific location. I feel that the Route state params should be meant for the View that it displays, not for the intermediate process that bridges the two routing approaches
  3. Adding this param in the state will further couple the provider with Ingest in having to know/define the baseRoute for the Route

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jfsiii @paul-tavares my original apprehension to using new URL() is that I didn't see a way to get the protocol required to create the object without accessing window.location or just faking one. I wasn't seeing it in the kibanaScopedHistory object anywhere.

After thinking about it, my understanding is that our goal here is to just use a library for URL parsing to be more safe, which makes complete sense. So, I propose we just fake the protocol so that we're able to properly create the URL object and strip out the pathname

I'm doing this locally which is working right now. As @paul-tavares said, we could back this out when we're no longer using a hashRouter.

forRoute: (new URL(`https://localhost${kibanaScopedHistory.location.hash.substr(1)}`)).pathname

Copy link
Contributor

Choose a reason for hiding this comment

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

@kevinlog The URL constructor has the signature

const url = new URL(url [, base])

If we have a relative URL and are only after the pathname I think we can follow the example from

const removeRelativePath = (relativePath: string): string =>
new URL(relativePath, 'http://example.com').pathname;

e.g.

const route = kibanaScopedHistory.location.hash.substr(1);
// url will have protocol, port, etc from second arg, but we're ignoring them
const url = new URL(route, 'http://any.tld.works'); 

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jfsiii sounds good, I built the URL as above - it's all working as expected

@ph ph requested a review from jfsiii June 26, 2020 13:39
routeState: kibanaScopedHistory.location.state as AnyIntraAppRouteState,
};
}, [kibanaScopedHistory.location.hash, kibanaScopedHistory.location.state]);
}, [kibanaScopedHistory.location.state, kibanaScopedHistory.location.hash]);
Copy link
Contributor

Choose a reason for hiding this comment

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

spiderman_meme.jpg

Copy link
Contributor

@jfsiii jfsiii left a comment

Choose a reason for hiding this comment

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

Approving in the spirit of iterative improvement and since my concerns were addressed but I only did a "linter"-type review. I didn't check it out and run it locally or think much about the way the parts interact.

@kevinlog
Copy link
Contributor Author

@elasticmachine merge upstream

@kibanamachine
Copy link
Contributor

💚 Build Succeeded

Build metrics

✅ unchanged

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

@kevinlog kevinlog merged commit 5236335 into elastic:master Jun 26, 2020
@kevinlog kevinlog deleted the task/host-list-empty-states branch June 26, 2020 22:08
gmmorris added a commit to gmmorris/kibana that referenced this pull request Jun 29, 2020
* master: (59 commits)
  [Lens] Fix broken test (elastic#70117)
  [SIEM] Import timeline fix (elastic#65448)
  [SECURITY SOLUTION][INGEST] UX update for ingest manager edit/create datasource for endpoint (elastic#70079)
  [Telemetry] Collector Schema (elastic#64942)
  [Endpoint] Add Endpoint empty states for onboarding (elastic#69626)
  Hide unused resolver buttons (elastic#70112)
  [Security] `Investigate in Resolver` Timeline Integration (elastic#70111)
  [Discover] Improve styling of graphs in sidebar (elastic#69440)
  [Metrics UI] Fix EuiTheme type issue (elastic#69735)
  skip failing suite (elastic#70104) (elastic#70103)
  [ENDPOINT] Hide the Timeline Flyout while on the Management Pages (elastic#69998)
  [SIEM][CASE] Persist callout when dismissed (elastic#68372)
  [SIEM][Exceptions] - Cleaned up and updated exception list item comment structure (elastic#69532)
  [Maps] remove indexing state from redux (elastic#69765)
  Add API integration test for deleting data streams. (elastic#70020)
  renames SIEM to Security Solution (elastic#70070)
  Adding saved_objects_page in OSS (elastic#69900)
  [Lens] Use accordion menus in field list for available and empty fields (elastic#68871)
  Dynamic uiActions & license support (elastic#68507)
  [SIEM] Update readme for timeline apis (elastic#67038)
  ...
kevinlog added a commit that referenced this pull request Jun 29, 2020
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
gmmorris added a commit to gmmorris/kibana that referenced this pull request Jun 29, 2020
…bana into alerting/consumer-based-rbac

* 'alerting/consumer-based-rbac' of github.com:gmmorris/kibana: (25 commits)
  [Lens] Fix broken test (elastic#70117)
  [SIEM] Import timeline fix (elastic#65448)
  [SECURITY SOLUTION][INGEST] UX update for ingest manager edit/create datasource for endpoint (elastic#70079)
  [Telemetry] Collector Schema (elastic#64942)
  [Endpoint] Add Endpoint empty states for onboarding (elastic#69626)
  Hide unused resolver buttons (elastic#70112)
  [Security] `Investigate in Resolver` Timeline Integration (elastic#70111)
  [Discover] Improve styling of graphs in sidebar (elastic#69440)
  [Metrics UI] Fix EuiTheme type issue (elastic#69735)
  skip failing suite (elastic#70104) (elastic#70103)
  [ENDPOINT] Hide the Timeline Flyout while on the Management Pages (elastic#69998)
  [SIEM][CASE] Persist callout when dismissed (elastic#68372)
  [SIEM][Exceptions] - Cleaned up and updated exception list item comment structure (elastic#69532)
  [Maps] remove indexing state from redux (elastic#69765)
  Add API integration test for deleting data streams. (elastic#70020)
  renames SIEM to Security Solution (elastic#70070)
  Adding saved_objects_page in OSS (elastic#69900)
  [Lens] Use accordion menus in field list for available and empty fields (elastic#68871)
  Dynamic uiActions & license support (elastic#68507)
  [SIEM] Update readme for timeline apis (elastic#67038)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release_note:skip Skip the PR/issue when compiling release notes Team:Fleet Team label for Observability Data Collection Fleet team v7.9.0 v8.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants