Skip to content

Commit

Permalink
Improve home screen for limited-access users (#77665)
Browse files Browse the repository at this point in the history
  • Loading branch information
legrego authored Sep 18, 2020
1 parent 5a31dce commit 613509d
Show file tree
Hide file tree
Showing 19 changed files with 287 additions and 32 deletions.

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

0 comments on commit 613509d

Please sign in to comment.