[Workplace Search] Convert Groups pages to new page template (#102449)

* Convert Groups page to new page template

* Convert Groups > Group overview to new page template

- Because dataLoading is no longer an early return, certain items need to be converted to conditional checks in order for the app to not crash

* Convert Groups > source prioritization to new page template

* Convert Group subnav to EuiSideNav format

* Update routers
This commit is contained in:
Constance 2021-06-17 13:01:24 -07:00 committed by GitHub
parent 8270959760
commit 02c1c61828
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 127 additions and 168 deletions

View file

@ -9,6 +9,9 @@ jest.mock('../../../shared/layout', () => ({
...jest.requireActual('../../../shared/layout'),
generateNavLink: jest.fn(({ to }) => ({ href: to })),
}));
jest.mock('../../views/groups/components/group_sub_nav', () => ({
useGroupSubNav: () => [],
}));
jest.mock('../../views/settings/components/settings_sub_nav', () => ({
useSettingsSubNav: () => [],
}));

View file

@ -19,6 +19,7 @@ import {
GROUPS_PATH,
ORG_SETTINGS_PATH,
} from '../../routes';
import { useGroupSubNav } from '../../views/groups/components/group_sub_nav';
import { useSettingsSubNav } from '../../views/settings/components/settings_sub_nav';
export const useWorkplaceSearchNav = () => {
@ -38,7 +39,7 @@ export const useWorkplaceSearchNav = () => {
id: 'groups',
name: NAV.GROUPS,
...generateNavLink({ to: GROUPS_PATH }),
items: [], // TODO: Group subnav
items: useGroupSubNav(),
},
{
id: 'usersRoles',

View file

@ -41,7 +41,6 @@ import { SourceAdded } from './views/content_sources/components/source_added';
import { SourceSubNav } from './views/content_sources/components/source_sub_nav';
import { ErrorState } from './views/error_state';
import { GroupsRouter } from './views/groups';
import { GroupSubNav } from './views/groups/components/group_sub_nav';
import { Overview } from './views/overview';
import { RoleMappings } from './views/role_mappings';
import { Security } from './views/security';
@ -63,7 +62,6 @@ export const WorkplaceSearchConfigured: React.FC<InitialAppData> = (props) => {
// We don't want so show the subnavs on the container root pages.
const showSourcesSubnav = pathname !== SOURCES_PATH && pathname !== PERSONAL_SOURCES_PATH;
const showGroupsSubnav = pathname !== GROUPS_PATH;
/**
* Personal dashboard urls begin with /p/
@ -125,13 +123,7 @@ export const WorkplaceSearchConfigured: React.FC<InitialAppData> = (props) => {
</Layout>
</Route>
<Route path={GROUPS_PATH}>
<Layout
navigation={<WorkplaceSearchNav groupsSubNav={showGroupsSubnav && <GroupSubNav />} />}
restrictWidth
readOnlyMode={readOnlyMode}
>
<GroupsRouter />
</Layout>
<GroupsRouter />
</Route>
<Route path={ROLE_MAPPINGS_PATH}>
<RoleMappings />

View file

@ -14,10 +14,8 @@ import { shallow } from 'enzyme';
import { EuiFieldText, EuiEmptyPrompt } from '@elastic/eui';
import { Loading } from '../../../../shared/loading';
import { ContentSection } from '../../../components/shared/content_section';
import { SourcesTable } from '../../../components/shared/sources_table';
import { ViewContentHeader } from '../../../components/shared/view_content_header';
import { GroupOverview } from './group_overview';
@ -49,19 +47,21 @@ describe('GroupOverview', () => {
});
setMockValues(mockValues);
});
it('renders', () => {
const wrapper = shallow(<GroupOverview />);
expect(wrapper.find(ContentSection)).toHaveLength(4);
expect(wrapper.find(ViewContentHeader)).toHaveLength(1);
expect(wrapper.find(SourcesTable)).toHaveLength(1);
});
it('returns loading when loading', () => {
setMockValues({ ...mockValues, dataLoading: true });
it('handles loading state fallbacks', () => {
setMockValues({ ...mockValues, group: {}, dataLoading: true });
const wrapper = shallow(<GroupOverview />);
expect(wrapper.find(Loading)).toHaveLength(1);
expect(wrapper.prop('isLoading')).toEqual(true);
expect(wrapper.prop('pageChrome')).toEqual(['Groups', '...']);
expect(wrapper.prop('pageHeader').pageTitle).toEqual(undefined);
});
it('updates the input value', () => {

View file

@ -23,14 +23,13 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Loading } from '../../../../shared/loading';
import { TruncatedContent } from '../../../../shared/truncate';
import { AppLogic } from '../../../app_logic';
import noSharedSourcesIcon from '../../../assets/share_circle.svg';
import { WorkplaceSearchPageTemplate } from '../../../components/layout';
import { ContentSection } from '../../../components/shared/content_section';
import { SourcesTable } from '../../../components/shared/sources_table';
import { ViewContentHeader } from '../../../components/shared/view_content_header';
import { CANCEL_BUTTON } from '../../../constants';
import { NAV, CANCEL_BUTTON } from '../../../constants';
import { GroupLogic, MAX_NAME_LENGTH } from '../group_logic';
import { GroupUsersTable } from './group_users_table';
@ -127,9 +126,7 @@ export const GroupOverview: React.FC = () => {
const { isFederatedAuth } = useValues(AppLogic);
if (dataLoading) return <Loading />;
const truncatedName = (
const truncatedName = name && (
<TruncatedContent tooltipType="title" content={name} length={MAX_NAME_LENGTH} />
);
@ -162,8 +159,8 @@ export const GroupOverview: React.FC = () => {
}
);
const hasContentSources = contentSources.length > 0;
const hasUsers = users.length > 0;
const hasContentSources = contentSources?.length > 0;
const hasUsers = users?.length > 0;
const manageSourcesButton = (
<EuiButton color="primary" onClick={showSharedSourcesModal}>
@ -272,13 +269,16 @@ export const GroupOverview: React.FC = () => {
);
return (
<>
<ViewContentHeader title={truncatedName} />
<EuiSpacer />
<WorkplaceSearchPageTemplate
pageChrome={[NAV.GROUPS, name || '...']}
pageViewTelemetry="group_overview"
pageHeader={{ pageTitle: truncatedName }}
isLoading={dataLoading}
>
{hasContentSources ? sourcesSection : sourcesEmptyState}
{usersSection}
{nameSection}
{canDeleteGroup && deleteSection}
</>
</WorkplaceSearchPageTemplate>
);
};

View file

@ -14,8 +14,6 @@ import { shallow } from 'enzyme';
import { EuiTable, EuiEmptyPrompt, EuiRange } from '@elastic/eui';
import { Loading } from '../../../../shared/loading';
import { GroupSourcePrioritization } from './group_source_prioritization';
const updatePriority = jest.fn();
@ -43,17 +41,19 @@ describe('GroupSourcePrioritization', () => {
setMockValues(mockValues);
});
it('renders', () => {
const wrapper = shallow(<GroupSourcePrioritization />);
expect(wrapper.find(EuiTable)).toHaveLength(1);
});
it('returns loading when loading', () => {
setMockValues({ ...mockValues, dataLoading: true });
it('handles loading state fallbacks', () => {
setMockValues({ ...mockValues, group: {}, dataLoading: true });
const wrapper = shallow(<GroupSourcePrioritization />);
expect(wrapper.find(Loading)).toHaveLength(1);
expect(wrapper.prop('isLoading')).toEqual(true);
expect(wrapper.prop('pageChrome')).toEqual(['Groups', '...', 'Source Prioritization']);
});
it('renders empty state', () => {

View file

@ -27,9 +27,9 @@ import {
import { i18n } from '@kbn/i18n';
import { SAVE_BUTTON_LABEL } from '../../../../shared/constants';
import { Loading } from '../../../../shared/loading';
import { WorkplaceSearchPageTemplate } from '../../../components/layout';
import { SourceIcon } from '../../../components/shared/source_icon';
import { ViewContentHeader } from '../../../components/shared/view_content_header';
import { NAV } from '../../../constants';
import { ContentSource } from '../../../types';
import { GroupLogic } from '../group_logic';
@ -76,14 +76,12 @@ export const GroupSourcePrioritization: React.FC = () => {
);
const {
group: { contentSources, name: groupName },
group: { contentSources = [], name: groupName },
dataLoading,
activeSourcePriorities,
groupPrioritiesUnchanged,
} = useValues(GroupLogic);
if (dataLoading) return <Loading />;
const headerAction = (
<EuiButton
disabled={groupPrioritiesUnchanged}
@ -167,13 +165,17 @@ export const GroupSourcePrioritization: React.FC = () => {
);
return (
<>
<ViewContentHeader
title={HEADER_TITLE}
description={HEADER_DESCRIPTION}
action={headerAction}
/>
<WorkplaceSearchPageTemplate
pageChrome={[NAV.GROUPS, groupName || '...', NAV.SOURCE_PRIORITIZATION]}
pageViewTelemetry="group_overview"
pageHeader={{
pageTitle: HEADER_TITLE,
description: HEADER_DESCRIPTION,
rightSideItems: [headerAction],
}}
isLoading={dataLoading}
>
{hasSources ? sourceTable : zeroState}
</>
</WorkplaceSearchPageTemplate>
);
};

View file

@ -7,26 +7,33 @@
import { setMockValues } from '../../../../__mocks__/kea_logic';
import React from 'react';
jest.mock('../../../../shared/layout', () => ({
generateNavLink: jest.fn(({ to }) => ({ href: to })),
}));
import { shallow } from 'enzyme';
import { SideNavLink } from '../../../../shared/layout';
import { GroupSubNav } from './group_sub_nav';
describe('GroupSubNav', () => {
it('renders empty when no group id present', () => {
setMockValues({ group: {} });
const wrapper = shallow(<GroupSubNav />);
expect(wrapper.find(SideNavLink)).toHaveLength(0);
});
import { useGroupSubNav } from './group_sub_nav';
describe('useGroupSubNav', () => {
it('renders nav items', () => {
setMockValues({ group: { id: '1' } });
const wrapper = shallow(<GroupSubNav />);
expect(wrapper.find(SideNavLink)).toHaveLength(2);
expect(useGroupSubNav()).toEqual([
{
id: 'groupOverview',
name: 'Overview',
href: '/groups/1',
},
{
id: 'groupSourcePrioritization',
name: 'Source Prioritization',
href: '/groups/1/source_prioritization',
},
]);
});
it('returns undefined when no group id is present', () => {
setMockValues({ group: {} });
expect(useGroupSubNav()).toEqual(undefined);
});
});

View file

@ -5,28 +5,34 @@
* 2.0.
*/
import React from 'react';
import { useValues } from 'kea';
import { SideNavLink } from '../../../../shared/layout';
import { EuiSideNavItemType } from '@elastic/eui';
import { generateNavLink } from '../../../../shared/layout';
import { NAV } from '../../../constants';
import { getGroupPath, getGroupSourcePrioritizationPath } from '../../../routes';
import { GroupLogic } from '../group_logic';
export const GroupSubNav: React.FC = () => {
export const useGroupSubNav = () => {
const {
group: { id },
} = useValues(GroupLogic);
if (!id) return null;
if (!id) return undefined;
return (
<>
<SideNavLink to={getGroupPath(id)}>{NAV.GROUP_OVERVIEW}</SideNavLink>
<SideNavLink to={getGroupSourcePrioritizationPath(id)}>
{NAV.SOURCE_PRIORITIZATION}
</SideNavLink>
</>
);
const navItems: Array<EuiSideNavItemType<unknown>> = [
{
id: 'groupOverview',
name: NAV.GROUP_OVERVIEW,
...generateNavLink({ to: getGroupPath(id) }),
},
{
id: 'groupSourcePrioritization',
name: NAV.SOURCE_PRIORITIZATION,
...generateNavLink({ to: getGroupSourcePrioritizationPath(id) }),
},
];
return navItems;
};

View file

@ -15,9 +15,6 @@ import { Route, Switch } from 'react-router-dom';
import { shallow } from 'enzyme';
import { FlashMessages } from '../../../shared/flash_messages';
import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
import { GroupOverview } from './components/group_overview';
import { GroupSourcePrioritization } from './components/group_source_prioritization';
import { ManageUsersModal } from './components/manage_users_modal';
@ -40,10 +37,10 @@ describe('GroupRouter', () => {
resetGroup,
});
});
it('renders', () => {
const wrapper = shallow(<GroupRouter />);
expect(wrapper.find(FlashMessages)).toHaveLength(1);
expect(wrapper.find(Switch)).toHaveLength(1);
expect(wrapper.find(Route)).toHaveLength(2);
expect(wrapper.find(GroupOverview)).toHaveLength(1);
@ -62,22 +59,4 @@ describe('GroupRouter', () => {
expect(wrapper.find(ManageUsersModal)).toHaveLength(1);
expect(wrapper.find(SharedSourcesModal)).toHaveLength(1);
});
it('handles breadcrumbs while loading', () => {
setMockValues({
sharedSourcesModalVisible: false,
manageUsersModalVisible: false,
group: {},
});
const loadingBreadcrumbs = ['Groups', '...'];
const wrapper = shallow(<GroupRouter />);
const firstBreadCrumb = wrapper.find(SetPageChrome).first();
const lastBreadCrumb = wrapper.find(SetPageChrome).last();
expect(firstBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, 'Source Prioritization']);
expect(lastBreadCrumb.prop('trail')).toEqual(loadingBreadcrumbs);
});
});

View file

@ -10,10 +10,6 @@ import { Route, Switch, useParams } from 'react-router-dom';
import { useActions, useValues } from 'kea';
import { FlashMessages } from '../../../shared/flash_messages';
import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry';
import { NAV } from '../../constants';
import { GROUP_SOURCE_PRIORITIZATION_PATH, GROUP_PATH } from '../../routes';
import { GroupOverview } from './components/group_overview';
@ -26,11 +22,7 @@ export const GroupRouter: React.FC = () => {
const { groupId } = useParams() as { groupId: string };
const { initializeGroup, resetGroup } = useActions(GroupLogic);
const {
sharedSourcesModalVisible,
manageUsersModalVisible,
group: { name },
} = useValues(GroupLogic);
const { sharedSourcesModalVisible, manageUsersModalVisible } = useValues(GroupLogic);
useEffect(() => {
initializeGroup(groupId);
@ -39,15 +31,11 @@ export const GroupRouter: React.FC = () => {
return (
<>
<FlashMessages />
<Switch>
<Route path={GROUP_SOURCE_PRIORITIZATION_PATH}>
<SetPageChrome trail={[NAV.GROUPS, name || '...', NAV.SOURCE_PRIORITIZATION]} />
<GroupSourcePrioritization />
</Route>
<Route path={GROUP_PATH}>
<SetPageChrome trail={[NAV.GROUPS, name || '...']} />
<SendTelemetry action="viewed" metric="group_overview" />
<GroupOverview />
</Route>
</Switch>

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import '../../../__mocks__/react_router';
import '../../../__mocks__/shallow_useeffect.mock';
import { setMockActions, setMockValues } from '../../../__mocks__/kea_logic';
import { groups } from '../../__mocks__/groups.mock';
@ -18,9 +19,8 @@ import { EuiFieldSearch, EuiLoadingSpinner } from '@elastic/eui';
import { DEFAULT_META } from '../../../shared/constants';
import { FlashMessages } from '../../../shared/flash_messages';
import { Loading } from '../../../shared/loading';
import { EuiButtonTo } from '../../../shared/react_router_helpers';
import { ViewContentHeader } from '../../components/shared/view_content_header';
import { getPageHeaderActions } from '../../../test_helpers';
import { AddGroupModal } from './components/add_group_modal';
import { ClearFiltersLink } from './components/clear_filters_link';
@ -68,18 +68,10 @@ describe('GroupOverview', () => {
it('renders', () => {
const wrapper = shallow(<Groups />);
expect(wrapper.find(ViewContentHeader)).toHaveLength(1);
expect(wrapper.find(GroupsTable)).toHaveLength(1);
expect(wrapper.find(TableFilters)).toHaveLength(1);
});
it('returns loading when loading', () => {
setMockValues({ ...mockValues, groupsDataLoading: true });
const wrapper = shallow(<Groups />);
expect(wrapper.find(Loading)).toHaveLength(1);
});
it('gets search results when filters changed', () => {
const wrapper = shallow(<Groups />);
@ -96,8 +88,13 @@ describe('GroupOverview', () => {
...mockValues,
newGroup: { name: 'group', id: '123' },
messages: [mockSuccessMessage],
// Needed for diving into page template
contentSource: {},
group: {},
});
const wrapper = shallow(<Groups />);
const wrapper = shallow(<Groups />)
.dive()
.dive();
const flashMessages = wrapper.find(FlashMessages).dive().childAt(0).dive();
expect(flashMessages.find('[data-test-subj="NewGroupManageButton"]')).toHaveLength(1);
@ -122,13 +119,10 @@ describe('GroupOverview', () => {
});
const wrapper = shallow(<Groups />);
const actions = getPageHeaderActions(wrapper);
const Action: React.FC = () =>
wrapper.find(ViewContentHeader).props().action as React.ReactElement<any, any> | null;
const action = shallow(<Action />);
expect(action.find('[data-test-subj="InviteUsersButton"]')).toHaveLength(1);
expect(action.find(EuiButtonTo)).toHaveLength(1);
expect(actions.find('[data-test-subj="InviteUsersButton"]')).toHaveLength(1);
expect(actions.find(EuiButtonTo)).toHaveLength(1);
});
it('does not render inviteUsersButton when federated auth', () => {
@ -138,12 +132,9 @@ describe('GroupOverview', () => {
});
const wrapper = shallow(<Groups />);
const actions = getPageHeaderActions(wrapper);
const Action: React.FC = () =>
wrapper.find(ViewContentHeader).props().action as React.ReactElement<any, any> | null;
const action = shallow(<Action />);
expect(action.find('[data-test-subj="InviteUsersButton"]')).toHaveLength(0);
expect(actions.find('[data-test-subj="InviteUsersButton"]')).toHaveLength(0);
});
it('renders EuiLoadingSpinner when loading', () => {

View file

@ -12,11 +12,11 @@ import { useActions, useValues } from 'kea';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FlashMessages, FlashMessagesLogic } from '../../../shared/flash_messages';
import { Loading } from '../../../shared/loading';
import { FlashMessagesLogic } from '../../../shared/flash_messages';
import { EuiButtonTo } from '../../../shared/react_router_helpers';
import { AppLogic } from '../../app_logic';
import { ViewContentHeader } from '../../components/shared/view_content_header';
import { WorkplaceSearchPageTemplate } from '../../components/layout';
import { NAV } from '../../constants';
import { getGroupPath, USERS_PATH } from '../../routes';
import { AddGroupModal } from './components/add_group_modal';
@ -52,10 +52,6 @@ export const Groups: React.FC = () => {
return resetGroups;
}, [filteredSources, filteredUsers, filterValue]);
if (groupsDataLoading) {
return <Loading />;
}
if (newGroup && hasMessages) {
messages[0].description = (
<EuiButtonTo
@ -71,26 +67,23 @@ export const Groups: React.FC = () => {
}
const clearFilters = hasFiltersSet && <ClearFiltersLink />;
const inviteUsersButton = !isFederatedAuth ? (
const inviteUsersButton = (
<EuiButtonTo to={USERS_PATH} data-test-subj="InviteUsersButton">
{i18n.translate('xpack.enterpriseSearch.workplaceSearch.groups.inviteUsers.action', {
defaultMessage: 'Invite users',
})}
</EuiButtonTo>
) : null;
const headerAction = (
<EuiFlexGroup responsive={false} gutterSize="m">
<EuiFlexItem grow={false}>
<EuiButton data-test-subj="AddGroupButton" fill onClick={openNewGroupModal}>
{i18n.translate('xpack.enterpriseSearch.workplaceSearch.groups.addGroupForm.action', {
defaultMessage: 'Create a group',
})}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>{inviteUsersButton}</EuiFlexItem>
</EuiFlexGroup>
);
const createGroupButton = (
<EuiButton data-test-subj="AddGroupButton" fill onClick={openNewGroupModal}>
{i18n.translate('xpack.enterpriseSearch.workplaceSearch.groups.addGroupForm.action', {
defaultMessage: 'Create a group',
})}
</EuiButton>
);
const headerActions = !isFederatedAuth
? [inviteUsersButton, createGroupButton]
: [createGroupButton];
const noResults = (
<EuiFlexGroup justifyContent="spaceAround">
@ -115,23 +108,25 @@ export const Groups: React.FC = () => {
);
return (
<>
<FlashMessages />
<ViewContentHeader
title={i18n.translate('xpack.enterpriseSearch.workplaceSearch.groups.heading', {
<WorkplaceSearchPageTemplate
pageChrome={[NAV.GROUPS]}
pageViewTelemetry="groups"
pageHeader={{
pageTitle: i18n.translate('xpack.enterpriseSearch.workplaceSearch.groups.heading', {
defaultMessage: 'Manage groups',
})}
description={i18n.translate('xpack.enterpriseSearch.workplaceSearch.groups.description', {
}),
description: i18n.translate('xpack.enterpriseSearch.workplaceSearch.groups.description', {
defaultMessage:
'Assign shared content sources and users to groups to create relevant search experiences for various internal teams.',
})}
action={headerAction}
/>
<EuiSpacer size="m" />
}),
rightSideItems: headerActions,
}}
isLoading={groupsDataLoading}
>
<TableFilters />
<EuiSpacer />
{numGroups > 0 && !groupListLoading ? <GroupsTable /> : noResults}
{newGroupModalOpen && <AddGroupModal />}
</>
</WorkplaceSearchPageTemplate>
);
};

View file

@ -10,9 +10,6 @@ import { Route, Switch } from 'react-router-dom';
import { useActions } from 'kea';
import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry';
import { NAV } from '../../constants';
import { GROUP_PATH, GROUPS_PATH } from '../../routes';
import { GroupRouter } from './group_router';
@ -31,8 +28,6 @@ export const GroupsRouter: React.FC = () => {
return (
<Switch>
<Route exact path={GROUPS_PATH}>
<SetPageChrome trail={[NAV.GROUPS]} />
<SendTelemetry action="viewed" metric="groups" />
<Groups />
</Route>
<Route path={GROUP_PATH}>