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
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f9982f1
add policy empty state to Host list, placeholder for add endpoint prompt
kevinlog Jun 19, 2020
a319fc1
add empty endpoint state
kevinlog Jun 22, 2020
d203642
fix types
kevinlog Jun 22, 2020
08afb5a
fix test, hide total when empty state
kevinlog Jun 22, 2020
899f0cf
Merge branch 'master' of github.com:kevinlog/kibana into task/host-li…
kevinlog Jun 22, 2020
4316454
fix types
kevinlog Jun 22, 2020
08bbc88
fix test
kevinlog Jun 22, 2020
6260e5e
add policy selection to onboarding
kevinlog Jun 23, 2020
1c55b23
begin unit tests
kevinlog Jun 23, 2020
f5e3999
additional tests
kevinlog Jun 24, 2020
eb4b259
add URL and roundtrip
kevinlog Jun 24, 2020
b9129eb
fix bug, add type
kevinlog Jun 24, 2020
a93a1fa
refactor middleware
kevinlog Jun 24, 2020
3206882
fix selection bug
kevinlog Jun 24, 2020
439e8e9
Merge branch 'master' of github.com:kevinlog/kibana into task/host-li…
kevinlog Jun 24, 2020
e683508
refactor route path check
kevinlog Jun 25, 2020
7f656f8
make loading smoother, fix types
kevinlog Jun 25, 2020
71690bf
Merge branch 'master' into task/host-list-empty-states
elasticmachine Jun 25, 2020
310b8ef
address comments, fix tests
kevinlog Jun 25, 2020
3529c1c
Merge branch 'master' of github.com:kevinlog/kibana into task/host-li…
kevinlog Jun 25, 2020
f364e74
address comments
kevinlog Jun 25, 2020
8399f0e
fix test
kevinlog Jun 25, 2020
18dd954
use URL, take out baseRoute
kevinlog Jun 26, 2020
9689d04
Merge branch 'master' of github.com:kevinlog/kibana into task/host-li…
kevinlog Jun 26, 2020
d425f46
Merge branch 'master' into task/host-list-empty-states
elasticmachine Jun 26, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ export const IntraAppStateProvider = memo<{
children: React.ReactNode;
}>(({ kibanaScopedHistory, children }) => {
const internalAppToAppState = useMemo<IntraAppState>(() => {
const intraAppState = kibanaScopedHistory.location.state as AnyIntraAppRouteState;
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

routeState: intraAppState,
};
}, [kibanaScopedHistory.location.hash, kibanaScopedHistory.location.state]);
}, [kibanaScopedHistory.location.state]);
return (
<IntraAppStateContext.Provider value={internalAppToAppState}>
{children}
Expand All @@ -57,6 +58,7 @@ export function useIntraAppState<S = AnyIntraAppRouteState>():
// once so that it does not impact navigation to the page from within the
// ingest app. side affect is that the browser back button would not work
// consistently either.

if (location.pathname === intraAppState.forRoute && !wasHandled.has(intraAppState)) {
wasHandled.add(intraAppState);
return intraAppState.routeState as S;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { memo, useState } from 'react';
import React, { memo, useState, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiContextMenuItem, EuiPortal } from '@elastic/eui';
import { AgentConfig } from '../../../types';
Expand All @@ -17,86 +17,106 @@ export const AgentConfigActionMenu = memo<{
config: AgentConfig;
onCopySuccess?: (newAgentConfig: AgentConfig) => void;
fullButton?: boolean;
}>(({ config, onCopySuccess, fullButton = false }) => {
const hasWriteCapabilities = useCapabilities().write;
const [isYamlFlyoutOpen, setIsYamlFlyoutOpen] = useState<boolean>(false);
const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState<boolean>(false);
enrollmentFlyoutOpenByDefault?: boolean;
onCancelEnrollment?: () => void;
}>(
({
config,
onCopySuccess,
fullButton = false,
enrollmentFlyoutOpenByDefault = false,
onCancelEnrollment,
}) => {
const hasWriteCapabilities = useCapabilities().write;
const [isYamlFlyoutOpen, setIsYamlFlyoutOpen] = useState<boolean>(false);
const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState<boolean>(
enrollmentFlyoutOpenByDefault
);

return (
<AgentConfigCopyProvider>
{(copyAgentConfigPrompt) => {
return (
<>
{isYamlFlyoutOpen ? (
<EuiPortal>
<ConfigYamlFlyout configId={config.id} onClose={() => setIsYamlFlyoutOpen(false)} />
</EuiPortal>
) : null}
{isEnrollmentFlyoutOpen && (
<EuiPortal>
<AgentEnrollmentFlyout
agentConfigs={[config]}
onClose={() => setIsEnrollmentFlyoutOpen(false)}
/>
</EuiPortal>
)}
<ContextMenuActions
button={
fullButton
? {
props: {
iconType: 'arrowDown',
iconSide: 'right',
},
children: (
<FormattedMessage
id="xpack.ingestManager.agentConfigActionMenu.buttonText"
defaultMessage="Actions"
/>
),
}
: undefined
}
items={[
<EuiContextMenuItem
disabled={!hasWriteCapabilities}
icon="plusInCircle"
onClick={() => setIsEnrollmentFlyoutOpen(true)}
key="enrollAgents"
>
<FormattedMessage
id="xpack.ingestManager.agentConfigActionMenu.enrollAgentActionText"
defaultMessage="Enroll agent"
/>
</EuiContextMenuItem>,
<EuiContextMenuItem
icon="inspect"
onClick={() => setIsYamlFlyoutOpen(!isYamlFlyoutOpen)}
key="viewConfig"
>
<FormattedMessage
id="xpack.ingestManager.agentConfigActionMenu.viewConfigText"
defaultMessage="View config"
/>
</EuiContextMenuItem>,
<EuiContextMenuItem
disabled={!hasWriteCapabilities}
icon="copy"
onClick={() => {
copyAgentConfigPrompt(config, onCopySuccess);
}}
key="copyConfig"
>
<FormattedMessage
id="xpack.ingestManager.agentConfigActionMenu.copyConfigActionText"
defaultMessage="Copy config"
const onClose = useMemo(() => {
if (onCancelEnrollment) {
return onCancelEnrollment;
} else {
return () => setIsEnrollmentFlyoutOpen(false);
}
}, [onCancelEnrollment, setIsEnrollmentFlyoutOpen]);

return (
<AgentConfigCopyProvider>
{(copyAgentConfigPrompt) => {
return (
<>
{isYamlFlyoutOpen ? (
<EuiPortal>
<ConfigYamlFlyout
configId={config.id}
onClose={() => setIsYamlFlyoutOpen(false)}
/>
</EuiContextMenuItem>,
]}
/>
</>
);
}}
</AgentConfigCopyProvider>
);
});
</EuiPortal>
) : null}
{isEnrollmentFlyoutOpen && (
<EuiPortal>
<AgentEnrollmentFlyout agentConfigs={[config]} onClose={onClose} />
</EuiPortal>
)}
<ContextMenuActions
button={
fullButton
? {
props: {
iconType: 'arrowDown',
iconSide: 'right',
},
children: (
<FormattedMessage
id="xpack.ingestManager.agentConfigActionMenu.buttonText"
defaultMessage="Actions"
/>
),
}
: undefined
}
items={[
<EuiContextMenuItem
disabled={!hasWriteCapabilities}
icon="plusInCircle"
onClick={() => setIsEnrollmentFlyoutOpen(true)}
key="enrollAgents"
>
<FormattedMessage
id="xpack.ingestManager.agentConfigActionMenu.enrollAgentActionText"
defaultMessage="Enroll agent"
/>
</EuiContextMenuItem>,
<EuiContextMenuItem
icon="inspect"
onClick={() => setIsYamlFlyoutOpen(!isYamlFlyoutOpen)}
key="viewConfig"
>
<FormattedMessage
id="xpack.ingestManager.agentConfigActionMenu.viewConfigText"
defaultMessage="View config"
/>
</EuiContextMenuItem>,
<EuiContextMenuItem
disabled={!hasWriteCapabilities}
icon="copy"
onClick={() => {
copyAgentConfigPrompt(config, onCopySuccess);
}}
key="copyConfig"
>
<FormattedMessage
id="xpack.ingestManager.agentConfigActionMenu.copyConfigActionText"
defaultMessage="Copy config"
/>
</EuiContextMenuItem>,
]}
/>
</>
);
}}
</AgentConfigCopyProvider>
);
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useMemo, useState } from 'react';
import { Redirect, useRouteMatch, Switch, Route, useHistory } from 'react-router-dom';
import React, { useMemo, useState, useCallback } from 'react';
import { Redirect, useRouteMatch, Switch, Route, useHistory, useLocation } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { FormattedMessage, FormattedDate } from '@kbn/i18n/react';
import {
Expand All @@ -21,14 +21,15 @@ import {
} from '@elastic/eui';
import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab';
import styled from 'styled-components';
import { AgentConfig } from '../../../types';
import { AgentConfig, AgentConfigDetailsDeployAgentAction } from '../../../types';
import { PAGE_ROUTING_PATHS } from '../../../constants';
import { useGetOneAgentConfig, useLink, useBreadcrumbs } from '../../../hooks';
import { useGetOneAgentConfig, useLink, useBreadcrumbs, useCore } from '../../../hooks';
import { Loading } from '../../../components';
import { WithHeaderLayout } from '../../../layouts';
import { ConfigRefreshContext, useGetAgentStatus, AgentStatusRefreshContext } from './hooks';
import { LinkedAgentCount, AgentConfigActionMenu } from '../components';
import { ConfigDatasourcesView, ConfigSettingsView } from './components';
import { useIntraAppState } from '../../../hooks/use_intra_app_state';

const Divider = styled.div`
width: 0;
Expand All @@ -48,7 +49,13 @@ export const AgentConfigDetailsPage: React.FunctionComponent = () => {
const [redirectToAgentConfigList] = useState<boolean>(false);
const agentStatusRequest = useGetAgentStatus(configId);
const { refreshAgentStatus } = agentStatusRequest;
const {
application: { navigateToApp },
} = useCore();
const routeState = useIntraAppState<AgentConfigDetailsDeployAgentAction>();
const agentStatus = agentStatusRequest.data?.results;
const queryParams = new URLSearchParams(useLocation().search);
const openEnrollmentFlyoutOpenByDefault = queryParams.get('openEnrollmentFlyout') === 'true';

const headerLeftContent = useMemo(
() => (
Expand Down Expand Up @@ -95,6 +102,12 @@ export const AgentConfigDetailsPage: React.FunctionComponent = () => {
[getHref, agentConfig, configId]
);

const enrollmentCancelClickHandler = useCallback(() => {
if (routeState && routeState.onDoneNavigateTo) {
navigateToApp(routeState.onDoneNavigateTo[0], routeState.onDoneNavigateTo[1]);
}
}, [routeState, navigateToApp]);

const headerRightContent = useMemo(
() => (
<EuiFlexGroup justifyContent={'flexEnd'} direction="row">
Expand Down Expand Up @@ -155,6 +168,12 @@ export const AgentConfigDetailsPage: React.FunctionComponent = () => {
onCopySuccess={(newAgentConfig: AgentConfig) => {
history.push(getPath('configuration_details', { configId: newAgentConfig.id }));
}}
enrollmentFlyoutOpenByDefault={openEnrollmentFlyoutOpenByDefault}
onCancelEnrollment={
routeState && routeState.onDoneNavigateTo
? enrollmentCancelClickHandler
: undefined
}
/>
),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,23 @@ export interface CreateDatasourceRouteState {
onCancelNavigateTo?: Parameters<ApplicationStart['navigateToApp']>;
/** Url to be used on cancel links */
onCancelUrl?: string;
/** The base route */
baseRoute?: string;
}

/**
* Supported routing state for the agent config details page routes with deploy agents action
*/
export interface AgentConfigDetailsDeployAgentAction {
/** On done, navigate to the given app */
onDoneNavigateTo?: Parameters<ApplicationStart['navigateToApp']>;
/** The base route */
baseRoute?: string;
}

/**
* All possible Route states.
*/
export type AnyIntraAppRouteState = CreateDatasourceRouteState;
export type AnyIntraAppRouteState =
| CreateDatasourceRouteState
| AgentConfigDetailsDeployAgentAction;
Loading