Improve home screen for limited-access users (#77665)

This commit is contained in:
Larry Gregory 2020-09-18 08:54:08 -04:00 committed by GitHub
parent 5a31dce92d
commit 613509d81a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 287 additions and 32 deletions

View file

@ -164,6 +164,7 @@ exports[`home directories should not render directory entry when showOnHomePage
</EuiFlexItem>
<EuiFlexItem
className="homHeader__actionItem"
data-test-subj="homManagementActionItem"
>
<EuiButtonEmpty
iconType="gear"
@ -906,6 +907,7 @@ exports[`home header should show "Manage" link if stack management is available
</EuiFlexItem>
<EuiFlexItem
className="homHeader__actionItem"
data-test-subj="homManagementActionItem"
>
<EuiButtonEmpty
iconType="gear"

View file

@ -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"

View file

@ -9,6 +9,7 @@ exports[`ManageData render 1`] = `
<section
aria-labelledby="homDataManage__title"
className="homDataManage"
data-test-subj="homDataManage"
>
<EuiTitle
size="s"
@ -89,3 +90,5 @@ exports[`ManageData render 1`] = `
</section>
</Fragment>
`;
exports[`ManageData render empty without any features 1`] = `<Fragment />`;

View file

@ -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();
});
});

View file

@ -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>
)}
</>
);

View file

@ -3,6 +3,7 @@
exports[`SolutionPanel renders the solution panel for the given solution 1`] = `
<EuiFlexItem
className="homSolutions__group homSolutions__group--single homSolutions__item"
data-test-subj="homSolutionPanel homSolutionPanel_kibana"
grow={1}
key="kibana"
>

View file

@ -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`}

View file

@ -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', () => {

View file

@ -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 */
@ -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'));
}

View file

@ -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) {
@ -65,6 +67,7 @@ export class ManagementPlugin implements Plugin<ManagementSetup, ManagementStart
path: '/app/management',
showOnHomePage: false,
category: FeatureCatalogueCategory.ADMIN,
visible: () => this.hasAnyEnabledApps,
});
}
@ -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,

View file

@ -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) {

View file

@ -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'],

View file

@ -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],

View file

@ -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,
});
});
});
});
}

View file

@ -0,0 +1,14 @@
/*
* 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 { FtrProviderContext } from '../../../ftr_provider_context';
export default ({ loadTestFile }: FtrProviderContext) => {
describe('feature controls', function () {
this.tags('skipFirefox');
loadTestFile(require.resolve('./home_security'));
});
};

View file

@ -0,0 +1,14 @@
/*
* 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 { FtrProviderContext } from '../../ftr_provider_context';
export default ({ loadTestFile }: FtrProviderContext) => {
describe('Home page', function () {
this.tags('ciGroup7');
loadTestFile(require.resolve('./feature_controls'));
});
};

View file

@ -13,7 +13,15 @@ import { UserAtSpaceScenarios } from '../scenarios';
export default function catalogueTests({ getService }: FtrProviderContext) {
const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities');
const esFeatureExceptions = ['security', 'rollup_jobs', 'reporting', 'transform', 'watcher'];
const esFeatureExceptions = [
'security',
'index_lifecycle_management',
'snapshot_restore',
'rollup_jobs',
'reporting',
'transform',
'watcher',
];
describe('catalogue', () => {
UserAtSpaceScenarios.forEach((scenario) => {

View file

@ -13,7 +13,15 @@ import { UserScenarios } from '../scenarios';
export default function catalogueTests({ getService }: FtrProviderContext) {
const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities');
const esFeatureExceptions = ['security', 'rollup_jobs', 'reporting', 'transform', 'watcher'];
const esFeatureExceptions = [
'security',
'index_lifecycle_management',
'snapshot_restore',
'rollup_jobs',
'reporting',
'transform',
'watcher',
];
describe('catalogue', () => {
UserScenarios.forEach((scenario) => {

View file

@ -13,7 +13,15 @@ import { SpaceScenarios } from '../scenarios';
export default function catalogueTests({ getService }: FtrProviderContext) {
const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities');
const esFeatureExceptions = ['security', 'rollup_jobs', 'reporting', 'transform', 'watcher'];
const esFeatureExceptions = [
'security',
'index_lifecycle_management',
'snapshot_restore',
'rollup_jobs',
'reporting',
'transform',
'watcher',
];
describe('catalogue', () => {
SpaceScenarios.forEach((scenario) => {