mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
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:
parent
bb76d20bf5
commit
e79d63b5d1
42 changed files with 2679 additions and 680 deletions
22
src/legacy/ui/public/capabilities/react/index.ts
Normal file
22
src/legacy/ui/public/capabilities/react/index.ts
Normal 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';
|
|
@ -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>
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
22
src/legacy/ui/public/capabilities/react/legacy/index.ts
Normal file
22
src/legacy/ui/public/capabilities/react/legacy/index.ts
Normal 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';
|
|
@ -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>
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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: {},
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -92,7 +92,7 @@ export function apm(kibana: any) {
|
|||
all: [],
|
||||
read: ['config']
|
||||
},
|
||||
ui: []
|
||||
ui: ['show']
|
||||
}
|
||||
},
|
||||
privilegesTooltip: i18n.translate('xpack.apm.privileges.tooltip', {
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
298
x-pack/test/api_integration/apis/infra/feature_controls.ts
Normal file
298
x-pack/test/api_integration/apis/infra/feature_controls.ts
Normal 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;
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
8
x-pack/test/functional/apps/infra/constants.ts
Normal file
8
x-pack/test/functional/apps/infra/constants.ts
Normal 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);
|
16
x-pack/test/functional/apps/infra/feature_controls/index.ts
Normal file
16
x-pack/test/functional/apps/infra/feature_controls/index.ts
Normal 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'));
|
||||
});
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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) => {
|
||||
|
|
|
@ -12,5 +12,6 @@ export default ({ loadTestFile }: KibanaFunctionalTestDefaultProviders) => {
|
|||
this.tags('ciGroup7');
|
||||
|
||||
loadTestFile(require.resolve('./home_page'));
|
||||
loadTestFile(require.resolve('./feature_controls'));
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
],
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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',
|
||||
],
|
||||
})
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue