Cleanup spaces plugin (#91976)

This commit is contained in:
Joe Portner 2021-03-01 07:56:44 -05:00 committed by GitHub
parent ccf1fcc00e
commit 8710a81bea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
236 changed files with 2451 additions and 1671 deletions

View file

@ -13,9 +13,12 @@ import { Query } from '@elastic/eui';
import { parse } from 'query-string';
import { i18n } from '@kbn/i18n';
import { CoreStart, ChromeBreadcrumb } from 'src/core/public';
import type {
SpacesAvailableStartContract,
SpacesContextProps,
} from 'src/plugins/spaces_oss/public';
import { DataPublicPluginStart } from '../../../data/public';
import { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public';
import type { SpacesAvailableStartContract } from '../../../spaces_oss/public';
import {
ISavedObjectsManagementServiceRegistry,
SavedObjectsManagementActionServiceStart,
@ -23,7 +26,7 @@ import {
} from '../services';
import { SavedObjectsTable } from './objects_table';
const EmptyFunctionComponent: React.FC = ({ children }) => <>{children}</>;
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
const SavedObjectsTablePage = ({
coreStart,
@ -71,7 +74,8 @@ const SavedObjectsTablePage = ({
}, [setBreadcrumbs]);
const ContextWrapper = useMemo(
() => spacesApi?.ui.components.SpacesContext || EmptyFunctionComponent,
() =>
spacesApi ? spacesApi.ui.components.getSpacesContextProvider : getEmptyFunctionComponent,
[spacesApi]
);

View file

@ -32,10 +32,11 @@ type SpacesApiUiComponentMock = jest.Mocked<SpacesApiUiComponent>;
const createApiUiComponentsMock = () => {
const mock: SpacesApiUiComponentMock = {
SpacesContext: jest.fn(),
ShareToSpaceFlyout: jest.fn(),
SpaceList: jest.fn(),
LegacyUrlConflict: jest.fn(),
getSpacesContextProvider: jest.fn(),
getShareToSpaceFlyout: jest.fn(),
getSpaceList: jest.fn(),
getLegacyUrlConflict: jest.fn(),
getSpaceAvatar: jest.fn(),
};
return mock;

View file

@ -7,7 +7,7 @@
*/
import { Observable } from 'rxjs';
import type { FunctionComponent } from 'react';
import type { ReactElement } from 'react';
import { Space } from '../common';
/**
@ -22,12 +22,19 @@ export interface SpacesApi {
ui: SpacesApiUi;
}
/**
* Function that returns a promise for a lazy-loadable component.
*
* @public
*/
export type LazyComponentFn<T> = (props: T) => ReactElement;
/**
* @public
*/
export interface SpacesApiUi {
/**
* {@link SpacesApiUiComponent | React components} to support the spaces feature.
* Lazy-loadable {@link SpacesApiUiComponent | React components} to support the spaces feature.
*/
components: SpacesApiUiComponent;
/**
@ -62,13 +69,13 @@ export interface SpacesApiUiComponent {
/**
* Provides a context that is required to render some Spaces components.
*/
SpacesContext: FunctionComponent<SpacesContextProps>;
getSpacesContextProvider: LazyComponentFn<SpacesContextProps>;
/**
* Displays a flyout to edit the spaces that an object is shared to.
*
* Note: must be rendered inside of a SpacesContext.
*/
ShareToSpaceFlyout: FunctionComponent<ShareToSpaceFlyoutProps>;
getShareToSpaceFlyout: LazyComponentFn<ShareToSpaceFlyoutProps>;
/**
* Displays a corresponding list of spaces for a given list of saved object namespaces. It shows up to five spaces (and an indicator for
* any number of spaces that the user is not authorized to see) by default. If more than five named spaces would be displayed, the extras
@ -77,7 +84,7 @@ export interface SpacesApiUiComponent {
*
* Note: must be rendered inside of a SpacesContext.
*/
SpaceList: FunctionComponent<SpaceListProps>;
getSpaceList: LazyComponentFn<SpaceListProps>;
/**
* Displays a callout that needs to be used if a call to `SavedObjectsClient.resolve()` results in an `"conflict"` outcome, which
* indicates that the user has loaded the page which is associated directly with one object (A), *and* with a legacy URL that points to a
@ -95,7 +102,11 @@ export interface SpacesApiUiComponent {
*
* New URL path: `#/workpad/workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e/page/1`
*/
LegacyUrlConflict: FunctionComponent<LegacyUrlConflictProps>;
getLegacyUrlConflict: LazyComponentFn<LegacyUrlConflictProps>;
/**
* Displays an avatar for the given space.
*/
getSpaceAvatar: LazyComponentFn<SpaceAvatarProps>;
}
/**
@ -251,3 +262,18 @@ export interface LegacyUrlConflictProps {
*/
otherObjectPath: string;
}
/**
* @public
*/
export interface SpaceAvatarProps {
space: Partial<Space>;
size?: 's' | 'm' | 'l' | 'xl';
className?: string;
/**
* When enabled, allows EUI to provide an aria-label for this component, which is announced on screen readers.
*
* Default value is true.
*/
announceSpaceName?: boolean;
}

View file

@ -16,6 +16,7 @@ export {
} from './types';
export {
LazyComponentFn,
SpacesApi,
SpacesApiUi,
SpacesApiUiComponent,
@ -24,6 +25,7 @@ export {
ShareToSpaceSavedObjectTarget,
SpaceListProps,
LegacyUrlConflictProps,
SpaceAvatarProps,
} from './api';
export const plugin = () => new SpacesOssPlugin();

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { FC, useState } from 'react';
import React, { FC, useCallback, useState } from 'react';
import { EuiButtonEmpty } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -66,7 +66,11 @@ export const JobSpacesList: FC<Props> = ({ spacesApi, spaceIds, jobId, jobType,
});
}
const { SpaceList, ShareToSpaceFlyout } = spacesApi.ui.components;
const LazySpaceList = useCallback(spacesApi.ui.components.getSpaceList, [spacesApi]);
const LazyShareToSpaceFlyout = useCallback(spacesApi.ui.components.getShareToSpaceFlyout, [
spacesApi,
]);
const shareToSpaceFlyoutProps: ShareToSpaceFlyoutProps = {
savedObjectTarget: {
type: ML_SAVED_OBJECT_TYPE,
@ -83,9 +87,9 @@ export const JobSpacesList: FC<Props> = ({ spacesApi, spaceIds, jobId, jobType,
return (
<>
<EuiButtonEmpty onClick={() => setShowFlyout(true)} style={{ height: 'auto' }}>
<SpaceList namespaces={spaceIds} displayLimit={0} behaviorContext="outside-space" />
<LazySpaceList namespaces={spaceIds} displayLimit={0} behaviorContext="outside-space" />
</EuiButtonEmpty>
{showFlyout && <ShareToSpaceFlyout {...shareToSpaceFlyoutProps} />}
{showFlyout && <LazyShareToSpaceFlyout {...shareToSpaceFlyoutProps} />}
</>
);
};

View file

@ -23,6 +23,7 @@ import {
EuiTabbedContentTab,
} from '@elastic/eui';
import type { SpacesContextProps } from 'src/plugins/spaces_oss/public';
import { PLUGIN_ID } from '../../../../../../common/constants/app';
import { ManagementAppMountParams } from '../../../../../../../../../src/plugins/management/public/';
@ -67,7 +68,7 @@ function usePageState<T extends ListingPageUrlState>(
return [pageState, updateState];
}
const EmptyFunctionComponent: React.FC = ({ children }) => <>{children}</>;
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
function useTabs(isMlEnabledInSpace: boolean, spacesApi: SpacesPluginStart | undefined): Tab[] {
const [adPageState, updateAdPageState] = usePageState(getDefaultAnomalyDetectionJobsListState());
@ -147,6 +148,11 @@ export const JobsListPage: FC<{
check();
}, []);
const ContextWrapper = useCallback(
spacesApi ? spacesApi.ui.components.getSpacesContextProvider : getEmptyFunctionComponent,
[spacesApi]
);
if (initialized === false) {
return null;
}
@ -185,8 +191,6 @@ export const JobsListPage: FC<{
return <AccessDeniedPage />;
}
const ContextWrapper = spacesApi?.ui.components.SpacesContext || EmptyFunctionComponent;
return (
<RedirectAppLinks application={coreStart.application}>
<I18nContext>

View file

@ -40,6 +40,7 @@ import {
NotificationsStart,
} from 'src/core/public';
import type { DocLinksStart, ScopedHistory } from 'kibana/public';
import type { SpacesApiUi } from 'src/plugins/spaces_oss/public';
import { FeaturesPluginStart } from '../../../../../features/public';
import { KibanaFeature } from '../../../../../features/common';
import { IndexPatternsContract } from '../../../../../../../src/plugins/data/public';
@ -84,6 +85,7 @@ interface Props {
notifications: NotificationsStart;
fatalErrors: FatalErrorsSetup;
history: ScopedHistory;
spacesApiUi?: SpacesApiUi;
}
function useRunAsUsers(
@ -289,6 +291,7 @@ export const EditRolePage: FunctionComponent<Props> = ({
uiCapabilities,
notifications,
history,
spacesApiUi,
}) => {
const backToRoleList = useCallback(() => history.push('/'), [history]);
@ -447,6 +450,7 @@ export const EditRolePage: FunctionComponent<Props> = ({
role={role}
onChange={onRoleChange}
validator={validator}
spacesApiUi={spacesApiUi}
/>
</div>
);

View file

@ -7,6 +7,7 @@
import React, { Component } from 'react';
import { Capabilities } from 'src/core/public';
import type { SpacesApiUi } from 'src/plugins/spaces_oss/public';
import { Space } from '../../../../../../../spaces/public';
import { Role } from '../../../../../../common/model';
import { RoleValidator } from '../../validate_role';
@ -26,6 +27,7 @@ interface Props {
kibanaPrivileges: KibanaPrivileges;
onChange: (role: Role) => void;
validator: RoleValidator;
spacesApiUi?: SpacesApiUi;
}
export class KibanaPrivilegesRegion extends Component<Props, {}> {
@ -48,6 +50,7 @@ export class KibanaPrivilegesRegion extends Component<Props, {}> {
onChange,
editable,
validator,
spacesApiUi,
} = this.props;
if (role._transform_error && role._transform_error.includes('kibana')) {
@ -65,6 +68,7 @@ export class KibanaPrivilegesRegion extends Component<Props, {}> {
editable={editable}
canCustomizeSubFeaturePrivileges={canCustomizeSubFeaturePrivileges}
validator={validator}
spacesApiUi={spacesApiUi!}
/>
);
} else {

View file

@ -7,12 +7,15 @@
import React from 'react';
import { mountWithIntl } from '@kbn/test/jest';
import { spacesManagerMock } from '../../../../../../../../spaces/public/spaces_manager/mocks';
import { getUiApi } from '../../../../../../../../spaces/public/ui_api';
import { createKibanaPrivileges } from '../../../../__fixtures__/kibana_privileges';
import { kibanaFeatures } from '../../../../__fixtures__/kibana_features';
import { RoleKibanaPrivilege } from '../../../../../../../common/model';
import { PrivilegeSummary } from '.';
import { findTestSubject } from '@kbn/test/jest';
import { PrivilegeSummaryTable } from './privilege_summary_table';
import { coreMock } from 'src/core/public/mocks';
const createRole = (roleKibanaPrivileges: RoleKibanaPrivilege[]) => ({
name: 'some-role',
@ -31,6 +34,9 @@ const spaces = [
disabledFeatures: [],
},
];
const spacesManager = spacesManagerMock.create();
const { getStartServices } = coreMock.createSetup();
const spacesApiUi = getUiApi({ spacesManager, getStartServices });
describe('PrivilegeSummary', () => {
it('initially renders a button', () => {
@ -50,6 +56,7 @@ describe('PrivilegeSummary', () => {
kibanaPrivileges={kibanaPrivileges}
role={role}
canCustomizeSubFeaturePrivileges={true}
spacesApiUi={spacesApiUi}
/>
);
@ -74,6 +81,7 @@ describe('PrivilegeSummary', () => {
kibanaPrivileges={kibanaPrivileges}
role={role}
canCustomizeSubFeaturePrivileges={true}
spacesApiUi={spacesApiUi}
/>
);

View file

@ -10,6 +10,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { EuiButtonEmpty, EuiOverlayMask, EuiButton } from '@elastic/eui';
import { EuiFlyout } from '@elastic/eui';
import { EuiFlyoutHeader, EuiTitle, EuiFlyoutBody, EuiFlyoutFooter } from '@elastic/eui';
import type { SpacesApiUi } from 'src/plugins/spaces_oss/public';
import { Space } from '../../../../../../../../spaces/public';
import { Role } from '../../../../../../../common/model';
import { PrivilegeSummaryTable } from './privilege_summary_table';
@ -20,6 +21,7 @@ interface Props {
spaces: Space[];
kibanaPrivileges: KibanaPrivileges;
canCustomizeSubFeaturePrivileges: boolean;
spacesApiUi: SpacesApiUi;
}
export const PrivilegeSummary = (props: Props) => {
const [isOpen, setIsOpen] = useState(false);
@ -54,6 +56,7 @@ export const PrivilegeSummary = (props: Props) => {
spaces={props.spaces}
kibanaPrivileges={props.kibanaPrivileges}
canCustomizeSubFeaturePrivileges={props.canCustomizeSubFeaturePrivileges}
spacesApiUi={props.spacesApiUi}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter>

View file

@ -5,13 +5,17 @@
* 2.0.
*/
import { act } from '@testing-library/react';
import React from 'react';
import { spacesManagerMock } from '../../../../../../../../spaces/public/spaces_manager/mocks';
import { getUiApi } from '../../../../../../../../spaces/public/ui_api';
import { createKibanaPrivileges } from '../../../../__fixtures__/kibana_privileges';
import { kibanaFeatures } from '../../../../__fixtures__/kibana_features';
import { mountWithIntl } from '@kbn/test/jest';
import { PrivilegeSummaryTable } from './privilege_summary_table';
import { PrivilegeSummaryTable, PrivilegeSummaryTableProps } from './privilege_summary_table';
import { RoleKibanaPrivilege } from '../../../../../../../common/model';
import { getDisplayedFeaturePrivileges } from './__fixtures__';
import { coreMock } from 'src/core/public/mocks';
const createRole = (roleKibanaPrivileges: RoleKibanaPrivilege[]) => ({
name: 'some-role',
@ -40,6 +44,9 @@ const spaces = [
disabledFeatures: [],
},
];
const spacesManager = spacesManagerMock.create();
const { getStartServices } = coreMock.createSetup();
const spacesApiUi = getUiApi({ spacesManager, getStartServices });
const maybeExpectSubFeaturePrivileges = (expect: boolean, subFeaturesPrivileges: unknown) => {
return expect ? { subFeaturesPrivileges } : {};
@ -83,12 +90,23 @@ const expectNoPrivileges = (displayedPrivileges: any, expectSubFeatures: boolean
});
};
const setup = async (props: PrivilegeSummaryTableProps) => {
const wrapper = mountWithIntl(<PrivilegeSummaryTable {...props} />);
// lazy-load SpaceAvatar
await act(async () => {
wrapper.update();
});
return wrapper;
};
describe('PrivilegeSummaryTable', () => {
[true, false].forEach((allowSubFeaturePrivileges) => {
describe(`when sub feature privileges are ${
allowSubFeaturePrivileges ? 'allowed' : 'disallowed'
}`, () => {
it('ignores unknown base privileges', () => {
it('ignores unknown base privileges', async () => {
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, {
allowSubFeaturePrivileges,
});
@ -101,21 +119,20 @@ describe('PrivilegeSummaryTable', () => {
},
]);
const wrapper = mountWithIntl(
<PrivilegeSummaryTable
spaces={spaces}
kibanaPrivileges={kibanaPrivileges}
role={role}
canCustomizeSubFeaturePrivileges={allowSubFeaturePrivileges}
/>
);
const wrapper = await setup({
spaces,
kibanaPrivileges,
role,
canCustomizeSubFeaturePrivileges: allowSubFeaturePrivileges,
spacesApiUi,
});
const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role);
expectNoPrivileges(displayedPrivileges, allowSubFeaturePrivileges);
});
it('ignores unknown feature privileges', () => {
it('ignores unknown feature privileges', async () => {
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, {
allowSubFeaturePrivileges,
});
@ -130,21 +147,20 @@ describe('PrivilegeSummaryTable', () => {
},
]);
const wrapper = mountWithIntl(
<PrivilegeSummaryTable
spaces={spaces}
kibanaPrivileges={kibanaPrivileges}
role={role}
canCustomizeSubFeaturePrivileges={allowSubFeaturePrivileges}
/>
);
const wrapper = await setup({
spaces,
kibanaPrivileges,
role,
canCustomizeSubFeaturePrivileges: allowSubFeaturePrivileges,
spacesApiUi,
});
const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role);
expectNoPrivileges(displayedPrivileges, allowSubFeaturePrivileges);
});
it('ignores unknown features', () => {
it('ignores unknown features', async () => {
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, {
allowSubFeaturePrivileges,
});
@ -159,21 +175,20 @@ describe('PrivilegeSummaryTable', () => {
},
]);
const wrapper = mountWithIntl(
<PrivilegeSummaryTable
spaces={spaces}
kibanaPrivileges={kibanaPrivileges}
role={role}
canCustomizeSubFeaturePrivileges={allowSubFeaturePrivileges}
/>
);
const wrapper = await setup({
spaces,
kibanaPrivileges,
role,
canCustomizeSubFeaturePrivileges: allowSubFeaturePrivileges,
spacesApiUi,
});
const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role);
expectNoPrivileges(displayedPrivileges, allowSubFeaturePrivileges);
});
it('renders effective privileges for the global base privilege', () => {
it('renders effective privileges for the global base privilege', async () => {
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, {
allowSubFeaturePrivileges,
});
@ -186,14 +201,13 @@ describe('PrivilegeSummaryTable', () => {
},
]);
const wrapper = mountWithIntl(
<PrivilegeSummaryTable
spaces={spaces}
kibanaPrivileges={kibanaPrivileges}
role={role}
canCustomizeSubFeaturePrivileges={allowSubFeaturePrivileges}
/>
);
const wrapper = await setup({
spaces,
kibanaPrivileges,
role,
canCustomizeSubFeaturePrivileges: allowSubFeaturePrivileges,
spacesApiUi,
});
const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role);
@ -234,7 +248,7 @@ describe('PrivilegeSummaryTable', () => {
});
});
it('renders effective privileges for a global feature privilege', () => {
it('renders effective privileges for a global feature privilege', async () => {
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, {
allowSubFeaturePrivileges,
});
@ -249,14 +263,13 @@ describe('PrivilegeSummaryTable', () => {
},
]);
const wrapper = mountWithIntl(
<PrivilegeSummaryTable
spaces={spaces}
kibanaPrivileges={kibanaPrivileges}
role={role}
canCustomizeSubFeaturePrivileges={allowSubFeaturePrivileges}
/>
);
const wrapper = await setup({
spaces,
kibanaPrivileges,
role,
canCustomizeSubFeaturePrivileges: allowSubFeaturePrivileges,
spacesApiUi,
});
const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role);
@ -297,7 +310,7 @@ describe('PrivilegeSummaryTable', () => {
});
});
it('renders effective privileges for the space base privilege', () => {
it('renders effective privileges for the space base privilege', async () => {
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, {
allowSubFeaturePrivileges,
});
@ -310,14 +323,13 @@ describe('PrivilegeSummaryTable', () => {
},
]);
const wrapper = mountWithIntl(
<PrivilegeSummaryTable
spaces={spaces}
kibanaPrivileges={kibanaPrivileges}
role={role}
canCustomizeSubFeaturePrivileges={allowSubFeaturePrivileges}
/>
);
const wrapper = await setup({
spaces,
kibanaPrivileges,
role,
canCustomizeSubFeaturePrivileges: allowSubFeaturePrivileges,
spacesApiUi,
});
const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role);
@ -358,7 +370,7 @@ describe('PrivilegeSummaryTable', () => {
});
});
it('renders effective privileges for a space feature privilege', () => {
it('renders effective privileges for a space feature privilege', async () => {
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, {
allowSubFeaturePrivileges,
});
@ -373,14 +385,13 @@ describe('PrivilegeSummaryTable', () => {
},
]);
const wrapper = mountWithIntl(
<PrivilegeSummaryTable
spaces={spaces}
kibanaPrivileges={kibanaPrivileges}
role={role}
canCustomizeSubFeaturePrivileges={allowSubFeaturePrivileges}
/>
);
const wrapper = await setup({
spaces,
kibanaPrivileges,
role,
canCustomizeSubFeaturePrivileges: allowSubFeaturePrivileges,
spacesApiUi,
});
const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role);
@ -421,7 +432,7 @@ describe('PrivilegeSummaryTable', () => {
});
});
it('renders effective privileges for global base + space base privileges', () => {
it('renders effective privileges for global base + space base privileges', async () => {
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, {
allowSubFeaturePrivileges,
});
@ -439,14 +450,13 @@ describe('PrivilegeSummaryTable', () => {
},
]);
const wrapper = mountWithIntl(
<PrivilegeSummaryTable
spaces={spaces}
kibanaPrivileges={kibanaPrivileges}
role={role}
canCustomizeSubFeaturePrivileges={allowSubFeaturePrivileges}
/>
);
const wrapper = await setup({
spaces,
kibanaPrivileges,
role,
canCustomizeSubFeaturePrivileges: allowSubFeaturePrivileges,
spacesApiUi,
});
const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role);
@ -512,7 +522,7 @@ describe('PrivilegeSummaryTable', () => {
});
});
it('renders effective privileges for global base + space feature privileges', () => {
it('renders effective privileges for global base + space feature privileges', async () => {
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, {
allowSubFeaturePrivileges,
});
@ -532,14 +542,13 @@ describe('PrivilegeSummaryTable', () => {
},
]);
const wrapper = mountWithIntl(
<PrivilegeSummaryTable
spaces={spaces}
kibanaPrivileges={kibanaPrivileges}
role={role}
canCustomizeSubFeaturePrivileges={allowSubFeaturePrivileges}
/>
);
const wrapper = await setup({
spaces,
kibanaPrivileges,
role,
canCustomizeSubFeaturePrivileges: allowSubFeaturePrivileges,
spacesApiUi,
});
const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role);
@ -605,7 +614,7 @@ describe('PrivilegeSummaryTable', () => {
});
});
it('renders effective privileges for global feature + space base privileges', () => {
it('renders effective privileges for global feature + space base privileges', async () => {
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, {
allowSubFeaturePrivileges,
});
@ -625,14 +634,13 @@ describe('PrivilegeSummaryTable', () => {
},
]);
const wrapper = mountWithIntl(
<PrivilegeSummaryTable
spaces={spaces}
kibanaPrivileges={kibanaPrivileges}
role={role}
canCustomizeSubFeaturePrivileges={allowSubFeaturePrivileges}
/>
);
const wrapper = await setup({
spaces,
kibanaPrivileges,
role,
canCustomizeSubFeaturePrivileges: allowSubFeaturePrivileges,
spacesApiUi,
});
const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role);
@ -698,7 +706,7 @@ describe('PrivilegeSummaryTable', () => {
});
});
it('renders effective privileges for global feature + space feature privileges', () => {
it('renders effective privileges for global feature + space feature privileges', async () => {
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, {
allowSubFeaturePrivileges,
});
@ -720,14 +728,13 @@ describe('PrivilegeSummaryTable', () => {
},
]);
const wrapper = mountWithIntl(
<PrivilegeSummaryTable
spaces={spaces}
kibanaPrivileges={kibanaPrivileges}
role={role}
canCustomizeSubFeaturePrivileges={allowSubFeaturePrivileges}
/>
);
const wrapper = await setup({
spaces,
kibanaPrivileges,
role,
canCustomizeSubFeaturePrivileges: allowSubFeaturePrivileges,
spacesApiUi,
});
const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role);
@ -793,7 +800,7 @@ describe('PrivilegeSummaryTable', () => {
});
});
it('renders effective privileges for a complex setup', () => {
it('renders effective privileges for a complex setup', async () => {
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, {
allowSubFeaturePrivileges,
});
@ -821,14 +828,13 @@ describe('PrivilegeSummaryTable', () => {
},
]);
const wrapper = mountWithIntl(
<PrivilegeSummaryTable
spaces={spaces}
kibanaPrivileges={kibanaPrivileges}
role={role}
canCustomizeSubFeaturePrivileges={allowSubFeaturePrivileges}
/>
);
const wrapper = await setup({
spaces,
kibanaPrivileges,
role,
canCustomizeSubFeaturePrivileges: allowSubFeaturePrivileges,
spacesApiUi,
});
const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role);

View file

@ -19,6 +19,7 @@ import {
EuiAccordion,
EuiTitle,
} from '@elastic/eui';
import type { SpacesApiUi } from 'src/plugins/spaces_oss/public';
import { Space } from '../../../../../../../../spaces/public';
import { Role, RoleKibanaPrivilege } from '../../../../../../../common/model';
import { isGlobalPrivilegeDefinition } from '../../../privilege_utils';
@ -31,18 +32,19 @@ import {
EffectiveFeaturePrivileges,
} from './privilege_summary_calculator';
interface Props {
export interface PrivilegeSummaryTableProps {
role: Role;
spaces: Space[];
kibanaPrivileges: KibanaPrivileges;
canCustomizeSubFeaturePrivileges: boolean;
spacesApiUi: SpacesApiUi;
}
function getColumnKey(entry: RoleKibanaPrivilege) {
return `privilege_entry_${entry.spaces.join('|')}`;
}
export const PrivilegeSummaryTable = (props: Props) => {
export const PrivilegeSummaryTable = (props: PrivilegeSummaryTableProps) => {
const [expandedFeatures, setExpandedFeatures] = useState<string[]>([]);
const featureCategories = useMemo(() => {
@ -113,7 +115,9 @@ export const PrivilegeSummaryTable = (props: Props) => {
const privilegeColumns = rawKibanaPrivileges.map((entry) => {
const key = getColumnKey(entry);
return {
name: <SpaceColumnHeader entry={entry} spaces={props.spaces} />,
name: (
<SpaceColumnHeader entry={entry} spaces={props.spaces} spacesApiUi={props.spacesApiUi} />
),
field: key,
render: (kibanaPrivilege: EffectiveFeaturePrivileges, record: { featureId: string }) => {
const { primary, hasCustomizedSubFeaturePrivileges } = kibanaPrivilege[record.featureId];

View file

@ -5,11 +5,16 @@
* 2.0.
*/
import { act } from '@testing-library/react';
import React from 'react';
import { mountWithIntl } from '@kbn/test/jest';
import { spacesManagerMock } from '../../../../../../../../spaces/public/spaces_manager/mocks';
import { getUiApi } from '../../../../../../../../spaces/public/ui_api';
import { SpaceAvatarInternal } from '../../../../../../../../spaces/public/space_avatar/space_avatar_internal';
import type { RoleKibanaPrivilege } from '../../../../../../../common/model';
import { SpaceColumnHeader } from './space_column_header';
import { SpacesPopoverList } from '../../../spaces_popover_list';
import { SpaceAvatar } from '../../../../../../../../spaces/public';
import { coreMock } from 'src/core/public/mocks';
const spaces = [
{
@ -43,80 +48,78 @@ const spaces = [
disabledFeatures: [],
},
];
const spacesManager = spacesManagerMock.create();
const { getStartServices } = coreMock.createSetup();
const spacesApiUi = getUiApi({ spacesManager, getStartServices });
describe('SpaceColumnHeader', () => {
it('renders the Global privilege definition with a special label', () => {
async function setup(entry: RoleKibanaPrivilege) {
const wrapper = mountWithIntl(
<SpaceColumnHeader
spaces={spaces}
entry={{
base: [],
feature: {},
spaces: ['*'],
}}
/>
<SpaceColumnHeader spaces={spaces} entry={entry} spacesApiUi={spacesApiUi} />
);
await act(async () => {});
// lazy-load SpaceAvatar
await act(async () => {
wrapper.update();
});
return wrapper;
}
it('renders the Global privilege definition with a special label', async () => {
const wrapper = await setup({
base: [],
feature: {},
spaces: ['*'],
});
// Snapshot includes space avatar (The first "G"), followed by the "Global" label,
// followed by the (all spaces) text as part of the SpacesPopoverList
expect(wrapper.text()).toMatchInlineSnapshot(`"G All Spaces"`);
});
it('renders a placeholder space when the requested space no longer exists', () => {
const wrapper = mountWithIntl(
<SpaceColumnHeader
spaces={spaces}
entry={{
base: [],
feature: {},
spaces: ['space-1', 'missing-space', 'space-3'],
}}
/>
);
it('renders a placeholder space when the requested space no longer exists', async () => {
const wrapper = await setup({
base: [],
feature: {},
spaces: ['space-1', 'missing-space', 'space-3'],
});
expect(wrapper.find(SpacesPopoverList)).toHaveLength(0);
const avatars = wrapper.find(SpaceAvatar);
const avatars = wrapper.find(SpaceAvatarInternal);
expect(avatars).toHaveLength(3);
expect(wrapper.text()).toMatchInlineSnapshot(`"S1 m S3 "`);
});
it('renders a space privilege definition with an avatar for each space in the group', () => {
const wrapper = mountWithIntl(
<SpaceColumnHeader
spaces={spaces}
entry={{
base: [],
feature: {},
spaces: ['space-1', 'space-2', 'space-3', 'space-4'],
}}
/>
);
it('renders a space privilege definition with an avatar for each space in the group', async () => {
const wrapper = await setup({
base: [],
feature: {},
spaces: ['space-1', 'space-2', 'space-3', 'space-4'],
});
expect(wrapper.find(SpacesPopoverList)).toHaveLength(0);
const avatars = wrapper.find(SpaceAvatar);
const avatars = wrapper.find(SpaceAvatarInternal);
expect(avatars).toHaveLength(4);
expect(wrapper.text()).toMatchInlineSnapshot(`"S1 S2 S3 S4 "`);
});
it('renders a space privilege definition with an avatar for the first 4 spaces in the group, with the popover control showing the rest', () => {
const wrapper = mountWithIntl(
<SpaceColumnHeader
spaces={spaces}
entry={{
base: [],
feature: {},
spaces: ['space-1', 'space-2', 'space-3', 'space-4', 'space-5'],
}}
/>
);
it('renders a space privilege definition with an avatar for the first 4 spaces in the group, with the popover control showing the rest', async () => {
const wrapper = await setup({
base: [],
feature: {},
spaces: ['space-1', 'space-2', 'space-3', 'space-4', 'space-5'],
});
expect(wrapper.find(SpacesPopoverList)).toHaveLength(1);
const avatars = wrapper.find(SpaceAvatar);
const avatars = wrapper.find(SpaceAvatarInternal);
expect(avatars).toHaveLength(4);
expect(wrapper.text()).toMatchInlineSnapshot(`"S1 S2 S3 S4 +1 more"`);

View file

@ -5,22 +5,25 @@
* 2.0.
*/
import React, { Fragment } from 'react';
import React, { Fragment, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { Space, SpaceAvatar } from '../../../../../../../../spaces/public';
import type { Space } from 'src/plugins/spaces_oss/common';
import type { SpacesApiUi } from 'src/plugins/spaces_oss/public';
import { RoleKibanaPrivilege } from '../../../../../../../common/model';
import { isGlobalPrivilegeDefinition } from '../../../privilege_utils';
import { SpacesPopoverList } from '../../../spaces_popover_list';
interface Props {
export interface SpaceColumnHeaderProps {
spaces: Space[];
entry: RoleKibanaPrivilege;
spacesApiUi: SpacesApiUi;
}
const SPACES_DISPLAY_COUNT = 4;
export const SpaceColumnHeader = (props: Props) => {
export const SpaceColumnHeader = (props: SpaceColumnHeaderProps) => {
const { spacesApiUi } = props;
const isGlobal = isGlobalPrivilegeDefinition(props.entry);
const entrySpaces = props.entry.spaces.map((spaceId) => {
return (
@ -31,12 +34,14 @@ export const SpaceColumnHeader = (props: Props) => {
}
);
});
const LazySpaceAvatar = useMemo(() => spacesApiUi.components.getSpaceAvatar, [spacesApiUi]);
return (
<div>
{entrySpaces.slice(0, SPACES_DISPLAY_COUNT).map((space) => {
return (
<span key={space.id}>
<SpaceAvatar size="s" space={space} />{' '}
<LazySpaceAvatar size="s" space={space} />{' '}
{isGlobal && (
<span>
<FormattedMessage
@ -62,6 +67,7 @@ export const SpaceColumnHeader = (props: Props) => {
},
}
)}
spacesApiUi={spacesApiUi}
/>
</Fragment>
)}

View file

@ -19,6 +19,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import _ from 'lodash';
import React, { Component, Fragment } from 'react';
import { Capabilities } from 'src/core/public';
import type { SpacesApiUi } from 'src/plugins/spaces_oss/public';
import { Space } from '../../../../../../../../spaces/public';
import { Role, isRoleReserved } from '../../../../../../../common/model';
import { RoleValidator } from '../../../validate_role';
@ -37,6 +38,7 @@ interface Props {
canCustomizeSubFeaturePrivileges: boolean;
validator: RoleValidator;
uiCapabilities: Capabilities;
spacesApiUi: SpacesApiUi;
}
interface State {
@ -215,6 +217,7 @@ export class SpaceAwarePrivilegeSection extends Component<Props, State> {
spaces={this.getDisplaySpaces()}
kibanaPrivileges={this.props.kibanaPrivileges}
canCustomizeSubFeaturePrivileges={this.props.canCustomizeSubFeaturePrivileges}
spacesApiUi={this.props.spacesApiUi}
/>
);

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { act } from '@testing-library/react';
import React from 'react';
import { mountWithIntl } from '@kbn/test/jest';
import { SpacesPopoverList } from '.';
@ -15,9 +16,13 @@ import {
EuiFieldSearch,
EuiPopover,
} from '@elastic/eui';
import { SpaceAvatar } from '../../../../../../spaces/public';
import type { Space } from 'src/plugins/spaces_oss/common';
import { spacesManagerMock } from '../../../../../../spaces/public/spaces_manager/mocks';
import { getUiApi } from '../../../../../../spaces/public/ui_api';
import { SpaceAvatarInternal } from '../../../../../../spaces/public/space_avatar/space_avatar_internal';
import { coreMock } from 'src/core/public/mocks';
const spaces = [
const mockSpaces = [
{
id: 'default',
name: 'Default Space',
@ -35,44 +40,62 @@ const spaces = [
disabledFeatures: [],
},
];
const spacesManager = spacesManagerMock.create();
const { getStartServices } = coreMock.createSetup();
const spacesApiUi = getUiApi({ spacesManager, getStartServices });
describe('SpacesPopoverList', () => {
it('renders a button with the provided text', () => {
const wrapper = mountWithIntl(<SpacesPopoverList spaces={spaces} buttonText="hello world" />);
async function setup(spaces: Space[]) {
const wrapper = mountWithIntl(
<SpacesPopoverList spaces={spaces} buttonText="hello world" spacesApiUi={spacesApiUi} />
);
// lazy-load SpaceAvatar
await act(async () => {
wrapper.update();
});
return wrapper;
}
it('renders a button with the provided text', async () => {
const wrapper = await setup(mockSpaces);
expect(wrapper.find(EuiButtonEmpty).text()).toEqual('hello world');
expect(wrapper.find(EuiContextMenuPanel)).toHaveLength(0);
});
it('clicking the button renders a context menu with the provided spaces', () => {
const wrapper = mountWithIntl(<SpacesPopoverList spaces={spaces} buttonText="hello world" />);
wrapper.find(EuiButtonEmpty).simulate('click');
it('clicking the button renders a context menu with the provided spaces', async () => {
const wrapper = await setup(mockSpaces);
await act(async () => {
wrapper.find(EuiButtonEmpty).simulate('click');
});
wrapper.update();
const menu = wrapper.find(EuiContextMenuPanel);
expect(menu).toHaveLength(1);
const items = menu.find(EuiContextMenuItem);
expect(items).toHaveLength(spaces.length);
expect(items).toHaveLength(mockSpaces.length);
spaces.forEach((space, index) => {
const spaceAvatar = items.at(index).find(SpaceAvatar);
mockSpaces.forEach((space, index) => {
const spaceAvatar = items.at(index).find(SpaceAvatarInternal);
expect(spaceAvatar.props().space).toEqual(space);
});
expect(wrapper.find(EuiFieldSearch)).toHaveLength(0);
});
it('renders a search box when there are 8 or more spaces', () => {
it('renders a search box when there are 8 or more spaces', async () => {
const lotsOfSpaces = [1, 2, 3, 4, 5, 6, 7, 8].map((num) => ({
id: `space-${num}`,
name: `Space ${num}`,
disabledFeatures: [],
}));
const wrapper = mountWithIntl(
<SpacesPopoverList spaces={lotsOfSpaces} buttonText="hello world" />
);
wrapper.find(EuiButtonEmpty).simulate('click');
const wrapper = await setup(lotsOfSpaces);
await act(async () => {
wrapper.find(EuiButtonEmpty).simulate('click');
});
wrapper.update();
const menu = wrapper.find(EuiContextMenuPanel).first();
@ -83,20 +106,23 @@ describe('SpacesPopoverList', () => {
expect(searchField).toHaveLength(1);
searchField.props().onSearch!('Space 6');
await act(async () => {});
wrapper.update();
expect(wrapper.find(SpaceAvatar)).toHaveLength(1);
expect(wrapper.find(SpaceAvatarInternal)).toHaveLength(1);
searchField.props().onSearch!('this does not match');
wrapper.update();
expect(wrapper.find(SpaceAvatar)).toHaveLength(0);
expect(wrapper.find(SpaceAvatarInternal)).toHaveLength(0);
const updatedMenu = wrapper.find(EuiContextMenuPanel).first();
expect(updatedMenu.text()).toMatchInlineSnapshot(`"Spaces no spaces found "`);
});
it('can close its popover', () => {
const wrapper = mountWithIntl(<SpacesPopoverList spaces={spaces} buttonText="hello world" />);
wrapper.find(EuiButtonEmpty).simulate('click');
it('can close its popover', async () => {
const wrapper = await setup(mockSpaces);
await act(async () => {
wrapper.find(EuiButtonEmpty).simulate('click');
});
wrapper.update();
expect(wrapper.find(EuiPopover).props().isOpen).toEqual(true);

View file

@ -17,13 +17,15 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import React, { Component } from 'react';
import { Space, SpaceAvatar } from '../../../../../../spaces/public';
import React, { Component, memo } from 'react';
import type { Space } from 'src/plugins/spaces_oss/common';
import type { SpacesApiUi } from 'src/plugins/spaces_oss/public';
import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../../../../spaces/common';
interface Props {
spaces: Space[];
buttonText: string;
spacesApiUi: SpacesApiUi;
}
interface State {
@ -191,7 +193,8 @@ export class SpacesPopoverList extends Component<Props, State> {
};
private renderSpaceMenuItem = (space: Space): JSX.Element => {
const icon = <SpaceAvatar space={space} size={'s'} />;
const LazySpaceAvatar = memo(this.props.spacesApiUi.components.getSpaceAvatar);
const icon = <LazySpaceAvatar space={space} size={'s'} />; // wrapped in a Suspense above
return (
<EuiContextMenuItem
key={space.id}

View file

@ -42,7 +42,7 @@ export const rolesManagementApp = Object.freeze({
const [
[
{ application, docLinks, http, i18n: i18nStart, notifications, chrome },
{ data, features },
{ data, features, spaces },
],
{ RolesGridPage },
{ EditRolePage },
@ -92,6 +92,8 @@ export const rolesManagementApp = Object.freeze({
},
]);
const spacesApiUi = spaces?.ui;
return (
<EditRolePage
action={action}
@ -109,6 +111,7 @@ export const rolesManagementApp = Object.freeze({
uiCapabilities={application.capabilities}
indexPatterns={data.indexPatterns}
history={history}
spacesApiUi={spacesApiUi}
/>
);
};

View file

@ -6,4 +6,4 @@
*/
export { SecurityNavControlService, SecurityNavControlServiceStart } from './nav_control_service';
export { UserMenuLink } from './nav_control_component';
export type { UserMenuLink } from './nav_control_component';

View file

@ -14,6 +14,7 @@ import {
PluginInitializerContext,
} from '../../../../src/core/public';
import { FeaturesPluginStart } from '../../features/public';
import type { SpacesPluginStart } from '../../spaces/public';
import { DataPublicPluginStart } from '../../../../src/plugins/data/public';
import {
FeatureCatalogueCategory,
@ -48,6 +49,7 @@ export interface PluginStartDependencies {
features: FeaturesPluginStart;
securityOss: SecurityOssPluginStart;
management?: ManagementStart;
spaces?: SpacesPluginStart;
}
export class SecurityPlugin

View file

@ -0,0 +1,5 @@
{
"rules": {
"@typescript-eslint/consistent-type-imports": 1
}
}

View file

@ -5,8 +5,9 @@
* 2.0.
*/
import type { Space } from 'src/plugins/spaces_oss/common';
import { isReservedSpace } from './is_reserved_space';
import { Space } from '../../../../src/plugins/spaces_oss/common';
test('it returns true for reserved spaces', () => {
const space: Space = {

View file

@ -6,7 +6,8 @@
*/
import { get } from 'lodash';
import { Space } from '../../../../src/plugins/spaces_oss/common';
import type { Space } from 'src/plugins/spaces_oss/common';
/**
* Returns whether the given Space is reserved or not.

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { SpacesLicense } from '.';
import type { SpacesLicense } from './license_service';
export const licenseMock = {
create: (): jest.Mocked<SpacesLicense> => ({

View file

@ -6,9 +6,11 @@
*/
import { of } from 'rxjs';
import { licenseMock } from '../../../licensing/common/licensing.mock';
import type { LicenseType } from '../../../licensing/common/types';
import { LICENSE_TYPE } from '../../../licensing/common/types';
import { SpacesLicenseService } from './license_service';
import { LICENSE_TYPE, LicenseType } from '../../../licensing/common/types';
describe('license#isEnabled', function () {
it('should indicate that Spaces is disabled when there is no license information', () => {

View file

@ -5,8 +5,9 @@
* 2.0.
*/
import { Observable, Subscription } from 'rxjs';
import { ILicense } from '../../../licensing/common/types';
import type { Observable, Subscription } from 'rxjs';
import type { ILicense } from '../../../licensing/common/types';
export interface SpacesLicense {
isEnabled(): boolean;

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { Space } from '../../../../src/plugins/spaces_oss/common';
import type { Space } from 'src/plugins/spaces_oss/common';
export interface GetAllSpacesOptions {
purpose?: GetAllSpacesPurpose;

View file

@ -5,8 +5,9 @@
* 2.0.
*/
import { advancedSettingsMock } from 'src/plugins/advanced_settings/public/mocks';
import { AdvancedSettingsService } from './advanced_settings_service';
import { advancedSettingsMock } from '../../../../../src/plugins/advanced_settings/public/mocks';
const componentRegistryMock = advancedSettingsMock.createSetupContract();

View file

@ -6,9 +6,11 @@
*/
import React from 'react';
import { AdvancedSettingsSetup } from 'src/plugins/advanced_settings/public';
import { Space } from '../../../../../src/plugins/spaces_oss/common';
import { AdvancedSettingsTitle, AdvancedSettingsSubtitle } from './components';
import type { AdvancedSettingsSetup } from 'src/plugins/advanced_settings/public';
import type { Space } from 'src/plugins/spaces_oss/common';
import { AdvancedSettingsSubtitle, AdvancedSettingsTitle } from './components';
interface SetupDeps {
getActiveSpace: () => Promise<Space>;

View file

@ -5,11 +5,13 @@
* 2.0.
*/
import React from 'react';
import { mountWithIntl, nextTick } from '@kbn/test/jest';
import { AdvancedSettingsSubtitle } from './advanced_settings_subtitle';
import { EuiCallOut } from '@elastic/eui';
import { act } from '@testing-library/react';
import React from 'react';
import { mountWithIntl, nextTick } from '@kbn/test/jest';
import { AdvancedSettingsSubtitle } from './advanced_settings_subtitle';
describe('AdvancedSettingsSubtitle', () => {
it('renders as expected', async () => {

View file

@ -6,9 +6,10 @@
*/
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import React, { Fragment, useEffect, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { Fragment, useState, useEffect } from 'react';
import { Space } from '../../../../../../../src/plugins/spaces_oss/common';
import type { Space } from 'src/plugins/spaces_oss/common';
interface Props {
getActiveSpace: () => Promise<Space>;

View file

@ -5,11 +5,13 @@
* 2.0.
*/
import React from 'react';
import { mountWithIntl, nextTick } from '@kbn/test/jest';
import { AdvancedSettingsTitle } from './advanced_settings_title';
import { SpaceAvatar } from '../../../space_avatar';
import { act } from '@testing-library/react';
import React from 'react';
import { mountWithIntl } from '@kbn/test/jest';
import { SpaceAvatarInternal } from '../../../space_avatar/space_avatar_internal';
import { AdvancedSettingsTitle } from './advanced_settings_title';
describe('AdvancedSettingsTitle', () => {
it('renders without crashing', async () => {
@ -23,11 +25,12 @@ describe('AdvancedSettingsTitle', () => {
<AdvancedSettingsTitle getActiveSpace={() => Promise.resolve(space)} />
);
await act(async () => {
await nextTick();
wrapper.update();
});
await act(async () => {});
expect(wrapper.find(SpaceAvatar)).toHaveLength(1);
// wait for SpaceAvatar to lazy-load
await act(async () => {});
wrapper.update();
expect(wrapper.find(SpaceAvatarInternal)).toHaveLength(1);
});
});

View file

@ -5,11 +5,18 @@
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiTitle } from '@elastic/eui';
import React, { lazy, Suspense, useEffect, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useState, useEffect } from 'react';
import { Space } from '../../../../../../../src/plugins/spaces_oss/common';
import { SpaceAvatar } from '../../../space_avatar';
import type { Space } from 'src/plugins/spaces_oss/common';
import { getSpaceAvatarComponent } from '../../../space_avatar';
// No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana.
const LazySpaceAvatar = lazy(() =>
getSpaceAvatarComponent().then((component) => ({ default: component }))
);
interface Props {
getActiveSpace: () => Promise<Space>;
@ -27,7 +34,9 @@ export const AdvancedSettingsTitle = (props: Props) => {
return (
<EuiFlexGroup gutterSize="s" responsive={false} alignItems={'center'}>
<EuiFlexItem grow={false}>
<SpaceAvatar space={activeSpace} />
<Suspense fallback={<EuiLoadingSpinner />}>
<LazySpaceAvatar space={activeSpace} />
</Suspense>
</EuiFlexItem>
<EuiFlexItem style={{ marginLeft: '10px' }}>
<EuiTitle size="m">

View file

@ -5,10 +5,13 @@
* 2.0.
*/
import type { ReactWrapper } from 'enzyme';
import React from 'react';
import { ReactWrapper } from 'enzyme';
import { mountWithIntl } from '@kbn/test/jest';
import { CopyModeControl, CopyModeControlProps } from './copy_mode_control';
import type { CopyModeControlProps } from './copy_mode_control';
import { CopyModeControl } from './copy_mode_control';
describe('CopyModeControl', () => {
const initialValues = { createNewCopies: true, overwrite: true }; // some test cases below make assumptions based on these initial values

View file

@ -5,18 +5,19 @@
* 2.0.
*/
import React, { useState } from 'react';
import {
EuiFormFieldset,
EuiTitle,
EuiCheckableCard,
EuiRadioGroup,
EuiText,
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
EuiFormFieldset,
EuiIconTip,
EuiRadioGroup,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
export interface CopyModeControlProps {

View file

@ -5,11 +5,13 @@
* 2.0.
*/
import { EuiIconTip, EuiLoadingSpinner } from '@elastic/eui';
import React, { Fragment } from 'react';
import { EuiLoadingSpinner, EuiIconTip } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { ImportRetry } from '../types';
import { SummarizedCopyToSpaceResult, SummarizedSavedObjectResult } from '..';
import type { SummarizedCopyToSpaceResult, SummarizedSavedObjectResult } from '../lib';
import type { ImportRetry } from '../types';
interface Props {
summarizedCopyResult: SummarizedCopyToSpaceResult;

View file

@ -6,13 +6,16 @@
*/
import './copy_status_summary_indicator.scss';
import { EuiBadge, EuiIconTip, EuiLoadingSpinner } from '@elastic/eui';
import React, { Fragment } from 'react';
import { EuiLoadingSpinner, EuiIconTip, EuiBadge } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { Space } from '../../../../../../src/plugins/spaces_oss/common';
import { ImportRetry } from '../types';
import type { Space } from 'src/plugins/spaces_oss/common';
import type { SummarizedCopyToSpaceResult } from '../lib';
import type { ImportRetry } from '../types';
import { ResolveAllConflicts } from './resolve_all_conflicts';
import { SummarizedCopyToSpaceResult } from '..';
interface Props {
space: Space;

View file

@ -5,302 +5,15 @@
* 2.0.
*/
import React, { useState, useEffect, useMemo } from 'react';
import {
EuiFlyout,
EuiIcon,
EuiFlyoutHeader,
EuiTitle,
EuiText,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiLoadingSpinner,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiEmptyPrompt,
} from '@elastic/eui';
import { mapValues } from 'lodash';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { ToastsStart } from 'src/core/public';
import {
ProcessedImportResponse,
processImportResponse,
} from '../../../../../../src/plugins/saved_objects_management/public';
import { Space } from '../../../../../../src/plugins/spaces_oss/common';
import { SpacesManager } from '../../spaces_manager';
import { ProcessingCopyToSpace } from './processing_copy_to_space';
import { CopyToSpaceFlyoutFooter } from './copy_to_space_flyout_footer';
import { CopyToSpaceForm } from './copy_to_space_form';
import { CopyOptions, ImportRetry, SavedObjectTarget } from '../types';
import React from 'react';
interface Props {
onClose: () => void;
savedObjectTarget: SavedObjectTarget;
spacesManager: SpacesManager;
toastNotifications: ToastsStart;
}
import type { CopyToSpaceFlyoutProps } from './copy_to_space_flyout_internal';
const INCLUDE_RELATED_DEFAULT = true;
const CREATE_NEW_COPIES_DEFAULT = true;
const OVERWRITE_ALL_DEFAULT = true;
export const CopySavedObjectsToSpaceFlyout = (props: Props) => {
const { onClose, savedObjectTarget: object, spacesManager, toastNotifications } = props;
const savedObjectTarget = useMemo(
() => ({
type: object.type,
id: object.id,
namespaces: object.namespaces,
icon: object.icon || 'apps',
title: object.title || `${object.type} [id=${object.id}]`,
}),
[object]
);
const [copyOptions, setCopyOptions] = useState<CopyOptions>({
includeRelated: INCLUDE_RELATED_DEFAULT,
createNewCopies: CREATE_NEW_COPIES_DEFAULT,
overwrite: OVERWRITE_ALL_DEFAULT,
selectedSpaceIds: [],
});
const [{ isLoading, spaces }, setSpacesState] = useState<{ isLoading: boolean; spaces: Space[] }>(
{
isLoading: true,
spaces: [],
}
);
useEffect(() => {
const getSpaces = spacesManager.getSpaces({ includeAuthorizedPurposes: true });
const getActiveSpace = spacesManager.getActiveSpace();
Promise.all([getSpaces, getActiveSpace])
.then(([allSpaces, activeSpace]) => {
setSpacesState({
isLoading: false,
spaces: allSpaces.filter(
({ id, authorizedPurposes }) =>
id !== activeSpace.id && authorizedPurposes?.copySavedObjectsIntoSpace !== false
),
});
})
.catch((e) => {
toastNotifications.addError(e, {
title: i18n.translate('xpack.spaces.management.copyToSpace.spacesLoadErrorTitle', {
defaultMessage: 'Error loading available spaces',
}),
});
});
}, [spacesManager, toastNotifications]);
const [copyInProgress, setCopyInProgress] = useState(false);
const [conflictResolutionInProgress, setConflictResolutionInProgress] = useState(false);
const [copyResult, setCopyResult] = useState<Record<string, ProcessedImportResponse>>({});
const [retries, setRetries] = useState<Record<string, ImportRetry[]>>({});
const initialCopyFinished = Object.values(copyResult).length > 0;
const onRetriesChange = (updatedRetries: Record<string, ImportRetry[]>) => {
setRetries(updatedRetries);
export const getCopyToSpaceFlyoutComponent = async (): Promise<
React.FC<CopyToSpaceFlyoutProps>
> => {
const { CopyToSpaceFlyoutInternal } = await import('./copy_to_space_flyout_internal');
return (props: CopyToSpaceFlyoutProps) => {
return <CopyToSpaceFlyoutInternal {...props} />;
};
async function startCopy() {
setCopyInProgress(true);
setCopyResult({});
try {
const copySavedObjectsResult = await spacesManager.copySavedObjects(
[{ type: savedObjectTarget.type, id: savedObjectTarget.id }],
copyOptions.selectedSpaceIds,
copyOptions.includeRelated,
copyOptions.createNewCopies,
copyOptions.overwrite
);
const processedResult = mapValues(copySavedObjectsResult, processImportResponse);
setCopyResult(processedResult);
// retry all successful imports
const getAutomaticRetries = (response: ProcessedImportResponse): ImportRetry[] => {
const { failedImports, successfulImports } = response;
if (!failedImports.length) {
// if no imports failed for this space, return an empty array
return [];
}
// get missing references failures that do not also have a conflict
const nonMissingReferencesFailures = failedImports
.filter(({ error }) => error.type !== 'missing_references')
.reduce((acc, { obj: { type, id } }) => acc.add(`${type}:${id}`), new Set<string>());
const missingReferencesToRetry = failedImports.filter(
({ obj: { type, id }, error }) =>
error.type === 'missing_references' &&
!nonMissingReferencesFailures.has(`${type}:${id}`)
);
// otherwise, some imports failed for this space, so retry any successful imports (if any)
return [
...successfulImports.map(({ type, id, overwrite, destinationId, createNewCopy }) => {
return { type, id, overwrite: overwrite === true, destinationId, createNewCopy };
}),
...missingReferencesToRetry.map(({ obj: { type, id } }) => ({
type,
id,
overwrite: false,
ignoreMissingReferences: true,
})),
];
};
const automaticRetries = mapValues(processedResult, getAutomaticRetries);
setRetries(automaticRetries);
} catch (e) {
setCopyInProgress(false);
toastNotifications.addError(e, {
title: i18n.translate('xpack.spaces.management.copyToSpace.copyErrorTitle', {
defaultMessage: 'Error copying saved object',
}),
});
}
}
async function finishCopy() {
// if any retries are present, attempt to resolve errors again
const needsErrorResolution = Object.values(retries).some((spaceRetry) => spaceRetry.length);
if (needsErrorResolution) {
setConflictResolutionInProgress(true);
try {
await spacesManager.resolveCopySavedObjectsErrors(
[{ type: savedObjectTarget.type, id: savedObjectTarget.id }],
retries,
copyOptions.includeRelated,
copyOptions.createNewCopies
);
toastNotifications.addSuccess(
i18n.translate('xpack.spaces.management.copyToSpace.resolveCopySuccessTitle', {
defaultMessage: 'Copy successful',
})
);
onClose();
} catch (e) {
setCopyInProgress(false);
toastNotifications.addError(e, {
title: i18n.translate('xpack.spaces.management.copyToSpace.resolveCopyErrorTitle', {
defaultMessage: 'Error resolving saved object conflicts',
}),
});
}
} else {
onClose();
}
}
const getFlyoutBody = () => {
// Step 1: loading assets for main form
if (isLoading) {
return <EuiLoadingSpinner />;
}
// Step 1a: assets loaded, but no spaces are available for copy.
if (spaces.length === 0) {
return (
<EuiEmptyPrompt
body={
<p>
<FormattedMessage
id="xpack.spaces.management.copyToSpace.noSpacesBody"
defaultMessage="There are no eligible spaces to copy into."
/>
</p>
}
title={
<h3>
<FormattedMessage
id="xpack.spaces.management.copyToSpace.noSpacesTitle"
defaultMessage="No spaces available"
/>
</h3>
}
/>
);
}
// Step 2: Copy has not been initiated yet; User must fill out form to continue.
if (!copyInProgress) {
return (
<CopyToSpaceForm
savedObjectTarget={savedObjectTarget}
spaces={spaces}
copyOptions={copyOptions}
onUpdate={setCopyOptions}
/>
);
}
// Step3: Copy operation is in progress
return (
<ProcessingCopyToSpace
savedObjectTarget={savedObjectTarget}
copyInProgress={copyInProgress}
conflictResolutionInProgress={conflictResolutionInProgress}
copyResult={copyResult}
spaces={spaces}
copyOptions={copyOptions}
retries={retries}
onRetriesChange={onRetriesChange}
/>
);
};
return (
<EuiFlyout onClose={onClose} maxWidth={600} data-test-subj="copy-to-space-flyout">
<EuiFlyoutHeader hasBorder>
<EuiFlexGroup alignItems="center" gutterSize="m">
<EuiFlexItem grow={false}>
<EuiIcon size="m" type="copy" />
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="m">
<h2>
<FormattedMessage
id="xpack.spaces.management.copyToSpaceFlyoutHeader"
defaultMessage="Copy to space"
/>
</h2>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiFlexGroup alignItems="center" gutterSize="m">
<EuiFlexItem grow={false}>
<EuiIcon type={savedObjectTarget.icon} />
</EuiFlexItem>
<EuiFlexItem>
<EuiText>
<p>{savedObjectTarget.title}</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin="m" />
{getFlyoutBody()}
</EuiFlyoutBody>
<EuiFlyoutFooter>
<CopyToSpaceFlyoutFooter
copyInProgress={copyInProgress}
conflictResolutionInProgress={conflictResolutionInProgress}
initialCopyFinished={initialCopyFinished}
copyResult={copyResult}
numberOfSelectedSpaces={copyOptions.selectedSpaceIds.length}
retries={retries}
onClose={onClose}
onCopyStart={startCopy}
onCopyFinish={finishCopy}
/>
</EuiFlyoutFooter>
</EuiFlyout>
);
};

View file

@ -5,19 +5,24 @@
* 2.0.
*/
import React, { Fragment } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiStat,
EuiHorizontalRule,
EuiStat,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { Fragment } from 'react';
import { i18n } from '@kbn/i18n';
import { ProcessedImportResponse, FailedImport } from 'src/plugins/saved_objects_management/public';
import { ImportRetry } from '../types';
import { FormattedMessage } from '@kbn/i18n/react';
import type {
FailedImport,
ProcessedImportResponse,
} from 'src/plugins/saved_objects_management/public';
import type { ImportRetry } from '../types';
interface Props {
copyInProgress: boolean;

View file

@ -5,22 +5,23 @@
* 2.0.
*/
import React from 'react';
import { EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui';
import Boom from '@hapi/boom';
import { mountWithIntl, nextTick } from '@kbn/test/jest';
import { CopySavedObjectsToSpaceFlyout } from './copy_to_space_flyout';
import { CopyToSpaceForm } from './copy_to_space_form';
import { EuiLoadingSpinner, EuiEmptyPrompt } from '@elastic/eui';
import { Space } from '../../../../../../src/plugins/spaces_oss/common';
import { findTestSubject } from '@kbn/test/jest';
import { SelectableSpacesControl } from './selectable_spaces_control';
import { CopyModeControl } from './copy_mode_control';
import { act } from '@testing-library/react';
import { ProcessingCopyToSpace } from './processing_copy_to_space';
import React from 'react';
import { findTestSubject, mountWithIntl, nextTick } from '@kbn/test/jest';
import { coreMock } from 'src/core/public/mocks';
import type { Space } from 'src/plugins/spaces_oss/common';
import { getSpacesContextProviderWrapper } from '../../spaces_context';
import { spacesManagerMock } from '../../spaces_manager/mocks';
import { SpacesManager } from '../../spaces_manager';
import { ToastsApi } from 'src/core/public';
import { SavedObjectTarget } from '../types';
import type { SavedObjectTarget } from '../types';
import { CopyModeControl } from './copy_mode_control';
import { getCopyToSpaceFlyoutComponent } from './copy_to_space_flyout';
import { CopyToSpaceForm } from './copy_to_space_form';
import { ProcessingCopyToSpace } from './processing_copy_to_space';
import { SelectableSpacesControl } from './selectable_spaces_control';
interface SetupOpts {
mockSpaces?: Space[];
@ -32,13 +33,14 @@ const setup = async (opts: SetupOpts = {}) => {
const mockSpacesManager = spacesManagerMock.create();
mockSpacesManager.getActiveSpace.mockResolvedValue({
const getActiveSpace = Promise.resolve({
id: 'my-active-space',
name: 'my active space',
disabledFeatures: [],
});
mockSpacesManager.getActiveSpace.mockReturnValue(getActiveSpace);
mockSpacesManager.getSpaces.mockResolvedValue(
const getSpaces = Promise.resolve(
opts.mockSpaces || [
{
id: 'space-1',
@ -62,11 +64,18 @@ const setup = async (opts: SetupOpts = {}) => {
},
]
);
mockSpacesManager.getSpaces.mockReturnValue(getSpaces);
const mockToastNotifications = {
addError: jest.fn(),
addSuccess: jest.fn(),
const { getStartServices } = coreMock.createSetup();
const startServices = coreMock.createStart();
startServices.application.capabilities = {
...startServices.application.capabilities,
spaces: { manage: true },
};
const mockToastNotifications = startServices.notifications.toasts;
const getStartServicesPromise = Promise.resolve<any>([startServices, , ,]);
getStartServices.mockReturnValue(getStartServicesPromise);
const savedObjectToCopy = {
type: 'dashboard',
id: 'my-dash',
@ -75,21 +84,29 @@ const setup = async (opts: SetupOpts = {}) => {
title: 'foo',
} as SavedObjectTarget;
const SpacesContext = await getSpacesContextProviderWrapper({
getStartServices,
spacesManager: mockSpacesManager,
});
const CopyToSpaceFlyout = await getCopyToSpaceFlyoutComponent();
const wrapper = mountWithIntl(
<CopySavedObjectsToSpaceFlyout
savedObjectTarget={savedObjectToCopy}
spacesManager={(mockSpacesManager as unknown) as SpacesManager}
toastNotifications={(mockToastNotifications as unknown) as ToastsApi}
onClose={onClose}
/>
<SpacesContext>
<CopyToSpaceFlyout savedObjectTarget={savedObjectToCopy} onClose={onClose} />
</SpacesContext>
);
// wait for context wrapper to rerender
await act(async () => {
await getStartServicesPromise;
wrapper.update();
});
await getActiveSpace;
await getSpaces;
if (!opts.returnBeforeSpacesLoad) {
// Wait for spaces manager to complete and flyout to rerender
await act(async () => {
await nextTick();
wrapper.update();
});
// rerender after spaces manager API calls are completed
wrapper.update();
}
return { wrapper, onClose, mockSpacesManager, mockToastNotifications, savedObjectToCopy };
@ -103,10 +120,7 @@ describe('CopyToSpaceFlyout', () => {
expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(0);
expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(1);
await act(async () => {
await nextTick();
wrapper.update();
});
wrapper.update();
expect(wrapper.find(CopyToSpaceForm)).toHaveLength(1);
expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0);

View file

@ -0,0 +1,307 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiHorizontalRule,
EuiIcon,
EuiLoadingSpinner,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { mapValues } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import type { ProcessedImportResponse } from 'src/plugins/saved_objects_management/public';
import type { Space } from 'src/plugins/spaces_oss/common';
import { processImportResponse } from '../../../../../../src/plugins/saved_objects_management/public';
import type { CopyOptions, ImportRetry, SavedObjectTarget } from '../types';
import { CopyToSpaceFlyoutFooter } from './copy_to_space_flyout_footer';
import { CopyToSpaceForm } from './copy_to_space_form';
import { ProcessingCopyToSpace } from './processing_copy_to_space';
import { useSpaces } from '../../spaces_context';
export interface CopyToSpaceFlyoutProps {
onClose: () => void;
savedObjectTarget: SavedObjectTarget;
}
const INCLUDE_RELATED_DEFAULT = true;
const CREATE_NEW_COPIES_DEFAULT = true;
const OVERWRITE_ALL_DEFAULT = true;
export const CopyToSpaceFlyoutInternal = (props: CopyToSpaceFlyoutProps) => {
const { spacesManager, services } = useSpaces();
const { notifications } = services;
const toastNotifications = notifications!.toasts;
const { onClose, savedObjectTarget: object } = props;
const savedObjectTarget = useMemo(
() => ({
type: object.type,
id: object.id,
namespaces: object.namespaces,
icon: object.icon || 'apps',
title: object.title || `${object.type} [id=${object.id}]`,
}),
[object]
);
const [copyOptions, setCopyOptions] = useState<CopyOptions>({
includeRelated: INCLUDE_RELATED_DEFAULT,
createNewCopies: CREATE_NEW_COPIES_DEFAULT,
overwrite: OVERWRITE_ALL_DEFAULT,
selectedSpaceIds: [],
});
const [{ isLoading, spaces }, setSpacesState] = useState<{ isLoading: boolean; spaces: Space[] }>(
{
isLoading: true,
spaces: [],
}
);
useEffect(() => {
const getSpaces = spacesManager.getSpaces({ includeAuthorizedPurposes: true });
const getActiveSpace = spacesManager.getActiveSpace();
Promise.all([getSpaces, getActiveSpace])
.then(([allSpaces, activeSpace]) => {
setSpacesState({
isLoading: false,
spaces: allSpaces.filter(
({ id, authorizedPurposes }) =>
id !== activeSpace.id && authorizedPurposes?.copySavedObjectsIntoSpace !== false
),
});
})
.catch((e) => {
toastNotifications.addError(e, {
title: i18n.translate('xpack.spaces.management.copyToSpace.spacesLoadErrorTitle', {
defaultMessage: 'Error loading available spaces',
}),
});
});
}, [spacesManager, toastNotifications]);
const [copyInProgress, setCopyInProgress] = useState(false);
const [conflictResolutionInProgress, setConflictResolutionInProgress] = useState(false);
const [copyResult, setCopyResult] = useState<Record<string, ProcessedImportResponse>>({});
const [retries, setRetries] = useState<Record<string, ImportRetry[]>>({});
const initialCopyFinished = Object.values(copyResult).length > 0;
const onRetriesChange = (updatedRetries: Record<string, ImportRetry[]>) => {
setRetries(updatedRetries);
};
async function startCopy() {
setCopyInProgress(true);
setCopyResult({});
try {
const copySavedObjectsResult = await spacesManager.copySavedObjects(
[{ type: savedObjectTarget.type, id: savedObjectTarget.id }],
copyOptions.selectedSpaceIds,
copyOptions.includeRelated,
copyOptions.createNewCopies,
copyOptions.overwrite
);
const processedResult = mapValues(copySavedObjectsResult, processImportResponse);
setCopyResult(processedResult);
// retry all successful imports
const getAutomaticRetries = (response: ProcessedImportResponse): ImportRetry[] => {
const { failedImports, successfulImports } = response;
if (!failedImports.length) {
// if no imports failed for this space, return an empty array
return [];
}
// get missing references failures that do not also have a conflict
const nonMissingReferencesFailures = failedImports
.filter(({ error }) => error.type !== 'missing_references')
.reduce((acc, { obj: { type, id } }) => acc.add(`${type}:${id}`), new Set<string>());
const missingReferencesToRetry = failedImports.filter(
({ obj: { type, id }, error }) =>
error.type === 'missing_references' &&
!nonMissingReferencesFailures.has(`${type}:${id}`)
);
// otherwise, some imports failed for this space, so retry any successful imports (if any)
return [
...successfulImports.map(({ type, id, overwrite, destinationId, createNewCopy }) => {
return { type, id, overwrite: overwrite === true, destinationId, createNewCopy };
}),
...missingReferencesToRetry.map(({ obj: { type, id } }) => ({
type,
id,
overwrite: false,
ignoreMissingReferences: true,
})),
];
};
const automaticRetries = mapValues(processedResult, getAutomaticRetries);
setRetries(automaticRetries);
} catch (e) {
setCopyInProgress(false);
toastNotifications.addError(e, {
title: i18n.translate('xpack.spaces.management.copyToSpace.copyErrorTitle', {
defaultMessage: 'Error copying saved object',
}),
});
}
}
async function finishCopy() {
// if any retries are present, attempt to resolve errors again
const needsErrorResolution = Object.values(retries).some((spaceRetry) => spaceRetry.length);
if (needsErrorResolution) {
setConflictResolutionInProgress(true);
try {
await spacesManager.resolveCopySavedObjectsErrors(
[{ type: savedObjectTarget.type, id: savedObjectTarget.id }],
retries,
copyOptions.includeRelated,
copyOptions.createNewCopies
);
toastNotifications.addSuccess(
i18n.translate('xpack.spaces.management.copyToSpace.resolveCopySuccessTitle', {
defaultMessage: 'Copy successful',
})
);
onClose();
} catch (e) {
setCopyInProgress(false);
toastNotifications.addError(e, {
title: i18n.translate('xpack.spaces.management.copyToSpace.resolveCopyErrorTitle', {
defaultMessage: 'Error resolving saved object conflicts',
}),
});
}
} else {
onClose();
}
}
const getFlyoutBody = () => {
// Step 1: loading assets for main form
if (isLoading) {
return <EuiLoadingSpinner />;
}
// Step 1a: assets loaded, but no spaces are available for copy.
if (spaces.length === 0) {
return (
<EuiEmptyPrompt
body={
<p>
<FormattedMessage
id="xpack.spaces.management.copyToSpace.noSpacesBody"
defaultMessage="There are no eligible spaces to copy into."
/>
</p>
}
title={
<h3>
<FormattedMessage
id="xpack.spaces.management.copyToSpace.noSpacesTitle"
defaultMessage="No spaces available"
/>
</h3>
}
/>
);
}
// Step 2: Copy has not been initiated yet; User must fill out form to continue.
if (!copyInProgress) {
return (
<CopyToSpaceForm
savedObjectTarget={savedObjectTarget}
spaces={spaces}
copyOptions={copyOptions}
onUpdate={setCopyOptions}
/>
);
}
// Step3: Copy operation is in progress
return (
<ProcessingCopyToSpace
savedObjectTarget={savedObjectTarget}
copyInProgress={copyInProgress}
conflictResolutionInProgress={conflictResolutionInProgress}
copyResult={copyResult}
spaces={spaces}
copyOptions={copyOptions}
retries={retries}
onRetriesChange={onRetriesChange}
/>
);
};
return (
<EuiFlyout onClose={onClose} maxWidth={600} data-test-subj="copy-to-space-flyout">
<EuiFlyoutHeader hasBorder>
<EuiFlexGroup alignItems="center" gutterSize="m">
<EuiFlexItem grow={false}>
<EuiIcon size="m" type="copy" />
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="m">
<h2>
<FormattedMessage
id="xpack.spaces.management.copyToSpaceFlyoutHeader"
defaultMessage="Copy to space"
/>
</h2>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiFlexGroup alignItems="center" gutterSize="m">
<EuiFlexItem grow={false}>
<EuiIcon type={savedObjectTarget.icon} />
</EuiFlexItem>
<EuiFlexItem>
<EuiText>
<p>{savedObjectTarget.title}</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin="m" />
{getFlyoutBody()}
</EuiFlyoutBody>
<EuiFlyoutFooter>
<CopyToSpaceFlyoutFooter
copyInProgress={copyInProgress}
conflictResolutionInProgress={conflictResolutionInProgress}
initialCopyFinished={initialCopyFinished}
copyResult={copyResult}
numberOfSelectedSpaces={copyOptions.selectedSpaceIds.length}
retries={retries}
onClose={onClose}
onCopyStart={startCopy}
onCopyFinish={finishCopy}
/>
</EuiFlyoutFooter>
</EuiFlyout>
);
};

View file

@ -5,13 +5,16 @@
* 2.0.
*/
import { EuiFormRow, EuiSpacer, EuiTitle } from '@elastic/eui';
import React from 'react';
import { EuiSpacer, EuiTitle, EuiFormRow } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { CopyOptions, SavedObjectTarget } from '../types';
import { Space } from '../../../../../../src/plugins/spaces_oss/common';
import type { Space } from 'src/plugins/spaces_oss/common';
import type { CopyOptions, SavedObjectTarget } from '../types';
import type { CopyMode } from './copy_mode_control';
import { CopyModeControl } from './copy_mode_control';
import { SelectableSpacesControl } from './selectable_spaces_control';
import { CopyModeControl, CopyMode } from './copy_mode_control';
interface Props {
savedObjectTarget: Required<SavedObjectTarget>;

View file

@ -5,4 +5,5 @@
* 2.0.
*/
export { CopySavedObjectsToSpaceFlyout } from './copy_to_space_flyout';
export { getCopyToSpaceFlyoutComponent } from './copy_to_space_flyout';
export type { CopyToSpaceFlyoutProps } from './copy_to_space_flyout_internal';

View file

@ -5,20 +5,22 @@
* 2.0.
*/
import React, { Fragment } from 'react';
import {
EuiSpacer,
EuiText,
EuiHorizontalRule,
EuiListGroup,
EuiListGroupItem,
EuiHorizontalRule,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import React, { Fragment } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { ProcessedImportResponse } from 'src/plugins/saved_objects_management/public';
import { Space } from '../../../../../../src/plugins/spaces_oss/common';
import { CopyOptions, ImportRetry, SavedObjectTarget } from '../types';
import type { ProcessedImportResponse } from 'src/plugins/saved_objects_management/public';
import type { Space } from 'src/plugins/spaces_oss/common';
import { summarizeCopyResult } from '../lib';
import type { CopyOptions, ImportRetry, SavedObjectTarget } from '../types';
import { SpaceResult, SpaceResultProcessing } from './space_result';
import { summarizeCopyResult } from '..';
interface Props {
savedObjectTarget: Required<SavedObjectTarget>;

View file

@ -5,14 +5,17 @@
* 2.0.
*/
import React from 'react';
import { ReactWrapper } from 'enzyme';
import { act } from '@testing-library/react';
import { shallowWithIntl, mountWithIntl, nextTick } from '@kbn/test/jest';
import { findTestSubject } from '@kbn/test/jest';
import { ResolveAllConflicts, ResolveAllConflictsProps } from './resolve_all_conflicts';
import { SummarizedCopyToSpaceResult } from '..';
import { ImportRetry } from '../types';
import type { ReactWrapper } from 'enzyme';
import React from 'react';
import { findTestSubject, mountWithIntl, nextTick, shallowWithIntl } from '@kbn/test/jest';
import type { SummarizedCopyToSpaceResult } from '../lib';
import type { ImportRetry } from '../types';
import type { ResolveAllConflictsProps } from './resolve_all_conflicts';
import { ResolveAllConflicts } from './resolve_all_conflicts';
describe('ResolveAllConflicts', () => {
const summarizedCopyResult = ({
objects: [

View file

@ -8,11 +8,13 @@
import './resolve_all_conflicts.scss';
import { EuiContextMenuItem, EuiContextMenuPanel, EuiLink, EuiPopover } from '@elastic/eui';
import React, { Component } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { Component } from 'react';
import { ImportRetry } from '../types';
import { SummarizedCopyToSpaceResult } from '..';
import type { SummarizedCopyToSpaceResult } from '../lib';
import type { ImportRetry } from '../types';
export interface ResolveAllConflictsProps {
summarizedCopyResult: SummarizedCopyToSpaceResult;

View file

@ -6,11 +6,20 @@
*/
import './selectable_spaces_control.scss';
import React, { Fragment } from 'react';
import type { EuiSelectableOption } from '@elastic/eui';
import { EuiIconTip, EuiLoadingSpinner, EuiSelectable } from '@elastic/eui';
import React, { lazy, Suspense } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiSelectable, EuiSelectableOption, EuiLoadingSpinner, EuiIconTip } from '@elastic/eui';
import { Space } from '../../../../../../src/plugins/spaces_oss/common';
import { SpaceAvatar } from '../../space_avatar';
import type { Space } from 'src/plugins/spaces_oss/common';
import { getSpaceAvatarComponent } from '../../space_avatar';
// No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana.
const LazySpaceAvatar = lazy(() =>
getSpaceAvatarComponent().then((component) => ({ default: component }))
);
interface Props {
spaces: Space[];
@ -44,7 +53,7 @@ export const SelectableSpacesControl = (props: Props) => {
const disabled = props.disabledSpaceIds.has(space.id);
return {
label: space.name,
prepend: <SpaceAvatar space={space} size={'s'} />,
prepend: <LazySpaceAvatar space={space} size={'s'} />, // wrapped in a Suspense below
append: disabled ? disabledIndicator : null,
checked: props.selectedSpaceIds.includes(space.id) ? 'on' : undefined,
disabled,
@ -64,25 +73,27 @@ export const SelectableSpacesControl = (props: Props) => {
}
return (
<EuiSelectable
options={options}
onChange={(newOptions) => updateSelectedSpaces(newOptions as SpaceOption[])}
listProps={{
bordered: true,
rowHeight: 40,
className: 'spcCopyToSpace__spacesList',
'data-test-subj': 'cts-form-space-selector',
}}
searchable={options.length > 6}
>
{(list, search) => {
return (
<Fragment>
{search}
{list}
</Fragment>
);
}}
</EuiSelectable>
<Suspense fallback={<EuiLoadingSpinner />}>
<EuiSelectable
options={options}
onChange={(newOptions) => updateSelectedSpaces(newOptions as SpaceOption[])}
listProps={{
bordered: true,
rowHeight: 40,
className: 'spcCopyToSpace__spacesList',
'data-test-subj': 'cts-form-space-selector',
}}
searchable={options.length > 6}
>
{(list, search) => {
return (
<>
{search}
{list}
</>
);
}}
</EuiSelectable>
</Suspense>
);
};

View file

@ -6,21 +6,29 @@
*/
import './space_result.scss';
import React, { useState } from 'react';
import {
EuiAccordion,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiSpacer,
EuiLoadingSpinner,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { Space } from '../../../../../../src/plugins/spaces_oss/common';
import { SummarizedCopyToSpaceResult } from '../index';
import { SpaceAvatar } from '../../space_avatar';
import React, { lazy, Suspense, useState } from 'react';
import type { Space } from 'src/plugins/spaces_oss/common';
import { getSpaceAvatarComponent } from '../../space_avatar';
import type { SummarizedCopyToSpaceResult } from '../lib';
import type { ImportRetry } from '../types';
import { CopyStatusSummaryIndicator } from './copy_status_summary_indicator';
import { SpaceCopyResultDetails } from './space_result_details';
import { ImportRetry } from '../types';
// No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana.
const LazySpaceAvatar = lazy(() =>
getSpaceAvatarComponent().then((component) => ({ default: component }))
);
interface Props {
space: Space;
@ -40,6 +48,7 @@ const getInitialDestinationMap = (objects: SummarizedCopyToSpaceResult['objects'
export const SpaceResultProcessing = (props: Pick<Props, 'space'>) => {
const { space } = props;
return (
<EuiAccordion
id={`copyToSpace-${space.id}`}
@ -48,7 +57,9 @@ export const SpaceResultProcessing = (props: Pick<Props, 'space'>) => {
buttonContent={
<EuiFlexGroup responsive={false}>
<EuiFlexItem grow={false}>
<SpaceAvatar space={space} size="s" />
<Suspense fallback={<EuiLoadingSpinner />}>
<LazySpaceAvatar space={space} size="s" />
</Suspense>
</EuiFlexItem>
<EuiFlexItem>
<EuiText>{space.name}</EuiText>
@ -86,7 +97,9 @@ export const SpaceResult = (props: Props) => {
buttonContent={
<EuiFlexGroup responsive={false}>
<EuiFlexItem grow={false}>
<SpaceAvatar space={space} size="s" />
<Suspense fallback={<EuiLoadingSpinner />}>
<LazySpaceAvatar space={space} size="s" />
</Suspense>
</EuiFlexItem>
<EuiFlexItem>
<EuiText>{space.name}</EuiText>

View file

@ -6,27 +6,30 @@
*/
import './space_result_details.scss';
import React, { Fragment } from 'react';
import type { EuiSwitchEvent } from '@elastic/eui';
import {
EuiText,
EuiFlexGroup,
EuiFlexItem,
EuiSwitch,
EuiSwitchEvent,
EuiToolTip,
EuiIcon,
EuiSuperSelect,
EuiSwitch,
EuiText,
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
SavedObjectsImportConflictError,
SavedObjectsImportAmbiguousConflictError,
} from 'kibana/public';
import { EuiSuperSelect } from '@elastic/eui';
import moment from 'moment';
import { SummarizedCopyToSpaceResult } from '../index';
import { Space } from '../../../../../../src/plugins/spaces_oss/common';
import React, { Fragment } from 'react';
import { i18n } from '@kbn/i18n';
import type {
SavedObjectsImportAmbiguousConflictError,
SavedObjectsImportConflictError,
} from 'src/core/public';
import type { Space } from 'src/plugins/spaces_oss/common';
import type { SummarizedCopyToSpaceResult } from '../lib';
import type { ImportRetry } from '../types';
import { CopyStatusIndicator } from './copy_status_indicator';
import { ImportRetry } from '../types';
interface Props {
summarizedCopyResult: SummarizedCopyToSpaceResult;

View file

@ -5,15 +5,42 @@
* 2.0.
*/
import React from 'react';
import React, { lazy } from 'react';
import useAsync from 'react-use/lib/useAsync';
import { i18n } from '@kbn/i18n';
import { NotificationsStart } from 'src/core/public';
import {
SavedObjectsManagementAction,
SavedObjectsManagementRecord,
} from '../../../../../src/plugins/saved_objects_management/public';
import { CopySavedObjectsToSpaceFlyout } from './components';
import { SpacesManager } from '../spaces_manager';
import type { StartServicesAccessor } from 'src/core/public';
import type { SavedObjectsManagementRecord } from 'src/plugins/saved_objects_management/public';
import { SavedObjectsManagementAction } from '../../../../../src/plugins/saved_objects_management/public';
import type { PluginsStart } from '../plugin';
import { SuspenseErrorBoundary } from '../suspense_error_boundary';
import type { CopyToSpaceFlyoutProps } from './components';
import { getCopyToSpaceFlyoutComponent } from './components';
const LazyCopyToSpaceFlyout = lazy(() =>
getCopyToSpaceFlyoutComponent().then((component) => ({ default: component }))
);
interface WrapperProps {
getStartServices: StartServicesAccessor<PluginsStart>;
props: CopyToSpaceFlyoutProps;
}
const Wrapper = ({ getStartServices, props }: WrapperProps) => {
const { value: startServices = [{ notifications: undefined }] } = useAsync(getStartServices);
const [{ notifications }] = startServices;
if (!notifications) {
return null;
}
return (
<SuspenseErrorBoundary notifications={notifications}>
<LazyCopyToSpaceFlyout {...props} />
</SuspenseErrorBoundary>
);
};
export class CopyToSpaceSavedObjectsManagementAction extends SavedObjectsManagementAction {
public id: string = 'copy_saved_objects_to_space';
@ -35,10 +62,7 @@ export class CopyToSpaceSavedObjectsManagementAction extends SavedObjectsManagem
},
};
constructor(
private readonly spacesManager: SpacesManager,
private readonly notifications: NotificationsStart
) {
constructor(private getStartServices: StartServicesAccessor<PluginsStart>) {
super();
}
@ -47,22 +71,18 @@ export class CopyToSpaceSavedObjectsManagementAction extends SavedObjectsManagem
throw new Error('No record available! `render()` was likely called before `start()`.');
}
const savedObjectTarget = {
type: this.record.type,
id: this.record.id,
namespaces: this.record.namespaces ?? [],
title: this.record.meta.title,
icon: this.record.meta.icon,
const props: CopyToSpaceFlyoutProps = {
onClose: this.onClose,
savedObjectTarget: {
type: this.record.type,
id: this.record.id,
namespaces: this.record.namespaces ?? [],
title: this.record.meta.title,
icon: this.record.meta.icon,
},
};
return (
<CopySavedObjectsToSpaceFlyout
onClose={this.onClose}
savedObjectTarget={savedObjectTarget}
spacesManager={this.spacesManager}
toastNotifications={this.notifications.toasts}
/>
);
return <Wrapper getStartServices={this.getStartServices} props={props} />;
};
private onClose = () => {

View file

@ -5,19 +5,19 @@
* 2.0.
*/
import { coreMock } from 'src/core/public/mocks';
import { savedObjectsManagementPluginMock } from 'src/plugins/saved_objects_management/public/mocks';
import { CopyToSpaceSavedObjectsManagementAction } from './copy_saved_objects_to_space_action';
import { spacesManagerMock } from '../spaces_manager/mocks';
import { CopySavedObjectsToSpaceService } from '.';
import { notificationServiceMock } from 'src/core/public/mocks';
import { savedObjectsManagementPluginMock } from '../../../../../src/plugins/saved_objects_management/public/mocks';
import { CopySavedObjectsToSpaceService } from './copy_saved_objects_to_space_service';
describe('CopySavedObjectsToSpaceService', () => {
describe('#setup', () => {
it('registers the CopyToSpaceSavedObjectsManagementAction', () => {
const { getStartServices } = coreMock.createSetup();
const deps = {
spacesManager: spacesManagerMock.create(),
notificationsSetup: notificationServiceMock.createSetupContract(),
savedObjectsManagementSetup: savedObjectsManagementPluginMock.createSetupContract(),
getStartServices,
};
const service = new CopySavedObjectsToSpaceService();

View file

@ -5,20 +5,20 @@
* 2.0.
*/
import { NotificationsSetup } from 'src/core/public';
import { SavedObjectsManagementPluginSetup } from 'src/plugins/saved_objects_management/public';
import type { StartServicesAccessor } from 'src/core/public';
import type { SavedObjectsManagementPluginSetup } from 'src/plugins/saved_objects_management/public';
import type { PluginsStart } from '../plugin';
import { CopyToSpaceSavedObjectsManagementAction } from './copy_saved_objects_to_space_action';
import { SpacesManager } from '../spaces_manager';
interface SetupDeps {
spacesManager: SpacesManager;
savedObjectsManagementSetup: SavedObjectsManagementPluginSetup;
notificationsSetup: NotificationsSetup;
getStartServices: StartServicesAccessor<PluginsStart>;
}
export class CopySavedObjectsToSpaceService {
public setup({ spacesManager, savedObjectsManagementSetup, notificationsSetup }: SetupDeps) {
const action = new CopyToSpaceSavedObjectsManagementAction(spacesManager, notificationsSetup);
public setup({ savedObjectsManagementSetup, getStartServices }: SetupDeps) {
const action = new CopyToSpaceSavedObjectsManagementAction(getStartServices);
savedObjectsManagementSetup.actions.register(action);
}
}

View file

@ -5,5 +5,5 @@
* 2.0.
*/
export * from './summarize_copy_result';
export { getCopyToSpaceFlyoutComponent } from './components';
export { CopySavedObjectsToSpaceService } from './copy_saved_objects_to_space_service';

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export type {
SummarizedCopyToSpaceResult,
SummarizedSavedObjectResult,
} from './summarize_copy_result';
export { summarizeCopyResult } from './summarize_copy_result';

View file

@ -5,13 +5,14 @@
* 2.0.
*/
import { summarizeCopyResult } from './summarize_copy_result';
import {
ProcessedImportResponse,
import type {
FailedImport,
ProcessedImportResponse,
SavedObjectsManagementRecord,
} from 'src/plugins/saved_objects_management/public';
import { SavedObjectTarget } from './types';
import { summarizeCopyResult } from './summarize_copy_result';
import type { SavedObjectTarget } from '../types';
// Sample data references:
//

View file

@ -5,12 +5,16 @@
* 2.0.
*/
import { ProcessedImportResponse, FailedImport } from 'src/plugins/saved_objects_management/public';
import {
SavedObjectsImportConflictError,
import type {
SavedObjectsImportAmbiguousConflictError,
} from 'kibana/public';
import { SavedObjectTarget } from './types';
SavedObjectsImportConflictError,
} from 'src/core/public';
import type {
FailedImport,
ProcessedImportResponse,
} from 'src/plugins/saved_objects_management/public';
import type { SavedObjectTarget } from '../types';
export interface SummarizedSavedObjectResult {
type: string;

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { SavedObjectsImportRetry, SavedObjectsImportResponse } from 'src/core/public';
import type { SavedObjectsImportResponse, SavedObjectsImportRetry } from 'src/core/public';
export interface CopyOptions {
includeRelated: boolean;

View file

@ -6,10 +6,10 @@
*/
import { i18n } from '@kbn/i18n';
import {
FeatureCatalogueEntry,
FeatureCatalogueCategory,
} from '../../../../src/plugins/home/public';
import type { FeatureCatalogueEntry } from 'src/plugins/home/public';
import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public';
import { getSpacesFeatureDescription } from './constants';
export const createSpacesFeatureCatalogueEntry = (): FeatureCatalogueEntry => {

View file

@ -7,14 +7,14 @@
import { SpacesPlugin } from './plugin';
export { SpaceAvatar, getSpaceColor, getSpaceImageUrl, getSpaceInitials } from './space_avatar';
export { getSpaceColor, getSpaceImageUrl, getSpaceInitials } from './space_avatar';
export { SpacesPluginSetup, SpacesPluginStart } from './plugin';
export type { GetAllSpacesPurpose, GetSpaceResult } from '../common';
// re-export types from oss definition
export type { Space } from '../../../../src/plugins/spaces_oss/common';
export type { Space } from 'src/plugins/spaces_oss/common';
export const plugin = () => {
return new SpacesPlugin();

View file

@ -5,7 +5,8 @@
* 2.0.
*/
import { docLinksServiceMock } from '../../../../../src/core/public/mocks';
import { docLinksServiceMock } from 'src/core/public/mocks';
import { DocumentationLinksService } from './documentation_links';
describe('DocumentationLinksService', () => {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { DocLinksStart } from 'src/core/public';
import type { DocLinksStart } from 'src/core/public';
export class DocumentationLinksService {
private readonly kbnPrivileges: string;

View file

@ -6,10 +6,12 @@
*/
import React from 'react';
import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest';
import { ConfirmDeleteModal } from './confirm_delete_modal';
import type { SpacesManager } from '../../../spaces_manager';
import { spacesManagerMock } from '../../../spaces_manager/mocks';
import { SpacesManager } from '../../../spaces_manager';
import { ConfirmDeleteModal } from './confirm_delete_modal';
describe('ConfirmDeleteModal', () => {
it('renders as expected', () => {

View file

@ -7,8 +7,8 @@
import './confirm_delete_modal.scss';
import type { CommonProps, EuiModalProps } from '@elastic/eui';
import {
CommonProps,
EuiButton,
EuiButtonEmpty,
EuiCallOut,
@ -19,14 +19,17 @@ import {
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiModalProps,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React, { ChangeEvent, Component } from 'react';
import { Space } from '../../../../../../../src/plugins/spaces_oss/common';
import { SpacesManager } from '../../../spaces_manager';
import type { ChangeEvent } from 'react';
import React, { Component } from 'react';
import type { InjectedIntl } from '@kbn/i18n/react';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import type { Space } from 'src/plugins/spaces_oss/common';
import type { SpacesManager } from '../../../spaces_manager';
interface Props {
space: Space;

View file

@ -6,7 +6,9 @@
*/
import React from 'react';
import { shallowWithIntl } from '@kbn/test/jest';
import { UnauthorizedPrompt } from './unauthorized_prompt';
describe('UnauthorizedPrompt', () => {

View file

@ -6,9 +6,10 @@
*/
import { EuiEmptyPrompt } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
export const UnauthorizedPrompt = () => (
<EuiEmptyPrompt
iconType="spacesApp"

View file

@ -6,7 +6,9 @@
*/
import React from 'react';
import { shallowWithIntl } from '@kbn/test/jest';
import { ConfirmAlterActiveSpaceModal } from './confirm_alter_active_space_modal';
describe('ConfirmAlterActiveSpaceModal', () => {

View file

@ -6,9 +6,11 @@
*/
import { EuiConfirmModal } from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import type { InjectedIntl } from '@kbn/i18n/react';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
interface Props {
onCancel: () => void;
onConfirm: () => void;

View file

@ -5,27 +5,37 @@
* 2.0.
*/
import type { EuiPopoverProps } from '@elastic/eui';
import {
EuiDescribedFormGroup,
EuiFieldText,
EuiFormRow,
EuiLoadingSpinner,
EuiPopover,
EuiPopoverProps,
EuiSpacer,
EuiTextArea,
EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import type { ChangeEvent } from 'react';
import React, { Component, Fragment, lazy, Suspense } from 'react';
import { i18n } from '@kbn/i18n';
import React, { ChangeEvent, Component, Fragment } from 'react';
import { Space } from '../../../../../../../src/plugins/spaces_oss/common';
import { FormattedMessage } from '@kbn/i18n/react';
import type { Space } from 'src/plugins/spaces_oss/common';
import { isReservedSpace } from '../../../../common';
import { SpaceAvatar } from '../../../space_avatar';
import { SpaceValidator, toSpaceIdentifier } from '../../lib';
import { getSpaceAvatarComponent } from '../../../space_avatar';
import type { SpaceValidator } from '../../lib';
import { toSpaceIdentifier } from '../../lib';
import { SectionPanel } from '../section_panel';
import { CustomizeSpaceAvatar } from './customize_space_avatar';
import { SpaceIdentifier } from './space_identifier';
// No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana.
const LazySpaceAvatar = lazy(() =>
getSpaceAvatarComponent().then((component) => ({ default: component }))
);
interface Props {
validator: SpaceValidator;
space: Partial<Space>;
@ -153,7 +163,9 @@ export class CustomizeSpace extends Component<Props, State> {
)}
onClick={this.togglePopover}
>
<SpaceAvatar space={this.props.space} size="l" />
<Suspense fallback={<EuiLoadingSpinner />}>
<LazySpaceAvatar space={this.props.space} size="l" />
</Suspense>
</button>
}
closePopover={this.closePopover}

View file

@ -5,10 +5,11 @@
* 2.0.
*/
// @ts-ignore
import { EuiColorPicker, EuiFieldText, EuiLink } from '@elastic/eui';
import React from 'react';
import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest';
import { CustomizeSpaceAvatar } from './customize_space_avatar';
const space = {

View file

@ -5,23 +5,26 @@
* 2.0.
*/
import React, { ChangeEvent, Component } from 'react';
import {
EuiButton,
EuiColorPicker,
EuiFieldText,
EuiFilePicker,
EuiFlexItem,
EuiFormRow,
// @ts-ignore (elastic/eui#1262) EuiFilePicker is not exported yet
EuiFilePicker,
EuiButton,
EuiSpacer,
isValidHex,
} from '@elastic/eui';
import type { ChangeEvent } from 'react';
import React, { Component } from 'react';
import { i18n } from '@kbn/i18n';
import { Space } from '../../../../../../../src/plugins/spaces_oss/common';
import { imageTypes, encode } from '../../../../common/lib/dataurl';
import { getSpaceColor, getSpaceInitials } from '../../../space_avatar';
import type { Space } from 'src/plugins/spaces_oss/common';
import { MAX_SPACE_INITIALS } from '../../../../common';
import { encode, imageTypes } from '../../../../common/lib/dataurl';
import { getSpaceColor, getSpaceInitials } from '../../../space_avatar';
interface Props {
space: Partial<Space>;
onChange: (space: Partial<Space>) => void;

View file

@ -6,7 +6,9 @@
*/
import React from 'react';
import { shallowWithIntl } from '@kbn/test/jest';
import { SpaceValidator } from '../../lib';
import { SpaceIdentifier } from './space_identifier';

View file

@ -6,10 +6,15 @@
*/
import { EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React, { ChangeEvent, Component, Fragment } from 'react';
import { Space } from '../../../../../../../src/plugins/spaces_oss/common';
import { SpaceValidator, toSpaceIdentifier } from '../../lib';
import type { ChangeEvent } from 'react';
import React, { Component, Fragment } from 'react';
import type { InjectedIntl } from '@kbn/i18n/react';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import type { Space } from 'src/plugins/spaces_oss/common';
import type { SpaceValidator } from '../../lib';
import { toSpaceIdentifier } from '../../lib';
interface Props {
space: Partial<Space>;

View file

@ -6,12 +6,14 @@
*/
import React from 'react';
import { shallowWithIntl } from '@kbn/test/jest';
import { DeleteSpacesButton } from './delete_spaces_button';
import { spacesManagerMock } from '../../spaces_manager/mocks';
import { SpacesManager } from '../../spaces_manager';
import { notificationServiceMock } from 'src/core/public/mocks';
import type { SpacesManager } from '../../spaces_manager';
import { spacesManagerMock } from '../../spaces_manager/mocks';
import { DeleteSpacesButton } from './delete_spaces_button';
const space = {
id: 'my-space',
name: 'My Space',

View file

@ -5,13 +5,16 @@
* 2.0.
*/
import { EuiButton, EuiButtonIcon, EuiButtonIconProps } from '@elastic/eui';
import type { EuiButtonIconProps } from '@elastic/eui';
import { EuiButton, EuiButtonIcon } from '@elastic/eui';
import React, { Component, Fragment } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { Component, Fragment } from 'react';
import { NotificationsStart } from 'src/core/public';
import { Space } from '../../../../../../src/plugins/spaces_oss/common';
import { SpacesManager } from '../../spaces_manager';
import type { NotificationsStart } from 'src/core/public';
import type { Space } from 'src/plugins/spaces_oss/common';
import type { SpacesManager } from '../../spaces_manager';
import { ConfirmDeleteModal } from '../components/confirm_delete_modal';
interface Props {

View file

@ -5,13 +5,14 @@
* 2.0.
*/
import type { EuiCheckboxProps } from '@elastic/eui';
import React from 'react';
import { mountWithIntl, nextTick, shallowWithIntl } from '@kbn/test/jest';
import { findTestSubject, mountWithIntl, nextTick, shallowWithIntl } from '@kbn/test/jest';
import { DEFAULT_APP_CATEGORIES } from 'src/core/public';
import type { KibanaFeatureConfig } from '../../../../../features/public';
import { EnabledFeatures } from './enabled_features';
import { KibanaFeatureConfig } from '../../../../../features/public';
import { DEFAULT_APP_CATEGORIES } from '../../../../../../../src/core/public';
import { findTestSubject } from '@kbn/test/jest';
import { EuiCheckboxProps } from '@elastic/eui';
const features: KibanaFeatureConfig[] = [
{

View file

@ -6,12 +6,15 @@
*/
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import type { ReactNode } from 'react';
import React, { Component, Fragment } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { Component, Fragment, ReactNode } from 'react';
import { ApplicationStart } from 'kibana/public';
import { Space } from '../../../../../../../src/plugins/spaces_oss/common';
import { KibanaFeatureConfig } from '../../../../../../plugins/features/public';
import type { ApplicationStart } from 'src/core/public';
import type { Space } from 'src/plugins/spaces_oss/common';
import type { KibanaFeatureConfig } from '../../../../../features/public';
import { getEnabledFeatures } from '../../lib/feature_utils';
import { SectionPanel } from '../section_panel';
import { FeatureTable } from './feature_table';

View file

@ -5,12 +5,13 @@
* 2.0.
*/
import { EuiCallOut } from '@elastic/eui';
import './feature_table.scss';
import type { EuiCheckboxProps } from '@elastic/eui';
import {
EuiAccordion,
EuiCallOut,
EuiCheckbox,
EuiCheckboxProps,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
@ -20,14 +21,16 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { AppCategory } from 'kibana/public';
import _ from 'lodash';
import React, { ChangeEvent, Component, ReactElement } from 'react';
import { Space } from '../../../../../../../src/plugins/spaces_oss/common';
import { KibanaFeatureConfig } from '../../../../../../plugins/features/public';
import type { ChangeEvent, ReactElement } from 'react';
import React, { Component } from 'react';
import { i18n } from '@kbn/i18n';
import type { AppCategory } from 'src/core/public';
import type { Space } from 'src/plugins/spaces_oss/common';
import type { KibanaFeatureConfig } from '../../../../../features/public';
import { getEnabledFeatures } from '../../lib/feature_utils';
import './feature_table.scss';
interface Props {
space: Partial<Space>;

View file

@ -8,9 +8,11 @@
import './toggle_all_features.scss';
import { EuiContextMenuItem, EuiContextMenuPanel, EuiLink, EuiPopover } from '@elastic/eui';
import React, { Component } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { Component } from 'react';
interface Props {
onChange: (visible: boolean) => void;
disabled?: boolean;

View file

@ -5,20 +5,22 @@
* 2.0.
*/
import { EuiButton, EuiCheckboxProps } from '@elastic/eui';
import { ReactWrapper } from 'enzyme';
import React from 'react';
import type { EuiCheckboxProps } from '@elastic/eui';
import { EuiButton } from '@elastic/eui';
import { waitFor } from '@testing-library/react';
import type { ReactWrapper } from 'enzyme';
import React from 'react';
import { mountWithIntl } from '@kbn/test/jest';
import { DEFAULT_APP_CATEGORIES } from 'src/core/public';
import { notificationServiceMock, scopedHistoryMock } from 'src/core/public/mocks';
import { KibanaFeature } from '../../../../features/public';
import { featuresPluginMock } from '../../../../features/public/mocks';
import type { SpacesManager } from '../../spaces_manager';
import { spacesManagerMock } from '../../spaces_manager/mocks';
import { ConfirmAlterActiveSpaceModal } from './confirm_alter_active_space_modal';
import { ManageSpacePage } from './manage_space_page';
import { spacesManagerMock } from '../../spaces_manager/mocks';
import { SpacesManager } from '../../spaces_manager';
import { notificationServiceMock, scopedHistoryMock } from 'src/core/public/mocks';
import { featuresPluginMock } from '../../../../features/public/mocks';
import { KibanaFeature } from '../../../../features/public';
import { DEFAULT_APP_CATEGORIES } from '../../../../../../src/core/public';
// To be resolved by EUI team.
// https://github.com/elastic/eui/issues/3712
@ -46,6 +48,13 @@ featuresStart.getFeatures.mockResolvedValue([
]);
describe('ManageSpacePage', () => {
beforeAll(() => {
Object.defineProperty(window, 'location', {
value: { reload: jest.fn() },
writable: true,
});
});
const getUrlForApp = (appId: string) => appId;
const history = scopedHistoryMock.create();

View file

@ -16,15 +16,22 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import React, { Component, Fragment } from 'react';
import { ApplicationStart, Capabilities, NotificationsStart, ScopedHistory } from 'src/core/public';
import { Space } from '../../../../../../src/plugins/spaces_oss/common';
import { KibanaFeature, FeaturesPluginStart } from '../../../../features/public';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import type {
ApplicationStart,
Capabilities,
NotificationsStart,
ScopedHistory,
} from 'src/core/public';
import type { Space } from 'src/plugins/spaces_oss/common';
import type { FeaturesPluginStart, KibanaFeature } from '../../../../features/public';
import { isReservedSpace } from '../../../common';
import { SpacesManager } from '../../spaces_manager';
import type { SpacesManager } from '../../spaces_manager';
import { UnauthorizedPrompt } from '../components';
import { toSpaceIdentifier } from '../lib';
import { SpaceValidator } from '../lib/validate_space';

View file

@ -7,7 +7,9 @@
import { EuiBadge } from '@elastic/eui';
import React from 'react';
import { shallowWithIntl } from '@kbn/test/jest';
import { ReservedSpaceBadge } from './reserved_space_badge';
const reservedSpace = {

View file

@ -5,11 +5,12 @@
* 2.0.
*/
import { EuiBadge, EuiToolTip } from '@elastic/eui';
import React from 'react';
import { EuiBadge, EuiToolTip } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { Space } from '../../../../../../src/plugins/spaces_oss/common';
import type { Space } from 'src/plugins/spaces_oss/common';
import { isReservedSpace } from '../../../common';
interface Props {

View file

@ -6,7 +6,9 @@
*/
import React from 'react';
import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest';
import { SectionPanel } from './section_panel';
test('it renders without blowing up', () => {

View file

@ -5,16 +5,10 @@
* 2.0.
*/
import {
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiPanel,
EuiSpacer,
EuiTitle,
IconType,
} from '@elastic/eui';
import React, { Component, Fragment, ReactNode } from 'react';
import type { IconType } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
import type { ReactNode } from 'react';
import React, { Component, Fragment } from 'react';
interface Props {
iconType?: IconType;

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import type { KibanaFeatureConfig } from '../../../../features/public';
import { getEnabledFeatures } from './feature_utils';
import { KibanaFeatureConfig } from '../../../../features/public';
const buildFeatures = () =>
[

View file

@ -5,9 +5,9 @@
* 2.0.
*/
import { KibanaFeatureConfig } from '../../../../features/common';
import type { Space } from 'src/plugins/spaces_oss/common';
import { Space } from '../..';
import type { KibanaFeatureConfig } from '../../../../features/common';
export function getEnabledFeatures(features: KibanaFeatureConfig[], space: Partial<Space>) {
return features.filter((feature) => !(space.disabledFeatures || []).includes(feature.id));

View file

@ -6,7 +6,8 @@
*/
import { i18n } from '@kbn/i18n';
import { Space } from '../../../../../../src/plugins/spaces_oss/common';
import type { Space } from 'src/plugins/spaces_oss/common';
import { isReservedSpace } from '../../../common/is_reserved_space';
import { isValidSpaceIdentifier } from './space_identifier_utils';

View file

@ -5,13 +5,14 @@
* 2.0.
*/
import { ManagementService } from '.';
import type { CoreSetup } from 'src/core/public';
import { coreMock } from 'src/core/public/mocks';
import type { ManagementSection } from 'src/plugins/management/public';
import { managementPluginMock } from 'src/plugins/management/public/mocks';
import type { PluginsStart } from '../plugin';
import { spacesManagerMock } from '../spaces_manager/mocks';
import { managementPluginMock } from '../../../../../src/plugins/management/public/mocks';
import { ManagementSection } from 'src/plugins/management/public';
import { PluginsStart } from '../plugin';
import { CoreSetup } from 'src/core/public';
import { ManagementService } from './management_service';
describe('ManagementService', () => {
describe('#setup', () => {

View file

@ -5,10 +5,11 @@
* 2.0.
*/
import { StartServicesAccessor } from 'src/core/public';
import { ManagementSetup, ManagementApp } from '../../../../../src/plugins/management/public';
import { SpacesManager } from '../spaces_manager';
import { PluginsStart } from '../plugin';
import type { StartServicesAccessor } from 'src/core/public';
import type { ManagementApp, ManagementSetup } from 'src/plugins/management/public';
import type { PluginsStart } from '../plugin';
import type { SpacesManager } from '../spaces_manager';
import { spacesManagementApp } from './spaces_management_app';
interface SetupDeps {

View file

@ -130,7 +130,7 @@ exports[`SpacesGridPage renders as expected 1`] = `
exports[`SpacesGridPage renders the list of spaces 1`] = `
Array [
<SpaceAvatar
<SpaceAvatarInternal
announceSpaceName={true}
size="s"
space={
@ -172,8 +172,8 @@ Array [
</span>
</div>
</EuiAvatar>
</SpaceAvatar>,
<SpaceAvatar
</SpaceAvatarInternal>,
<SpaceAvatarInternal
announceSpaceName={true}
size="s"
space={
@ -214,8 +214,8 @@ Array [
</span>
</div>
</EuiAvatar>
</SpaceAvatar>,
<SpaceAvatar
</SpaceAvatarInternal>,
<SpaceAvatarInternal
announceSpaceName={true}
size="s"
space={
@ -259,6 +259,6 @@ Array [
</span>
</div>
</EuiAvatar>
</SpaceAvatar>,
</SpaceAvatarInternal>,
]
`;

View file

@ -5,8 +5,6 @@
* 2.0.
*/
import React, { Component, Fragment } from 'react';
import {
EuiButton,
EuiButtonIcon,
@ -14,24 +12,38 @@ import {
EuiFlexItem,
EuiInMemoryTable,
EuiLink,
EuiLoadingSpinner,
EuiPageContent,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import React, { Component, Fragment, lazy, Suspense } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { ApplicationStart, Capabilities, NotificationsStart, ScopedHistory } from 'src/core/public';
import { Space } from '../../../../../../src/plugins/spaces_oss/common';
import { KibanaFeature, FeaturesPluginStart } from '../../../../features/public';
import type {
ApplicationStart,
Capabilities,
NotificationsStart,
ScopedHistory,
} from 'src/core/public';
import type { Space } from 'src/plugins/spaces_oss/common';
import { reactRouterNavigate } from '../../../../../../src/plugins/kibana_react/public';
import type { FeaturesPluginStart, KibanaFeature } from '../../../../features/public';
import { isReservedSpace } from '../../../common';
import { DEFAULT_SPACE_ID } from '../../../common/constants';
import { SpaceAvatar } from '../../space_avatar';
import { getSpacesFeatureDescription } from '../../constants';
import { SpacesManager } from '../../spaces_manager';
import { getSpaceAvatarComponent } from '../../space_avatar';
import type { SpacesManager } from '../../spaces_manager';
import { ConfirmDeleteModal, UnauthorizedPrompt } from '../components';
import { getEnabledFeatures } from '../lib/feature_utils';
import { reactRouterNavigate } from '../../../../../../src/plugins/kibana_react/public';
// No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana.
const LazySpaceAvatar = lazy(() =>
getSpaceAvatarComponent().then((component) => ({ default: component }))
);
interface Props {
spacesManager: SpacesManager;
@ -251,11 +263,15 @@ export class SpacesGridPage extends Component<Props, State> {
field: 'initials',
name: '',
width: '50px',
render: (value: string, record: Space) => (
<EuiLink {...reactRouterNavigate(this.props.history, this.getEditSpacePath(record))}>
<SpaceAvatar space={record} size="s" />
</EuiLink>
),
render: (value: string, record: Space) => {
return (
<Suspense fallback={<EuiLoadingSpinner />}>
<EuiLink {...reactRouterNavigate(this.props.history, this.getEditSpacePath(record))}>
<LazySpaceAvatar space={record} size="s" />
</EuiLink>
</Suspense>
);
},
},
{
field: 'name',

View file

@ -5,16 +5,18 @@
* 2.0.
*/
import { act } from '@testing-library/react';
import React from 'react';
import { mountWithIntl, shallowWithIntl, nextTick } from '@kbn/test/jest';
import { SpaceAvatar } from '../../space_avatar';
import { spacesManagerMock } from '../../spaces_manager/mocks';
import { SpacesManager } from '../../spaces_manager';
import { SpacesGridPage } from './spaces_grid_page';
import { httpServiceMock, scopedHistoryMock } from 'src/core/public/mocks';
import { notificationServiceMock } from 'src/core/public/mocks';
import { featuresPluginMock } from '../../../../features/public/mocks';
import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest';
import { httpServiceMock, notificationServiceMock, scopedHistoryMock } from 'src/core/public/mocks';
import { KibanaFeature } from '../../../../features/public';
import { featuresPluginMock } from '../../../../features/public/mocks';
import { SpaceAvatarInternal } from '../../space_avatar/space_avatar_internal';
import type { SpacesManager } from '../../spaces_manager';
import { spacesManagerMock } from '../../spaces_manager/mocks';
import { SpacesGridPage } from './spaces_grid_page';
const spaces = [
{
@ -99,12 +101,12 @@ describe('SpacesGridPage', () => {
/>
);
// allow spacesManager to load spaces
await nextTick();
// allow spacesManager to load spaces and lazy-load SpaceAvatar
await act(async () => {});
wrapper.update();
expect(wrapper.find(SpaceAvatar)).toHaveLength(spaces.length);
expect(wrapper.find(SpaceAvatar)).toMatchSnapshot();
expect(wrapper.find(SpaceAvatarInternal)).toHaveLength(spaces.length);
expect(wrapper.find(SpaceAvatarInternal)).toMatchSnapshot();
});
it('notifies when spaces fail to load', async () => {
@ -132,11 +134,11 @@ describe('SpacesGridPage', () => {
/>
);
// allow spacesManager to load spaces
await nextTick();
// allow spacesManager to load spaces and lazy-load SpaceAvatar
await act(async () => {});
wrapper.update();
expect(wrapper.find(SpaceAvatar)).toHaveLength(0);
expect(wrapper.find(SpaceAvatarInternal)).toHaveLength(0);
expect(notifications.toasts.addError).toHaveBeenCalledWith(error, {
title: 'Error loading spaces',
});
@ -166,11 +168,11 @@ describe('SpacesGridPage', () => {
/>
);
// allow spacesManager to load spaces
await nextTick();
// allow spacesManager to load spaces and lazy-load SpaceAvatar
await act(async () => {});
wrapper.update();
expect(wrapper.find(SpaceAvatar)).toHaveLength(0);
expect(wrapper.find(SpaceAvatarInternal)).toHaveLength(0);
// For end-users, the effect is that spaces won't load, even though this was a request to retrieve features.
expect(notifications.toasts.addError).toHaveBeenCalledWith(error, {
title: 'Error loading spaces',

View file

@ -18,12 +18,12 @@ jest.mock('./edit_space', () => ({
},
}));
import { spacesManagementApp } from './spaces_management_app';
import { coreMock, scopedHistoryMock } from 'src/core/public/mocks';
import { coreMock, scopedHistoryMock } from '../../../../../src/core/public/mocks';
import { spacesManagerMock } from '../spaces_manager/mocks';
import { featuresPluginMock } from '../../../features/public/mocks';
import { PluginsStart } from '../plugin';
import type { PluginsStart } from '../plugin';
import { spacesManagerMock } from '../spaces_manager/mocks';
import { spacesManagementApp } from './spaces_management_app';
async function mountApp(basePath: string, pathname: string, spaceId?: string) {
const container = document.createElement('div');

View file

@ -7,16 +7,16 @@
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { Router, Route, Switch, useParams } from 'react-router-dom';
import { Route, Router, Switch, useParams } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { StartServicesAccessor } from 'src/core/public';
import type { StartServicesAccessor } from 'src/core/public';
import type { RegisterManagementAppArgs } from 'src/plugins/management/public';
import type { Space } from 'src/plugins/spaces_oss/common';
import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public';
import { RegisterManagementAppArgs } from '../../../../../src/plugins/management/public';
import { PluginsStart } from '../plugin';
import { SpacesManager } from '../spaces_manager';
import { SpacesGridPage } from './spaces_grid';
import { ManageSpacePage } from './edit_space';
import { Space } from '..';
import type { PluginsStart } from '../plugin';
import type { SpacesManager } from '../spaces_manager';
interface CreateParams {
getStartServices: StartServicesAccessor<PluginsStart>;
@ -36,10 +36,16 @@ export const spacesManagementApp = Object.freeze({
title,
async mount({ element, setBreadcrumbs, history }) {
const [startServices, { SpacesGridPage }, { ManageSpacePage }] = await Promise.all([
getStartServices(),
import('./spaces_grid'),
import('./edit_space'),
]);
const [
{ notifications, i18n: i18nStart, application, chrome },
{ features },
] = await getStartServices();
] = startServices;
const spacesBreadcrumbs = [
{
text: title,

View file

@ -6,7 +6,9 @@
*/
import React from 'react';
import { shallowWithIntl } from '@kbn/test/jest';
import { ManageSpacesButton } from './manage_spaces_button';
describe('ManageSpacesButton', () => {

View file

@ -6,9 +6,11 @@
*/
import { EuiButton } from '@elastic/eui';
import type { CSSProperties } from 'react';
import React, { Component } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { Component, CSSProperties } from 'react';
import { Capabilities, ApplicationStart } from 'src/core/public';
import type { ApplicationStart, Capabilities } from 'src/core/public';
interface Props {
isDisabled?: boolean;

View file

@ -6,11 +6,15 @@
*/
import './spaces_description.scss';
import { EuiContextMenuPanel, EuiText } from '@elastic/eui';
import React, { FC } from 'react';
import { Capabilities, ApplicationStart } from 'src/core/public';
import { ManageSpacesButton } from './manage_spaces_button';
import type { FC } from 'react';
import React from 'react';
import type { ApplicationStart, Capabilities } from 'src/core/public';
import { getSpacesFeatureDescription } from '../../constants';
import { ManageSpacesButton } from './manage_spaces_button';
interface Props {
id: string;

View file

@ -6,20 +6,31 @@
*/
import './spaces_menu.scss';
import {
EuiContextMenuItem,
EuiContextMenuPanel,
EuiFieldSearch,
EuiText,
EuiLoadingContent,
EuiLoadingSpinner,
EuiText,
} from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React, { Component, ReactElement } from 'react';
import { Capabilities, ApplicationStart } from 'src/core/public';
import { Space } from '../../../../../../src/plugins/spaces_oss/common';
import { addSpaceIdToPath, SPACE_SEARCH_COUNT_THRESHOLD, ENTER_SPACE_PATH } from '../../../common';
import type { ReactElement } from 'react';
import React, { Component, lazy, Suspense } from 'react';
import type { InjectedIntl } from '@kbn/i18n/react';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import type { ApplicationStart, Capabilities } from 'src/core/public';
import type { Space } from 'src/plugins/spaces_oss/common';
import { addSpaceIdToPath, ENTER_SPACE_PATH, SPACE_SEARCH_COUNT_THRESHOLD } from '../../../common';
import { getSpaceAvatarComponent } from '../../space_avatar';
import { ManageSpacesButton } from './manage_spaces_button';
import { SpaceAvatar } from '../../space_avatar';
// No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana.
const LazySpaceAvatar = lazy(() =>
getSpaceAvatarComponent().then((component) => ({ default: component }))
);
interface Props {
id: string;
@ -181,7 +192,11 @@ class SpacesMenuUI extends Component<Props, State> {
};
private renderSpaceMenuItem = (space: Space): JSX.Element => {
const icon = <SpaceAvatar space={space} size={'s'} />;
const icon = (
<Suspense fallback={<EuiLoadingSpinner />}>
<LazySpaceAvatar space={space} size={'s'} />
</Suspense>
);
return (
<EuiContextMenuItem
key={space.id}

View file

@ -5,11 +5,13 @@
* 2.0.
*/
import React from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import React, { lazy, Suspense } from 'react';
import ReactDOM from 'react-dom';
import { CoreStart } from 'src/core/public';
import { SpacesManager } from '../spaces_manager';
import { NavControlPopover } from './nav_control_popover';
import type { CoreStart } from 'src/core/public';
import type { SpacesManager } from '../spaces_manager';
export function initSpacesNavControl(spacesManager: SpacesManager, core: CoreStart) {
const I18nContext = core.i18n.Context;
@ -20,15 +22,23 @@ export function initSpacesNavControl(spacesManager: SpacesManager, core: CoreSta
return () => null;
}
const LazyNavControlPopover = lazy(() =>
import('./nav_control_popover').then(({ NavControlPopover }) => ({
default: NavControlPopover,
}))
);
ReactDOM.render(
<I18nContext>
<NavControlPopover
spacesManager={spacesManager}
serverBasePath={core.http.basePath.serverBasePath}
anchorPosition="downLeft"
capabilities={core.application.capabilities}
navigateToApp={core.application.navigateToApp}
/>
<Suspense fallback={<EuiLoadingSpinner />}>
<LazyNavControlPopover
spacesManager={spacesManager}
serverBasePath={core.http.basePath.serverBasePath}
anchorPosition="downLeft"
capabilities={core.application.capabilities}
navigateToApp={core.application.navigateToApp}
/>
</Suspense>
</I18nContext>,
targetDomElement
);

View file

@ -5,16 +5,18 @@
* 2.0.
*/
import * as Rx from 'rxjs';
import { EuiHeaderSectionItemButton } from '@elastic/eui';
import { waitFor } from '@testing-library/react';
import { shallow } from 'enzyme';
import React from 'react';
import { SpaceAvatar } from '../space_avatar';
import { spacesManagerMock } from '../spaces_manager/mocks';
import { SpacesManager } from '../spaces_manager';
import { NavControlPopover } from './nav_control_popover';
import { EuiHeaderSectionItemButton } from '@elastic/eui';
import * as Rx from 'rxjs';
import { mountWithIntl } from '@kbn/test/jest';
import { waitFor } from '@testing-library/react';
import { SpaceAvatarInternal } from '../space_avatar/space_avatar_internal';
import type { SpacesManager } from '../spaces_manager';
import { spacesManagerMock } from '../spaces_manager/mocks';
import { NavControlPopover } from './nav_control_popover';
describe('NavControlPopover', () => {
it('renders without crashing', () => {
@ -68,7 +70,7 @@ describe('NavControlPopover', () => {
// Wait for `getSpaces` promise to resolve
await waitFor(() => {
wrapper.update();
expect(wrapper.find(SpaceAvatar)).toHaveLength(3);
expect(wrapper.find(SpaceAvatarInternal)).toHaveLength(3);
});
});
});

Some files were not shown because too many files have changed in this diff Show more