Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve home screen for limited-access users #77665

Merged
merged 2 commits into from
Sep 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion src/plugins/home/public/application/components/home.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,10 @@ export class Home extends Component {
</EuiFlexItem>

{stackManagement ? (
<EuiFlexItem className="homHeader__actionItem">
<EuiFlexItem
className="homHeader__actionItem"
data-test-subj="homManagementActionItem"
>
<EuiButtonEmpty
onClick={createAppNavigationHandler(stackManagement.path)}
iconType="gear"
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,9 @@ describe('ManageData', () => {
);
expect(component).toMatchSnapshot();
});

test('render empty without any features', () => {
const component = shallowWithIntl(<ManageData addBasePath={addBasePathMock} features={[]} />);
expect(component).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -36,31 +36,37 @@ export const ManageData: FC<Props> = ({ addBasePath, features }) => (
<>
{features.length > 1 && <EuiHorizontalRule margin="xl" aria-hidden="true" />}

<section className="homDataManage" aria-labelledby="homDataManage__title">
<EuiTitle size="s">
<h2 id="homDataManage__title">
<FormattedMessage id="home.manageData.sectionTitle" defaultMessage="Manage your data" />
</h2>
</EuiTitle>
{features.length > 0 && (
<section
className="homDataManage"
aria-labelledby="homDataManage__title"
data-test-subj="homDataManage"
>
<EuiTitle size="s">
<h2 id="homDataManage__title">
<FormattedMessage id="home.manageData.sectionTitle" defaultMessage="Manage your data" />
</h2>
</EuiTitle>

<EuiSpacer size="m" />
<EuiSpacer size="m" />

<EuiFlexGroup className="homDataManage__content">
{features.map((feature) => (
<EuiFlexItem key={feature.id}>
<Synopsis
id={feature.id}
onClick={createAppNavigationHandler(feature.path)}
description={feature.description}
iconType={feature.icon}
title={feature.title}
url={addBasePath(feature.path)}
wrapInPanel
/>
</EuiFlexItem>
))}
</EuiFlexGroup>
</section>
<EuiFlexGroup className="homDataManage__content">
{features.map((feature) => (
<EuiFlexItem key={feature.id}>
<Synopsis
id={feature.id}
onClick={createAppNavigationHandler(feature.path)}
description={feature.description}
iconType={feature.icon}
title={feature.title}
url={addBasePath(feature.path)}
wrapInPanel
/>
</EuiFlexItem>
))}
</EuiFlexGroup>
</section>
)}
</>
);

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ interface Props {
export const SolutionPanel: FC<Props> = ({ addBasePath, solution }) => (
<EuiFlexItem
key={solution.id}
data-test-subj={`homSolutionPanel homSolutionPanel_${solution.id}`}
className={`${
solution.id === 'kibana' ? 'homSolutions__group homSolutions__group--single' : ''
} homSolutions__item`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,40 @@ describe('FeatureCatalogueRegistry', () => {
expect(service.get()).toEqual([]);
});
});

describe('visibility filtering', () => {
test('retains items with no "visible" callback', () => {
const service = new FeatureCatalogueRegistry();
service.setup().register(DASHBOARD_FEATURE);
const capabilities = { catalogue: {} } as any;
service.start({ capabilities });
expect(service.get()).toEqual([DASHBOARD_FEATURE]);
});

test('retains items with a "visible" callback which returns "true"', () => {
const service = new FeatureCatalogueRegistry();
const feature = {
...DASHBOARD_FEATURE,
visible: () => true,
};
service.setup().register(feature);
const capabilities = { catalogue: {} } as any;
service.start({ capabilities });
expect(service.get()).toEqual([feature]);
});

test('removes items with a "visible" callback which returns "false"', () => {
const service = new FeatureCatalogueRegistry();
const feature = {
...DASHBOARD_FEATURE,
visible: () => false,
};
service.setup().register(feature);
const capabilities = { catalogue: {} } as any;
service.start({ capabilities });
expect(service.get()).toEqual([]);
});
});
});

describe('title sorting', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export interface FeatureCatalogueEntry {
readonly showOnHomePage: boolean;
/** An ordinal used to sort features relative to one another for display on the home page */
readonly order?: number;
/** Optional function to control visibility of this feature. */
readonly visible?: () => boolean;
}

/** @public */
Expand Down Expand Up @@ -103,7 +105,10 @@ export class FeatureCatalogueRegistry {
}
const capabilities = this.capabilities;
return [...this.features.values()]
.filter((entry) => capabilities.catalogue[entry.id] !== false)
.filter(
(entry) =>
capabilities.catalogue[entry.id] !== false && (entry.visible ? entry.visible() : true)
)
.sort(compareByKey('title'));
}

Expand Down
7 changes: 5 additions & 2 deletions src/plugins/management/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export class ManagementPlugin implements Plugin<ManagementSetup, ManagementStart

private readonly appUpdater = new BehaviorSubject<AppUpdater>(() => ({}));

private hasAnyEnabledApps = true;

constructor(private initializerContext: PluginInitializerContext) {}

public setup(core: CoreSetup, { home }: ManagementSetupDependencies) {
Expand All @@ -65,6 +67,7 @@ export class ManagementPlugin implements Plugin<ManagementSetup, ManagementStart
path: '/app/management',
showOnHomePage: false,
category: FeatureCatalogueCategory.ADMIN,
visible: () => this.hasAnyEnabledApps,
});
}

Expand Down Expand Up @@ -96,11 +99,11 @@ export class ManagementPlugin implements Plugin<ManagementSetup, ManagementStart

public start(core: CoreStart) {
this.managementSections.start({ capabilities: core.application.capabilities });
const hasAnyEnabledApps = getSectionsServiceStartPrivate()
this.hasAnyEnabledApps = getSectionsServiceStartPrivate()
.getSectionsEnabled()
.some((section) => section.getAppsEnabled().length > 0);

if (!hasAnyEnabledApps) {
if (!this.hasAnyEnabledApps) {
this.appUpdater.next(() => {
return {
status: AppStatus.inaccessible,
Expand Down
8 changes: 8 additions & 0 deletions test/functional/page_objects/home_page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ export function HomePageProvider({ getService, getPageObjects }: FtrProviderCont
return !(await testSubjects.exists(`addSampleDataSet${id}`));
}

async getVisibileSolutions() {
const solutionPanels = await testSubjects.findAll('~homSolutionPanel', 2000);
const panelAttributes = await Promise.all(
solutionPanels.map((panel) => panel.getAttribute('data-test-subj'))
);
return panelAttributes.map((attributeValue) => attributeValue.split('homSolutionPanel_')[1]);
}

async addSampleDataSet(id: string) {
const isInstalled = await this.isSampleDataSetInstalled(id);
if (!isInstalled) {
Expand Down
5 changes: 3 additions & 2 deletions x-pack/plugins/index_lifecycle_management/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,11 @@ export class IndexLifecycleManagementServerPlugin implements Plugin<void, void,
);

features.registerElasticsearchFeature({
id: 'index_lifecycle_management',
id: PLUGIN.ID,
management: {
data: ['index_lifecycle_management'],
data: [PLUGIN.ID],
},
catalogue: [PLUGIN.ID],
privileges: [
{
requiredClusterPrivileges: ['manage_ilm'],
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/snapshot_restore/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export class SnapshotRestoreServerPlugin implements Plugin<void, void, any, any>
management: {
data: [PLUGIN.id],
},
catalogue: [PLUGIN.id],
privileges: [
{
requiredClusterPrivileges: [...APP_REQUIRED_CLUSTER_PRIVILEGES],
Expand Down
130 changes: 130 additions & 0 deletions x-pack/test/functional/apps/home/feature_controls/home_security.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* 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 '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';

export default function ({ getPageObjects, getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const security = getService('security');
const PageObjects = getPageObjects(['security', 'home']);
const testSubjects = getService('testSubjects');

describe('security', () => {
before(async () => {
await esArchiver.load('dashboard/feature_controls/security');
await esArchiver.loadIfNeeded('logstash_functional');

// ensure we're logged out so we can login as the appropriate users
await PageObjects.security.forceLogout();
});

after(async () => {
await esArchiver.unload('dashboard/feature_controls/security');

// logout, so the other tests don't accidentally run as the custom users we're testing below
await PageObjects.security.forceLogout();
});

describe('global all privileges', () => {
before(async () => {
await security.role.create('global_all_role', {
elasticsearch: {},
kibana: [
{
base: ['all'],
spaces: ['*'],
},
],
});

await security.user.create('global_all_user', {
password: 'global_all_user-password',
roles: ['global_all_role'],
full_name: 'test user',
});

await PageObjects.security.login('global_all_user', 'global_all_user-password', {
expectSpaceSelector: false,
});
});

after(async () => {
await security.role.delete('global_all_role');
await security.user.delete('global_all_user');
});

it('shows all available solutions', async () => {
const solutions = await PageObjects.home.getVisibileSolutions();
expect(solutions).to.eql([
'enterpriseSearch',
'observability',
'securitySolution',
'kibana',
]);
});

it('shows the management section', async () => {
await testSubjects.existOrFail('homDataManage', { timeout: 2000 });
});

it('shows the "Manage" action item', async () => {
await testSubjects.existOrFail('homManagementActionItem', {
timeout: 2000,
});
});
});

describe('global dashboard all privileges', () => {
before(async () => {
await security.role.create('global_dashboard_all_role', {
elasticsearch: {},
kibana: [
{
feature: {
dashboard: ['all'],
},
spaces: ['*'],
},
],
});

await security.user.create('global_dashboard_all_user', {
password: 'global_dashboard_all_user-password',
roles: ['global_dashboard_all_role'],
full_name: 'test user',
});

await PageObjects.security.login(
'global_dashboard_all_user',
'global_dashboard_all_user-password',
{
expectSpaceSelector: false,
}
);
});

after(async () => {
await security.role.delete('global_dashboard_all_role');
await security.user.delete('global_dashboard_all_user');
});

it('shows only the kibana solution', async () => {
const solutions = await PageObjects.home.getVisibileSolutions();
expect(solutions).to.eql(['kibana']);
});

it('does not show the management section', async () => {
await testSubjects.missingOrFail('homDataManage', { timeout: 2000 });
});

it('does not show the "Manage" action item', async () => {
await testSubjects.missingOrFail('homManagementActionItem', {
timeout: 2000,
});
});
});
});
}
Loading