mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Cleanup spaces plugin (#91976)
This commit is contained in:
parent
ccf1fcc00e
commit
8710a81bea
236 changed files with 2451 additions and 1671 deletions
|
@ -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]
|
||||
);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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"`);
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,4 +6,4 @@
|
|||
*/
|
||||
|
||||
export { SecurityNavControlService, SecurityNavControlServiceStart } from './nav_control_service';
|
||||
export { UserMenuLink } from './nav_control_component';
|
||||
export type { UserMenuLink } from './nav_control_component';
|
||||
|
|
|
@ -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
|
||||
|
|
5
x-pack/plugins/spaces/.eslintrc.json
Normal file
5
x-pack/plugins/spaces/.eslintrc.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"rules": {
|
||||
"@typescript-eslint/consistent-type-imports": 1
|
||||
}
|
||||
}
|
|
@ -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 = {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SpacesLicense } from '.';
|
||||
import type { SpacesLicense } from './license_service';
|
||||
|
||||
export const licenseMock = {
|
||||
create: (): jest.Mocked<SpacesLicense> => ({
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,5 +5,5 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './summarize_copy_result';
|
||||
export { getCopyToSpaceFlyoutComponent } from './components';
|
||||
export { CopySavedObjectsToSpaceService } from './copy_saved_objects_to_space_service';
|
||||
|
|
|
@ -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';
|
|
@ -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:
|
||||
//
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallowWithIntl } from '@kbn/test/jest';
|
||||
|
||||
import { UnauthorizedPrompt } from './unauthorized_prompt';
|
||||
|
||||
describe('UnauthorizedPrompt', () => {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallowWithIntl } from '@kbn/test/jest';
|
||||
|
||||
import { ConfirmAlterActiveSpaceModal } from './confirm_alter_active_space_modal';
|
||||
|
||||
describe('ConfirmAlterActiveSpaceModal', () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallowWithIntl } from '@kbn/test/jest';
|
||||
|
||||
import { SpaceValidator } from '../../lib';
|
||||
import { SpaceIdentifier } from './space_identifier';
|
||||
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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[] = [
|
||||
{
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { KibanaFeatureConfig } from '../../../../features/public';
|
||||
import { getEnabledFeatures } from './feature_utils';
|
||||
import { KibanaFeatureConfig } from '../../../../features/public';
|
||||
|
||||
const buildFeatures = () =>
|
||||
[
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>,
|
||||
]
|
||||
`;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallowWithIntl } from '@kbn/test/jest';
|
||||
|
||||
import { ManageSpacesButton } from './manage_spaces_button';
|
||||
|
||||
describe('ManageSpacesButton', () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue