mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
* Spaces - Client NP Migration, Phase 1 (#40856) * shimming NP for spaces client-side plugin * refresh active space in nav control when updated * fix advanced settings screen * allow npStart from unauthed routes * use NP for deriving space management url * remove security's usage of SpacesManager * remove usages of ui/capabilities * fix tests * implement NP plugin interface * remove hack in favor of convention in migration guide * shim feature catalogue registration * streamline nav control, and handle async loading more gracefully * adding opaqueId * fixes from merge * fix merge from master * fixing merge from master * move _active_space route to NP * moving to the NP feature catalogue registry * moving setup to setup phase * optimizing active space retrieval * reverting test isolation change * Apply suggestions from code review Co-Authored-By: Aleh Zasypkin <aleh.zasypkin@gmail.com> * removing unnecessary PluginInitializerContext * updating advanced settings subtitle * using NP anonymousPaths service * additional nav_control_popover cleanup * additional cleanup * testing out onActiveSpaceChange$ property * make the linter happy * make the type checker happy * fixing types * fix merge from master * spaces LP init should run on all pages, not just the kibana app * address nits * fix infra/logs, and the spaces disabled scenario * fix typescript errors * revert changes to infra plugin * reintroducing activeSpace injected var for legacy plugins * fixing react deprecation warning and unhandled promise rejection * restore activeSpace default var * spaces does not need to check its own enabled status * fix from merge Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> * fix backport merge
This commit is contained in:
parent
4e3095be0e
commit
9c2fa892ca
57 changed files with 791 additions and 666 deletions
|
@ -11,6 +11,7 @@ import * as rt from 'io-ts';
|
|||
import { useKibanaInjectedVar } from './use_kibana_injected_var';
|
||||
|
||||
export const useKibanaSpaceId = (): string => {
|
||||
// NOTICE: use of `activeSpace` is deprecated and will not be made available in the New Platform.
|
||||
const activeSpace = useKibanaInjectedVar('activeSpace');
|
||||
|
||||
return pipe(
|
||||
|
|
|
@ -15,7 +15,6 @@ import 'plugins/security/services/shield_user';
|
|||
import 'plugins/security/services/shield_role';
|
||||
import 'plugins/security/services/shield_indices';
|
||||
import { xpackInfo } from 'plugins/xpack_main/services/xpack_info';
|
||||
import { SpacesManager } from '../../../../../spaces/public/lib';
|
||||
import { ROLES_PATH, CLONE_ROLES_PATH, EDIT_ROLES_PATH } from '../management_urls';
|
||||
import { getEditRoleBreadcrumbs, getCreateRoleBreadcrumbs } from '../breadcrumbs';
|
||||
|
||||
|
@ -79,7 +78,7 @@ const routeDefinition = action => ({
|
|||
},
|
||||
spaces(spacesEnabled) {
|
||||
if (spacesEnabled) {
|
||||
return new SpacesManager().getSpaces();
|
||||
return kfetch({ method: 'get', pathname: '/api/spaces/space' });
|
||||
}
|
||||
return [];
|
||||
},
|
||||
|
|
|
@ -48,7 +48,6 @@ export const spaces = (kibana: Record<string, any>) =>
|
|||
},
|
||||
|
||||
uiExports: {
|
||||
chromeNavControls: ['plugins/spaces/views/nav_control'],
|
||||
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
|
||||
managementSections: ['plugins/spaces/views/management'],
|
||||
apps: [
|
||||
|
@ -60,7 +59,7 @@ export const spaces = (kibana: Record<string, any>) =>
|
|||
hidden: true,
|
||||
},
|
||||
],
|
||||
hacks: [],
|
||||
hacks: ['plugins/spaces/legacy'],
|
||||
mappings,
|
||||
migrations: {
|
||||
space: {
|
||||
|
@ -73,19 +72,21 @@ export const spaces = (kibana: Record<string, any>) =>
|
|||
hidden: true,
|
||||
},
|
||||
},
|
||||
home: ['plugins/spaces/register_feature'],
|
||||
injectDefaultVars(server: any) {
|
||||
home: [],
|
||||
injectDefaultVars(server: Server) {
|
||||
return {
|
||||
spaces: [],
|
||||
activeSpace: null,
|
||||
serverBasePath: server.config().get('server.basePath'),
|
||||
activeSpace: null,
|
||||
};
|
||||
},
|
||||
async replaceInjectedVars(
|
||||
vars: Record<string, any>,
|
||||
request: Legacy.Request,
|
||||
server: Record<string, any>
|
||||
server: Server
|
||||
) {
|
||||
// NOTICE: use of `activeSpace` is deprecated and will not be made available in the New Platform.
|
||||
// Known usages:
|
||||
// - x-pack/legacy/plugins/infra/public/utils/use_kibana_space_id.ts
|
||||
const spacesPlugin = server.newPlatform.setup.plugins.spaces as SpacesPluginSetup;
|
||||
if (!spacesPlugin) {
|
||||
throw new Error('New Platform XPack Spaces plugin is not available.');
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
jest.mock('ui/capabilities', () => ({
|
||||
capabilities: {
|
||||
get: jest.fn().mockReturnValue({
|
||||
navLinks: {},
|
||||
management: {},
|
||||
catalogue: {},
|
||||
spaces: {
|
||||
manage: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
import { capabilities, UICapabilities } from 'ui/capabilities';
|
||||
|
||||
export function setMockCapabilities(mockCapabilities: UICapabilities) {
|
||||
((capabilities.get as unknown) as jest.Mock).mockReturnValue(mockCapabilities);
|
||||
}
|
|
@ -4,37 +4,40 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { setMockCapabilities } from '../__mocks__/ui_capabilities';
|
||||
import React from 'react';
|
||||
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { ManageSpacesButton } from './manage_spaces_button';
|
||||
|
||||
describe('ManageSpacesButton', () => {
|
||||
it('renders as expected', () => {
|
||||
setMockCapabilities({
|
||||
navLinks: {},
|
||||
management: {},
|
||||
catalogue: {},
|
||||
spaces: {
|
||||
manage: true,
|
||||
},
|
||||
});
|
||||
|
||||
const component = <ManageSpacesButton />;
|
||||
const component = (
|
||||
<ManageSpacesButton
|
||||
capabilities={{
|
||||
navLinks: {},
|
||||
management: {},
|
||||
catalogue: {},
|
||||
spaces: {
|
||||
manage: true,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(shallowWithIntl(component)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it(`doesn't render if user profile forbids managing spaces`, () => {
|
||||
setMockCapabilities({
|
||||
navLinks: {},
|
||||
management: {},
|
||||
catalogue: {},
|
||||
spaces: {
|
||||
manage: false,
|
||||
},
|
||||
});
|
||||
|
||||
const component = <ManageSpacesButton />;
|
||||
const component = (
|
||||
<ManageSpacesButton
|
||||
capabilities={{
|
||||
navLinks: {},
|
||||
management: {},
|
||||
catalogue: {},
|
||||
spaces: {
|
||||
manage: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(shallowWithIntl(component)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
import { EuiButton } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { Component, CSSProperties } from 'react';
|
||||
import { capabilities } from 'ui/capabilities';
|
||||
import { MANAGE_SPACES_URL } from '../lib/constants';
|
||||
import { Capabilities } from 'src/core/public';
|
||||
import { getManageSpacesUrl } from '../lib/constants';
|
||||
|
||||
interface Props {
|
||||
isDisabled?: boolean;
|
||||
|
@ -16,11 +16,12 @@ interface Props {
|
|||
size?: 's' | 'm';
|
||||
style?: CSSProperties;
|
||||
onClick?: () => void;
|
||||
capabilities: Capabilities;
|
||||
}
|
||||
|
||||
export class ManageSpacesButton extends Component<Props, {}> {
|
||||
public render() {
|
||||
if (!capabilities.get().spaces.manage) {
|
||||
if (!this.props.capabilities.spaces.manage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -44,6 +45,6 @@ export class ManageSpacesButton extends Component<Props, {}> {
|
|||
if (this.props.onClick) {
|
||||
this.props.onClick();
|
||||
}
|
||||
window.location.replace(MANAGE_SPACES_URL);
|
||||
window.location.replace(getManageSpacesUrl());
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,13 +6,12 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
FeatureCatalogueEntry,
|
||||
FeatureCatalogueCategory,
|
||||
FeatureCatalogueRegistryProvider,
|
||||
// @ts-ignore
|
||||
} from 'ui/registry/feature_catalogue';
|
||||
} from '../../../../../src/plugins/home/public';
|
||||
import { getSpacesFeatureDescription } from './lib/constants';
|
||||
|
||||
FeatureCatalogueRegistryProvider.register(() => {
|
||||
export const createSpacesFeatureCatalogueEntry = (): FeatureCatalogueEntry => {
|
||||
return {
|
||||
id: 'spaces',
|
||||
title: i18n.translate('xpack.spaces.spacesTitle', {
|
||||
|
@ -24,4 +23,4 @@ FeatureCatalogueRegistryProvider.register(() => {
|
|||
showOnHomePage: true,
|
||||
category: FeatureCatalogueCategory.ADMIN,
|
||||
};
|
||||
});
|
||||
};
|
10
x-pack/legacy/plugins/spaces/public/index.ts
Normal file
10
x-pack/legacy/plugins/spaces/public/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 { SpacesPlugin } from './plugin';
|
||||
|
||||
export const plugin = () => {
|
||||
return new SpacesPlugin();
|
||||
};
|
18
x-pack/legacy/plugins/spaces/public/legacy.ts
Normal file
18
x-pack/legacy/plugins/spaces/public/legacy.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { npSetup, npStart } from 'ui/new_platform';
|
||||
import { plugin } from '.';
|
||||
import { SpacesPlugin, PluginsSetup } from './plugin';
|
||||
|
||||
const spacesPlugin: SpacesPlugin = plugin();
|
||||
|
||||
const plugins: PluginsSetup = {
|
||||
home: npSetup.plugins.home,
|
||||
};
|
||||
|
||||
export const setup = spacesPlugin.setup(npSetup.core, plugins);
|
||||
export const start = spacesPlugin.start(npStart.core);
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import chrome from 'ui/chrome';
|
||||
import { npSetup } from 'ui/new_platform';
|
||||
|
||||
let spacesFeatureDescription: string;
|
||||
|
||||
|
@ -20,4 +20,5 @@ export const getSpacesFeatureDescription = () => {
|
|||
return spacesFeatureDescription;
|
||||
};
|
||||
|
||||
export const MANAGE_SPACES_URL = chrome.addBasePath(`/app/kibana#/management/spaces/list`);
|
||||
export const getManageSpacesUrl = () =>
|
||||
npSetup.core.http.basePath.prepend(`/app/kibana#/management/spaces/list`);
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
SavedObjectsManagementRecord,
|
||||
} from '../../../../../../../src/legacy/core_plugins/management/public';
|
||||
import { CopySavedObjectsToSpaceFlyout } from '../../views/management/components/copy_saved_objects_to_space';
|
||||
import { Space } from '../../../common/model/space';
|
||||
import { SpacesManager } from '../spaces_manager';
|
||||
|
||||
export class CopyToSpaceSavedObjectsManagementAction extends SavedObjectsManagementAction {
|
||||
|
@ -31,7 +30,7 @@ export class CopyToSpaceSavedObjectsManagementAction extends SavedObjectsManagem
|
|||
},
|
||||
};
|
||||
|
||||
constructor(private readonly spacesManager: SpacesManager, private readonly activeSpace: Space) {
|
||||
constructor(private readonly spacesManager: SpacesManager) {
|
||||
super();
|
||||
}
|
||||
|
||||
|
@ -44,7 +43,6 @@ export class CopyToSpaceSavedObjectsManagementAction extends SavedObjectsManagem
|
|||
onClose={this.onClose}
|
||||
savedObject={this.record}
|
||||
spacesManager={this.spacesManager}
|
||||
activeSpace={this.activeSpace}
|
||||
toastNotifications={toastNotifications}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -4,18 +4,21 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { of, Observable } from 'rxjs';
|
||||
import { Space } from '../../common/model/space';
|
||||
|
||||
function createSpacesManagerMock() {
|
||||
return {
|
||||
onActiveSpaceChange$: (of(undefined) as unknown) as Observable<Space>,
|
||||
getSpaces: jest.fn().mockResolvedValue([]),
|
||||
getSpace: jest.fn().mockResolvedValue(undefined),
|
||||
getActiveSpace: jest.fn().mockResolvedValue(undefined),
|
||||
createSpace: jest.fn().mockResolvedValue(undefined),
|
||||
updateSpace: jest.fn().mockResolvedValue(undefined),
|
||||
deleteSpace: jest.fn().mockResolvedValue(undefined),
|
||||
copySavedObjects: jest.fn().mockResolvedValue(undefined),
|
||||
resolveCopySavedObjectsErrors: jest.fn().mockResolvedValue(undefined),
|
||||
redirectToSpaceSelector: jest.fn().mockResolvedValue(undefined),
|
||||
requestRefresh: jest.fn(),
|
||||
on: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,9 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { EventEmitter } from 'events';
|
||||
import { kfetch } from 'ui/kfetch';
|
||||
import { Observable, BehaviorSubject } from 'rxjs';
|
||||
import { skipWhile } from 'rxjs/operators';
|
||||
import { HttpSetup } from 'src/core/public';
|
||||
import { SavedObjectsManagementRecord } from '../../../../../../src/legacy/core_plugins/management/public';
|
||||
import { Space } from '../../common/model/space';
|
||||
import { GetSpacePurpose } from '../../common/model/types';
|
||||
|
@ -12,43 +13,57 @@ import { CopySavedObjectsToSpaceResponse } from './copy_saved_objects_to_space/t
|
|||
import { ENTER_SPACE_PATH } from '../../common/constants';
|
||||
import { addSpaceIdToPath } from '../../../../../plugins/spaces/common';
|
||||
|
||||
export class SpacesManager extends EventEmitter {
|
||||
constructor(private readonly serverBasePath: string) {
|
||||
super();
|
||||
export class SpacesManager {
|
||||
private activeSpace$: BehaviorSubject<Space | null> = new BehaviorSubject<Space | null>(null);
|
||||
|
||||
public readonly onActiveSpaceChange$: Observable<Space>;
|
||||
|
||||
constructor(private readonly serverBasePath: string, private readonly http: HttpSetup) {
|
||||
this.onActiveSpaceChange$ = this.activeSpace$
|
||||
.asObservable()
|
||||
.pipe(skipWhile((v: Space | null) => v == null)) as Observable<Space>;
|
||||
|
||||
this.refreshActiveSpace();
|
||||
}
|
||||
|
||||
public async getSpaces(purpose?: GetSpacePurpose): Promise<Space[]> {
|
||||
return await kfetch({ pathname: '/api/spaces/space', query: { purpose } });
|
||||
return await this.http.get('/api/spaces/space', { query: { purpose } });
|
||||
}
|
||||
|
||||
public async getSpace(id: string): Promise<Space> {
|
||||
return await kfetch({ pathname: `/api/spaces/space/${encodeURIComponent(id)}` });
|
||||
return await this.http.get(`/api/spaces/space/${encodeURIComponent(id)}`);
|
||||
}
|
||||
|
||||
public getActiveSpace({ forceRefresh = false } = {}) {
|
||||
if (!forceRefresh && this.activeSpace$.value) {
|
||||
return Promise.resolve(this.activeSpace$.value);
|
||||
}
|
||||
return this.http.get('/internal/spaces/_active_space') as Promise<Space>;
|
||||
}
|
||||
|
||||
public async createSpace(space: Space) {
|
||||
return await kfetch({
|
||||
pathname: `/api/spaces/space`,
|
||||
method: 'POST',
|
||||
await this.http.post(`/api/spaces/space`, {
|
||||
body: JSON.stringify(space),
|
||||
});
|
||||
}
|
||||
|
||||
public async updateSpace(space: Space) {
|
||||
return await kfetch({
|
||||
pathname: `/api/spaces/space/${encodeURIComponent(space.id)}`,
|
||||
await this.http.put(`/api/spaces/space/${encodeURIComponent(space.id)}`, {
|
||||
query: {
|
||||
overwrite: true,
|
||||
},
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(space),
|
||||
});
|
||||
|
||||
const activeSpaceId = (await this.getActiveSpace()).id;
|
||||
|
||||
if (space.id === activeSpaceId) {
|
||||
this.refreshActiveSpace();
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteSpace(space: Space) {
|
||||
return await kfetch({
|
||||
pathname: `/api/spaces/space/${encodeURIComponent(space.id)}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
await this.http.delete(`/api/spaces/space/${encodeURIComponent(space.id)}`);
|
||||
}
|
||||
|
||||
public async copySavedObjects(
|
||||
|
@ -57,9 +72,7 @@ export class SpacesManager extends EventEmitter {
|
|||
includeReferences: boolean,
|
||||
overwrite: boolean
|
||||
): Promise<CopySavedObjectsToSpaceResponse> {
|
||||
return await kfetch({
|
||||
pathname: `/api/spaces/_copy_saved_objects`,
|
||||
method: 'POST',
|
||||
return this.http.post('/api/spaces/_copy_saved_objects', {
|
||||
body: JSON.stringify({
|
||||
objects,
|
||||
spaces,
|
||||
|
@ -74,9 +87,7 @@ export class SpacesManager extends EventEmitter {
|
|||
retries: unknown,
|
||||
includeReferences: boolean
|
||||
): Promise<CopySavedObjectsToSpaceResponse> {
|
||||
return await kfetch({
|
||||
pathname: `/api/spaces/_resolve_copy_saved_objects_errors`,
|
||||
method: 'POST',
|
||||
return this.http.post(`/api/spaces/_resolve_copy_saved_objects_errors`, {
|
||||
body: JSON.stringify({
|
||||
objects,
|
||||
includeReferences,
|
||||
|
@ -93,7 +104,8 @@ export class SpacesManager extends EventEmitter {
|
|||
window.location.href = `${this.serverBasePath}/spaces/space_selector`;
|
||||
}
|
||||
|
||||
public async requestRefresh() {
|
||||
this.emit('request_refresh');
|
||||
private async refreshActiveSpace() {
|
||||
const activeSpace = await this.getActiveSpace({ forceRefresh: true });
|
||||
this.activeSpace$.next(activeSpace);
|
||||
}
|
||||
}
|
||||
|
|
40
x-pack/legacy/plugins/spaces/public/plugin.tsx
Normal file
40
x-pack/legacy/plugins/spaces/public/plugin.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 { CoreSetup, CoreStart, Plugin } from 'src/core/public';
|
||||
import { HomePublicPluginSetup } from 'src/plugins/home/public';
|
||||
import { SpacesManager } from './lib';
|
||||
import { initSpacesNavControl } from './views/nav_control';
|
||||
import { createSpacesFeatureCatalogueEntry } from './create_feature_catalogue_entry';
|
||||
|
||||
export interface SpacesPluginStart {
|
||||
spacesManager: SpacesManager | null;
|
||||
}
|
||||
|
||||
export interface PluginsSetup {
|
||||
home?: HomePublicPluginSetup;
|
||||
}
|
||||
|
||||
export class SpacesPlugin implements Plugin<void, SpacesPluginStart, PluginsSetup> {
|
||||
private spacesManager: SpacesManager | null = null;
|
||||
|
||||
public async start(core: CoreStart) {
|
||||
const serverBasePath = core.injectedMetadata.getInjectedVar('serverBasePath') as string;
|
||||
|
||||
this.spacesManager = new SpacesManager(serverBasePath, core.http);
|
||||
initSpacesNavControl(this.spacesManager, core);
|
||||
|
||||
return {
|
||||
spacesManager: this.spacesManager,
|
||||
};
|
||||
}
|
||||
|
||||
public async setup(core: CoreSetup, plugins: PluginsSetup) {
|
||||
if (plugins.home) {
|
||||
plugins.home.featureCatalogue.register(createSpacesFeatureCatalogueEntry());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -65,28 +65,6 @@ exports[`ConfirmDeleteModal renders as expected 1`] = `
|
|||
value=""
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer />
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
>
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
defaultMessage="You are about to delete your current space {name}. You will be redirected to choose a different space if you continue."
|
||||
id="xpack.spaces.management.confirmDeleteModal.redirectAfterDeletingCurrentSpaceWarningMessage"
|
||||
values={
|
||||
Object {
|
||||
"name": <span>
|
||||
(
|
||||
<strong>
|
||||
My Space
|
||||
</strong>
|
||||
)
|
||||
</span>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiCallOut>
|
||||
</EuiText>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AdvancedSettingsSubtitle renders as expected 1`] = `
|
||||
<Fragment>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
<EuiCallOut
|
||||
color="primary"
|
||||
iconType="spacesApp"
|
||||
title={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="The settings on this page apply to the {spaceName} space, unless otherwise specified."
|
||||
id="xpack.spaces.management.advancedSettingsSubtitle.applyingSettingsOnPageToSpaceDescription"
|
||||
values={
|
||||
Object {
|
||||
"spaceName": <strong>
|
||||
My Space
|
||||
</strong>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
|
@ -4,16 +4,28 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { AdvancedSettingsSubtitle } from './advanced_settings_subtitle';
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
|
||||
describe('AdvancedSettingsSubtitle', () => {
|
||||
it('renders as expected', () => {
|
||||
it('renders as expected', async () => {
|
||||
const space = {
|
||||
id: 'my-space',
|
||||
name: 'My Space',
|
||||
disabledFeatures: [],
|
||||
};
|
||||
expect(shallowWithIntl(<AdvancedSettingsSubtitle space={space} />)).toMatchSnapshot();
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<AdvancedSettingsSubtitle getActiveSpace={() => Promise.resolve(space)} />
|
||||
);
|
||||
|
||||
// Wait for active space to resolve before requesting the component to update
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find(EuiCallOut)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,30 +6,40 @@
|
|||
|
||||
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { Fragment } from 'react';
|
||||
import React, { Fragment, useState, useEffect } from 'react';
|
||||
import { Space } from '../../../../../common/model/space';
|
||||
|
||||
interface Props {
|
||||
space: Space;
|
||||
getActiveSpace: () => Promise<Space>;
|
||||
}
|
||||
|
||||
export const AdvancedSettingsSubtitle = (props: Props) => (
|
||||
<Fragment>
|
||||
<EuiSpacer size={'m'} />
|
||||
<EuiCallOut
|
||||
color="primary"
|
||||
iconType="spacesApp"
|
||||
title={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.spaces.management.advancedSettingsSubtitle.applyingSettingsOnPageToSpaceDescription"
|
||||
defaultMessage="The settings on this page apply to the {spaceName} space, unless otherwise specified."
|
||||
values={{
|
||||
spaceName: <strong>{props.space.name}</strong>,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
export const AdvancedSettingsSubtitle = (props: Props) => {
|
||||
const [activeSpace, setActiveSpace] = useState<Space | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
props.getActiveSpace().then(space => setActiveSpace(space));
|
||||
}, [props]);
|
||||
|
||||
if (!activeSpace) return null;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSpacer size={'m'} />
|
||||
<EuiCallOut
|
||||
color="primary"
|
||||
iconType="spacesApp"
|
||||
title={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.spaces.management.advancedSettingsSubtitle.applyingSettingsOnPageToSpaceDescription"
|
||||
defaultMessage="The settings on this page apply to the {spaceName} space, unless otherwise specified."
|
||||
values={{
|
||||
spaceName: <strong>{activeSpace.name}</strong>,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AdvancedSettingsTitle renders as expected 1`] = `
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<SpaceAvatar
|
||||
announceSpaceName={true}
|
||||
space={
|
||||
Object {
|
||||
"disabledFeatures": Array [],
|
||||
"id": "my-space",
|
||||
"name": "My Space",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
style={
|
||||
Object {
|
||||
"marginLeft": "10px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<EuiTitle
|
||||
size="m"
|
||||
>
|
||||
<h1
|
||||
data-test-subj="managementSettingsTitle"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Settings"
|
||||
id="xpack.spaces.management.advancedSettingsTitle.settingsTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
`;
|
|
@ -4,16 +4,25 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { AdvancedSettingsTitle } from './advanced_settings_title';
|
||||
import { SpaceAvatar } from '../../../../components';
|
||||
|
||||
describe('AdvancedSettingsTitle', () => {
|
||||
it('renders as expected', () => {
|
||||
it('renders without crashing', async () => {
|
||||
const space = {
|
||||
id: 'my-space',
|
||||
name: 'My Space',
|
||||
disabledFeatures: [],
|
||||
};
|
||||
expect(shallowWithIntl(<AdvancedSettingsTitle space={space} />)).toMatchSnapshot();
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<AdvancedSettingsTitle getActiveSpace={() => Promise.resolve(space)} />
|
||||
);
|
||||
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
wrapper.update();
|
||||
expect(wrapper.find(SpaceAvatar)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,28 +6,38 @@
|
|||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Space } from '../../../../../common/model/space';
|
||||
import { SpaceAvatar } from '../../../../components';
|
||||
|
||||
interface Props {
|
||||
space: Space;
|
||||
getActiveSpace: () => Promise<Space>;
|
||||
}
|
||||
|
||||
export const AdvancedSettingsTitle = (props: Props) => (
|
||||
<EuiFlexGroup gutterSize="s" responsive={false} alignItems={'center'}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<SpaceAvatar space={props.space} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem style={{ marginLeft: '10px' }}>
|
||||
<EuiTitle size="m">
|
||||
<h1 data-test-subj="managementSettingsTitle">
|
||||
<FormattedMessage
|
||||
id="xpack.spaces.management.advancedSettingsTitle.settingsTitle"
|
||||
defaultMessage="Settings"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
export const AdvancedSettingsTitle = (props: Props) => {
|
||||
const [activeSpace, setActiveSpace] = useState<Space | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
props.getActiveSpace().then(space => setActiveSpace(space));
|
||||
}, [props]);
|
||||
|
||||
if (!activeSpace) return null;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" responsive={false} alignItems={'center'}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<SpaceAvatar space={activeSpace} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem style={{ marginLeft: '10px' }}>
|
||||
<EuiTitle size="m">
|
||||
<h1 data-test-subj="managementSettingsTitle">
|
||||
<FormattedMessage
|
||||
id="xpack.spaces.management.advancedSettingsTitle.settingsTitle"
|
||||
defaultMessage="Settings"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import React from 'react';
|
||||
import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { SpacesNavState } from '../../nav_control';
|
||||
import { ConfirmDeleteModal } from './confirm_delete_modal';
|
||||
import { spacesManagerMock } from '../../../lib/mocks';
|
||||
import { SpacesManager } from '../../../lib';
|
||||
|
@ -20,11 +19,7 @@ describe('ConfirmDeleteModal', () => {
|
|||
};
|
||||
|
||||
const spacesManager = spacesManagerMock.create();
|
||||
|
||||
const spacesNavState: SpacesNavState = {
|
||||
getActiveSpace: () => space,
|
||||
refreshSpacesList: jest.fn(),
|
||||
};
|
||||
spacesManager.getActiveSpace.mockResolvedValue(space);
|
||||
|
||||
const onCancel = jest.fn();
|
||||
const onConfirm = jest.fn();
|
||||
|
@ -34,7 +29,6 @@ describe('ConfirmDeleteModal', () => {
|
|||
<ConfirmDeleteModal.WrappedComponent
|
||||
space={space}
|
||||
spacesManager={(spacesManager as unknown) as SpacesManager}
|
||||
spacesNavState={spacesNavState}
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirm}
|
||||
intl={null as any}
|
||||
|
@ -51,11 +45,7 @@ describe('ConfirmDeleteModal', () => {
|
|||
};
|
||||
|
||||
const spacesManager = spacesManagerMock.create();
|
||||
|
||||
const spacesNavState: SpacesNavState = {
|
||||
getActiveSpace: () => space,
|
||||
refreshSpacesList: jest.fn(),
|
||||
};
|
||||
spacesManager.getActiveSpace.mockResolvedValue(space);
|
||||
|
||||
const onCancel = jest.fn();
|
||||
const onConfirm = jest.fn();
|
||||
|
@ -64,7 +54,6 @@ describe('ConfirmDeleteModal', () => {
|
|||
<ConfirmDeleteModal.WrappedComponent
|
||||
space={space}
|
||||
spacesManager={(spacesManager as unknown) as SpacesManager}
|
||||
spacesNavState={spacesNavState}
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirm}
|
||||
intl={null as any}
|
||||
|
|
|
@ -24,7 +24,6 @@ import {
|
|||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import { SpacesNavState } from 'plugins/spaces/views/nav_control';
|
||||
import React, { ChangeEvent, Component } from 'react';
|
||||
import { Space } from '../../../../common/model/space';
|
||||
import { SpacesManager } from '../../../lib';
|
||||
|
@ -32,7 +31,6 @@ import { SpacesManager } from '../../../lib';
|
|||
interface Props {
|
||||
space: Space;
|
||||
spacesManager: SpacesManager;
|
||||
spacesNavState: SpacesNavState;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
intl: InjectedIntl;
|
||||
|
@ -42,6 +40,7 @@ interface State {
|
|||
confirmSpaceName: string;
|
||||
error: boolean | null;
|
||||
deleteInProgress: boolean;
|
||||
isDeletingCurrentSpace: boolean;
|
||||
}
|
||||
|
||||
class ConfirmDeleteModalUI extends Component<Props, State> {
|
||||
|
@ -49,13 +48,23 @@ class ConfirmDeleteModalUI extends Component<Props, State> {
|
|||
confirmSpaceName: '',
|
||||
error: null,
|
||||
deleteInProgress: false,
|
||||
isDeletingCurrentSpace: false,
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
isCurrentSpace(this.props.space, this.props.spacesManager).then(result => {
|
||||
this.setState({
|
||||
isDeletingCurrentSpace: result,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { space, spacesNavState, onCancel, intl } = this.props;
|
||||
const { space, onCancel, intl } = this.props;
|
||||
const { isDeletingCurrentSpace } = this.state;
|
||||
|
||||
let warning = null;
|
||||
if (isDeletingCurrentSpace(space, spacesNavState)) {
|
||||
if (isDeletingCurrentSpace) {
|
||||
const name = (
|
||||
<span>
|
||||
(<strong>{space.name}</strong>)
|
||||
|
@ -186,7 +195,7 @@ class ConfirmDeleteModalUI extends Component<Props, State> {
|
|||
|
||||
private onConfirm = async () => {
|
||||
if (this.state.confirmSpaceName === this.props.space.name) {
|
||||
const needsRedirect = isDeletingCurrentSpace(this.props.space, this.props.spacesNavState);
|
||||
const needsRedirect = this.state.isDeletingCurrentSpace;
|
||||
const spacesManager = this.props.spacesManager;
|
||||
|
||||
this.setState({
|
||||
|
@ -210,8 +219,8 @@ class ConfirmDeleteModalUI extends Component<Props, State> {
|
|||
};
|
||||
}
|
||||
|
||||
function isDeletingCurrentSpace(space: Space, spacesNavState: SpacesNavState) {
|
||||
return space.id === spacesNavState.getActiveSpace().id;
|
||||
async function isCurrentSpace(space: Space, spacesManager: SpacesManager) {
|
||||
return space.id === (await spacesManager.getActiveSpace()).id;
|
||||
}
|
||||
|
||||
export const ConfirmDeleteModal = injectI18n(ConfirmDeleteModalUI);
|
||||
|
|
|
@ -33,6 +33,13 @@ const setup = async (opts: SetupOpts = {}) => {
|
|||
const onClose = jest.fn();
|
||||
|
||||
const mockSpacesManager = spacesManagerMock.create();
|
||||
|
||||
mockSpacesManager.getActiveSpace.mockResolvedValue({
|
||||
id: 'my-active-space',
|
||||
name: 'my active space',
|
||||
disabledFeatures: [],
|
||||
});
|
||||
|
||||
mockSpacesManager.getSpaces.mockResolvedValue(
|
||||
opts.mockSpaces || [
|
||||
{
|
||||
|
@ -79,11 +86,6 @@ const setup = async (opts: SetupOpts = {}) => {
|
|||
<CopySavedObjectsToSpaceFlyout
|
||||
savedObject={savedObjectToCopy}
|
||||
spacesManager={(mockSpacesManager as unknown) as SpacesManager}
|
||||
activeSpace={{
|
||||
id: 'my-active-space',
|
||||
name: 'my active space',
|
||||
disabledFeatures: [],
|
||||
}}
|
||||
toastNotifications={(mockToastNotifications as unknown) as ToastNotifications}
|
||||
onClose={onClose}
|
||||
/>
|
||||
|
@ -92,6 +94,7 @@ const setup = async (opts: SetupOpts = {}) => {
|
|||
if (!opts.returnBeforeSpacesLoad) {
|
||||
// Wait for spaces manager to complete and flyout to rerender
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
wrapper.update();
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,6 @@ interface Props {
|
|||
onClose: () => void;
|
||||
savedObject: SavedObjectsManagementRecord;
|
||||
spacesManager: SpacesManager;
|
||||
activeSpace: Space;
|
||||
toastNotifications: ToastNotifications;
|
||||
}
|
||||
|
||||
|
@ -57,12 +56,13 @@ export const CopySavedObjectsToSpaceFlyout = (props: Props) => {
|
|||
}
|
||||
);
|
||||
useEffect(() => {
|
||||
spacesManager
|
||||
.getSpaces('copySavedObjectsIntoSpace')
|
||||
.then(response => {
|
||||
const getSpaces = spacesManager.getSpaces('copySavedObjectsIntoSpace');
|
||||
const getActiveSpace = spacesManager.getActiveSpace();
|
||||
Promise.all([getSpaces, getActiveSpace])
|
||||
.then(([allSpaces, activeSpace]) => {
|
||||
setSpacesState({
|
||||
isLoading: false,
|
||||
spaces: response,
|
||||
spaces: allSpaces.filter(space => space.id !== activeSpace.id),
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
|
@ -73,7 +73,6 @@ export const CopySavedObjectsToSpaceFlyout = (props: Props) => {
|
|||
});
|
||||
});
|
||||
}, [spacesManager, toastNotifications]);
|
||||
const eligibleSpaces = spaces.filter(space => space.id !== props.activeSpace.id);
|
||||
|
||||
const [copyInProgress, setCopyInProgress] = useState(false);
|
||||
const [conflictResolutionInProgress, setConflictResolutionInProgress] = useState(false);
|
||||
|
@ -159,7 +158,7 @@ export const CopySavedObjectsToSpaceFlyout = (props: Props) => {
|
|||
}
|
||||
|
||||
// Step 1a: assets loaded, but no spaces are available for copy.
|
||||
if (eligibleSpaces.length === 0) {
|
||||
if (spaces.length === 0) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
body={
|
||||
|
@ -185,11 +184,7 @@ export const CopySavedObjectsToSpaceFlyout = (props: Props) => {
|
|||
// Step 2: Copy has not been initiated yet; User must fill out form to continue.
|
||||
if (!copyInProgress) {
|
||||
return (
|
||||
<CopyToSpaceForm
|
||||
spaces={eligibleSpaces}
|
||||
copyOptions={copyOptions}
|
||||
onUpdate={setCopyOptions}
|
||||
/>
|
||||
<CopyToSpaceForm spaces={spaces} copyOptions={copyOptions} onUpdate={setCopyOptions} />
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -200,7 +195,7 @@ export const CopySavedObjectsToSpaceFlyout = (props: Props) => {
|
|||
copyInProgress={copyInProgress}
|
||||
conflictResolutionInProgress={conflictResolutionInProgress}
|
||||
copyResult={copyResult}
|
||||
spaces={eligibleSpaces}
|
||||
spaces={spaces}
|
||||
copyOptions={copyOptions}
|
||||
retries={retries}
|
||||
onRetriesChange={onRetriesChange}
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import React from 'react';
|
||||
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { SpacesNavState } from '../../nav_control';
|
||||
import { DeleteSpacesButton } from './delete_spaces_button';
|
||||
import { spacesManagerMock } from '../../../lib/mocks';
|
||||
import { SpacesManager } from '../../../lib';
|
||||
|
@ -21,16 +20,10 @@ describe('DeleteSpacesButton', () => {
|
|||
it('renders as expected', () => {
|
||||
const spacesManager = spacesManagerMock.create();
|
||||
|
||||
const spacesNavState: SpacesNavState = {
|
||||
getActiveSpace: () => space,
|
||||
refreshSpacesList: jest.fn(),
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<DeleteSpacesButton.WrappedComponent
|
||||
space={space}
|
||||
spacesManager={(spacesManager as unknown) as SpacesManager}
|
||||
spacesNavState={spacesNavState}
|
||||
onDelete={jest.fn()}
|
||||
intl={null as any}
|
||||
/>
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import { EuiButton, EuiButtonIcon, EuiButtonIconProps } from '@elastic/eui';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import { SpacesNavState } from 'plugins/spaces/views/nav_control';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
// @ts-ignore
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
|
@ -18,7 +17,6 @@ interface Props {
|
|||
style?: 'button' | 'icon';
|
||||
space: Space;
|
||||
spacesManager: SpacesManager;
|
||||
spacesNavState: SpacesNavState;
|
||||
onDelete: () => void;
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
|
@ -81,12 +79,11 @@ class DeleteSpacesButtonUI extends Component<Props, State> {
|
|||
return null;
|
||||
}
|
||||
|
||||
const { spacesNavState, spacesManager } = this.props;
|
||||
const { spacesManager } = this.props;
|
||||
|
||||
return (
|
||||
<ConfirmDeleteModal
|
||||
space={this.props.space}
|
||||
spacesNavState={spacesNavState}
|
||||
spacesManager={spacesManager}
|
||||
onCancel={() => {
|
||||
this.setState({
|
||||
|
@ -99,7 +96,7 @@ class DeleteSpacesButtonUI extends Component<Props, State> {
|
|||
};
|
||||
|
||||
public deleteSpaces = async () => {
|
||||
const { spacesManager, space, spacesNavState, intl } = this.props;
|
||||
const { spacesManager, space, intl } = this.props;
|
||||
|
||||
try {
|
||||
await spacesManager.deleteSpace(space);
|
||||
|
@ -139,8 +136,6 @@ class DeleteSpacesButtonUI extends Component<Props, State> {
|
|||
if (this.props.onDelete) {
|
||||
this.props.onDelete();
|
||||
}
|
||||
|
||||
spacesNavState.refreshSpacesList();
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ const space: Space = {
|
|||
disabledFeatures: ['feature-1', 'feature-2'],
|
||||
};
|
||||
|
||||
const uiCapabilities = {
|
||||
const capabilities = {
|
||||
navLinks: {},
|
||||
management: {},
|
||||
catalogue: {},
|
||||
|
@ -49,7 +49,7 @@ describe('EnabledFeatures', () => {
|
|||
<EnabledFeatures
|
||||
features={features}
|
||||
space={space}
|
||||
uiCapabilities={uiCapabilities}
|
||||
capabilities={capabilities}
|
||||
intl={null as any}
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
|
@ -64,7 +64,7 @@ describe('EnabledFeatures', () => {
|
|||
<EnabledFeatures
|
||||
features={features}
|
||||
space={space}
|
||||
uiCapabilities={uiCapabilities}
|
||||
capabilities={capabilities}
|
||||
intl={null as any}
|
||||
onChange={changeHandler}
|
||||
/>
|
||||
|
@ -99,7 +99,7 @@ describe('EnabledFeatures', () => {
|
|||
<EnabledFeatures
|
||||
features={features}
|
||||
space={space}
|
||||
uiCapabilities={uiCapabilities}
|
||||
capabilities={capabilities}
|
||||
intl={null as any}
|
||||
onChange={changeHandler}
|
||||
/>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react';
|
||||
import React, { Component, Fragment, ReactNode } from 'react';
|
||||
import { UICapabilities } from 'ui/capabilities';
|
||||
import { Capabilities } from 'src/core/public';
|
||||
import { Feature } from '../../../../../../../../plugins/features/server';
|
||||
import { Space } from '../../../../../common/model/space';
|
||||
import { getEnabledFeatures } from '../../lib/feature_utils';
|
||||
|
@ -17,7 +17,7 @@ import { FeatureTable } from './feature_table';
|
|||
interface Props {
|
||||
space: Partial<Space>;
|
||||
features: Feature[];
|
||||
uiCapabilities: UICapabilities;
|
||||
capabilities: Capabilities;
|
||||
intl: InjectedIntl;
|
||||
onChange: (space: Partial<Space>) => void;
|
||||
}
|
||||
|
@ -130,7 +130,7 @@ export class EnabledFeatures extends Component<Props, {}> {
|
|||
defaultMessage="The feature is hidden in the UI, but is not disabled."
|
||||
/>
|
||||
</p>
|
||||
{this.props.uiCapabilities.spaces.manage && (
|
||||
{this.props.capabilities.spaces.manage && (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.spaces.management.enabledSpaceFeatures.goToRolesLink"
|
||||
|
|
|
@ -6,13 +6,11 @@
|
|||
jest.mock('ui/kfetch', () => ({
|
||||
kfetch: () => Promise.resolve([{ id: 'feature-1', name: 'feature 1' }]),
|
||||
}));
|
||||
import '../../../__mocks__/ui_capabilities';
|
||||
import '../../../__mocks__/xpack_info';
|
||||
import { EuiButton, EuiLink, EuiSwitch } from '@elastic/eui';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { SpacesNavState } from '../../nav_control';
|
||||
import { ConfirmAlterActiveSpaceModal } from './confirm_alter_active_space_modal';
|
||||
import { ManageSpacePage } from './manage_space_page';
|
||||
import { SectionPanel } from './section_panel';
|
||||
|
@ -29,17 +27,18 @@ describe('ManageSpacePage', () => {
|
|||
it('allows a space to be created', async () => {
|
||||
const spacesManager = spacesManagerMock.create();
|
||||
spacesManager.createSpace = jest.fn(spacesManager.createSpace);
|
||||
|
||||
const spacesNavState: SpacesNavState = {
|
||||
getActiveSpace: () => space,
|
||||
refreshSpacesList: jest.fn(),
|
||||
};
|
||||
spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<ManageSpacePage.WrappedComponent
|
||||
spacesManager={(spacesManager as unknown) as SpacesManager}
|
||||
spacesNavState={spacesNavState}
|
||||
intl={null as any}
|
||||
capabilities={{
|
||||
navLinks: {},
|
||||
management: {},
|
||||
catalogue: {},
|
||||
spaces: { manage: true },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -75,17 +74,19 @@ describe('ManageSpacePage', () => {
|
|||
initials: 'AB',
|
||||
disabledFeatures: [],
|
||||
});
|
||||
spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space);
|
||||
|
||||
const spacesNavState: SpacesNavState = {
|
||||
getActiveSpace: () => space,
|
||||
refreshSpacesList: jest.fn(),
|
||||
};
|
||||
const wrapper = mountWithIntl(
|
||||
<ManageSpacePage.WrappedComponent
|
||||
spaceId={'existing-space'}
|
||||
spacesManager={(spacesManager as unknown) as SpacesManager}
|
||||
spacesNavState={spacesNavState}
|
||||
intl={null as any}
|
||||
capabilities={{
|
||||
navLinks: {},
|
||||
management: {},
|
||||
catalogue: {},
|
||||
spaces: { manage: true },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -121,17 +122,19 @@ describe('ManageSpacePage', () => {
|
|||
initials: 'AB',
|
||||
disabledFeatures: [],
|
||||
});
|
||||
spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space);
|
||||
|
||||
const spacesNavState: SpacesNavState = {
|
||||
getActiveSpace: () => space,
|
||||
refreshSpacesList: jest.fn(),
|
||||
};
|
||||
const wrapper = mountWithIntl(
|
||||
<ManageSpacePage.WrappedComponent
|
||||
spaceId={'my-space'}
|
||||
spacesManager={(spacesManager as unknown) as SpacesManager}
|
||||
spacesNavState={spacesNavState}
|
||||
intl={null as any}
|
||||
capabilities={{
|
||||
navLinks: {},
|
||||
management: {},
|
||||
catalogue: {},
|
||||
spaces: { manage: true },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -176,17 +179,19 @@ describe('ManageSpacePage', () => {
|
|||
initials: 'AB',
|
||||
disabledFeatures: [],
|
||||
});
|
||||
spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space);
|
||||
|
||||
const spacesNavState: SpacesNavState = {
|
||||
getActiveSpace: () => space,
|
||||
refreshSpacesList: jest.fn(),
|
||||
};
|
||||
const wrapper = mountWithIntl(
|
||||
<ManageSpacePage.WrappedComponent
|
||||
spaceId={'my-space'}
|
||||
spacesManager={(spacesManager as unknown) as SpacesManager}
|
||||
spacesNavState={spacesNavState}
|
||||
intl={null as any}
|
||||
capabilities={{
|
||||
navLinks: {},
|
||||
management: {},
|
||||
catalogue: {},
|
||||
spaces: { manage: true },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -16,12 +16,11 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import _ from 'lodash';
|
||||
import { SpacesNavState } from 'plugins/spaces/views/nav_control';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { capabilities } from 'ui/capabilities';
|
||||
import { Breadcrumb } from 'ui/chrome';
|
||||
import { kfetch } from 'ui/kfetch';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { Capabilities } from 'src/core/public';
|
||||
import { Feature } from '../../../../../../../plugins/features/server';
|
||||
import { isReservedSpace } from '../../../../common';
|
||||
import { Space } from '../../../../common/model/space';
|
||||
|
@ -39,9 +38,9 @@ import { ReservedSpaceBadge } from './reserved_space_badge';
|
|||
interface Props {
|
||||
spacesManager: SpacesManager;
|
||||
spaceId?: string;
|
||||
spacesNavState: SpacesNavState;
|
||||
intl: InjectedIntl;
|
||||
setBreadcrumbs?: (breadcrumbs: Breadcrumb[]) => void;
|
||||
capabilities: Capabilities;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -73,7 +72,7 @@ class ManageSpacePageUI extends Component<Props, State> {
|
|||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
if (!capabilities.get().spaces.manage) {
|
||||
if (!this.props.capabilities.spaces.manage) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -139,7 +138,7 @@ class ManageSpacePageUI extends Component<Props, State> {
|
|||
);
|
||||
|
||||
public getForm = () => {
|
||||
if (!capabilities.get().spaces.manage) {
|
||||
if (!this.props.capabilities.spaces.manage) {
|
||||
return <UnauthorizedPrompt />;
|
||||
}
|
||||
|
||||
|
@ -173,7 +172,7 @@ class ManageSpacePageUI extends Component<Props, State> {
|
|||
<EnabledFeatures
|
||||
space={this.state.space}
|
||||
features={this.state.features}
|
||||
uiCapabilities={capabilities.get()}
|
||||
capabilities={this.props.capabilities}
|
||||
onChange={this.onSpaceChange}
|
||||
intl={this.props.intl}
|
||||
/>
|
||||
|
@ -269,7 +268,6 @@ class ManageSpacePageUI extends Component<Props, State> {
|
|||
data-test-subj="delete-space-button"
|
||||
space={this.state.space as Space}
|
||||
spacesManager={this.props.spacesManager}
|
||||
spacesNavState={this.props.spacesNavState}
|
||||
onDelete={this.backToSpacesList}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
@ -298,27 +296,30 @@ class ManageSpacePageUI extends Component<Props, State> {
|
|||
}
|
||||
|
||||
if (this.editingExistingSpace()) {
|
||||
const { spacesNavState } = this.props;
|
||||
const { spacesManager } = this.props;
|
||||
|
||||
const originalSpace: Space = this.state.originalSpace as Space;
|
||||
const space: Space = this.state.space as Space;
|
||||
|
||||
const editingActiveSpace = spacesNavState.getActiveSpace().id === originalSpace.id;
|
||||
spacesManager.getActiveSpace().then(activeSpace => {
|
||||
const editingActiveSpace = activeSpace.id === originalSpace.id;
|
||||
|
||||
const haveDisabledFeaturesChanged =
|
||||
space.disabledFeatures.length !== originalSpace.disabledFeatures.length ||
|
||||
_.difference(space.disabledFeatures, originalSpace.disabledFeatures).length > 0;
|
||||
const haveDisabledFeaturesChanged =
|
||||
space.disabledFeatures.length !== originalSpace.disabledFeatures.length ||
|
||||
_.difference(space.disabledFeatures, originalSpace.disabledFeatures).length > 0;
|
||||
|
||||
if (editingActiveSpace && haveDisabledFeaturesChanged) {
|
||||
this.setState({
|
||||
showAlteringActiveSpaceDialog: true,
|
||||
});
|
||||
if (editingActiveSpace && haveDisabledFeaturesChanged) {
|
||||
this.setState({
|
||||
showAlteringActiveSpaceDialog: true,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.performSave();
|
||||
});
|
||||
} else {
|
||||
this.performSave();
|
||||
}
|
||||
|
||||
this.performSave();
|
||||
};
|
||||
|
||||
private performSave = (requireRefresh = false) => {
|
||||
|
@ -358,7 +359,6 @@ class ManageSpacePageUI extends Component<Props, State> {
|
|||
|
||||
action
|
||||
.then(() => {
|
||||
this.props.spacesNavState.refreshSpacesList();
|
||||
toastNotifications.addSuccess(
|
||||
intl.formatMessage(
|
||||
{
|
||||
|
|
|
@ -15,16 +15,16 @@ import {
|
|||
// @ts-ignore
|
||||
import routes from 'ui/routes';
|
||||
import { setup as managementSetup } from '../../../../../../../src/legacy/core_plugins/management/public/legacy';
|
||||
import { SpacesManager } from '../../lib';
|
||||
import { AdvancedSettingsSubtitle } from './components/advanced_settings_subtitle';
|
||||
import { AdvancedSettingsTitle } from './components/advanced_settings_title';
|
||||
import { start as spacesNPStart } from '../../legacy';
|
||||
import { CopyToSpaceSavedObjectsManagementAction } from '../../lib/copy_saved_objects_to_space';
|
||||
|
||||
const MANAGE_SPACES_KEY = 'spaces';
|
||||
|
||||
routes.defaults(/\/management/, {
|
||||
resolve: {
|
||||
spacesManagementSection(activeSpace: any, serverBasePath: string) {
|
||||
spacesManagementSection() {
|
||||
function getKibanaSection() {
|
||||
return management.getSection('kibana');
|
||||
}
|
||||
|
@ -48,21 +48,24 @@ routes.defaults(/\/management/, {
|
|||
}
|
||||
|
||||
// Customize Saved Objects Management
|
||||
const action = new CopyToSpaceSavedObjectsManagementAction(
|
||||
new SpacesManager(serverBasePath),
|
||||
activeSpace.space
|
||||
);
|
||||
// This route resolve function executes any time the management screen is loaded, and we want to ensure
|
||||
// that this action is only registered once.
|
||||
if (!managementSetup.savedObjects.registry.has(action.id)) {
|
||||
managementSetup.savedObjects.registry.register(action);
|
||||
}
|
||||
spacesNPStart.then(({ spacesManager }) => {
|
||||
const action = new CopyToSpaceSavedObjectsManagementAction(spacesManager!);
|
||||
// This route resolve function executes any time the management screen is loaded, and we want to ensure
|
||||
// that this action is only registered once.
|
||||
if (!managementSetup.savedObjects.registry.has(action.id)) {
|
||||
managementSetup.savedObjects.registry.register(action);
|
||||
}
|
||||
});
|
||||
|
||||
// Customize Advanced Settings
|
||||
const PageTitle = () => <AdvancedSettingsTitle space={activeSpace.space} />;
|
||||
const getActiveSpace = async () => {
|
||||
const { spacesManager } = await spacesNPStart;
|
||||
return spacesManager!.getActiveSpace();
|
||||
};
|
||||
|
||||
const PageTitle = () => <AdvancedSettingsTitle getActiveSpace={getActiveSpace} />;
|
||||
registerSettingsComponent(PAGE_TITLE_COMPONENT, PageTitle, true);
|
||||
|
||||
const SubTitle = () => <AdvancedSettingsSubtitle space={activeSpace.space} />;
|
||||
const SubTitle = () => <AdvancedSettingsSubtitle getActiveSpace={getActiveSpace} />;
|
||||
registerSettingsComponent(PAGE_SUBTITLE_COMPONENT, SubTitle, true);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,31 +5,36 @@
|
|||
*/
|
||||
// @ts-ignore
|
||||
import template from 'plugins/spaces/views/management/template.html';
|
||||
import { SpacesNavState } from 'plugins/spaces/views/nav_control';
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { I18nContext } from 'ui/i18n';
|
||||
// @ts-ignore
|
||||
import routes from 'ui/routes';
|
||||
import { SpacesManager } from '../../lib/spaces_manager';
|
||||
import { npStart } from 'ui/new_platform';
|
||||
import { ManageSpacePage } from './edit_space';
|
||||
import { getCreateBreadcrumbs, getEditBreadcrumbs, getListBreadcrumbs } from './lib';
|
||||
import { SpacesGridPage } from './spaces_grid';
|
||||
|
||||
import { start as spacesNPStart } from '../../legacy';
|
||||
|
||||
const reactRootNodeId = 'manageSpacesReactRoot';
|
||||
|
||||
routes.when('/management/spaces/list', {
|
||||
template,
|
||||
k7Breadcrumbs: getListBreadcrumbs,
|
||||
requireUICapability: 'management.kibana.spaces',
|
||||
controller($scope: any, spacesNavState: SpacesNavState, serverBasePath: string) {
|
||||
controller($scope: any) {
|
||||
$scope.$$postDigest(async () => {
|
||||
const domNode = document.getElementById(reactRootNodeId);
|
||||
|
||||
const spacesManager = new SpacesManager(serverBasePath);
|
||||
const { spacesManager } = await spacesNPStart;
|
||||
|
||||
render(
|
||||
<I18nContext>
|
||||
<SpacesGridPage spacesManager={spacesManager} spacesNavState={spacesNavState} />
|
||||
<SpacesGridPage
|
||||
spacesManager={spacesManager!}
|
||||
capabilities={npStart.core.application.capabilities}
|
||||
/>
|
||||
</I18nContext>,
|
||||
domNode
|
||||
);
|
||||
|
@ -48,15 +53,18 @@ routes.when('/management/spaces/create', {
|
|||
template,
|
||||
k7Breadcrumbs: getCreateBreadcrumbs,
|
||||
requireUICapability: 'management.kibana.spaces',
|
||||
controller($scope: any, spacesNavState: SpacesNavState, serverBasePath: string) {
|
||||
controller($scope: any) {
|
||||
$scope.$$postDigest(async () => {
|
||||
const domNode = document.getElementById(reactRootNodeId);
|
||||
|
||||
const spacesManager = new SpacesManager(serverBasePath);
|
||||
const { spacesManager } = await spacesNPStart;
|
||||
|
||||
render(
|
||||
<I18nContext>
|
||||
<ManageSpacePage spacesManager={spacesManager} spacesNavState={spacesNavState} />
|
||||
<ManageSpacePage
|
||||
spacesManager={spacesManager!}
|
||||
capabilities={npStart.core.application.capabilities}
|
||||
/>
|
||||
</I18nContext>,
|
||||
domNode
|
||||
);
|
||||
|
@ -79,29 +87,21 @@ routes.when('/management/spaces/edit/:spaceId', {
|
|||
template,
|
||||
k7Breadcrumbs: () => getEditBreadcrumbs(),
|
||||
requireUICapability: 'management.kibana.spaces',
|
||||
controller(
|
||||
$scope: any,
|
||||
$route: any,
|
||||
chrome: any,
|
||||
spacesNavState: SpacesNavState,
|
||||
serverBasePath: string
|
||||
) {
|
||||
controller($scope: any, $route: any) {
|
||||
$scope.$$postDigest(async () => {
|
||||
const domNode = document.getElementById(reactRootNodeId);
|
||||
|
||||
const { spaceId } = $route.current.params;
|
||||
|
||||
const spacesManager = new SpacesManager(serverBasePath);
|
||||
const { spacesManager } = await spacesNPStart;
|
||||
|
||||
render(
|
||||
<I18nContext>
|
||||
<ManageSpacePage
|
||||
spaceId={spaceId}
|
||||
spacesManager={spacesManager}
|
||||
spacesNavState={spacesNavState}
|
||||
setBreadcrumbs={breadcrumbs => {
|
||||
chrome.breadcrumbs.set(breadcrumbs);
|
||||
}}
|
||||
spacesManager={spacesManager!}
|
||||
setBreadcrumbs={npStart.core.chrome.setBreadcrumbs}
|
||||
capabilities={npStart.core.application.capabilities}
|
||||
/>
|
||||
</I18nContext>,
|
||||
domNode
|
||||
|
|
|
@ -19,10 +19,9 @@ import {
|
|||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import { capabilities } from 'ui/capabilities';
|
||||
import { kfetch } from 'ui/kfetch';
|
||||
// @ts-ignore
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { Capabilities } from 'src/core/public';
|
||||
import { Feature } from '../../../../../../../plugins/features/server';
|
||||
import { isReservedSpace } from '../../../../common';
|
||||
import { DEFAULT_SPACE_ID } from '../../../../common/constants';
|
||||
|
@ -30,7 +29,6 @@ import { Space } from '../../../../common/model/space';
|
|||
import { SpaceAvatar } from '../../../components';
|
||||
import { getSpacesFeatureDescription } from '../../../lib/constants';
|
||||
import { SpacesManager } from '../../../lib/spaces_manager';
|
||||
import { SpacesNavState } from '../../nav_control';
|
||||
import { ConfirmDeleteModal } from '../components/confirm_delete_modal';
|
||||
import { SecureSpaceMessage } from '../components/secure_space_message';
|
||||
import { UnauthorizedPrompt } from '../components/unauthorized_prompt';
|
||||
|
@ -38,8 +36,8 @@ import { getEnabledFeatures } from '../lib/feature_utils';
|
|||
|
||||
interface Props {
|
||||
spacesManager: SpacesManager;
|
||||
spacesNavState: SpacesNavState;
|
||||
intl: InjectedIntl;
|
||||
capabilities: Capabilities;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -65,7 +63,7 @@ class SpacesGridPageUI extends Component<Props, State> {
|
|||
}
|
||||
|
||||
public componentDidMount() {
|
||||
if (capabilities.get().spaces.manage) {
|
||||
if (this.props.capabilities.spaces.manage) {
|
||||
this.loadGrid();
|
||||
}
|
||||
}
|
||||
|
@ -83,7 +81,7 @@ class SpacesGridPageUI extends Component<Props, State> {
|
|||
public getPageContent() {
|
||||
const { intl } = this.props;
|
||||
|
||||
if (!capabilities.get().spaces.manage) {
|
||||
if (!this.props.capabilities.spaces.manage) {
|
||||
return <UnauthorizedPrompt />;
|
||||
}
|
||||
|
||||
|
@ -159,12 +157,11 @@ class SpacesGridPageUI extends Component<Props, State> {
|
|||
return null;
|
||||
}
|
||||
|
||||
const { spacesNavState, spacesManager } = this.props;
|
||||
const { spacesManager } = this.props;
|
||||
|
||||
return (
|
||||
<ConfirmDeleteModal
|
||||
space={this.state.selectedSpace}
|
||||
spacesNavState={spacesNavState}
|
||||
spacesManager={spacesManager}
|
||||
onCancel={() => {
|
||||
this.setState({
|
||||
|
@ -178,7 +175,7 @@ class SpacesGridPageUI extends Component<Props, State> {
|
|||
|
||||
public deleteSpace = async () => {
|
||||
const { intl } = this.props;
|
||||
const { spacesManager, spacesNavState } = this.props;
|
||||
const { spacesManager } = this.props;
|
||||
|
||||
const space = this.state.selectedSpace;
|
||||
|
||||
|
@ -221,8 +218,6 @@ class SpacesGridPageUI extends Component<Props, State> {
|
|||
);
|
||||
|
||||
toastNotifications.addSuccess(message);
|
||||
|
||||
spacesNavState.refreshSpacesList();
|
||||
};
|
||||
|
||||
public loadGrid = async () => {
|
||||
|
|
|
@ -6,14 +6,12 @@
|
|||
jest.mock('ui/kfetch', () => ({
|
||||
kfetch: () => Promise.resolve([]),
|
||||
}));
|
||||
import '../../../__mocks__/ui_capabilities';
|
||||
import '../../../__mocks__/xpack_info';
|
||||
import React from 'react';
|
||||
import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { SpaceAvatar } from '../../../components';
|
||||
import { spacesManagerMock } from '../../../lib/mocks';
|
||||
import { SpacesManager } from '../../../lib';
|
||||
import { SpacesNavState } from '../../nav_control';
|
||||
import { SpacesGridPage } from './spaces_grid_page';
|
||||
|
||||
const spaces = [
|
||||
|
@ -38,11 +36,6 @@ const spaces = [
|
|||
},
|
||||
];
|
||||
|
||||
const spacesNavState: SpacesNavState = {
|
||||
getActiveSpace: () => spaces[0],
|
||||
refreshSpacesList: jest.fn(),
|
||||
};
|
||||
|
||||
const spacesManager = spacesManagerMock.create();
|
||||
spacesManager.getSpaces = jest.fn().mockResolvedValue(spaces);
|
||||
|
||||
|
@ -52,8 +45,13 @@ describe('SpacesGridPage', () => {
|
|||
shallowWithIntl(
|
||||
<SpacesGridPage.WrappedComponent
|
||||
spacesManager={(spacesManager as unknown) as SpacesManager}
|
||||
spacesNavState={spacesNavState}
|
||||
intl={null as any}
|
||||
capabilities={{
|
||||
navLinks: {},
|
||||
management: {},
|
||||
catalogue: {},
|
||||
spaces: { manage: true },
|
||||
}}
|
||||
/>
|
||||
)
|
||||
).toMatchSnapshot();
|
||||
|
@ -63,8 +61,13 @@ describe('SpacesGridPage', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<SpacesGridPage.WrappedComponent
|
||||
spacesManager={(spacesManager as unknown) as SpacesManager}
|
||||
spacesNavState={spacesNavState}
|
||||
intl={null as any}
|
||||
capabilities={{
|
||||
navLinks: {},
|
||||
management: {},
|
||||
catalogue: {},
|
||||
spaces: { manage: true },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -4,25 +4,18 @@ exports[`NavControlPopover renders without crashing 1`] = `
|
|||
<EuiPopover
|
||||
anchorPosition="downRight"
|
||||
button={
|
||||
<SpacesHeaderNavButton
|
||||
linkIcon={
|
||||
<SpaceAvatar
|
||||
announceSpaceName={true}
|
||||
className="spaceNavGraphic"
|
||||
size="s"
|
||||
space={
|
||||
Object {
|
||||
"disabledFeatures": Array [],
|
||||
"id": "",
|
||||
"name": "foo",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
linkTitle="foo"
|
||||
spaceSelectorShown={false}
|
||||
toggleSpaceSelector={[Function]}
|
||||
/>
|
||||
<EuiHeaderSectionItemButton
|
||||
aria-controls="headerSpacesMenuList"
|
||||
aria-expanded={false}
|
||||
aria-haspopup="true"
|
||||
aria-label="loading"
|
||||
onClick={[Function]}
|
||||
title="loading"
|
||||
>
|
||||
<EuiLoadingSpinner
|
||||
size="m"
|
||||
/>
|
||||
</EuiHeaderSectionItemButton>
|
||||
}
|
||||
closePopover={[Function]}
|
||||
data-test-subj="spacesNavSelector"
|
||||
|
@ -36,6 +29,16 @@ exports[`NavControlPopover renders without crashing 1`] = `
|
|||
withTitle={true}
|
||||
>
|
||||
<SpacesDescription
|
||||
capabilities={
|
||||
Object {
|
||||
"catalogue": Object {},
|
||||
"management": Object {},
|
||||
"navLinks": Object {},
|
||||
"spaces": Object {
|
||||
"manage": true,
|
||||
},
|
||||
}
|
||||
}
|
||||
onManageSpacesClick={[Function]}
|
||||
/>
|
||||
</EuiPopover>
|
||||
|
|
|
@ -19,6 +19,16 @@ exports[`SpacesDescription renders without crashing 1`] = `
|
|||
key="manageSpacesButton"
|
||||
>
|
||||
<ManageSpacesButton
|
||||
capabilities={
|
||||
Object {
|
||||
"catalogue": Object {},
|
||||
"management": Object {},
|
||||
"navLinks": Object {},
|
||||
"spaces": Object {
|
||||
"manage": true,
|
||||
},
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction]}
|
||||
size="s"
|
||||
style={
|
||||
|
|
|
@ -10,6 +10,20 @@ import { SpacesDescription } from './spaces_description';
|
|||
|
||||
describe('SpacesDescription', () => {
|
||||
it('renders without crashing', () => {
|
||||
expect(shallow(<SpacesDescription onManageSpacesClick={jest.fn()} />)).toMatchSnapshot();
|
||||
expect(
|
||||
shallow(
|
||||
<SpacesDescription
|
||||
capabilities={{
|
||||
navLinks: {},
|
||||
management: {},
|
||||
catalogue: {},
|
||||
spaces: {
|
||||
manage: true,
|
||||
},
|
||||
}}
|
||||
onManageSpacesClick={jest.fn()}
|
||||
/>
|
||||
)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,11 +6,13 @@
|
|||
|
||||
import { EuiContextMenuPanel, EuiText } from '@elastic/eui';
|
||||
import React, { FC } from 'react';
|
||||
import { Capabilities } from 'src/core/public';
|
||||
import { ManageSpacesButton } from '../../../components';
|
||||
import { getSpacesFeatureDescription } from '../../../lib/constants';
|
||||
|
||||
interface Props {
|
||||
onManageSpacesClick: () => void;
|
||||
capabilities: Capabilities;
|
||||
}
|
||||
|
||||
export const SpacesDescription: FC<Props> = (props: Props) => {
|
||||
|
@ -29,6 +31,7 @@ export const SpacesDescription: FC<Props> = (props: Props) => {
|
|||
size="s"
|
||||
style={{ width: `100%` }}
|
||||
onClick={props.onManageSpacesClick}
|
||||
capabilities={props.capabilities}
|
||||
/>
|
||||
</div>
|
||||
</EuiContextMenuPanel>
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
/*
|
||||
* 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 {
|
||||
// @ts-ignore
|
||||
EuiHeaderSectionItemButton,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { ButtonProps } from '../types';
|
||||
|
||||
export const SpacesHeaderNavButton: React.FC<ButtonProps> = props => (
|
||||
<EuiHeaderSectionItemButton
|
||||
aria-controls="headerSpacesMenuList"
|
||||
aria-expanded={props.spaceSelectorShown}
|
||||
aria-haspopup="true"
|
||||
aria-label={props.linkTitle}
|
||||
title={props.linkTitle}
|
||||
onClick={props.toggleSpaceSelector}
|
||||
>
|
||||
{props.linkIcon}
|
||||
</EuiHeaderSectionItemButton>
|
||||
);
|
|
@ -4,18 +4,27 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiContextMenuItem, EuiContextMenuPanel, EuiFieldSearch, EuiText } from '@elastic/eui';
|
||||
import {
|
||||
EuiContextMenuItem,
|
||||
EuiContextMenuPanel,
|
||||
EuiFieldSearch,
|
||||
EuiText,
|
||||
EuiLoadingContent,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component, ReactElement } from 'react';
|
||||
import { Capabilities } from 'src/core/public';
|
||||
import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../../common/constants';
|
||||
import { Space } from '../../../../common/model/space';
|
||||
import { ManageSpacesButton, SpaceAvatar } from '../../../components';
|
||||
|
||||
interface Props {
|
||||
spaces: Space[];
|
||||
isLoading: boolean;
|
||||
onSelectSpace: (space: Space) => void;
|
||||
onManageSpacesClick: () => void;
|
||||
intl: InjectedIntl;
|
||||
capabilities: Capabilities;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -30,10 +39,12 @@ class SpacesMenuUI extends Component<Props, State> {
|
|||
};
|
||||
|
||||
public render() {
|
||||
const { intl } = this.props;
|
||||
const { intl, isLoading } = this.props;
|
||||
const { searchTerm } = this.state;
|
||||
|
||||
const items = this.getVisibleSpaces(searchTerm).map(this.renderSpaceMenuItem);
|
||||
const items = isLoading
|
||||
? [1, 2, 3].map(this.renderPlaceholderMenuItem)
|
||||
: this.getVisibleSpaces(searchTerm).map(this.renderSpaceMenuItem);
|
||||
|
||||
const panelProps = {
|
||||
className: 'spcMenu',
|
||||
|
@ -76,7 +87,7 @@ class SpacesMenuUI extends Component<Props, State> {
|
|||
return filteredSpaces;
|
||||
};
|
||||
|
||||
private renderSpacesListPanel = (items: JSX.Element[], searchTerm: string) => {
|
||||
private renderSpacesListPanel = (items: ReactElement[], searchTerm: string) => {
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<EuiText color="subdued" className="eui-textCenter">
|
||||
|
@ -151,6 +162,7 @@ class SpacesMenuUI extends Component<Props, State> {
|
|||
className="spcMenu__manageButton"
|
||||
size="s"
|
||||
onClick={this.props.onManageSpacesClick}
|
||||
capabilities={this.props.capabilities}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -175,6 +187,14 @@ class SpacesMenuUI extends Component<Props, State> {
|
|||
</EuiContextMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
private renderPlaceholderMenuItem = (key: string | number): JSX.Element => {
|
||||
return (
|
||||
<EuiContextMenuItem key={key} disabled={true}>
|
||||
<EuiLoadingContent lines={1} />
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const SpacesMenu = injectI18n(SpacesMenuUI);
|
||||
|
|
|
@ -4,6 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import './nav_control';
|
||||
|
||||
export { SpacesNavState } from './nav_control';
|
||||
export { initSpacesNavControl } from './nav_control';
|
||||
|
|
|
@ -5,69 +5,34 @@
|
|||
*/
|
||||
|
||||
import { SpacesManager } from 'plugins/spaces/lib/spaces_manager';
|
||||
// @ts-ignore
|
||||
import template from 'plugins/spaces/views/nav_control/nav_control.html';
|
||||
import { NavControlPopover } from 'plugins/spaces/views/nav_control/nav_control_popover';
|
||||
// @ts-ignore
|
||||
import { Path } from 'plugins/xpack_main/services/path';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { I18nContext } from 'ui/i18n';
|
||||
// @ts-ignore
|
||||
import { uiModules } from 'ui/modules';
|
||||
import {
|
||||
chromeHeaderNavControlsRegistry,
|
||||
NavControlSide,
|
||||
} from 'ui/registry/chrome_header_nav_controls';
|
||||
// @ts-ignore
|
||||
import { Space } from '../../../common/model/space';
|
||||
import { SpacesHeaderNavButton } from './components/spaces_header_nav_button';
|
||||
import { CoreStart } from 'src/core/public';
|
||||
import { NavControlPopover } from './nav_control_popover';
|
||||
|
||||
const module = uiModules.get('spaces_nav', ['kibana']);
|
||||
|
||||
export interface SpacesNavState {
|
||||
getActiveSpace: () => Space;
|
||||
refreshSpacesList: () => void;
|
||||
}
|
||||
|
||||
let spacesManager: SpacesManager;
|
||||
|
||||
module.service('spacesNavState', (activeSpace: any) => {
|
||||
return {
|
||||
getActiveSpace: () => {
|
||||
return activeSpace.space;
|
||||
},
|
||||
refreshSpacesList: () => {
|
||||
if (spacesManager) {
|
||||
spacesManager.requestRefresh();
|
||||
export function initSpacesNavControl(spacesManager: SpacesManager, core: CoreStart) {
|
||||
const I18nContext = core.i18n.Context;
|
||||
core.chrome.navControls.registerLeft({
|
||||
order: 1000,
|
||||
mount(targetDomElement: HTMLElement) {
|
||||
if (core.http.anonymousPaths.isAnonymous(window.location.pathname)) {
|
||||
return () => null;
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<I18nContext>
|
||||
<NavControlPopover
|
||||
spacesManager={spacesManager}
|
||||
anchorPosition="downLeft"
|
||||
capabilities={core.application.capabilities}
|
||||
/>
|
||||
</I18nContext>,
|
||||
targetDomElement
|
||||
);
|
||||
|
||||
return () => {
|
||||
ReactDOM.unmountComponentAtNode(targetDomElement);
|
||||
};
|
||||
},
|
||||
} as SpacesNavState;
|
||||
});
|
||||
|
||||
chromeHeaderNavControlsRegistry.register((chrome: any, activeSpace: any) => ({
|
||||
name: 'spaces',
|
||||
order: 1000,
|
||||
side: NavControlSide.Left,
|
||||
render(el: HTMLElement) {
|
||||
if (Path.isUnauthenticated()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const serverBasePath = chrome.getInjected('serverBasePath');
|
||||
|
||||
spacesManager = new SpacesManager(serverBasePath);
|
||||
|
||||
ReactDOM.render(
|
||||
<I18nContext>
|
||||
<NavControlPopover
|
||||
spacesManager={spacesManager}
|
||||
activeSpace={activeSpace}
|
||||
anchorPosition="downLeft"
|
||||
buttonClass={SpacesHeaderNavButton}
|
||||
/>
|
||||
</I18nContext>,
|
||||
el
|
||||
);
|
||||
},
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -4,40 +4,31 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import * as Rx from 'rxjs';
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { SpaceAvatar } from '../../components';
|
||||
import { spacesManagerMock } from '../../lib/mocks';
|
||||
import { SpacesManager } from '../../lib';
|
||||
import { SpacesHeaderNavButton } from './components/spaces_header_nav_button';
|
||||
import { NavControlPopover } from './nav_control_popover';
|
||||
import { EuiHeaderSectionItemButton } from '@elastic/eui';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
|
||||
describe('NavControlPopover', () => {
|
||||
it('renders without crashing', () => {
|
||||
const activeSpace = {
|
||||
space: { id: '', name: 'foo', disabledFeatures: [] },
|
||||
valid: true,
|
||||
};
|
||||
|
||||
const spacesManager = spacesManagerMock.create();
|
||||
|
||||
const wrapper = shallow(
|
||||
<NavControlPopover
|
||||
activeSpace={activeSpace}
|
||||
spacesManager={(spacesManager as unknown) as SpacesManager}
|
||||
anchorPosition={'downRight'}
|
||||
buttonClass={SpacesHeaderNavButton}
|
||||
capabilities={{ navLinks: {}, management: {}, catalogue: {}, spaces: { manage: true } }}
|
||||
/>
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders a SpaceAvatar with the active space', async () => {
|
||||
const activeSpace = {
|
||||
space: { id: 'foo-space', name: 'foo', disabledFeatures: [] },
|
||||
valid: true,
|
||||
};
|
||||
|
||||
const spacesManager = spacesManagerMock.create();
|
||||
spacesManager.getSpaces = jest.fn().mockResolvedValue([
|
||||
{
|
||||
|
@ -51,23 +42,27 @@ describe('NavControlPopover', () => {
|
|||
disabledFeatures: [],
|
||||
},
|
||||
]);
|
||||
spacesManager.onActiveSpaceChange$ = Rx.of({
|
||||
id: 'foo-space',
|
||||
name: 'foo',
|
||||
disabledFeatures: [],
|
||||
});
|
||||
|
||||
const wrapper = mount<any, any>(
|
||||
const wrapper = mountWithIntl(
|
||||
<NavControlPopover
|
||||
activeSpace={activeSpace}
|
||||
spacesManager={(spacesManager as unknown) as SpacesManager}
|
||||
anchorPosition={'rightCenter'}
|
||||
buttonClass={SpacesHeaderNavButton}
|
||||
capabilities={{ navLinks: {}, management: {}, catalogue: {}, spaces: { manage: true } }}
|
||||
/>
|
||||
);
|
||||
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
expect(wrapper.state().spaces).toHaveLength(2);
|
||||
wrapper.update();
|
||||
expect(wrapper.find(SpaceAvatar)).toHaveLength(1);
|
||||
resolve();
|
||||
}, 20);
|
||||
});
|
||||
wrapper.find(EuiHeaderSectionItemButton).simulate('click');
|
||||
|
||||
// Wait for `getSpaces` promise to resolve
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find(SpaceAvatar)).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,24 +4,25 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiAvatar, EuiPopover, PopoverAnchorPosition } from '@elastic/eui';
|
||||
import {
|
||||
EuiPopover,
|
||||
PopoverAnchorPosition,
|
||||
EuiLoadingSpinner,
|
||||
EuiHeaderSectionItemButton,
|
||||
} from '@elastic/eui';
|
||||
import React, { Component } from 'react';
|
||||
import { Capabilities } from 'src/core/public';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Space } from '../../../common/model/space';
|
||||
import { SpaceAvatar } from '../../components';
|
||||
import { SpacesManager } from '../../lib/spaces_manager';
|
||||
import { SpacesDescription } from './components/spaces_description';
|
||||
import { SpacesMenu } from './components/spaces_menu';
|
||||
import { ButtonProps } from './types';
|
||||
|
||||
interface Props {
|
||||
spacesManager: SpacesManager;
|
||||
activeSpace: {
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
space: Space;
|
||||
};
|
||||
anchorPosition: PopoverAnchorPosition;
|
||||
buttonClass: React.ComponentType<ButtonProps>;
|
||||
capabilities: Capabilities;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -32,23 +33,31 @@ interface State {
|
|||
}
|
||||
|
||||
export class NavControlPopover extends Component<Props, State> {
|
||||
private activeSpace$?: Subscription;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showSpaceSelector: false,
|
||||
loading: false,
|
||||
activeSpace: props.activeSpace.space,
|
||||
activeSpace: null,
|
||||
spaces: [],
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.loadSpaces();
|
||||
public componentWillMount() {
|
||||
this.activeSpace$ = this.props.spacesManager.onActiveSpaceChange$.subscribe({
|
||||
next: activeSpace => {
|
||||
this.setState({
|
||||
activeSpace,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (this.props.spacesManager) {
|
||||
this.props.spacesManager.on('request_refresh', () => {
|
||||
this.loadSpaces();
|
||||
});
|
||||
public componentWillUnmount() {
|
||||
if (this.activeSpace$) {
|
||||
this.activeSpace$.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,20 +68,26 @@ export class NavControlPopover extends Component<Props, State> {
|
|||
}
|
||||
|
||||
let element: React.ReactNode;
|
||||
if (this.state.spaces.length < 2) {
|
||||
element = <SpacesDescription onManageSpacesClick={this.toggleSpaceSelector} />;
|
||||
if (!this.state.loading && this.state.spaces.length < 2) {
|
||||
element = (
|
||||
<SpacesDescription
|
||||
onManageSpacesClick={this.toggleSpaceSelector}
|
||||
capabilities={this.props.capabilities}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
element = (
|
||||
<SpacesMenu
|
||||
spaces={this.state.spaces}
|
||||
isLoading={this.state.loading}
|
||||
onSelectSpace={this.onSelectSpace}
|
||||
onManageSpacesClick={this.toggleSpaceSelector}
|
||||
capabilities={this.props.capabilities}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
// @ts-ignore repositionOnScroll doesn't exist on EuiPopover
|
||||
<EuiPopover
|
||||
id={'spcMenuPopover'}
|
||||
data-test-subj={`spacesNavSelector`}
|
||||
|
@ -91,7 +106,11 @@ export class NavControlPopover extends Component<Props, State> {
|
|||
}
|
||||
|
||||
private async loadSpaces() {
|
||||
const { spacesManager, activeSpace } = this.props;
|
||||
const { spacesManager } = this.props;
|
||||
|
||||
if (this.state.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loading: true,
|
||||
|
@ -99,16 +118,8 @@ export class NavControlPopover extends Component<Props, State> {
|
|||
|
||||
const spaces = await spacesManager.getSpaces();
|
||||
|
||||
// Update the active space definition, if it changed since the last load operation
|
||||
let activeSpaceEntry: Space | null = activeSpace.space;
|
||||
|
||||
if (activeSpace.valid) {
|
||||
activeSpaceEntry = spaces.find(space => space.id === this.props.activeSpace.space.id) || null;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
spaces,
|
||||
activeSpace: activeSpaceEntry,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
@ -117,10 +128,7 @@ export class NavControlPopover extends Component<Props, State> {
|
|||
const { activeSpace } = this.state;
|
||||
|
||||
if (!activeSpace) {
|
||||
return this.getButton(
|
||||
<EuiAvatar size={'s'} className={'spaceNavGraphic'} name={'error'} />,
|
||||
'error'
|
||||
);
|
||||
return this.getButton(<EuiLoadingSpinner size="m" />, 'loading');
|
||||
}
|
||||
|
||||
return this.getButton(
|
||||
|
@ -130,14 +138,17 @@ export class NavControlPopover extends Component<Props, State> {
|
|||
};
|
||||
|
||||
private getButton = (linkIcon: JSX.Element, linkTitle: string) => {
|
||||
const Button = this.props.buttonClass;
|
||||
return (
|
||||
<Button
|
||||
linkTitle={linkTitle}
|
||||
linkIcon={linkIcon}
|
||||
toggleSpaceSelector={this.toggleSpaceSelector}
|
||||
spaceSelectorShown={this.state.showSpaceSelector}
|
||||
/>
|
||||
<EuiHeaderSectionItemButton
|
||||
aria-controls="headerSpacesMenuList"
|
||||
aria-expanded={this.state.showSpaceSelector}
|
||||
aria-haspopup="true"
|
||||
aria-label={linkTitle}
|
||||
title={linkTitle}
|
||||
onClick={this.toggleSpaceSelector}
|
||||
>
|
||||
{linkIcon}
|
||||
</EuiHeaderSectionItemButton>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -63,21 +63,9 @@ exports[`it renders without crashing 1`] = `
|
|||
<EuiSpacer
|
||||
size="xl"
|
||||
/>
|
||||
<SpaceCards
|
||||
onSpaceSelect={[Function]}
|
||||
spaces={Array []}
|
||||
<EuiLoadingSpinner
|
||||
size="xl"
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<EuiText
|
||||
color="subdued"
|
||||
textAlign="center"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="No spaces match search criteria"
|
||||
id="xpack.spaces.spaceSelector.noSpacesMatchSearchCriteriaDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SpacesManager } from 'plugins/spaces/lib/spaces_manager';
|
||||
// @ts-ignore
|
||||
import template from 'plugins/spaces/views/space_selector/space_selector.html';
|
||||
import chrome from 'ui/chrome';
|
||||
|
@ -14,20 +13,20 @@ import { uiModules } from 'ui/modules';
|
|||
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { Space } from '../../../common/model/space';
|
||||
import { SpaceSelector } from './space_selector';
|
||||
|
||||
import { start as spacesNPStart } from '../../legacy';
|
||||
|
||||
const module = uiModules.get('spaces_selector', []);
|
||||
module.controller(
|
||||
'spacesSelectorController',
|
||||
($scope: any, spaces: Space[], serverBasePath: string) => {
|
||||
module.controller('spacesSelectorController', ($scope: any) => {
|
||||
$scope.$$postDigest(async () => {
|
||||
const domNode = document.getElementById('spaceSelectorRoot');
|
||||
|
||||
const spacesManager = new SpacesManager(serverBasePath);
|
||||
const { spacesManager } = await spacesNPStart;
|
||||
|
||||
render(
|
||||
<I18nContext>
|
||||
<SpaceSelector spaces={spaces} spacesManager={spacesManager} />
|
||||
<SpaceSelector spacesManager={spacesManager!} />
|
||||
</I18nContext>,
|
||||
domNode
|
||||
);
|
||||
|
@ -38,7 +37,7 @@ module.controller(
|
|||
unmountComponentAtNode(domNode);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
chrome.setVisible(false).setRootTemplate(template);
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { Space } from '../../../common/model/space';
|
||||
import { spacesManagerMock } from '../../lib/mocks';
|
||||
import { SpaceSelector } from './space_selector';
|
||||
|
@ -19,42 +19,12 @@ function getSpacesManager(spaces: Space[] = []) {
|
|||
test('it renders without crashing', () => {
|
||||
const spacesManager = getSpacesManager();
|
||||
const component = shallowWithIntl(
|
||||
<SpaceSelector.WrappedComponent
|
||||
spaces={[]}
|
||||
spacesManager={spacesManager as any}
|
||||
intl={null as any}
|
||||
/>
|
||||
<SpaceSelector.WrappedComponent spacesManager={spacesManager as any} intl={null as any} />
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it uses the spaces on props, when provided', () => {
|
||||
const spacesManager = getSpacesManager();
|
||||
|
||||
const spaces = [
|
||||
{
|
||||
id: 'space-1',
|
||||
name: 'Space 1',
|
||||
description: 'This is the first space',
|
||||
disabledFeatures: [],
|
||||
},
|
||||
];
|
||||
|
||||
const component = renderWithIntl(
|
||||
<SpaceSelector.WrappedComponent
|
||||
spaces={spaces}
|
||||
spacesManager={spacesManager as any}
|
||||
intl={null as any}
|
||||
/>
|
||||
);
|
||||
|
||||
return Promise.resolve().then(() => {
|
||||
expect(component.find('.spaceCard')).toHaveLength(1);
|
||||
expect(spacesManager.getSpaces).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('it queries for spaces when not provided on props', () => {
|
||||
test('it queries for spaces when loaded', () => {
|
||||
const spaces = [
|
||||
{
|
||||
id: 'space-1',
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiLoadingSpinner,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import { SpacesManager } from 'plugins/spaces/lib';
|
||||
|
@ -25,7 +26,6 @@ import { Space } from '../../../common/model/space';
|
|||
import { SpaceCards } from '../components/space_cards';
|
||||
|
||||
interface Props {
|
||||
spaces?: Space[];
|
||||
spacesManager: SpacesManager;
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
|
@ -41,17 +41,11 @@ class SpaceSelectorUI extends Component<Props, State> {
|
|||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const state: State = {
|
||||
this.state = {
|
||||
loading: false,
|
||||
searchTerm: '',
|
||||
spaces: [],
|
||||
};
|
||||
|
||||
if (Array.isArray(props.spaces)) {
|
||||
state.spaces = [...props.spaces];
|
||||
}
|
||||
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public setHeaderRef = (ref: HTMLElement | null) => {
|
||||
|
@ -130,9 +124,13 @@ class SpaceSelectorUI extends Component<Props, State> {
|
|||
|
||||
<EuiSpacer size="xl" />
|
||||
|
||||
<SpaceCards spaces={filteredSpaces} onSpaceSelect={this.onSelectSpace} />
|
||||
{this.state.loading && <EuiLoadingSpinner size="xl" />}
|
||||
|
||||
{filteredSpaces.length === 0 && (
|
||||
{!this.state.loading && (
|
||||
<SpaceCards spaces={filteredSpaces} onSpaceSelect={this.onSelectSpace} />
|
||||
)}
|
||||
|
||||
{!this.state.loading && filteredSpaces.length === 0 && (
|
||||
<Fragment>
|
||||
<EuiSpacer />
|
||||
<EuiText
|
||||
|
@ -155,7 +153,7 @@ class SpaceSelectorUI extends Component<Props, State> {
|
|||
|
||||
public getSearchField = () => {
|
||||
const { intl } = this.props;
|
||||
if (!this.props.spaces || this.props.spaces.length < SPACE_SEARCH_COUNT_THRESHOLD) {
|
||||
if (!this.state.spaces || this.state.spaces.length < SPACE_SEARCH_COUNT_THRESHOLD) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { UICapabilities } from 'ui/capabilities';
|
||||
import { Feature } from '../../../../plugins/features/server';
|
||||
import { Space } from '../../common/model/space';
|
||||
import { toggleUICapabilities } from './toggle_ui_capabilities';
|
||||
import { Capabilities } from 'src/core/public';
|
||||
|
||||
const features: Feature[] = [
|
||||
{
|
||||
|
@ -58,7 +58,7 @@ const features: Feature[] = [
|
|||
},
|
||||
];
|
||||
|
||||
const buildUiCapabilities = () =>
|
||||
const buildCapabilities = () =>
|
||||
Object.freeze({
|
||||
navLinks: {
|
||||
feature1: true,
|
||||
|
@ -89,7 +89,7 @@ const buildUiCapabilities = () =>
|
|||
foo: true,
|
||||
bar: true,
|
||||
},
|
||||
}) as UICapabilities;
|
||||
}) as Capabilities;
|
||||
|
||||
describe('toggleUiCapabilities', () => {
|
||||
it('does not toggle capabilities when the space has no disabled features', () => {
|
||||
|
@ -99,9 +99,9 @@ describe('toggleUiCapabilities', () => {
|
|||
disabledFeatures: [],
|
||||
};
|
||||
|
||||
const uiCapabilities: UICapabilities = buildUiCapabilities();
|
||||
const result = toggleUICapabilities(features, uiCapabilities, space);
|
||||
expect(result).toEqual(buildUiCapabilities());
|
||||
const capabilities = buildCapabilities();
|
||||
const result = toggleUICapabilities(features, capabilities, space);
|
||||
expect(result).toEqual(buildCapabilities());
|
||||
});
|
||||
|
||||
it('ignores unknown disabledFeatures', () => {
|
||||
|
@ -111,9 +111,9 @@ describe('toggleUiCapabilities', () => {
|
|||
disabledFeatures: ['i-do-not-exist'],
|
||||
};
|
||||
|
||||
const uiCapabilities: UICapabilities = buildUiCapabilities();
|
||||
const result = toggleUICapabilities(features, uiCapabilities, space);
|
||||
expect(result).toEqual(buildUiCapabilities());
|
||||
const capabilities = buildCapabilities();
|
||||
const result = toggleUICapabilities(features, capabilities, space);
|
||||
expect(result).toEqual(buildCapabilities());
|
||||
});
|
||||
|
||||
it('disables the corresponding navLink, catalogue, management sections, and all capability flags for disabled features', () => {
|
||||
|
@ -123,10 +123,10 @@ describe('toggleUiCapabilities', () => {
|
|||
disabledFeatures: ['feature_2'],
|
||||
};
|
||||
|
||||
const uiCapabilities: UICapabilities = buildUiCapabilities();
|
||||
const result = toggleUICapabilities(features, uiCapabilities, space);
|
||||
const capabilities = buildCapabilities();
|
||||
const result = toggleUICapabilities(features, capabilities, space);
|
||||
|
||||
const expectedCapabilities = buildUiCapabilities();
|
||||
const expectedCapabilities = buildCapabilities();
|
||||
|
||||
expectedCapabilities.navLinks.feature2 = false;
|
||||
expectedCapabilities.catalogue.feature2Entry = false;
|
||||
|
@ -144,10 +144,10 @@ describe('toggleUiCapabilities', () => {
|
|||
disabledFeatures: ['feature_1', 'feature_2', 'feature_3'],
|
||||
};
|
||||
|
||||
const uiCapabilities: UICapabilities = buildUiCapabilities();
|
||||
const result = toggleUICapabilities(features, uiCapabilities, space);
|
||||
const capabilities = buildCapabilities();
|
||||
const result = toggleUICapabilities(features, capabilities, space);
|
||||
|
||||
const expectedCapabilities = buildUiCapabilities();
|
||||
const expectedCapabilities = buildCapabilities();
|
||||
|
||||
expectedCapabilities.feature_1.bar = false;
|
||||
expectedCapabilities.feature_1.foo = false;
|
||||
|
|
|
@ -10,10 +10,10 @@ import { Space } from '../../common/model/space';
|
|||
|
||||
export function toggleUICapabilities(
|
||||
features: Feature[],
|
||||
uiCapabilities: UICapabilities,
|
||||
capabilities: UICapabilities,
|
||||
activeSpace: Space
|
||||
) {
|
||||
const clonedCapabilities = _.cloneDeep(uiCapabilities);
|
||||
const clonedCapabilities = _.cloneDeep(capabilities);
|
||||
|
||||
toggleDisabledFeatures(features, clonedCapabilities, activeSpace);
|
||||
|
||||
|
@ -22,18 +22,18 @@ export function toggleUICapabilities(
|
|||
|
||||
function toggleDisabledFeatures(
|
||||
features: Feature[],
|
||||
uiCapabilities: UICapabilities,
|
||||
capabilities: UICapabilities,
|
||||
activeSpace: Space
|
||||
) {
|
||||
const disabledFeatureKeys: string[] = activeSpace.disabledFeatures;
|
||||
const disabledFeatureKeys = activeSpace.disabledFeatures;
|
||||
|
||||
const disabledFeatures: Feature[] = disabledFeatureKeys
|
||||
const disabledFeatures = disabledFeatureKeys
|
||||
.map(key => features.find(feature => feature.id === key))
|
||||
.filter(feature => typeof feature !== 'undefined') as Feature[];
|
||||
|
||||
const navLinks: Record<string, boolean> = uiCapabilities.navLinks;
|
||||
const catalogueEntries: Record<string, boolean> = uiCapabilities.catalogue;
|
||||
const managementItems: Record<string, Record<string, boolean>> = uiCapabilities.management;
|
||||
const navLinks = capabilities.navLinks;
|
||||
const catalogueEntries = capabilities.catalogue;
|
||||
const managementItems = capabilities.management;
|
||||
|
||||
for (const feature of disabledFeatures) {
|
||||
// Disable associated navLink, if one exists
|
||||
|
@ -42,13 +42,13 @@ function toggleDisabledFeatures(
|
|||
}
|
||||
|
||||
// Disable associated catalogue entries
|
||||
const privilegeCatalogueEntries: string[] = feature.catalogue || [];
|
||||
const privilegeCatalogueEntries = feature.catalogue || [];
|
||||
privilegeCatalogueEntries.forEach(catalogueEntryId => {
|
||||
catalogueEntries[catalogueEntryId] = false;
|
||||
});
|
||||
|
||||
// Disable associated management items
|
||||
const privilegeManagementSections: Record<string, string[]> = feature.management || {};
|
||||
const privilegeManagementSections = feature.management || {};
|
||||
Object.entries(privilegeManagementSections).forEach(([sectionId, sectionItems]) => {
|
||||
sectionItems.forEach(item => {
|
||||
if (
|
||||
|
@ -61,8 +61,8 @@ function toggleDisabledFeatures(
|
|||
});
|
||||
|
||||
// Disable "sub features" that match the disabled feature
|
||||
if (uiCapabilities.hasOwnProperty(feature.id)) {
|
||||
const capability = uiCapabilities[feature.id];
|
||||
if (capabilities.hasOwnProperty(feature.id)) {
|
||||
const capability = capabilities[feature.id];
|
||||
Object.keys(capability).forEach(featureKey => {
|
||||
capability[featureKey] = false;
|
||||
});
|
||||
|
|
|
@ -31,6 +31,8 @@ import { ConfigType } from './config';
|
|||
import { toggleUICapabilities } from './lib/toggle_ui_capabilities';
|
||||
import { initSpacesRequestInterceptors } from './lib/request_interceptors';
|
||||
import { initExternalSpacesApi } from './routes/api/external';
|
||||
import { initInternalSpacesApi } from './routes/api/internal';
|
||||
|
||||
/**
|
||||
* Describes a set of APIs that is available in the legacy platform only and required by this plugin
|
||||
* to function properly.
|
||||
|
@ -119,6 +121,12 @@ export class Plugin {
|
|||
spacesService,
|
||||
});
|
||||
|
||||
const internalRouter = core.http.createRouter();
|
||||
initInternalSpacesApi({
|
||||
internalRouter,
|
||||
spacesService,
|
||||
});
|
||||
|
||||
initSpacesRequestInterceptors({
|
||||
http: core.http,
|
||||
log: this.log,
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 * as Rx from 'rxjs';
|
||||
import { createLegacyAPI, mockRouteContextWithInvalidLicense } from '../__fixtures__';
|
||||
import { CoreSetup, kibanaResponseFactory } from 'src/core/server';
|
||||
import { httpServiceMock, httpServerMock, elasticsearchServiceMock } from 'src/core/server/mocks';
|
||||
import { SpacesService } from '../../../spaces_service';
|
||||
import { SpacesAuditLogger } from '../../../lib/audit_logger';
|
||||
import { spacesConfig } from '../../../lib/__fixtures__';
|
||||
import { initGetActiveSpaceApi } from './get_active_space';
|
||||
|
||||
describe('GET /internal/spaces/_active_space', () => {
|
||||
const setup = async () => {
|
||||
const httpService = httpServiceMock.createSetupContract();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
const legacyAPI = createLegacyAPI();
|
||||
|
||||
const service = new SpacesService(null as any, () => legacyAPI);
|
||||
const spacesService = await service.setup({
|
||||
http: (httpService as unknown) as CoreSetup['http'],
|
||||
elasticsearch: elasticsearchServiceMock.createSetupContract(),
|
||||
authorization: null,
|
||||
getSpacesAuditLogger: () => ({} as SpacesAuditLogger),
|
||||
config$: Rx.of(spacesConfig),
|
||||
});
|
||||
|
||||
initGetActiveSpaceApi({
|
||||
internalRouter: router,
|
||||
spacesService,
|
||||
});
|
||||
|
||||
return {
|
||||
routeHandler: router.get.mock.calls[0][1],
|
||||
};
|
||||
};
|
||||
|
||||
it(`returns http/403 when the license is invalid`, async () => {
|
||||
const { routeHandler } = await setup();
|
||||
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
const response = await routeHandler(
|
||||
mockRouteContextWithInvalidLicense,
|
||||
request,
|
||||
kibanaResponseFactory
|
||||
);
|
||||
|
||||
expect(response.status).toEqual(403);
|
||||
expect(response.payload).toEqual({
|
||||
message: 'License is invalid for spaces',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { wrapError } from '../../../lib/errors';
|
||||
import { InternalRouteDeps } from '.';
|
||||
import { createLicensedRouteHandler } from '../../lib';
|
||||
|
||||
export function initGetActiveSpaceApi(deps: InternalRouteDeps) {
|
||||
const { internalRouter, spacesService } = deps;
|
||||
|
||||
internalRouter.get(
|
||||
{
|
||||
path: '/internal/spaces/_active_space',
|
||||
validate: false,
|
||||
},
|
||||
createLicensedRouteHandler(async (context, request, response) => {
|
||||
try {
|
||||
const space = await spacesService.getActiveSpace(request);
|
||||
return response.ok({ body: space });
|
||||
} catch (error) {
|
||||
return response.customError(wrapError(error));
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
18
x-pack/plugins/spaces/server/routes/api/internal/index.ts
Normal file
18
x-pack/plugins/spaces/server/routes/api/internal/index.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { IRouter } from 'src/core/server';
|
||||
import { SpacesServiceSetup } from '../../../spaces_service/spaces_service';
|
||||
import { initGetActiveSpaceApi } from './get_active_space';
|
||||
|
||||
export interface InternalRouteDeps {
|
||||
internalRouter: IRouter;
|
||||
spacesService: SpacesServiceSetup;
|
||||
}
|
||||
|
||||
export function initInternalSpacesApi(deps: InternalRouteDeps) {
|
||||
initGetActiveSpaceApi(deps);
|
||||
}
|
64
x-pack/test/api_integration/apis/spaces/get_active_space.ts
Normal file
64
x-pack/test/api_integration/apis/spaces/get_active_space.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 function({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const spacesService = getService('spaces');
|
||||
|
||||
describe('GET /internal/spaces/_active_space', () => {
|
||||
before(async () => {
|
||||
await spacesService.create({
|
||||
id: 'foo-space',
|
||||
name: 'Foo Space',
|
||||
disabledFeatures: ['timelion'],
|
||||
color: '#AABBCC',
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await spacesService.delete('foo-space');
|
||||
});
|
||||
|
||||
it('returns the default space', async () => {
|
||||
await supertest
|
||||
.get('/internal/spaces/_active_space')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect(200, {
|
||||
id: 'default',
|
||||
name: 'Default',
|
||||
description: 'This is your default space!',
|
||||
color: '#00bfb3',
|
||||
disabledFeatures: [],
|
||||
_reserved: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the foo space', async () => {
|
||||
await supertest
|
||||
.get('/s/foo-space/internal/spaces/_active_space')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect(200, {
|
||||
id: 'foo-space',
|
||||
name: 'Foo Space',
|
||||
disabledFeatures: ['timelion'],
|
||||
color: '#AABBCC',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns 404 when the space is not found', async () => {
|
||||
await supertest
|
||||
.get('/s/not-found-space/internal/spaces/_active_space')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect(404, {
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Saved object [space/not-found-space] not found',
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -9,6 +9,7 @@ export default function({ loadTestFile }: FtrProviderContext) {
|
|||
describe('spaces', function() {
|
||||
this.tags('ciGroup6');
|
||||
|
||||
loadTestFile(require.resolve('./get_active_space'));
|
||||
loadTestFile(require.resolve('./saved_objects'));
|
||||
loadTestFile(require.resolve('./space_attributes'));
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue