Feature Controls - Infrastructure and Logging (#31843)

* hide infra/logs apps if disabled via UICapabilities

* adds tests

* adds UICapability tests for infra and log apps

* update expected privilege/action mapping

* adds feature controls security tests for infraHome

* adds infra spaces feature control tests

* remove debug code

* a sample readonly implementation, ignoring 'logs' privileges

* ts fixes

* fix capability expectations

* Removing RequiresUICapability component, since there are no usages

* Driving the source configuration seperately for logs/infrastructure

* Adding infrastructure feature controls security functional tests

* Adding spaces infrastructure tests

* Adding logs functional tests

* Reworking the ui capability tests to be more consistent

* Fixing privileges API

* Forcing logout

* Fixing comma issue introduced by merge

* Fix merge conflicts and loading/unloading esarchives more consistently

* Removing unnecessary !!

* Fixing saved object management tests

* Fixing more tests

* Using the new context APIs

* Revert "Using the new context APIs"

This reverts commit 4776f1fc86.

* Adding future version of ui capabilities react provider

* Switching the order of the HOC's for infra and making the future the
default

* Applying Felix's PR feedback

* Protecting Infra's GraphQL APIs

* Updating privileges list

* Using the introspection query

* No longer using apollo context library, rephrasing test descriptions

* Fixing issue introduced by merge conflict, I forgot a }

* Putting back missplaced data test subj
This commit is contained in:
Brandon Kobel 2019-03-11 06:42:44 -07:00 committed by GitHub
parent bb76d20bf5
commit e79d63b5d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 2679 additions and 680 deletions

View file

@ -0,0 +1,22 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { UICapabilitiesProvider } from './ui_capabilities_provider';
export { injectUICapabilities } from './inject_ui_capabilities';
export { UICapabilities } from '../ui_capabilities';

View file

@ -0,0 +1,117 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
jest.mock('ui/chrome', () => ({
getInjected(key: string) {
if (key === 'uiCapabilities') {
return {
uiCapability1: true,
uiCapability2: {
nestedProp: 'nestedValue',
},
};
}
},
}));
import { mount } from 'enzyme';
import React from 'react';
import { UICapabilities } from '..';
import { injectUICapabilities } from './inject_ui_capabilities';
import { UICapabilitiesProvider } from './ui_capabilities_provider';
describe('injectUICapabilities', () => {
it('provides UICapabilities to SFCs', () => {
interface SFCProps {
uiCapabilities: UICapabilities;
}
const MySFC = injectUICapabilities(({ uiCapabilities }: SFCProps) => {
return <span>{uiCapabilities.uiCapability2.nestedProp}</span>;
});
const wrapper = mount(
<UICapabilitiesProvider>
<MySFC />
</UICapabilitiesProvider>
);
expect(wrapper).toMatchInlineSnapshot(`
<UICapabilitiesProvider>
<InjectUICapabilities(Component)>
<Component
uiCapabilities={
Object {
"uiCapability1": true,
"uiCapability2": Object {
"nestedProp": "nestedValue",
},
}
}
>
<span>
nestedValue
</span>
</Component>
</InjectUICapabilities(Component)>
</UICapabilitiesProvider>
`);
});
it('provides UICapabilities to class components', () => {
interface ClassProps {
uiCapabilities: UICapabilities;
}
class MyClassComponent extends React.Component<ClassProps, {}> {
public render() {
return <span>{this.props.uiCapabilities.uiCapability2.nestedProp}</span>;
}
}
const WrappedComponent = injectUICapabilities(MyClassComponent);
const wrapper = mount(
<UICapabilitiesProvider>
<WrappedComponent />
</UICapabilitiesProvider>
);
expect(wrapper).toMatchInlineSnapshot(`
<UICapabilitiesProvider>
<InjectUICapabilities(MyClassComponent)>
<MyClassComponent
uiCapabilities={
Object {
"uiCapability1": true,
"uiCapability2": Object {
"nestedProp": "nestedValue",
},
}
}
>
<span>
nestedValue
</span>
</MyClassComponent>
</InjectUICapabilities(MyClassComponent)>
</UICapabilitiesProvider>
`);
});
});

View file

@ -0,0 +1,53 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { Component, ComponentClass, ComponentType } from 'react';
import { UICapabilities } from '../ui_capabilities';
import { UICapabilitiesContext } from './ui_capabilities_context';
function getDisplayName(component: ComponentType<any>) {
return component.displayName || component.name || 'Component';
}
interface InjectedProps {
uiCapabilities: UICapabilities;
}
export function injectUICapabilities<P>(
WrappedComponent: ComponentType<P & InjectedProps>
): ComponentClass<Pick<P, Exclude<keyof P, keyof InjectedProps>>> & {
WrappedComponent: ComponentType<P & InjectedProps>;
} {
class InjectUICapabilities extends Component<P, any> {
public static displayName = `InjectUICapabilities(${getDisplayName(WrappedComponent)})`;
public static WrappedComponent: ComponentType<P & InjectedProps> = WrappedComponent;
public static contextType = UICapabilitiesContext;
constructor(props: any, context: any) {
super(props, context);
}
public render() {
return <WrappedComponent {...this.props} {...{ uiCapabilities: this.context }} />;
}
}
return InjectUICapabilities;
}

View file

@ -0,0 +1,22 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { UICapabilitiesProvider } from './ui_capabilities_provider';
export { injectUICapabilities } from './inject_ui_capabilities';
export { UICapabilities } from '../../ui_capabilities';

View file

@ -0,0 +1,117 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
jest.mock('ui/chrome', () => ({
getInjected(key: string) {
if (key === 'uiCapabilities') {
return {
uiCapability1: true,
uiCapability2: {
nestedProp: 'nestedValue',
},
};
}
},
}));
import { mount } from 'enzyme';
import React from 'react';
import { UICapabilities } from '..';
import { injectUICapabilities } from './inject_ui_capabilities';
import { UICapabilitiesProvider } from './ui_capabilities_provider';
describe('injectUICapabilities', () => {
it('provides UICapabilities to SFCs', () => {
interface SFCProps {
uiCapabilities: UICapabilities;
}
const MySFC = injectUICapabilities(({ uiCapabilities }: SFCProps) => {
return <span>{uiCapabilities.uiCapability2.nestedProp}</span>;
});
const wrapper = mount(
<UICapabilitiesProvider>
<MySFC />
</UICapabilitiesProvider>
);
expect(wrapper).toMatchInlineSnapshot(`
<UICapabilitiesProvider>
<InjectUICapabilities(Component)>
<Component
uiCapabilities={
Object {
"uiCapability1": true,
"uiCapability2": Object {
"nestedProp": "nestedValue",
},
}
}
>
<span>
nestedValue
</span>
</Component>
</InjectUICapabilities(Component)>
</UICapabilitiesProvider>
`);
});
it('provides UICapabilities to class components', () => {
interface ClassProps {
uiCapabilities: UICapabilities;
}
class MyClassComponent extends React.Component<ClassProps, {}> {
public render() {
return <span>{this.props.uiCapabilities.uiCapability2.nestedProp}</span>;
}
}
const WrappedComponent = injectUICapabilities(MyClassComponent);
const wrapper = mount(
<UICapabilitiesProvider>
<WrappedComponent />
</UICapabilitiesProvider>
);
expect(wrapper).toMatchInlineSnapshot(`
<UICapabilitiesProvider>
<InjectUICapabilities(MyClassComponent)>
<MyClassComponent
uiCapabilities={
Object {
"uiCapability1": true,
"uiCapability2": Object {
"nestedProp": "nestedValue",
},
}
}
>
<span>
nestedValue
</span>
</MyClassComponent>
</InjectUICapabilities(MyClassComponent)>
</UICapabilitiesProvider>
`);
});
});

View file

@ -0,0 +1,57 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import PropTypes from 'prop-types';
import React, { Component, ComponentClass, ComponentType } from 'react';
import { UICapabilities } from '../../ui_capabilities';
function getDisplayName(component: ComponentType<any>) {
return component.displayName || component.name || 'Component';
}
interface InjectedProps {
uiCapabilities: UICapabilities;
}
export function injectUICapabilities<P>(
WrappedComponent: ComponentType<P & InjectedProps>
): ComponentClass<Pick<P, Exclude<keyof P, keyof InjectedProps>>> & {
WrappedComponent: ComponentType<P & InjectedProps>;
} {
class InjectUICapabilities extends Component<P, any> {
public static displayName = `InjectUICapabilities(${getDisplayName(WrappedComponent)})`;
public static WrappedComponent: ComponentType<P & InjectedProps> = WrappedComponent;
public static contextTypes = {
uiCapabilities: PropTypes.object.isRequired,
};
constructor(props: any, context: any) {
super(props, context);
}
public render() {
return (
<WrappedComponent {...this.props} {...{ uiCapabilities: this.context.uiCapabilities }} />
);
}
}
return InjectUICapabilities;
}

View file

@ -0,0 +1,48 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import PropTypes from 'prop-types';
import React, { ReactNode } from 'react';
import { uiCapabilities, UICapabilities } from '../../ui_capabilities';
interface Props {
children: ReactNode;
}
interface ProviderContext {
uiCapabilities: UICapabilities;
}
export class UICapabilitiesProvider extends React.Component<Props, {}> {
public static displayName: string = 'UICapabilitiesProvider';
public static childContextTypes = {
uiCapabilities: PropTypes.object.isRequired,
};
public getChildContext(): ProviderContext {
return {
uiCapabilities,
};
}
public render() {
return React.Children.only(this.props.children);
}
}

View file

@ -0,0 +1,27 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { UICapabilities } from '../ui_capabilities';
export const UICapabilitiesContext = React.createContext<UICapabilities>({
navLinks: {},
catalogue: {},
management: {},
});

View file

@ -0,0 +1,38 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { ReactNode } from 'react';
import { uiCapabilities } from '../ui_capabilities';
import { UICapabilitiesContext } from './ui_capabilities_context';
interface Props {
children: ReactNode;
}
export class UICapabilitiesProvider extends React.Component<Props, {}> {
public static displayName: string = 'UICapabilitiesProvider';
public render() {
return (
<UICapabilitiesContext.Provider value={uiCapabilities}>
{this.props.children}
</UICapabilitiesContext.Provider>
);
}
}

View file

@ -92,7 +92,7 @@ export function apm(kibana: any) {
all: [],
read: ['config']
},
ui: []
ui: ['show']
}
},
privilegesTooltip: i18n.translate('xpack.apm.privileges.tooltip', {

View file

@ -23,7 +23,7 @@ const MainContainer = styled.div`
export function Main() {
return (
<MainContainer>
<MainContainer data-test-subj="apmMainContainer">
<UpdateBreadcrumbs />
<Route component={ConnectRouterToRedux} />
<Route component={ScrollToTopOnPathChange} />

View file

@ -17,6 +17,7 @@ import { ThemeProvider } from 'styled-components';
import { EuiErrorBoundary } from '@elastic/eui';
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { UICapabilitiesProvider } from 'ui/capabilities/react';
import { I18nContext } from 'ui/i18n';
import { InfraFrontendLibs } from '../lib/lib';
import { PageRouter } from '../routes';
@ -33,22 +34,24 @@ export async function startApp(libs: InfraFrontendLibs) {
libs.framework.render(
<I18nContext>
<EuiErrorBoundary>
<ConstateProvider devtools>
<ReduxStoreProvider store={store}>
<ApolloProvider client={libs.apolloClient}>
<ThemeProvider
theme={() => ({
eui: libs.framework.darkMode ? euiDarkVars : euiLightVars,
darkMode: libs.framework.darkMode,
})}
>
<PageRouter history={history} />
</ThemeProvider>
</ApolloProvider>
</ReduxStoreProvider>
</ConstateProvider>
</EuiErrorBoundary>
<UICapabilitiesProvider>
<EuiErrorBoundary>
<ConstateProvider devtools>
<ReduxStoreProvider store={store}>
<ApolloProvider client={libs.apolloClient}>
<ThemeProvider
theme={() => ({
eui: libs.framework.darkMode ? euiDarkVars : euiLightVars,
darkMode: libs.framework.darkMode,
})}
>
<PageRouter history={history} />
</ThemeProvider>
</ApolloProvider>
</ReduxStoreProvider>
</ConstateProvider>
</EuiErrorBoundary>
</UICapabilitiesProvider>
</I18nContext>
);
}

View file

@ -14,6 +14,7 @@ interface FieldsConfigurationPanelProps {
containerFieldProps: InputFieldProps;
hostFieldProps: InputFieldProps;
isLoading: boolean;
readOnly: boolean;
podFieldProps: InputFieldProps;
tiebreakerFieldProps: InputFieldProps;
timestampFieldProps: InputFieldProps;
@ -23,6 +24,7 @@ export const FieldsConfigurationPanel = ({
containerFieldProps,
hostFieldProps,
isLoading,
readOnly,
podFieldProps,
tiebreakerFieldProps,
timestampFieldProps,
@ -57,7 +59,13 @@ export const FieldsConfigurationPanel = ({
/>
}
>
<EuiFieldText fullWidth disabled={isLoading} isLoading={isLoading} {...timestampFieldProps} />
<EuiFieldText
fullWidth
disabled={isLoading}
readOnly={readOnly}
isLoading={isLoading}
{...timestampFieldProps}
/>
</EuiFormRow>
<EuiFormRow
error={tiebreakerFieldProps.error}
@ -82,6 +90,7 @@ export const FieldsConfigurationPanel = ({
<EuiFieldText
fullWidth
disabled={isLoading}
readOnly={readOnly}
isLoading={isLoading}
{...tiebreakerFieldProps}
/>
@ -106,7 +115,13 @@ export const FieldsConfigurationPanel = ({
/>
}
>
<EuiFieldText fullWidth disabled={isLoading} isLoading={isLoading} {...containerFieldProps} />
<EuiFieldText
fullWidth
disabled={isLoading}
readOnly={readOnly}
isLoading={isLoading}
{...containerFieldProps}
/>
</EuiFormRow>
<EuiFormRow
error={hostFieldProps.error}
@ -128,7 +143,13 @@ export const FieldsConfigurationPanel = ({
/>
}
>
<EuiFieldText fullWidth disabled={isLoading} isLoading={isLoading} {...hostFieldProps} />
<EuiFieldText
fullWidth
disabled={isLoading}
readOnly={readOnly}
isLoading={isLoading}
{...hostFieldProps}
/>
</EuiFormRow>
<EuiFormRow
error={podFieldProps.error}
@ -150,7 +171,13 @@ export const FieldsConfigurationPanel = ({
/>
}
>
<EuiFieldText fullWidth disabled={isLoading} isLoading={isLoading} {...podFieldProps} />
<EuiFieldText
fullWidth
disabled={isLoading}
readOnly={readOnly}
isLoading={isLoading}
{...podFieldProps}
/>
</EuiFormRow>
</EuiForm>
);

View file

@ -12,12 +12,14 @@ import { InputFieldProps } from './source_configuration_form_state';
interface IndicesConfigurationPanelProps {
isLoading: boolean;
readOnly: boolean;
logAliasFieldProps: InputFieldProps;
metricAliasFieldProps: InputFieldProps;
}
export const IndicesConfigurationPanel = ({
isLoading,
readOnly,
logAliasFieldProps,
metricAliasFieldProps,
}: IndicesConfigurationPanelProps) => (
@ -54,6 +56,7 @@ export const IndicesConfigurationPanel = ({
<EuiFieldText
fullWidth
disabled={isLoading}
readOnly={readOnly}
isLoading={isLoading}
{...metricAliasFieldProps}
/>
@ -78,7 +81,13 @@ export const IndicesConfigurationPanel = ({
/>
}
>
<EuiFieldText fullWidth disabled={isLoading} isLoading={isLoading} {...logAliasFieldProps} />
<EuiFieldText
fullWidth
disabled={isLoading}
readOnly={readOnly}
isLoading={isLoading}
{...logAliasFieldProps}
/>
</EuiFormRow>
</EuiForm>
);

View file

@ -12,11 +12,13 @@ import { InputFieldProps } from './source_configuration_form_state';
interface NameConfigurationPanelProps {
isLoading: boolean;
readOnly: boolean;
nameFieldProps: InputFieldProps;
}
export const NameConfigurationPanel = ({
isLoading,
readOnly,
nameFieldProps,
}: NameConfigurationPanelProps) => (
<EuiForm>
@ -37,7 +39,13 @@ export const NameConfigurationPanel = ({
<FormattedMessage id="xpack.infra.sourceConfiguration.nameLabel" defaultMessage="Name" />
}
>
<EuiFieldText fullWidth disabled={isLoading} isLoading={isLoading} {...nameFieldProps} />
<EuiFieldText
fullWidth
disabled={isLoading}
readOnly={readOnly}
isLoading={isLoading}
{...nameFieldProps}
/>
</EuiFormRow>
</EuiForm>
);

View file

@ -30,141 +30,156 @@ const noop = () => undefined;
interface SourceConfigurationFlyoutProps {
intl: InjectedIntl;
shouldAllowEdit: boolean;
}
export const SourceConfigurationFlyout = injectI18n(({ intl }: SourceConfigurationFlyoutProps) => (
<WithSourceConfigurationFlyoutState>
{({ disable: close, value: isVisible }) =>
isVisible ? (
<WithSource>
{({ create, configuration, exists, isLoading, update }) =>
configuration ? (
<WithSourceConfigurationFormState
initialFormState={{
name: configuration.name,
description: configuration.description,
fields: {
container: configuration.fields.container,
host: configuration.fields.host,
message: configuration.fields.message,
pod: configuration.fields.pod,
tiebreaker: configuration.fields.tiebreaker,
timestamp: configuration.fields.timestamp,
},
logAlias: configuration.logAlias,
metricAlias: configuration.metricAlias,
}}
>
{({
getCurrentFormState,
getNameFieldProps,
getLogAliasFieldProps,
getMetricAliasFieldProps,
getFieldFieldProps,
isFormValid,
resetForm,
updates,
}) => (
<EuiFlyout
aria-labelledby="sourceConfigurationTitle"
hideCloseButton
onClose={noop}
>
<EuiFlyoutHeader>
<EuiTitle>
<h2 id="sourceConfigurationTitle">
<FormattedMessage
id="xpack.infra.sourceConfiguration.sourceConfigurationTitle"
defaultMessage="Configure source"
/>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<NameConfigurationPanel
isLoading={isLoading}
nameFieldProps={getNameFieldProps()}
/>
<EuiSpacer />
<IndicesConfigurationPanel
isLoading={isLoading}
logAliasFieldProps={getLogAliasFieldProps()}
metricAliasFieldProps={getMetricAliasFieldProps()}
/>
<EuiSpacer />
<FieldsConfigurationPanel
containerFieldProps={getFieldFieldProps('container')}
hostFieldProps={getFieldFieldProps('host')}
isLoading={isLoading}
podFieldProps={getFieldFieldProps('pod')}
tiebreakerFieldProps={getFieldFieldProps('tiebreaker')}
timestampFieldProps={getFieldFieldProps('timestamp')}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
{updates.length === 0 ? (
<EuiButtonEmpty
iconType="cross"
isDisabled={isLoading}
onClick={() => close()}
>
export const SourceConfigurationFlyout = injectI18n(
({ intl, shouldAllowEdit }: SourceConfigurationFlyoutProps) => (
<WithSourceConfigurationFlyoutState>
{({ disable: close, value: isVisible }) =>
isVisible ? (
<WithSource>
{({ create, configuration, exists, isLoading, update }) =>
configuration ? (
<WithSourceConfigurationFormState
initialFormState={{
name: configuration.name,
description: configuration.description,
fields: {
container: configuration.fields.container,
host: configuration.fields.host,
message: configuration.fields.message,
pod: configuration.fields.pod,
tiebreaker: configuration.fields.tiebreaker,
timestamp: configuration.fields.timestamp,
},
logAlias: configuration.logAlias,
metricAlias: configuration.metricAlias,
}}
>
{({
getCurrentFormState,
getNameFieldProps,
getLogAliasFieldProps,
getMetricAliasFieldProps,
getFieldFieldProps,
isFormValid,
resetForm,
updates,
}) => (
<EuiFlyout
aria-labelledby="sourceConfigurationTitle"
hideCloseButton
onClose={noop}
>
<EuiFlyoutHeader>
<EuiTitle>
<h2 id="sourceConfigurationTitle">
{shouldAllowEdit ? (
<FormattedMessage
id="xpack.infra.sourceConfiguration.closeButtonLabel"
defaultMessage="Close"
id="xpack.infra.sourceConfiguration.sourceConfigurationTitle"
defaultMessage="Configure source"
/>
</EuiButtonEmpty>
) : (
<EuiButtonEmpty
color="danger"
iconType="cross"
isDisabled={isLoading}
onClick={() => {
resetForm();
close();
}}
>
) : (
<FormattedMessage
id="xpack.infra.sourceConfiguration.discardAndCloseButtonLabel"
defaultMessage="Discard and Close"
id="xpack.infra.sourceConfiguration.sourceConfigurationReadonlyTitle"
defaultMessage="View source configuration"
/>
</EuiButtonEmpty>
)}
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<NameConfigurationPanel
isLoading={isLoading}
readOnly={!shouldAllowEdit}
nameFieldProps={getNameFieldProps()}
/>
<EuiSpacer />
<IndicesConfigurationPanel
isLoading={isLoading}
readOnly={!shouldAllowEdit}
logAliasFieldProps={getLogAliasFieldProps()}
metricAliasFieldProps={getMetricAliasFieldProps()}
/>
<EuiSpacer />
<FieldsConfigurationPanel
containerFieldProps={getFieldFieldProps('container')}
hostFieldProps={getFieldFieldProps('host')}
isLoading={isLoading}
readOnly={!shouldAllowEdit}
podFieldProps={getFieldFieldProps('pod')}
tiebreakerFieldProps={getFieldFieldProps('tiebreaker')}
timestampFieldProps={getFieldFieldProps('timestamp')}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
{updates.length === 0 ? (
<EuiButtonEmpty
iconType="cross"
isDisabled={isLoading}
onClick={() => close()}
>
<FormattedMessage
id="xpack.infra.sourceConfiguration.closeButtonLabel"
defaultMessage="Close"
/>
</EuiButtonEmpty>
) : (
<EuiButtonEmpty
color="danger"
iconType="cross"
isDisabled={isLoading}
onClick={() => {
resetForm();
close();
}}
>
<FormattedMessage
id="xpack.infra.sourceConfiguration.discardAndCloseButtonLabel"
defaultMessage="Discard and Close"
/>
</EuiButtonEmpty>
)}
</EuiFlexItem>
<EuiFlexItem />
{shouldAllowEdit && (
<EuiFlexItem grow={false}>
{isLoading ? (
<EuiButton color="primary" isLoading fill>
Loading
</EuiButton>
) : (
<EuiButton
color="primary"
isDisabled={updates.length === 0 || !isFormValid()}
fill
onClick={() =>
(exists ? update(updates) : create(getCurrentFormState())).then(
() => resetForm()
)
}
>
<FormattedMessage
id="xpack.infra.sourceConfiguration.updateSourceConfigurationButtonLabel"
defaultMessage="Update Source"
/>
</EuiButton>
)}
</EuiFlexItem>
)}
</EuiFlexItem>
<EuiFlexItem />
<EuiFlexItem grow={false}>
{isLoading ? (
<EuiButton color="primary" isLoading fill>
Loading
</EuiButton>
) : (
<EuiButton
color="primary"
isDisabled={updates.length === 0 || !isFormValid()}
fill
onClick={() =>
(exists ? update(updates) : create(getCurrentFormState())).then(
() => resetForm()
)
}
>
<FormattedMessage
id="xpack.infra.sourceConfiguration.updateSourceConfigurationButtonLabel"
defaultMessage="Update Source"
/>
</EuiButton>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
)}
</WithSourceConfigurationFormState>
) : null
}
</WithSource>
) : null
}
</WithSourceConfigurationFlyoutState>
));
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
)}
</WithSourceConfigurationFormState>
) : null
}
</WithSource>
) : null
}
</WithSourceConfigurationFlyoutState>
)
);

View file

@ -79,6 +79,7 @@ export const Node = injectI18n(
>
<EuiToolTip position="top" content={`${node.name} | ${value}`}>
<NodeContainer
data-test-subj="nodeContainer"
style={{ width: squareSize || 0, height: squareSize || 0 }}
onClick={this.togglePopover}
>

View file

@ -7,7 +7,7 @@
import { EuiContextMenu, EuiContextMenuPanelDescriptor, EuiPopover } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import { injectUICapabilities, UICapabilities } from 'ui/capabilities/react';
import { InfraNodeType, InfraTimerangeInput } from '../../graphql/types';
import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib';
import { getNodeDetailUrl, getNodeLogsUrl } from '../../pages/link_to';
@ -21,89 +21,107 @@ interface Props {
isPopoverOpen: boolean;
closePopover: () => void;
intl: InjectedIntl;
uiCapabilities: UICapabilities;
}
export const NodeContextMenu = injectI18n(
({ options, timeRange, children, node, isPopoverOpen, closePopover, nodeType, intl }: Props) => {
// Due to the changing nature of the fields between APM and this UI,
// We need to have some exceptions until 7.0 & ECS is finalized. Reference
// #26620 for the details for these fields.
// TODO: This is tech debt, remove it after 7.0 & ECS migration.
const APM_FIELDS = {
[InfraNodeType.host]: 'host.hostname',
[InfraNodeType.container]: 'container.id',
[InfraNodeType.pod]: 'kubernetes.pod.uid',
};
export const NodeContextMenu = injectUICapabilities(
injectI18n(
({
options,
timeRange,
children,
node,
isPopoverOpen,
closePopover,
nodeType,
intl,
uiCapabilities,
}: Props) => {
// Due to the changing nature of the fields between APM and this UI,
// We need to have some exceptions until 7.0 & ECS is finalized. Reference
// #26620 for the details for these fields.
// TODO: This is tech debt, remove it after 7.0 & ECS migration.
const APM_FIELDS = {
[InfraNodeType.host]: 'host.hostname',
[InfraNodeType.container]: 'container.id',
[InfraNodeType.pod]: 'kubernetes.pod.uid',
};
const nodeLogsUrl = node.id
? getNodeLogsUrl({
nodeType,
nodeId: node.id,
time: timeRange.to,
})
: undefined;
const nodeDetailUrl = node.id
? getNodeDetailUrl({
nodeType,
nodeId: node.id,
from: timeRange.from,
to: timeRange.to,
})
: undefined;
const nodeLogsUrl =
node.id && uiCapabilities.logs.show
? getNodeLogsUrl({
nodeType,
nodeId: node.id,
time: timeRange.to,
})
: undefined;
const nodeDetailUrl = node.id
? getNodeDetailUrl({
nodeType,
nodeId: node.id,
from: timeRange.from,
to: timeRange.to,
})
: undefined;
const apmTracesUrl = {
name: intl.formatMessage(
const apmTracesUrl = uiCapabilities.apm.show
? {
name: intl.formatMessage(
{
id: 'xpack.infra.nodeContextMenu.viewAPMTraces',
defaultMessage: 'View {nodeType} APM traces',
},
{ nodeType }
),
href: `../app/apm#/traces?_g=()&kuery=${APM_FIELDS[nodeType]}~20~3A~20~22${node.id}~22`,
'data-test-subj': 'viewApmTracesContextMenuItem',
}
: undefined;
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 'xpack.infra.nodeContextMenu.viewAPMTraces',
defaultMessage: 'View {nodeType} APM traces',
id: 0,
title: '',
items: [
...(nodeLogsUrl
? [
{
name: intl.formatMessage({
id: 'xpack.infra.nodeContextMenu.viewLogsName',
defaultMessage: 'View logs',
}),
href: nodeLogsUrl,
'data-test-subj': 'viewLogsContextMenuItem',
},
]
: []),
...(nodeDetailUrl
? [
{
name: intl.formatMessage({
id: 'xpack.infra.nodeContextMenu.viewMetricsName',
defaultMessage: 'View metrics',
}),
href: nodeDetailUrl,
},
]
: []),
...(apmTracesUrl ? [apmTracesUrl] : []),
],
},
{ nodeType }
),
href: `../app/apm#/traces?_g=()&kuery=${APM_FIELDS[nodeType]}~20~3A~20~22${node.id}~22`,
};
];
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 0,
title: '',
items: [
...(nodeLogsUrl
? [
{
name: intl.formatMessage({
id: 'xpack.infra.nodeContextMenu.viewLogsName',
defaultMessage: 'View logs',
}),
href: nodeLogsUrl,
},
]
: []),
...(nodeDetailUrl
? [
{
name: intl.formatMessage({
id: 'xpack.infra.nodeContextMenu.viewMetricsName',
defaultMessage: 'View metrics',
}),
href: nodeDetailUrl,
},
]
: []),
...[apmTracesUrl],
],
},
];
return (
<EuiPopover
closePopover={closePopover}
id={`${node.pathId}-popover`}
isOpen={isPopoverOpen}
button={children}
panelPaddingSize="none"
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
);
}
return (
<EuiPopover
closePopover={closePopover}
id={`${node.pathId}-popover`}
isOpen={isPopoverOpen}
button={children}
panelPaddingSize="none"
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
);
}
)
);

View file

@ -10,7 +10,7 @@ import React from 'react';
export class NotFoundPage extends React.PureComponent {
public render() {
return (
<div>
<div data-test-subj="infraNotFoundPage">
<FormattedMessage
id="xpack.infra.notFoundPage.noContentFoundErrorTitle"
defaultMessage="No content found"

View file

@ -7,6 +7,7 @@
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import { injectUICapabilities, UICapabilities } from 'ui/capabilities/react';
import { HomePageContent } from './page_content';
import { HomeToolbar } from './toolbar';
@ -27,112 +28,125 @@ import { SourceErrorPage, SourceLoadingPage, WithSource } from '../../containers
interface HomePageProps {
intl: InjectedIntl;
uiCapabilities: UICapabilities;
}
export const HomePage = injectI18n(
class extends React.Component<HomePageProps, {}> {
public static displayName = 'HomePage';
export const HomePage = injectUICapabilities(
injectI18n(
class extends React.Component<HomePageProps, {}> {
public static displayName = 'HomePage';
public render() {
const { intl } = this.props;
public render() {
const { intl, uiCapabilities } = this.props;
return (
<ColumnarPage>
<DocumentTitle
title={intl.formatMessage({
id: 'xpack.infra.homePage.documentTitle',
defaultMessage: 'Infrastructure',
})}
/>
<HelpCenterContent
feedbackLink="https://discuss.elastic.co/c/infrastructure"
feedbackLinkText={intl.formatMessage({
id: 'xpack.infra.infrastructure.infrastructureHelpContent.feedbackLinkText',
defaultMessage: 'Provide feedback for Infrastructure',
})}
/>
<Header
breadcrumbs={[
{
href: '#/',
text: intl.formatMessage({
id: 'xpack.infra.header.infrastructureTitle',
defaultMessage: 'Infrastructure',
}),
},
]}
/>
<SourceConfigurationFlyout />
<WithSource>
{({
derivedIndexPattern,
hasFailed,
isLoading,
lastFailureMessage,
load,
metricIndicesExist,
}) =>
isLoading ? (
<SourceLoadingPage />
) : metricIndicesExist ? (
<>
<WithWaffleTimeUrlState />
<WithWaffleFilterUrlState indexPattern={derivedIndexPattern} />
<WithWaffleOptionsUrlState />
<HomeToolbar />
<HomePageContent />
</>
) : hasFailed ? (
<SourceErrorPage errorMessage={lastFailureMessage || ''} retry={load} />
) : (
<WithKibanaChrome>
{({ basePath }) => (
<NoIndices
title={intl.formatMessage({
id: 'xpack.infra.homePage.noMetricsIndicesTitle',
defaultMessage: "Looks like you don't have any metrics indices.",
})}
message={intl.formatMessage({
id: 'xpack.infra.homePage.noMetricsIndicesDescription',
defaultMessage: "Let's add some!",
})}
actions={
<EuiFlexGroup>
<EuiFlexItem>
<EuiButton
href={`${basePath}/app/kibana#/home/tutorial_directory/metrics`}
color="primary"
fill
>
{intl.formatMessage({
id: 'xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel',
defaultMessage: 'View setup instructions',
})}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<WithSourceConfigurationFlyoutState>
{({ enable }) => (
<EuiButton color="primary" onClick={enable}>
{intl.formatMessage({
id: 'xpack.infra.configureSourceActionLabel',
defaultMessage: 'Change source configuration',
})}
</EuiButton>
)}
</WithSourceConfigurationFlyoutState>
</EuiFlexItem>
</EuiFlexGroup>
}
data-test-subj="noMetricsIndicesPrompt"
/>
)}
</WithKibanaChrome>
)
}
</WithSource>
</ColumnarPage>
);
return (
<ColumnarPage>
<DocumentTitle
title={intl.formatMessage({
id: 'xpack.infra.homePage.documentTitle',
defaultMessage: 'Infrastructure',
})}
/>
<HelpCenterContent
feedbackLink="https://discuss.elastic.co/c/infrastructure"
feedbackLinkText={intl.formatMessage({
id: 'xpack.infra.infrastructure.infrastructureHelpContent.feedbackLinkText',
defaultMessage: 'Provide feedback for Infrastructure',
})}
/>
<Header
breadcrumbs={[
{
href: '#/',
text: intl.formatMessage({
id: 'xpack.infra.header.infrastructureTitle',
defaultMessage: 'Infrastructure',
}),
},
]}
/>
<SourceConfigurationFlyout
shouldAllowEdit={uiCapabilities.infrastructure.configureSource as boolean}
/>
<WithSource>
{({
derivedIndexPattern,
hasFailed,
isLoading,
lastFailureMessage,
load,
metricIndicesExist,
}) =>
isLoading ? (
<SourceLoadingPage />
) : metricIndicesExist ? (
<>
<WithWaffleTimeUrlState />
<WithWaffleFilterUrlState indexPattern={derivedIndexPattern} />
<WithWaffleOptionsUrlState />
<HomeToolbar />
<HomePageContent />
</>
) : hasFailed ? (
<SourceErrorPage errorMessage={lastFailureMessage || ''} retry={load} />
) : (
<WithKibanaChrome>
{({ basePath }) => (
<NoIndices
title={intl.formatMessage({
id: 'xpack.infra.homePage.noMetricsIndicesTitle',
defaultMessage: "Looks like you don't have any metrics indices.",
})}
message={intl.formatMessage({
id: 'xpack.infra.homePage.noMetricsIndicesDescription',
defaultMessage: "Let's add some!",
})}
actions={
<EuiFlexGroup>
<EuiFlexItem>
<EuiButton
href={`${basePath}/app/kibana#/home/tutorial_directory/metrics`}
color="primary"
fill
data-test-subj="infrastructureViewSetupInstructionsButton"
>
{intl.formatMessage({
id:
'xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel',
defaultMessage: 'View setup instructions',
})}
</EuiButton>
</EuiFlexItem>
{uiCapabilities.infrastructure.configureSource ? (
<EuiFlexItem>
<WithSourceConfigurationFlyoutState>
{({ enable }) => (
<EuiButton
color="primary"
onClick={enable}
data-test-subj="infrastructureChangeSourceConfigurationButton"
>
{intl.formatMessage({
id: 'xpack.infra.configureSourceActionLabel',
defaultMessage: 'Change source configuration',
})}
</EuiButton>
)}
</WithSourceConfigurationFlyoutState>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
}
data-test-subj="noMetricsIndicesPrompt"
/>
)}
</WithKibanaChrome>
)
}
</WithSource>
</ColumnarPage>
);
}
}
}
)
);

View file

@ -7,6 +7,7 @@
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import { injectUICapabilities, UICapabilities } from 'ui/capabilities/react';
import { LogsPageContent } from './page_content';
import { LogsToolbar } from './toolbar';
@ -34,141 +35,153 @@ import { SourceErrorPage, SourceLoadingPage, WithSource } from '../../containers
interface Props {
intl: InjectedIntl;
uiCapabilities: UICapabilities;
}
export const LogsPage = injectI18n(
class extends React.Component<Props> {
public static displayName = 'LogsPage';
export const LogsPage = injectUICapabilities(
injectI18n(
class extends React.Component<Props> {
public static displayName = 'LogsPage';
public render() {
const { intl } = this.props;
public render() {
const { intl, uiCapabilities } = this.props;
return (
<LogViewConfiguration.Provider>
<ColumnarPage>
<Header
breadcrumbs={[
{
text: intl.formatMessage({
id: 'xpack.infra.logsPage.logsBreadcrumbsText',
defaultMessage: 'Logs',
}),
},
]}
/>
<WithSource>
{({
derivedIndexPattern,
hasFailed,
isLoading,
lastFailureMessage,
load,
logIndicesExist,
sourceId,
}) => (
<>
<DocumentTitle
title={intl.formatMessage({
id: 'xpack.infra.logsPage.documentTitle',
return (
<LogViewConfiguration.Provider>
<ColumnarPage data-test-subj="infraLogsPage">
<Header
breadcrumbs={[
{
text: intl.formatMessage({
id: 'xpack.infra.logsPage.logsBreadcrumbsText',
defaultMessage: 'Logs',
})}
/>
<HelpCenterContent
feedbackLink="https://discuss.elastic.co/c/logs"
feedbackLinkText={intl.formatMessage({
id: 'xpack.infra.logsPage.logsHelpContent.feedbackLinkText',
defaultMessage: 'Provide feedback for Logs',
})}
/>
<SourceConfigurationFlyout />
{isLoading ? (
<SourceLoadingPage />
) : logIndicesExist ? (
<>
<WithLogFilterUrlState indexPattern={derivedIndexPattern} />
<WithLogPositionUrlState />
<WithLogMinimapUrlState />
<WithLogTextviewUrlState />
<WithFlyoutOptionsUrlState />
<LogsToolbar />
<WithLogFilter indexPattern={derivedIndexPattern}>
{({ applyFilterQueryFromKueryExpression }) => (
<React.Fragment>
<WithFlyoutOptions>
{({ showFlyout, setFlyoutItem }) => (
<LogsPageContent
showFlyout={showFlyout}
setFlyoutItem={setFlyoutItem}
/>
)}
</WithFlyoutOptions>
<WithLogFlyout sourceId={sourceId}>
{({ flyoutItem, hideFlyout, loading }) => (
<LogFlyout
setFilter={applyFilterQueryFromKueryExpression}
flyoutItem={flyoutItem}
hideFlyout={hideFlyout}
loading={loading}
/>
)}
</WithLogFlyout>
</React.Fragment>
}),
},
]}
/>
<WithSource>
{({
derivedIndexPattern,
hasFailed,
isLoading,
lastFailureMessage,
load,
logIndicesExist,
sourceId,
}) => (
<>
<DocumentTitle
title={intl.formatMessage({
id: 'xpack.infra.logsPage.documentTitle',
defaultMessage: 'Logs',
})}
/>
<HelpCenterContent
feedbackLink="https://discuss.elastic.co/c/logs"
feedbackLinkText={intl.formatMessage({
id: 'xpack.infra.logsPage.logsHelpContent.feedbackLinkText',
defaultMessage: 'Provide feedback for Logs',
})}
/>
<SourceConfigurationFlyout
shouldAllowEdit={uiCapabilities.logs.configureSource as boolean}
/>
{isLoading ? (
<SourceLoadingPage />
) : logIndicesExist ? (
<>
<WithLogFilterUrlState indexPattern={derivedIndexPattern} />
<WithLogPositionUrlState />
<WithLogMinimapUrlState />
<WithLogTextviewUrlState />
<WithFlyoutOptionsUrlState />
<LogsToolbar />
<WithLogFilter indexPattern={derivedIndexPattern}>
{({ applyFilterQueryFromKueryExpression }) => (
<React.Fragment>
<WithFlyoutOptions>
{({ showFlyout, setFlyoutItem }) => (
<LogsPageContent
showFlyout={showFlyout}
setFlyoutItem={setFlyoutItem}
/>
)}
</WithFlyoutOptions>
<WithLogFlyout sourceId={sourceId}>
{({ flyoutItem, hideFlyout, loading }) => (
<LogFlyout
setFilter={applyFilterQueryFromKueryExpression}
flyoutItem={flyoutItem}
hideFlyout={hideFlyout}
loading={loading}
/>
)}
</WithLogFlyout>
</React.Fragment>
)}
</WithLogFilter>
</>
) : hasFailed ? (
<SourceErrorPage errorMessage={lastFailureMessage || ''} retry={load} />
) : (
<WithKibanaChrome>
{({ basePath }) => (
<NoIndices
title={intl.formatMessage({
id: 'xpack.infra.logsPage.noLoggingIndicesTitle',
defaultMessage: "Looks like you don't have any logging indices.",
})}
message={intl.formatMessage({
id: 'xpack.infra.logsPage.noLoggingIndicesDescription',
defaultMessage: "Let's add some!",
})}
actions={
<EuiFlexGroup>
<EuiFlexItem>
<EuiButton
href={`${basePath}/app/kibana#/home/tutorial_directory/logging`}
color="primary"
fill
data-test-subj="logsViewSetupInstructionsButton"
>
{intl.formatMessage({
id:
'xpack.infra.logsPage.noLoggingIndicesInstructionsActionLabel',
defaultMessage: 'View setup instructions',
})}
</EuiButton>
</EuiFlexItem>
{uiCapabilities.logs.configureSource ? (
<EuiFlexItem>
<WithSourceConfigurationFlyoutState>
{({ enable }) => (
<EuiButton
color="primary"
onClick={enable}
data-test-subj="logsChangeSourceConfigurationButton"
>
{intl.formatMessage({
id: 'xpack.infra.configureSourceActionLabel',
defaultMessage: 'Change source configuration',
})}
</EuiButton>
)}
</WithSourceConfigurationFlyoutState>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
}
/>
)}
</WithLogFilter>
</>
) : hasFailed ? (
<SourceErrorPage errorMessage={lastFailureMessage || ''} retry={load} />
) : (
<WithKibanaChrome>
{({ basePath }) => (
<NoIndices
title={intl.formatMessage({
id: 'xpack.infra.logsPage.noLoggingIndicesTitle',
defaultMessage: "Looks like you don't have any logging indices.",
})}
message={intl.formatMessage({
id: 'xpack.infra.logsPage.noLoggingIndicesDescription',
defaultMessage: "Let's add some!",
})}
actions={
<EuiFlexGroup>
<EuiFlexItem>
<EuiButton
href={`${basePath}/app/kibana#/home/tutorial_directory/logging`}
color="primary"
fill
>
{intl.formatMessage({
id:
'xpack.infra.logsPage.noLoggingIndicesInstructionsActionLabel',
defaultMessage: 'View setup instructions',
})}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<WithSourceConfigurationFlyoutState>
{({ enable }) => (
<EuiButton color="primary" onClick={enable}>
{intl.formatMessage({
id: 'xpack.infra.configureSourceActionLabel',
defaultMessage: 'Change source configuration',
})}
</EuiButton>
)}
</WithSourceConfigurationFlyoutState>
</EuiFlexItem>
</EuiFlexGroup>
}
/>
)}
</WithKibanaChrome>
)}
</>
)}
</WithSource>
</ColumnarPage>
</LogViewConfiguration.Provider>
);
</WithKibanaChrome>
)}
</>
)}
</WithSource>
</ColumnarPage>
</LogViewConfiguration.Provider>
);
}
}
}
)
);

View file

@ -17,6 +17,7 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { GraphQLFormattedError } from 'graphql';
import React from 'react';
import styled, { withTheme } from 'styled-components';
import { injectUICapabilities, UICapabilities } from 'ui/capabilities/react';
import { InfraMetricsErrorCodes } from '../../../common/errors';
import { AutoSizer } from '../../components/auto_sizer';
@ -58,195 +59,203 @@ interface Props {
};
};
intl: InjectedIntl;
uiCapabilities: UICapabilities;
}
export const MetricDetail = withTheme(
injectI18n(
class extends React.PureComponent<Props> {
public static displayName = 'MetricDetailPage';
export const MetricDetail = injectUICapabilities(
withTheme(
injectI18n(
class extends React.PureComponent<Props> {
public static displayName = 'MetricDetailPage';
public render() {
const { intl, uiCapabilities } = this.props;
const nodeId = this.props.match.params.node;
const nodeType = this.props.match.params.type as InfraNodeType;
const layoutCreator = layoutCreators[nodeType];
if (!layoutCreator) {
return (
<Error
message={intl.formatMessage(
{
id: 'xpack.infra.metricDetailPage.invalidNodeTypeErrorMessage',
defaultMessage: '{nodeType} is not a valid node type',
},
{
nodeType: `"${nodeType}"`,
}
)}
/>
);
}
const layouts = layoutCreator(this.props.theme);
public render() {
const { intl } = this.props;
const nodeId = this.props.match.params.node;
const nodeType = this.props.match.params.type as InfraNodeType;
const layoutCreator = layoutCreators[nodeType];
if (!layoutCreator) {
return (
<Error
message={intl.formatMessage(
{
id: 'xpack.infra.metricDetailPage.invalidNodeTypeErrorMessage',
defaultMessage: '{nodeType} is not a valid node type',
},
{
nodeType: `"${nodeType}"`,
}
<WithSource>
{({ sourceId }) => (
<WithMetricsTime resetOnUnmount>
{({
currentTimeRange,
isAutoReloading,
setRangeTime,
startMetricsAutoReload,
stopMetricsAutoReload,
}) => (
<WithMetadata
layouts={layouts}
sourceId={sourceId}
nodeType={nodeType}
nodeId={nodeId}
>
{({ name, filteredLayouts, loading: metadataLoading }) => {
const breadcrumbs = [
{
href: '#/',
text: intl.formatMessage({
id: 'xpack.infra.header.infrastructureTitle',
defaultMessage: 'Infrastructure',
}),
},
{ text: name },
];
return (
<ColumnarPage>
<Header breadcrumbs={breadcrumbs} />
<SourceConfigurationFlyout
shouldAllowEdit={
uiCapabilities.infrastructure.configureSource as boolean
}
/>
<WithMetricsTimeUrlState />
<DocumentTitle
title={intl.formatMessage(
{
id: 'xpack.infra.metricDetailPage.documentTitle',
defaultMessage: 'Infrastructure | Metrics | {name}',
},
{
name,
}
)}
/>
<DetailPageContent data-test-subj="infraMetricsPage">
<WithMetrics
layouts={filteredLayouts}
sourceId={sourceId}
timerange={currentTimeRange as InfraTimerangeInput}
nodeType={nodeType}
nodeId={nodeId}
>
{({ metrics, error, loading, refetch }) => {
if (error) {
const invalidNodeError = error.graphQLErrors.some(
(err: GraphQLFormattedError) =>
err.code === InfraMetricsErrorCodes.invalid_node
);
return (
<>
<DocumentTitle
title={(previousTitle: string) =>
intl.formatMessage(
{
id:
'xpack.infra.metricDetailPage.documentTitleError',
defaultMessage: '{previousTitle} | Uh oh',
},
{
previousTitle,
}
)
}
/>
{invalidNodeError ? (
<InvalidNodeError nodeName={name} />
) : (
<ErrorPageBody message={error.message} />
)}
</>
);
}
return (
<EuiPage style={{ flex: '1 0 auto' }}>
<MetricsSideNav
layouts={filteredLayouts}
loading={metadataLoading}
nodeName={name}
handleClick={this.handleClick}
/>
<AutoSizer content={false} bounds detectAnyWindowResize>
{({ measureRef, bounds: { width = 0 } }) => {
return (
<MetricsDetailsPageColumn innerRef={measureRef}>
<EuiPageBody style={{ width: `${width}px` }}>
<EuiPageHeader style={{ flex: '0 0 auto' }}>
<EuiPageHeaderSection style={{ width: '100%' }}>
<MetricsTitleTimeRangeContainer>
<EuiHideFor sizes={['xs', 's']}>
<EuiTitle size="m">
<h1>{name}</h1>
</EuiTitle>
</EuiHideFor>
<MetricsTimeControls
currentTimeRange={currentTimeRange}
isLiveStreaming={isAutoReloading}
onChangeRangeTime={setRangeTime}
startLiveStreaming={startMetricsAutoReload}
stopLiveStreaming={stopMetricsAutoReload}
/>
</MetricsTitleTimeRangeContainer>
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiPageContentWithRelative>
<Metrics
label={name}
nodeId={nodeId}
layouts={filteredLayouts}
metrics={metrics}
loading={
metrics.length > 0 && isAutoReloading
? false
: loading
}
refetch={refetch}
onChangeRangeTime={setRangeTime}
isLiveStreaming={isAutoReloading}
stopLiveStreaming={stopMetricsAutoReload}
/>
</EuiPageContentWithRelative>
</EuiPageBody>
</MetricsDetailsPageColumn>
);
}}
</AutoSizer>
</EuiPage>
);
}}
</WithMetrics>
</DetailPageContent>
</ColumnarPage>
);
}}
</WithMetadata>
)}
</WithMetricsTime>
)}
/>
</WithSource>
);
}
const layouts = layoutCreator(this.props.theme);
return (
<WithSource>
{({ sourceId }) => (
<WithMetricsTime resetOnUnmount>
{({
currentTimeRange,
isAutoReloading,
setRangeTime,
startMetricsAutoReload,
stopMetricsAutoReload,
}) => (
<WithMetadata
layouts={layouts}
sourceId={sourceId}
nodeType={nodeType}
nodeId={nodeId}
>
{({ name, filteredLayouts, loading: metadataLoading }) => {
const breadcrumbs = [
{
href: '#/',
text: intl.formatMessage({
id: 'xpack.infra.header.infrastructureTitle',
defaultMessage: 'Infrastructure',
}),
},
{ text: name },
];
return (
<ColumnarPage>
<Header breadcrumbs={breadcrumbs} />
<SourceConfigurationFlyout />
<WithMetricsTimeUrlState />
<DocumentTitle
title={intl.formatMessage(
{
id: 'xpack.infra.metricDetailPage.documentTitle',
defaultMessage: 'Infrastructure | Metrics | {name}',
},
{
name,
}
)}
/>
<DetailPageContent>
<WithMetrics
layouts={filteredLayouts}
sourceId={sourceId}
timerange={currentTimeRange as InfraTimerangeInput}
nodeType={nodeType}
nodeId={nodeId}
>
{({ metrics, error, loading, refetch }) => {
if (error) {
const invalidNodeError = error.graphQLErrors.some(
(err: GraphQLFormattedError) =>
err.code === InfraMetricsErrorCodes.invalid_node
);
return (
<>
<DocumentTitle
title={(previousTitle: string) =>
intl.formatMessage(
{
id: 'xpack.infra.metricDetailPage.documentTitleError',
defaultMessage: '{previousTitle} | Uh oh',
},
{
previousTitle,
}
)
}
/>
{invalidNodeError ? (
<InvalidNodeError nodeName={name} />
) : (
<ErrorPageBody message={error.message} />
)}
</>
);
}
return (
<EuiPage style={{ flex: '1 0 auto' }}>
<MetricsSideNav
layouts={filteredLayouts}
loading={metadataLoading}
nodeName={name}
handleClick={this.handleClick}
/>
<AutoSizer content={false} bounds detectAnyWindowResize>
{({ measureRef, bounds: { width = 0 } }) => {
return (
<MetricsDetailsPageColumn innerRef={measureRef}>
<EuiPageBody style={{ width: `${width}px` }}>
<EuiPageHeader style={{ flex: '0 0 auto' }}>
<EuiPageHeaderSection style={{ width: '100%' }}>
<MetricsTitleTimeRangeContainer>
<EuiHideFor sizes={['xs', 's']}>
<EuiTitle size="m">
<h1>{name}</h1>
</EuiTitle>
</EuiHideFor>
<MetricsTimeControls
currentTimeRange={currentTimeRange}
isLiveStreaming={isAutoReloading}
onChangeRangeTime={setRangeTime}
startLiveStreaming={startMetricsAutoReload}
stopLiveStreaming={stopMetricsAutoReload}
/>
</MetricsTitleTimeRangeContainer>
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiPageContentWithRelative>
<Metrics
label={name}
nodeId={nodeId}
layouts={filteredLayouts}
metrics={metrics}
loading={
metrics.length > 0 && isAutoReloading
? false
: loading
}
refetch={refetch}
onChangeRangeTime={setRangeTime}
isLiveStreaming={isAutoReloading}
stopLiveStreaming={stopMetricsAutoReload}
/>
</EuiPageContentWithRelative>
</EuiPageBody>
</MetricsDetailsPageColumn>
);
}}
</AutoSizer>
</EuiPage>
);
}}
</WithMetrics>
</DetailPageContent>
</ColumnarPage>
);
}}
</WithMetadata>
)}
</WithMetricsTime>
)}
</WithSource>
);
private handleClick = (section: InfraMetricLayoutSection) => () => {
const id = section.linkToId || section.id;
const el = document.getElementById(id);
if (el) {
el.scrollIntoView();
}
};
}
private handleClick = (section: InfraMetricLayoutSection) => () => {
const id = section.linkToId || section.id;
const el = document.getElementById(id);
if (el) {
el.scrollIntoView();
}
};
}
)
)
);

View file

@ -8,6 +8,8 @@ import { History } from 'history';
import React from 'react';
import { Redirect, Route, Router, Switch } from 'react-router-dom';
import { UICapabilities } from 'ui/capabilities';
import { injectUICapabilities } from 'ui/capabilities/react';
import { NotFoundPage } from './pages/404';
import { HomePage } from './pages/home';
import { LinkToPage } from './pages/link_to';
@ -16,19 +18,25 @@ import { MetricDetail } from './pages/metrics';
interface RouterProps {
history: History;
uiCapabilities: UICapabilities;
}
export const PageRouter: React.SFC<RouterProps> = ({ history }) => {
const PageRouterComponent: React.SFC<RouterProps> = ({ history, uiCapabilities }) => {
const defaultRoute = uiCapabilities.infrastructure.show ? '/home' : '/logs';
return (
<Router history={history}>
<Switch>
<Redirect from="/" exact={true} to="/home" />
<Route path="/logs" component={LogsPage} />
<Route path="/home" component={HomePage} />
<Redirect from="/" exact={true} to={defaultRoute} />
{uiCapabilities.logs.show && <Route path="/logs" component={LogsPage} />}
{uiCapabilities.infrastructure.show && <Route path="/home" component={HomePage} />}
<Route path="/link-to" component={LinkToPage} />
<Route path="/metrics/:type/:node" component={MetricDetail} />
{uiCapabilities.infrastructure.show && (
<Route path="/metrics/:type/:node" component={MetricDetail} />
)}
<Route component={NotFoundPage} />
</Switch>
</Router>
);
};
export const PageRouter = injectUICapabilities(PageRouterComponent);

View file

@ -34,18 +34,20 @@ export const initServerWithKibana = (kbnServer: KbnServer) => {
catalogue: ['infraops'],
privileges: {
all: {
api: ['infra/graphql'],
savedObject: {
all: [],
all: ['infrastructure-ui-source'],
read: ['config'],
},
ui: [],
ui: ['show', 'configureSource'],
},
read: {
api: ['infra/graphql'],
savedObject: {
all: [],
read: ['config'],
read: ['config', 'infrastructure-ui-source'],
},
ui: [],
ui: ['show'],
},
},
});
@ -61,18 +63,20 @@ export const initServerWithKibana = (kbnServer: KbnServer) => {
catalogue: ['infralogging'],
privileges: {
all: {
api: ['infra/graphql'],
savedObject: {
all: [],
all: ['infrastructure-ui-source'],
read: ['config'],
},
ui: [],
ui: ['show', 'configureSource'],
},
read: {
api: ['infra/graphql'],
savedObject: {
all: [],
read: ['config'],
read: ['config', 'infrastructure-ui-source'],
},
ui: [],
ui: ['show'],
},
},
});

View file

@ -56,6 +56,9 @@ export class InfraKibanaBackendFrameworkAdapter implements InfraBackendFramework
schema,
}),
path: routePath,
route: {
tags: ['access:graphql'],
},
},
plugin: graphqlHapi,
});
@ -67,6 +70,9 @@ export class InfraKibanaBackendFrameworkAdapter implements InfraBackendFramework
passHeader: `'kbn-version': '${this.version}'`,
}),
path: `${routePath}/graphiql`,
route: {
tags: ['access:graphql'],
},
},
plugin: graphiqlHapi,
});

View file

@ -0,0 +1,298 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from 'expect.js';
import gql from 'graphql-tag';
import { SecurityService, SpacesService } from 'x-pack/test/common/services';
import { KbnTestProvider } from './types';
const introspectionQuery = gql`
query Schema {
__schema {
queryType {
name
}
}
}
`;
// tslint:disable:no-default-export
const featureControlsTests: KbnTestProvider = ({ getService }) => {
const supertest = getService('supertestWithoutAuth');
const security: SecurityService = getService('security');
const spaces: SpacesService = getService('spaces');
const clientFactory = getService('infraOpsGraphQLClientFactory');
const expectGraphQL404 = (result: any) => {
expect(result.response).to.be(undefined);
expect(result.error).not.to.be(undefined);
expect(result.error).to.have.property('networkError');
expect(result.error.networkError).to.have.property('statusCode', 404);
};
const expectGraphQLResponse = (result: any) => {
expect(result.error).to.be(undefined);
expect(result.response).to.have.property('data');
expect(result.response.data).to.be.an('object');
};
const expectGraphIQL404 = (result: any) => {
expect(result.error).to.be(undefined);
expect(result.response).not.to.be(undefined);
expect(result.response).to.have.property('statusCode', 404);
};
const expectGraphIQLResponse = (result: any) => {
expect(result.error).to.be(undefined);
expect(result.response).not.to.be(undefined);
expect(result.response).to.have.property('statusCode', 200);
};
const executeGraphQLQuery = async (username: string, password: string, spaceId?: string) => {
const queryOptions = {
query: introspectionQuery,
};
const basePath = spaceId ? `/s/${spaceId}` : '';
const client = clientFactory({ username, password, basePath });
let error;
let response;
try {
response = await client.query(queryOptions);
} catch (err) {
error = err;
}
return {
error,
response,
};
};
const executeGraphIQLRequest = async (username: string, password: string, spaceId?: string) => {
const basePath = spaceId ? `/s/${spaceId}` : '';
return supertest
.get(`${basePath}/api/infra/graphql/graphiql`)
.auth(username, password)
.then((response: any) => ({ error: undefined, response }))
.catch((error: any) => ({ error, response: undefined }));
};
describe('feature controls', () => {
it(`APIs can't be accessed by user with logstash-* "read" privileges`, async () => {
const username = 'logstash_read';
const roleName = 'logstash_read';
const password = `${username}-password`;
try {
await security.role.create(roleName, {
elasticsearch: {
indices: [
{
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
},
],
},
});
await security.user.create(username, {
password,
roles: [roleName],
full_name: 'a kibana user',
});
const graphQLResult = await executeGraphQLQuery(username, password);
expectGraphQL404(graphQLResult);
const graphQLIResult = await executeGraphIQLRequest(username, password);
expectGraphIQL404(graphQLIResult);
} finally {
await security.role.delete(roleName);
await security.user.delete(username);
}
});
it('APIs can be accessed user with global "all" and logstash-* "read" privileges', async () => {
const username = 'global_all';
const roleName = 'global_all';
const password = `${username}-password`;
try {
await security.role.create(roleName, {
elasticsearch: {
indices: [
{
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
},
],
},
kibana: [
{
base: ['all'],
spaces: ['*'],
},
],
});
await security.user.create(username, {
password,
roles: [roleName],
full_name: 'a kibana user',
});
const graphQLResult = await executeGraphQLQuery(username, password);
expectGraphQLResponse(graphQLResult);
const graphQLIResult = await executeGraphIQLRequest(username, password);
expectGraphIQLResponse(graphQLIResult);
} finally {
await security.role.delete(roleName);
await security.user.delete(username);
}
});
// this could be any role which doesn't have access to the infra feature
it(`APIs can't be accessed by user with dashboard "all" and logstash-* "read" privileges`, async () => {
const username = 'dashboard_all';
const roleName = 'dashboard_all';
const password = `${username}-password`;
try {
await security.role.create(roleName, {
elasticsearch: {
indices: [
{
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
},
],
},
kibana: [
{
feature: {
dashboard: ['all'],
},
spaces: ['*'],
},
],
});
await security.user.create(username, {
password,
roles: [roleName],
full_name: 'a kibana user',
});
const graphQLResult = await executeGraphQLQuery(username, password);
expectGraphQL404(graphQLResult);
const graphQLIResult = await executeGraphIQLRequest(username, password);
expectGraphIQL404(graphQLIResult);
} finally {
await security.role.delete(roleName);
await security.user.delete(username);
}
});
describe('spaces', () => {
// the following tests create a user_1 which has infrastructure read access to space_1, logs read access to space_2 and dashboard all access to space_3
const space1Id = 'space_1';
const space2Id = 'space_2';
const space3Id = 'space_3';
const roleName = 'user_1';
const username = 'user_1';
const password = 'user_1-password';
before(async () => {
await spaces.create({
id: space1Id,
name: space1Id,
disabledFeatures: [],
});
await spaces.create({
id: space2Id,
name: space2Id,
disabledFeatures: [],
});
await spaces.create({
id: space3Id,
name: space3Id,
disabledFeatures: [],
});
await security.role.create(roleName, {
elasticsearch: {
indices: [
{
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
},
],
},
kibana: [
{
feature: {
infrastructure: ['read'],
},
spaces: [space1Id],
},
{
feature: {
logs: ['read'],
},
spaces: [space2Id],
},
{
feature: {
dashboard: ['all'],
},
spaces: [space3Id],
},
],
});
await security.user.create(username, {
password,
roles: [roleName],
});
});
after(async () => {
await spaces.delete(space1Id);
await spaces.delete(space2Id);
await spaces.delete(space3Id);
await security.role.delete(roleName);
await security.user.delete(username);
});
it('user_1 can access APIs in space_1', async () => {
const graphQLResult = await executeGraphQLQuery(username, password, space1Id);
expectGraphQLResponse(graphQLResult);
const graphQLIResult = await executeGraphIQLRequest(username, password, space1Id);
expectGraphIQLResponse(graphQLIResult);
});
it(`user_1 can access APIs in space_2`, async () => {
const graphQLResult = await executeGraphQLQuery(username, password, space2Id);
expectGraphQLResponse(graphQLResult);
const graphQLIResult = await executeGraphIQLRequest(username, password, space2Id);
expectGraphIQLResponse(graphQLIResult);
});
it(`user_1 can't access APIs in space_3`, async () => {
const graphQLResult = await executeGraphQLQuery(username, password, space3Id);
expectGraphQL404(graphQLResult);
const graphQLIResult = await executeGraphIQLRequest(username, password, space3Id);
expectGraphIQL404(graphQLIResult);
});
});
});
};
// tslint:disable-next-line no-default-export
export default featureControlsTests;

View file

@ -14,5 +14,6 @@ export default function ({ loadTestFile }) {
loadTestFile(require.resolve('./sources'));
loadTestFile(require.resolve('./waffle'));
loadTestFile(require.resolve('./log_item'));
loadTestFile(require.resolve('./feature_controls'));
});
}

View file

@ -12,10 +12,19 @@ export interface EsArchiver {
unload(name: string): void;
}
interface InfraOpsGraphQLClientFactoryOptions {
username: string;
password: string;
basePath: string;
}
export interface KbnTestProviderOptions {
getService(name: string): any;
getService(name: 'esArchiver'): EsArchiver;
getService(name: 'infraOpsGraphQLClient'): ApolloClient<InMemoryCache>;
getService(
name: 'infraOpsGraphQLClientFactory'
): (options: InfraOpsGraphQLClientFactoryOptions) => ApolloClient<InMemoryCache>;
}
export type KbnTestProvider = (options: KbnTestProviderOptions) => void;

View file

@ -470,6 +470,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
'saved_object:config/get',
'saved_object:config/find',
'ui:savedObjectsManagement/config/read',
'ui:apm/show',
],
},
maps: {
@ -574,18 +575,32 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
all: [
'login:',
`version:${version}`,
'api:infra/graphql',
'app:infra',
'app:kibana',
'ui:catalogue/infraops',
'ui:navLinks/infra:home',
'saved_object:infrastructure-ui-source/bulk_get',
'saved_object:infrastructure-ui-source/get',
'saved_object:infrastructure-ui-source/find',
'saved_object:infrastructure-ui-source/create',
'saved_object:infrastructure-ui-source/bulk_create',
'saved_object:infrastructure-ui-source/update',
'saved_object:infrastructure-ui-source/delete',
'saved_object:config/bulk_get',
'saved_object:config/get',
'saved_object:config/find',
'ui:savedObjectsManagement/infrastructure-ui-source/delete',
'ui:savedObjectsManagement/infrastructure-ui-source/edit',
'ui:savedObjectsManagement/infrastructure-ui-source/read',
'ui:savedObjectsManagement/config/read',
'ui:infrastructure/show',
'ui:infrastructure/configureSource',
],
read: [
'login:',
`version:${version}`,
'api:infra/graphql',
'app:infra',
'app:kibana',
'ui:catalogue/infraops',
@ -593,25 +608,44 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
'saved_object:config/bulk_get',
'saved_object:config/get',
'saved_object:config/find',
'saved_object:infrastructure-ui-source/bulk_get',
'saved_object:infrastructure-ui-source/get',
'saved_object:infrastructure-ui-source/find',
'ui:savedObjectsManagement/config/read',
'ui:savedObjectsManagement/infrastructure-ui-source/read',
'ui:infrastructure/show',
],
},
logs: {
all: [
'login:',
`version:${version}`,
'api:infra/graphql',
'app:infra',
'app:kibana',
'ui:catalogue/infralogging',
'ui:navLinks/infra:logs',
'saved_object:infrastructure-ui-source/bulk_get',
'saved_object:infrastructure-ui-source/get',
'saved_object:infrastructure-ui-source/find',
'saved_object:infrastructure-ui-source/create',
'saved_object:infrastructure-ui-source/bulk_create',
'saved_object:infrastructure-ui-source/update',
'saved_object:infrastructure-ui-source/delete',
'saved_object:config/bulk_get',
'saved_object:config/get',
'saved_object:config/find',
'ui:savedObjectsManagement/infrastructure-ui-source/delete',
'ui:savedObjectsManagement/infrastructure-ui-source/edit',
'ui:savedObjectsManagement/infrastructure-ui-source/read',
'ui:savedObjectsManagement/config/read',
'ui:logs/show',
'ui:logs/configureSource',
],
read: [
'login:',
`version:${version}`,
'api:infra/graphql',
'app:infra',
'app:kibana',
'ui:catalogue/infralogging',
@ -619,7 +653,12 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
'saved_object:config/bulk_get',
'saved_object:config/get',
'saved_object:config/find',
'saved_object:infrastructure-ui-source/bulk_get',
'saved_object:infrastructure-ui-source/get',
'saved_object:infrastructure-ui-source/find',
'ui:savedObjectsManagement/config/read',
'ui:savedObjectsManagement/infrastructure-ui-source/read',
'ui:logs/show',
],
},
uptime: {
@ -782,6 +821,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
'app:apm',
'ui:catalogue/apm',
'ui:navLinks/apm',
'ui:apm/show',
'app:maps',
'ui:catalogue/maps',
'ui:navLinks/maps',
@ -806,11 +846,26 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
'ui:savedObjectsManagement/canvas-workpad/delete',
'ui:savedObjectsManagement/canvas-workpad/edit',
'ui:canvas/save',
'api:infra/graphql',
'app:infra',
'ui:catalogue/infraops',
'ui:navLinks/infra:home',
'saved_object:infrastructure-ui-source/bulk_get',
'saved_object:infrastructure-ui-source/get',
'saved_object:infrastructure-ui-source/find',
'saved_object:infrastructure-ui-source/create',
'saved_object:infrastructure-ui-source/bulk_create',
'saved_object:infrastructure-ui-source/update',
'saved_object:infrastructure-ui-source/delete',
'ui:savedObjectsManagement/infrastructure-ui-source/delete',
'ui:savedObjectsManagement/infrastructure-ui-source/edit',
'ui:savedObjectsManagement/infrastructure-ui-source/read',
'ui:infrastructure/show',
'ui:infrastructure/configureSource',
'ui:catalogue/infralogging',
'ui:navLinks/infra:logs',
'ui:logs/show',
'ui:logs/configureSource',
'app:uptime',
'ui:catalogue/uptime',
'ui:navLinks/uptime',
@ -889,6 +944,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
'app:apm',
'ui:catalogue/apm',
'ui:navLinks/apm',
'ui:apm/show',
'app:maps',
'ui:catalogue/maps',
'ui:navLinks/maps',
@ -899,11 +955,18 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
'app:canvas',
'ui:catalogue/canvas',
'ui:navLinks/canvas',
'api:infra/graphql',
'app:infra',
'ui:catalogue/infraops',
'ui:navLinks/infra:home',
'saved_object:infrastructure-ui-source/bulk_get',
'saved_object:infrastructure-ui-source/get',
'saved_object:infrastructure-ui-source/find',
'ui:savedObjectsManagement/infrastructure-ui-source/read',
'ui:infrastructure/show',
'ui:catalogue/infralogging',
'ui:navLinks/infra:logs',
'ui:logs/show',
'app:uptime',
'ui:catalogue/uptime',
'ui:navLinks/uptime',
@ -1040,6 +1103,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
'app:apm',
'ui:catalogue/apm',
'ui:navLinks/apm',
'ui:apm/show',
'app:maps',
'ui:catalogue/maps',
'ui:navLinks/maps',
@ -1064,11 +1128,26 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
'ui:savedObjectsManagement/canvas-workpad/delete',
'ui:savedObjectsManagement/canvas-workpad/edit',
'ui:canvas/save',
'api:infra/graphql',
'app:infra',
'ui:catalogue/infraops',
'ui:navLinks/infra:home',
'saved_object:infrastructure-ui-source/bulk_get',
'saved_object:infrastructure-ui-source/get',
'saved_object:infrastructure-ui-source/find',
'saved_object:infrastructure-ui-source/create',
'saved_object:infrastructure-ui-source/bulk_create',
'saved_object:infrastructure-ui-source/update',
'saved_object:infrastructure-ui-source/delete',
'ui:savedObjectsManagement/infrastructure-ui-source/delete',
'ui:savedObjectsManagement/infrastructure-ui-source/edit',
'ui:savedObjectsManagement/infrastructure-ui-source/read',
'ui:infrastructure/show',
'ui:infrastructure/configureSource',
'ui:catalogue/infralogging',
'ui:navLinks/infra:logs',
'ui:logs/show',
'ui:logs/configureSource',
'app:uptime',
'ui:catalogue/uptime',
'ui:navLinks/uptime',
@ -1147,6 +1226,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
'app:apm',
'ui:catalogue/apm',
'ui:navLinks/apm',
'ui:apm/show',
'app:maps',
'ui:catalogue/maps',
'ui:navLinks/maps',
@ -1157,11 +1237,18 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
'app:canvas',
'ui:catalogue/canvas',
'ui:navLinks/canvas',
'api:infra/graphql',
'app:infra',
'ui:catalogue/infraops',
'ui:navLinks/infra:home',
'saved_object:infrastructure-ui-source/bulk_get',
'saved_object:infrastructure-ui-source/get',
'saved_object:infrastructure-ui-source/find',
'ui:savedObjectsManagement/infrastructure-ui-source/read',
'ui:infrastructure/show',
'ui:catalogue/infralogging',
'ui:navLinks/infra:logs',
'ui:logs/show',
'app:uptime',
'ui:catalogue/uptime',
'ui:navLinks/uptime',

View file

@ -9,7 +9,8 @@ import {
EsSupertestWithoutAuthProvider,
SupertestWithoutAuthProvider,
UsageAPIProvider,
InfraOpsGraphQLProvider
InfraOpsGraphQLClientProvider,
InfraOpsGraphQLClientFactoryProvider,
} from './services';
import {
@ -31,7 +32,8 @@ export default async function ({ readConfigFile }) {
esSupertest: kibanaAPITestsConfig.get('services.esSupertest'),
supertestWithoutAuth: SupertestWithoutAuthProvider,
esSupertestWithoutAuth: EsSupertestWithoutAuthProvider,
infraOpsGraphQLClient: InfraOpsGraphQLProvider,
infraOpsGraphQLClient: InfraOpsGraphQLClientProvider,
infraOpsGraphQLClientFactory: InfraOpsGraphQLClientFactoryProvider,
es: EsProvider,
esArchiver: kibanaCommonConfig.get('services.esArchiver'),
usageAPI: UsageAPIProvider,

View file

@ -8,4 +8,4 @@ export { EsProvider } from './es';
export { EsSupertestWithoutAuthProvider } from './es_supertest_without_auth';
export { SupertestWithoutAuthProvider } from './supertest_without_auth';
export { UsageAPIProvider } from './usage_api';
export { InfraOpsGraphQLProvider } from './infraops_graphql_client';
export { InfraOpsGraphQLClientProvider, InfraOpsGraphQLClientFactoryProvider } from './infraops_graphql_client';

View file

@ -12,23 +12,34 @@ import { HttpLink } from 'apollo-link-http';
import introspectionQueryResultData from '../../../plugins/infra/public/graphql/introspection.json';
export function InfraOpsGraphQLProvider({ getService }) {
const config = getService('config');
const kbnURL = formatUrl(config.get('servers.kibana'));
export function InfraOpsGraphQLClientProvider({ getService }) {
return new InfraOpsGraphQLClientFactoryProvider({ getService })();
}
return new ApolloClient({
cache: new InMemoryCache({
fragmentMatcher: new IntrospectionFragmentMatcher({
introspectionQueryResultData,
}),
}),
link: new HttpLink({
export function InfraOpsGraphQLClientFactoryProvider({ getService }) {
const config = getService('config');
const [superUsername, superPassword] = config.get('servers.elasticsearch.auth').split(':');
return function ({ username = superUsername, password = superPassword, basePath = null } = {}) {
const kbnURLWithoutAuth = formatUrl({ ...config.get('servers.kibana'), auth: false });
const httpLink = new HttpLink({
credentials: 'same-origin',
fetch,
headers: {
'kbn-xsrf': 'xxx',
authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`,
},
uri: `${kbnURL}/api/infra/graphql`,
}),
});
uri: `${kbnURLWithoutAuth}${basePath || ''}/api/infra/graphql`,
});
return new ApolloClient({
cache: new InMemoryCache({
fragmentMatcher: new IntrospectionFragmentMatcher({
introspectionQueryResultData,
}),
}),
link: httpLink,
});
};
}

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const DATE_WITH_DATA = new Date(1539806283000);
export const DATE_WITHOUT_DATA = new Date(1539122400000);

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers';
// tslint:disable:no-default-export
export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) {
describe('feature controls', () => {
loadTestFile(require.resolve('./infrastructure_security'));
loadTestFile(require.resolve('./infrastructure_spaces'));
loadTestFile(require.resolve('./logs_security'));
loadTestFile(require.resolve('./logs_spaces'));
});
}

View file

@ -0,0 +1,423 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from 'expect.js';
import { KibanaFunctionalTestDefaultProviders } from 'x-pack/test/types/providers';
import { DATE_WITH_DATA } from '../constants';
// tslint:disable no-default-export
export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) {
const esArchiver = getService('esArchiver');
const security = getService('security');
const PageObjects = getPageObjects(['common', 'infraHome', 'security']);
const testSubjects = getService('testSubjects');
const appsMenu = getService('appsMenu');
describe('infrastructure security', () => {
describe('global infrastructure all privileges', () => {
before(async () => {
await security.role.create('global_infrastructure_all_role', {
elasticsearch: {
indices: [{ names: ['metricbeat-*'], privileges: ['read', 'view_index_metadata'] }],
},
kibana: [
{
feature: {
infrastructure: ['all'],
},
spaces: ['*'],
},
],
});
await security.user.create('global_infrastructure_all_user', {
password: 'global_infrastructure_all_user-password',
roles: ['global_infrastructure_all_role'],
full_name: 'test user',
});
await PageObjects.security.forceLogout();
await PageObjects.security.login(
'global_infrastructure_all_user',
'global_infrastructure_all_user-password',
{
expectSpaceSelector: false,
}
);
});
after(async () => {
await Promise.all([
security.role.delete('global_infrastructure_all_role'),
security.user.delete('global_infrastructure_all_user'),
PageObjects.security.forceLogout(),
]);
});
it('shows infrastructure navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map(
(link: Record<string, string>) => link.text
);
expect(navLinks).to.eql(['Infrastructure', 'Management']);
});
describe('infrastructure landing page without data', () => {
it(`shows 'Change source configuration' button`, async () => {
await PageObjects.common.navigateToActualUrl('infraOps', 'home', {
ensureCurrentUrl: true,
shouldLoginIfPrompted: false,
});
await testSubjects.existOrFail('infrastructureViewSetupInstructionsButton');
await testSubjects.existOrFail('infrastructureChangeSourceConfigurationButton');
});
});
describe('infrastructure landing page with data', () => {
before(async () => {
await esArchiver.load('infra/metrics_and_logs');
});
after(async () => {
await esArchiver.unload('infra/metrics_and_logs');
});
it(`shows Wafflemap`, async () => {
await PageObjects.common.navigateToActualUrl('infraOps', 'home', {
ensureCurrentUrl: true,
shouldLoginIfPrompted: false,
});
await PageObjects.infraHome.goToTime(DATE_WITH_DATA);
await testSubjects.existOrFail('waffleMap');
});
describe('context menu', () => {
before(async () => {
await testSubjects.click('nodeContainer');
});
it(`does not show link to view logs`, async () => {
await testSubjects.missingOrFail('viewLogsContextMenuItem');
});
it(`does not show link to view apm traces`, async () => {
await testSubjects.missingOrFail('viewApmTracesContextMenuItem');
});
});
});
it(`metrics page is visible`, async () => {
await PageObjects.common.navigateToActualUrl(
'infraOps',
'/metrics/host/demo-stack-redis-01',
{
ensureCurrentUrl: false,
shouldLoginIfPrompted: false,
}
);
await testSubjects.existOrFail('infraMetricsPage');
});
});
describe('global infrastructure read privileges', () => {
before(async () => {
await security.role.create('global_infrastructure_read_role', {
elasticsearch: {
indices: [{ names: ['metricbeat-*'], privileges: ['read', 'view_index_metadata'] }],
},
kibana: [
{
feature: {
infrastructure: ['read'],
},
spaces: ['*'],
},
],
});
await security.user.create('global_infrastructure_read_user', {
password: 'global_infrastructure_read_user-password',
roles: ['global_infrastructure_read_role'],
full_name: 'test user',
});
await PageObjects.security.forceLogout();
await PageObjects.security.login(
'global_infrastructure_read_user',
'global_infrastructure_read_user-password',
{
expectSpaceSelector: false,
}
);
});
after(async () => {
await Promise.all([
security.role.delete('global_infrastructure_read_role'),
security.user.delete('global_infrastructure_read_user'),
PageObjects.security.forceLogout(),
]);
});
it('shows infrastructure navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map(
(link: Record<string, string>) => link.text
);
expect(navLinks).to.eql(['Infrastructure', 'Management']);
});
describe('infrastructure landing page without data', () => {
it(`doesn't show 'Change source configuration' button`, async () => {
await PageObjects.common.navigateToActualUrl('infraOps', 'home', {
ensureCurrentUrl: true,
shouldLoginIfPrompted: false,
});
await testSubjects.existOrFail('infrastructureViewSetupInstructionsButton');
await testSubjects.missingOrFail('infrastructureChangeSourceConfigurationButton');
});
});
describe('infrastructure landing page with data', () => {
before(async () => {
await esArchiver.load('infra/metrics_and_logs');
});
after(async () => {
await esArchiver.unload('infra/metrics_and_logs');
});
it(`shows Wafflemap`, async () => {
await PageObjects.common.navigateToActualUrl('infraOps', 'home', {
ensureCurrentUrl: true,
shouldLoginIfPrompted: false,
});
await PageObjects.infraHome.goToTime(DATE_WITH_DATA);
await testSubjects.existOrFail('waffleMap');
});
describe('context menu', () => {
before(async () => {
await testSubjects.click('nodeContainer');
});
it(`does not show link to view logs`, async () => {
await testSubjects.missingOrFail('viewLogsContextMenuItem');
});
it(`does not show link to view apm traces`, async () => {
await testSubjects.missingOrFail('viewApmTracesContextMenuItem');
});
});
});
it(`metrics page is visible`, async () => {
await PageObjects.common.navigateToActualUrl(
'infraOps',
'/metrics/host/demo-stack-redis-01',
{
ensureCurrentUrl: false,
shouldLoginIfPrompted: false,
}
);
await testSubjects.existOrFail('infraMetricsPage');
});
});
describe('global infrastructure read & logs read privileges', () => {
before(async () => {
await security.role.create('global_infrastructure_logs_read_role', {
elasticsearch: {
indices: [
{
names: ['metricbeat-*', 'filebeat-*'],
privileges: ['read', 'view_index_metadata'],
},
],
},
kibana: [
{
feature: {
infrastructure: ['read'],
logs: ['read'],
},
spaces: ['*'],
},
],
});
await security.user.create('global_infrastructure_logs_read_user', {
password: 'global_infrastructure_logs_read_user-password',
roles: ['global_infrastructure_logs_read_role'],
full_name: 'test user',
});
await PageObjects.security.login(
'global_infrastructure_logs_read_user',
'global_infrastructure_logs_read_user-password',
{
expectSpaceSelector: false,
}
);
});
after(async () => {
await security.role.delete('global_infrastructure_logs_read_role');
await security.user.delete('global_infrastructure_logs_read_user');
});
describe('infrastructure landing page with data', () => {
before(async () => {
await esArchiver.load('infra/metrics_and_logs');
});
after(async () => {
await esArchiver.unload('infra/metrics_and_logs');
});
it(`context menu allows user to view logs`, async () => {
await PageObjects.common.navigateToActualUrl('infraOps', 'home', {
ensureCurrentUrl: true,
shouldLoginIfPrompted: false,
});
await PageObjects.infraHome.goToTime(DATE_WITH_DATA);
await testSubjects.existOrFail('waffleMap');
await testSubjects.click('nodeContainer');
await testSubjects.click('viewLogsContextMenuItem');
await testSubjects.existOrFail('infraLogsPage');
});
});
});
describe('global infrastructure read & apm privileges', () => {
before(async () => {
await security.role.create('global_infrastructure_apm_read_role', {
elasticsearch: {
indices: [
{
names: ['metricbeat-*', 'filebeat-*'],
privileges: ['read', 'view_index_metadata'],
},
],
},
kibana: [
{
feature: {
infrastructure: ['read'],
apm: ['all'],
},
spaces: ['*'],
},
],
});
await security.user.create('global_infrastructure_apm_read_user', {
password: 'global_infrastructure_apm_read_user-password',
roles: ['global_infrastructure_apm_read_role'],
full_name: 'test user',
});
await PageObjects.security.login(
'global_infrastructure_apm_read_user',
'global_infrastructure_apm_read_user-password',
{
expectSpaceSelector: false,
}
);
});
after(async () => {
await security.role.delete('global_infrastructure_apm_read_role');
await security.user.delete('global_infrastructure_apm_read_user');
});
describe('infrastructure landing page with data', () => {
before(async () => {
await esArchiver.load('infra/metrics_and_logs');
});
after(async () => {
await esArchiver.unload('infra/metrics_and_logs');
});
it(`context menu allows user to view APM traces`, async () => {
await PageObjects.common.navigateToActualUrl('infraOps', 'home', {
ensureCurrentUrl: true,
shouldLoginIfPrompted: false,
});
await PageObjects.infraHome.goToTime(DATE_WITH_DATA);
await testSubjects.existOrFail('waffleMap');
await testSubjects.click('nodeContainer');
await testSubjects.click('viewApmTracesContextMenuItem');
await testSubjects.existOrFail('apmMainContainer');
});
});
});
describe('global infrastructure no privileges', () => {
before(async () => {
await security.role.create('no_infrastructure_privileges_role', {
elasticsearch: {
indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }],
},
kibana: [
{
feature: {
logs: ['all'],
},
spaces: ['*'],
},
],
});
await security.user.create('no_infrastructure_privileges_user', {
password: 'no_infrastructure_privileges_user-password',
roles: ['no_infrastructure_privileges_role'],
full_name: 'test user',
});
await PageObjects.security.login(
'no_infrastructure_privileges_user',
'no_infrastructure_privileges_user-password',
{
expectSpaceSelector: false,
}
);
});
after(async () => {
await security.role.delete('no_infrastructure_privileges_role');
await security.user.delete('no_infrastructure_privileges_user');
});
it(`doesn't show infrastructure navlink`, async () => {
const navLinks = (await appsMenu.readLinks()).map(
(link: Record<string, string>) => link.text
);
expect(navLinks).to.not.contain(['Infrastructure']);
});
it(`infrastructure landing page renders not found page`, async () => {
await PageObjects.common.navigateToActualUrl('infraOps', 'home', {
ensureCurrentUrl: false,
shouldLoginIfPrompted: false,
});
await testSubjects.existOrFail('infraNotFoundPage');
});
it(`metrics page renders not found page`, async () => {
await PageObjects.common.navigateToActualUrl(
'infraOps',
'/metrics/host/demo-stack-redis-01',
{
ensureCurrentUrl: false,
shouldLoginIfPrompted: false,
}
);
await testSubjects.existOrFail('infraNotFoundPage');
});
});
});
}

View file

@ -0,0 +1,210 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from 'expect.js';
import { SpacesService } from 'x-pack/test/common/services';
import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers';
import { DATE_WITH_DATA } from '../constants';
// tslint:disable:no-default-export
export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) {
const esArchiver = getService('esArchiver');
const spacesService: SpacesService = getService('spaces');
const PageObjects = getPageObjects(['common', 'infraHome', 'security', 'spaceSelector']);
const testSubjects = getService('testSubjects');
const appsMenu = getService('appsMenu');
describe('infrastructure spaces', () => {
before(async () => {
await esArchiver.load('infra/metrics_and_logs');
});
after(async () => {
await esArchiver.unload('infra/metrics_and_logs');
});
describe('space with no features disabled', () => {
before(async () => {
// we need to load the following in every situation as deleting
// a space deletes all of the associated saved objects
await esArchiver.load('empty_kibana');
await spacesService.create({
id: 'custom_space',
name: 'custom_space',
disabledFeatures: [],
});
});
after(async () => {
await spacesService.delete('custom_space');
await esArchiver.unload('empty_kibana');
});
it('shows Infrastructure navlink', async () => {
await PageObjects.common.navigateToApp('home', {
basePath: '/s/custom_space',
});
const navLinks = (await appsMenu.readLinks()).map(
(link: Record<string, string>) => link.text
);
expect(navLinks).to.contain('Infrastructure');
});
it(`landing page shows Wafflemap`, async () => {
await PageObjects.common.navigateToActualUrl('infraOps', 'home', {
basePath: '/s/custom_space',
ensureCurrentUrl: true,
});
await PageObjects.infraHome.goToTime(DATE_WITH_DATA);
await testSubjects.existOrFail('waffleMap');
});
describe('context menu', () => {
before(async () => {
await testSubjects.click('nodeContainer');
});
it(`shows link to view logs`, async () => {
await testSubjects.existOrFail('viewLogsContextMenuItem');
});
it(`shows link to view apm traces`, async () => {
await testSubjects.existOrFail('viewApmTracesContextMenuItem');
});
});
});
describe('space with Infrastructure disabled', () => {
before(async () => {
// we need to load the following in every situation as deleting
// a space deletes all of the associated saved objects
await esArchiver.load('empty_kibana');
await spacesService.create({
id: 'custom_space',
name: 'custom_space',
disabledFeatures: ['infrastructure'],
});
});
after(async () => {
await spacesService.delete('custom_space');
await esArchiver.unload('empty_kibana');
});
it(`doesn't show infrastructure navlink`, async () => {
await PageObjects.common.navigateToApp('home', {
basePath: '/s/custom_space',
});
const navLinks = (await appsMenu.readLinks()).map(
(link: Record<string, string>) => link.text
);
expect(navLinks).not.to.contain('Infrastructure');
});
it(`infrastructure landing page renders not found page`, async () => {
await PageObjects.common.navigateToActualUrl('infraOps', 'home', {
basePath: '/s/custom_space',
ensureCurrentUrl: true,
});
await testSubjects.existOrFail('infraNotFoundPage');
});
it(`metrics page renders not found page`, async () => {
await PageObjects.common.navigateToActualUrl(
'infraOps',
'/metrics/host/demo-stack-redis-01',
{
basePath: '/s/custom_space',
ensureCurrentUrl: true,
}
);
await testSubjects.existOrFail('infraNotFoundPage');
});
});
describe('space with Logs disabled', () => {
before(async () => {
// we need to load the following in every situation as deleting
// a space deletes all of the associated saved objects
await esArchiver.load('empty_kibana');
await spacesService.create({
id: 'custom_space',
name: 'custom_space',
disabledFeatures: ['logs'],
});
});
after(async () => {
await spacesService.delete('custom_space');
await esArchiver.unload('empty_kibana');
});
it(`landing page shows Wafflemap`, async () => {
await PageObjects.common.navigateToActualUrl('infraOps', 'home', {
basePath: '/s/custom_space',
ensureCurrentUrl: true,
});
await PageObjects.infraHome.goToTime(DATE_WITH_DATA);
await testSubjects.existOrFail('waffleMap');
});
describe('context menu', () => {
before(async () => {
await testSubjects.click('nodeContainer');
});
it(`doesn't show link to view logs`, async () => {
await testSubjects.missingOrFail('viewLogsContextMenuItem');
});
it(`shows link to view apm traces`, async () => {
await testSubjects.existOrFail('viewApmTracesContextMenuItem');
});
});
});
describe('space with APM disabled', () => {
before(async () => {
// we need to load the following in every situation as deleting
// a space deletes all of the associated saved objects
await esArchiver.load('empty_kibana');
await spacesService.create({
id: 'custom_space',
name: 'custom_space',
disabledFeatures: ['apm'],
});
});
after(async () => {
await spacesService.delete('custom_space');
await esArchiver.unload('empty_kibana');
});
it(`landing page shows Wafflemap`, async () => {
await PageObjects.common.navigateToActualUrl('infraOps', 'home', {
basePath: '/s/custom_space',
ensureCurrentUrl: true,
});
await PageObjects.infraHome.goToTime(DATE_WITH_DATA);
await testSubjects.existOrFail('waffleMap');
});
describe('context menu', () => {
before(async () => {
await testSubjects.click('nodeContainer');
});
it(`shows link to view logs`, async () => {
await testSubjects.existOrFail('viewLogsContextMenuItem');
});
it(`doesn't show link to view apm traces`, async () => {
await testSubjects.missingOrFail('viewApmTracesContextMenuItem');
});
});
});
});
}

View file

@ -0,0 +1,197 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from 'expect.js';
import { KibanaFunctionalTestDefaultProviders } from 'x-pack/test/types/providers';
// tslint:disable no-default-export
export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) {
const esArchiver = getService('esArchiver');
const security = getService('security');
const PageObjects = getPageObjects(['common', 'infraHome', 'security']);
const testSubjects = getService('testSubjects');
const appsMenu = getService('appsMenu');
describe('logs security', () => {
before(async () => {
esArchiver.load('empty_kibana');
});
describe('global logs all privileges', () => {
before(async () => {
await security.role.create('global_logs_all_role', {
elasticsearch: {
indices: [{ names: ['metricbeat-*'], privileges: ['read', 'view_index_metadata'] }],
},
kibana: [
{
feature: {
logs: ['all'],
},
spaces: ['*'],
},
],
});
await security.user.create('global_logs_all_user', {
password: 'global_logs_all_user-password',
roles: ['global_logs_all_role'],
full_name: 'test user',
});
await PageObjects.security.forceLogout();
await PageObjects.security.login('global_logs_all_user', 'global_logs_all_user-password', {
expectSpaceSelector: false,
});
});
after(async () => {
await Promise.all([
security.role.delete('global_logs_all_role'),
security.user.delete('global_logs_all_user'),
PageObjects.security.forceLogout(),
]);
});
it('shows logs navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map(
(link: Record<string, string>) => link.text
);
expect(navLinks).to.eql(['Logs', 'Management']);
});
describe('logs landing page without data', () => {
it(`shows 'Change source configuration' button`, async () => {
await PageObjects.common.navigateToActualUrl('infraOps', 'logs', {
ensureCurrentUrl: true,
shouldLoginIfPrompted: false,
});
await testSubjects.existOrFail('infraLogsPage');
await testSubjects.existOrFail('logsViewSetupInstructionsButton');
await testSubjects.existOrFail('logsChangeSourceConfigurationButton');
});
});
});
describe('global logs read privileges', () => {
before(async () => {
await security.role.create('global_logs_read_role', {
elasticsearch: {
indices: [{ names: ['metricbeat-*'], privileges: ['read', 'view_index_metadata'] }],
},
kibana: [
{
feature: {
logs: ['read'],
},
spaces: ['*'],
},
],
});
await security.user.create('global_logs_read_user', {
password: 'global_logs_read_user-password',
roles: ['global_logs_read_role'],
full_name: 'test user',
});
await PageObjects.security.forceLogout();
await PageObjects.security.login(
'global_logs_read_user',
'global_logs_read_user-password',
{
expectSpaceSelector: false,
}
);
});
after(async () => {
await Promise.all([
security.role.delete('global_logs_read_role'),
security.user.delete('global_logs_read_user'),
PageObjects.security.forceLogout(),
]);
});
it('shows logs navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map(
(link: Record<string, string>) => link.text
);
expect(navLinks).to.eql(['Logs', 'Management']);
});
describe('logs landing page without data', () => {
it(`doesn't show 'Change source configuration' button`, async () => {
await PageObjects.common.navigateToActualUrl('infraOps', 'logs', {
ensureCurrentUrl: true,
shouldLoginIfPrompted: false,
});
await testSubjects.existOrFail('infraLogsPage');
await testSubjects.existOrFail('logsViewSetupInstructionsButton');
await testSubjects.missingOrFail('logsChangeSourceConfigurationButton');
});
});
});
describe('global logs no privileges', () => {
before(async () => {
await security.role.create('global_logs_no_privileges_role', {
elasticsearch: {
indices: [{ names: ['metricbeat-*'], privileges: ['read', 'view_index_metadata'] }],
},
kibana: [
{
feature: {
infrastructure: ['all'],
},
spaces: ['*'],
},
],
});
await security.user.create('global_logs_no_privileges_user', {
password: 'global_logs_no_privileges_user-password',
roles: ['global_logs_no_privileges_role'],
full_name: 'test user',
});
await PageObjects.security.forceLogout();
await PageObjects.security.login(
'global_logs_no_privileges_user',
'global_logs_no_privileges_user-password',
{
expectSpaceSelector: false,
}
);
});
after(async () => {
await Promise.all([
security.role.delete('global_logs_no_privileges_role'),
security.user.delete('global_logs_no_privileges_user'),
PageObjects.security.forceLogout(),
]);
});
it(`doesn't show logs navlink`, async () => {
const navLinks = (await appsMenu.readLinks()).map(
(link: Record<string, string>) => link.text
);
expect(navLinks).to.not.contain('Logs');
});
it('logs landing page renders not found page', async () => {
await PageObjects.common.navigateToActualUrl('infraOps', 'logs', {
ensureCurrentUrl: true,
shouldLoginIfPrompted: false,
});
await testSubjects.existOrFail('infraNotFoundPage');
});
});
});
}

View file

@ -0,0 +1,98 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from 'expect.js';
import { SpacesService } from 'x-pack/test/common/services';
import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers';
// tslint:disable:no-default-export
export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) {
const esArchiver = getService('esArchiver');
const spacesService: SpacesService = getService('spaces');
const PageObjects = getPageObjects(['common', 'infraHome', 'security', 'spaceSelector']);
const testSubjects = getService('testSubjects');
const appsMenu = getService('appsMenu');
describe('logs spaces', () => {
describe('space with no features disabled', () => {
before(async () => {
// we need to load the following in every situation as deleting
// a space deletes all of the associated saved objects
await esArchiver.load('empty_kibana');
await spacesService.create({
id: 'custom_space',
name: 'custom_space',
disabledFeatures: [],
});
});
after(async () => {
await spacesService.delete('custom_space');
await esArchiver.unload('empty_kibana');
});
it('shows Logs navlink', async () => {
await PageObjects.common.navigateToApp('home', {
basePath: '/s/custom_space',
});
const navLinks = (await appsMenu.readLinks()).map(
(link: Record<string, string>) => link.text
);
expect(navLinks).to.contain('Logs');
});
describe('logs landing page without data', () => {
it(`shows 'Change source configuration' button`, async () => {
await PageObjects.common.navigateToActualUrl('infraOps', 'logs', {
basePath: '/s/custom_space',
ensureCurrentUrl: true,
shouldLoginIfPrompted: false,
});
await testSubjects.existOrFail('infraLogsPage');
await testSubjects.existOrFail('logsViewSetupInstructionsButton');
await testSubjects.existOrFail('logsChangeSourceConfigurationButton');
});
});
});
describe('space with Logs disabled', () => {
before(async () => {
// we need to load the following in every situation as deleting
// a space deletes all of the associated saved objects
await esArchiver.load('empty_kibana');
await spacesService.create({
id: 'custom_space',
name: 'custom_space',
disabledFeatures: ['logs'],
});
});
after(async () => {
await spacesService.delete('custom_space');
await esArchiver.unload('empty_kibana');
});
it(`doesn't show Logs navlink`, async () => {
await PageObjects.common.navigateToApp('home', {
basePath: '/s/custom_space',
});
const navLinks = (await appsMenu.readLinks()).map(
(link: Record<string, string>) => link.text
);
expect(navLinks).to.not.contain('Logs');
});
it('logs landing page renders not found page', async () => {
await PageObjects.common.navigateToActualUrl('infraOps', 'logs', {
basePath: '/s/custom_space',
ensureCurrentUrl: true,
shouldLoginIfPrompted: false,
});
await testSubjects.existOrFail('infraNotFoundPage');
});
});
});
}

View file

@ -5,9 +5,7 @@
*/
import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers';
const DATE_WITH_DATA = new Date(1539806283000);
const DATE_WITHOUT_DATA = new Date(1539122400000);
import { DATE_WITH_DATA, DATE_WITHOUT_DATA } from './constants';
// tslint:disable-next-line:no-default-export
export default ({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) => {

View file

@ -12,5 +12,6 @@ export default ({ loadTestFile }: KibanaFunctionalTestDefaultProviders) => {
this.tags('ciGroup7');
loadTestFile(require.resolve('./home_page'));
loadTestFile(require.resolve('./feature_controls'));
});
};

View file

@ -61,6 +61,7 @@ export default function savedObjectsManagementTests({
'dashboard',
'timelion-sheet',
'url',
'infrastructure-ui-source',
],
})
);
@ -86,6 +87,7 @@ export default function savedObjectsManagementTests({
'dashboard',
'timelion-sheet',
'url',
'infrastructure-ui-source',
],
})
);

View file

@ -55,6 +55,7 @@ export default function savedObjectsManagementTests({
'dashboard',
'timelion-sheet',
'url',
'infrastructure-ui-source',
],
})
);
@ -76,6 +77,7 @@ export default function savedObjectsManagementTests({
'dashboard',
'timelion-sheet',
'url',
'infrastructure-ui-source',
],
})
);