Skip to content

Commit

Permalink
Update user management page (#87133)
Browse files Browse the repository at this point in the history
* Update user management page

* Fixed i18n errors

* Fix linting errors

* Add ids required for accessability

* Added suggestions from code review

* Fix test errors

* Fix types in fleet

* fix translations

* Fix i18n

* Added suggestions from code review

* Fix i18n errors

* Fix linting errors

* Update messaging

* Updated unit tests

* Updated functional tests

* Fixed functional tests

* Fix linting errors

* Fix React warnings

* Added suggestions from code review

* Added tests and renamed routes

* Fix functional tests

* Simplified API integration tests

* Updated copy based on writing suggestions

* Fixed unit tests

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
thomheymann and kibanamachine authored Jan 25, 2021
1 parent a5bb864 commit aeb6df3
Show file tree
Hide file tree
Showing 57 changed files with 3,242 additions and 1,680 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@
"react-resizable": "^1.7.5",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-use": "^13.27.0",
"react-use": "^15.3.4",
"recompose": "^0.26.0",
"redux": "^4.0.5",
"redux-actions": "^2.6.5",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export const AgentLogsUI: React.FunctionComponent<AgentLogsProps> = memo(({ agen
[http.basePath, state.start, state.end, logStreamQuery]
);

const [logsPanelRef, { height: logPanelHeight }] = useMeasure();
const [logsPanelRef, { height: logPanelHeight }] = useMeasure<HTMLDivElement>();

const agentVersion = agent.local_metadata?.elastic?.agent?.version;
const isLogFeatureAvailable = useMemo(() => {
Expand Down
13 changes: 13 additions & 0 deletions x-pack/plugins/security/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,16 @@ export const AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER = 'auth_provider_hint';
export const LOGOUT_PROVIDER_QUERY_STRING_PARAMETER = 'provider';
export const LOGOUT_REASON_QUERY_STRING_PARAMETER = 'msg';
export const NEXT_URL_QUERY_STRING_PARAMETER = 'next';

/**
* Matches valid usernames and role names.
*
* - Must contain only letters, numbers, spaces, punctuation and printable symbols.
* - Must not contain leading or trailing spaces.
*/
export const NAME_REGEX = /^(?! )[a-zA-Z0-9 !"#$%&'()*+,\-./\\:;<=>?@\[\]^_`{|}~]+(?<! )$/;

/**
* Maximum length of usernames and role names.
*/
export const MAX_NAME_LENGTH = 1024;
2 changes: 2 additions & 0 deletions x-pack/plugins/security/common/model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export {
isRoleDeprecated,
isRoleReadOnly,
isRoleReserved,
isRoleSystem,
isRoleAdmin,
isRoleEnabled,
prepareRoleClone,
getExtendedRoleDeprecationNotice,
Expand Down
22 changes: 21 additions & 1 deletion x-pack/plugins/security/common/model/role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,27 @@ export function isRoleReserved(role: Partial<Role>) {
* @param {role} the Role as returned by roles API
*/
export function isRoleDeprecated(role: Partial<Role>) {
return role.metadata?._deprecated ?? false;
return (role.metadata?._deprecated as boolean) ?? false;
}

/**
* Returns whether given role is a system role or not.
*
* @param {role} the Role as returned by roles API
*/
export function isRoleSystem(role: Partial<Role>) {
return (isRoleReserved(role) && role.name?.endsWith('_system')) ?? false;
}

/**
* Returns whether given role is an admin role or not.
*
* @param {role} the Role as returned by roles API
*/
export function isRoleAdmin(role: Partial<Role>) {
return (
(isRoleReserved(role) && (role.name?.endsWith('_admin') || role.name === 'superuser')) ?? false
);
}

/**
Expand Down
137 changes: 137 additions & 0 deletions x-pack/plugins/security/public/components/breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { createContext, useEffect, useRef, useContext, FunctionComponent } from 'react';
import { EuiBreadcrumb } from '@elastic/eui';
import { useKibana } from '../../../../../src/plugins/kibana_react/public';

interface BreadcrumbsContext {
parents: BreadcrumbProps[];
onMount(breadcrumbs: BreadcrumbProps[]): void;
onUnmount(breadcrumbs: BreadcrumbProps[]): void;
}

const BreadcrumbsContext = createContext<BreadcrumbsContext | undefined>(undefined);

export interface BreadcrumbProps extends EuiBreadcrumb {
text: string;
}

/**
* Component that automatically sets breadcrumbs and document title based on the render tree.
*
* @example
* // Breadcrumbs will be set to: "Users > Create"
* // Document title will be set to: "Create - Users"
*
* ```typescript
* <Breadcrumb text="Users">
* <Table />
* {showForm && (
* <Breadcrumb text="Create">
* <Form />
* </Breadcrumb>
* )}
* </Breadcrumb>
* ```
*/
export const Breadcrumb: FunctionComponent<BreadcrumbProps> = ({ children, ...breadcrumb }) => {
const context = useContext(BreadcrumbsContext);
const component = <InnerBreadcrumb breadcrumb={breadcrumb}>{children}</InnerBreadcrumb>;

if (context) {
return component;
}

return <BreadcrumbsProvider>{component}</BreadcrumbsProvider>;
};

export interface BreadcrumbsProviderProps {
onChange?: BreadcrumbsChangeHandler;
}

export type BreadcrumbsChangeHandler = (breadcrumbs: BreadcrumbProps[]) => void;

/**
* Component that can be used to define any side effects that should occur when breadcrumbs change.
*
* By default the breadcrumbs in application chrome are set and the document title is updated.
*
* @example
* ```typescript
* <Breadcrumbs onChange={(breadcrumbs) => setBreadcrumbs(breadcrumbs)}>
* <Breadcrumb text="Users" />
* </Breadcrumbs>
* ```
*/
export const BreadcrumbsProvider: FunctionComponent<BreadcrumbsProviderProps> = ({
children,
onChange,
}) => {
const { services } = useKibana();
const breadcrumbsRef = useRef<BreadcrumbProps[]>([]);

const handleChange = (breadcrumbs: BreadcrumbProps[]) => {
if (onChange) {
onChange(breadcrumbs);
} else if (services.chrome) {
services.chrome.setBreadcrumbs(breadcrumbs);
services.chrome.docTitle.change(getDocTitle(breadcrumbs));
}
};

return (
<BreadcrumbsContext.Provider
value={{
parents: [],
onMount: (breadcrumbs) => {
if (breadcrumbs.length > breadcrumbsRef.current.length) {
breadcrumbsRef.current = breadcrumbs;
handleChange(breadcrumbs);
}
},
onUnmount: (breadcrumbs) => {
if (breadcrumbs.length < breadcrumbsRef.current.length) {
breadcrumbsRef.current = breadcrumbs;
handleChange(breadcrumbs);
}
},
}}
>
{children}
</BreadcrumbsContext.Provider>
);
};

export interface InnerBreadcrumbProps {
breadcrumb: BreadcrumbProps;
}

export const InnerBreadcrumb: FunctionComponent<InnerBreadcrumbProps> = ({
breadcrumb,
children,
}) => {
const { parents, onMount, onUnmount } = useContext(BreadcrumbsContext)!;
const nextParents = [...parents, breadcrumb];

useEffect(() => {
onMount(nextParents);
return () => onUnmount(parents);
}, [breadcrumb.text, breadcrumb.href]); // eslint-disable-line react-hooks/exhaustive-deps

return (
<BreadcrumbsContext.Provider value={{ parents: nextParents, onMount, onUnmount }}>
{children}
</BreadcrumbsContext.Provider>
);
};

export function getDocTitle(breadcrumbs: BreadcrumbProps[], maxBreadcrumbs = 2) {
return breadcrumbs
.slice(0, maxBreadcrumbs)
.reverse()
.map(({ text }) => text);
}
94 changes: 94 additions & 0 deletions x-pack/plugins/security/public/components/confirm_modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { FunctionComponent } from 'react';
import {
EuiButton,
EuiButtonProps,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiModalProps,
EuiOverlayMask,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';

export interface ConfirmModalProps extends Omit<EuiModalProps, 'onClose' | 'initialFocus'> {
confirmButtonText: string;
confirmButtonColor?: EuiButtonProps['color'];
isLoading?: EuiButtonProps['isLoading'];
isDisabled?: EuiButtonProps['isDisabled'];
onCancel(): void;
onConfirm(): void;
ownFocus?: boolean;
}

/**
* Component that renders a confirmation modal similar to `EuiConfirmModal`, except that
* it adds `isLoading` prop, which renders a loading spinner and disables action buttons,
* and `ownFocus` prop to render overlay mask.
*/
export const ConfirmModal: FunctionComponent<ConfirmModalProps> = ({
children,
confirmButtonColor: buttonColor,
confirmButtonText,
isLoading,
isDisabled,
onCancel,
onConfirm,
ownFocus = true,
title,
...rest
}) => {
const modal = (
<EuiModal role="dialog" title={title} onClose={onCancel} {...rest}>
<EuiModalHeader>
<EuiModalHeaderTitle>{title}</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody data-test-subj="confirmModalBodyText">{children}</EuiModalBody>
<EuiModalFooter>
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="confirmModalCancelButton"
flush="right"
isDisabled={isLoading}
onClick={onCancel}
>
<FormattedMessage
id="xpack.security.confirmModal.cancelButton"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="confirmModalConfirmButton"
color={buttonColor}
fill
isLoading={isLoading}
isDisabled={isDisabled}
onClick={onConfirm}
>
{confirmButtonText}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiModalFooter>
</EuiModal>
);

return ownFocus ? (
<EuiOverlayMask onClick={!isLoading ? onCancel : undefined}>{modal}</EuiOverlayMask>
) : (
modal
);
};
60 changes: 60 additions & 0 deletions x-pack/plugins/security/public/components/doc_link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useCallback, FunctionComponent } from 'react';
import { EuiLink } from '@elastic/eui';
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
import { CoreStart } from '../../../../../src/core/public';

export type DocLinks = CoreStart['docLinks']['links'];
export type GetDocLinkFunction = (app: string, doc: string) => string;

/**
* Creates links to the documentation.
*
* @see {@link DocLink} for a component that creates a link to the docs.
*
* @example
* ```typescript
* <DocLink app="elasticsearch" doc="built-in-roles.html">
* Learn what privileges individual roles grant.
* </DocLink>
* ```
*
* @example
* ```typescript
* const [docs] = useDocLinks();
*
* <EuiLink href={docs.dashboard.guide} target="_blank" external>
* Learn how to get started with dashboards.
* </EuiLink>
* ```
*/
export function useDocLinks(): [DocLinks, GetDocLinkFunction] {
const { services } = useKibana();
const { links, ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = services.docLinks!;
const getDocLink = useCallback<GetDocLinkFunction>(
(app, doc) => {
return `${ELASTIC_WEBSITE_URL}guide/en/${app}/reference/${DOC_LINK_VERSION}/${doc}`;
},
[ELASTIC_WEBSITE_URL, DOC_LINK_VERSION]
);
return [links, getDocLink];
}

export interface DocLinkProps {
app: string;
doc: string;
}

export const DocLink: FunctionComponent<DocLinkProps> = ({ app, doc, children }) => {
const [, getDocLink] = useDocLinks();
return (
<EuiLink href={getDocLink(app, doc)} target="_blank" external>
{children}
</EuiLink>
);
};
Loading

0 comments on commit aeb6df3

Please sign in to comment.