RolePicker: Optimise rendering inside lists of items (#77297)

* Role picker: Load users roles in batch

* Use orgId in request

* Add roles to OrgUser type

* Improve loading logic

* Improve loading indicator

* Fix org page

* Update service accounts page

* Use bulk roles query for teams

* Use POST requests for search

* Use post request for teams

* Update betterer results

* Review suggestions

* AdminEditOrgPage: move API calls to separate file
This commit is contained in:
Alexander Zobnin
2023-11-01 11:57:02 +01:00
committed by GitHub
parent d1798819c0
commit cf7a2ea733
22 changed files with 227 additions and 76 deletions

View File

@@ -1,12 +1,11 @@
import React, { FormEvent, useCallback, useEffect, useState, useRef } from 'react';
import { ClickOutsideWrapper, Spinner, useStyles2, useTheme2 } from '@grafana/ui';
import { ClickOutsideWrapper, useTheme2 } from '@grafana/ui';
import { Role, OrgRole } from 'app/types';
import { RolePickerInput } from './RolePickerInput';
import { RolePickerMenu } from './RolePickerMenu';
import { MENU_MAX_HEIGHT, ROLE_PICKER_SUBMENU_MIN_WIDTH, ROLE_PICKER_WIDTH } from './constants';
import { getStyles } from './styles';
export interface Props {
basicRole?: OrgRole;
@@ -50,7 +49,6 @@ export const RolePicker = ({
const [query, setQuery] = useState('');
const [offset, setOffset] = useState({ vertical: 0, horizontal: 0 });
const ref = useRef<HTMLDivElement>(null);
const styles = useStyles2(getStyles);
const theme = useTheme2();
const widthPx = typeof width === 'number' ? theme.spacing(width) : width;
@@ -152,15 +150,6 @@ export const RolePicker = ({
return options;
};
if (isLoading) {
return (
<div style={{ maxWidth: widthPx || maxWidth, width: widthPx }}>
<span>Loading...</span>
<Spinner inline className={styles.loadingSpinner} />
</div>
);
}
return (
<div
data-testid="role-picker"
@@ -183,6 +172,7 @@ export const RolePicker = ({
disabled={disabled}
showBasicRole={showBasicRole}
width={widthPx}
isLoading={isLoading}
/>
{isOpen && (
<RolePickerMenu

View File

@@ -2,7 +2,7 @@ import { css, cx } from '@emotion/css';
import React, { FormEvent, HTMLProps, useEffect, useRef } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2, getInputStyles, sharedInputStyle, styleMixins, Tooltip, Icon } from '@grafana/ui';
import { useStyles2, getInputStyles, sharedInputStyle, styleMixins, Tooltip, Icon, Spinner } from '@grafana/ui';
import { Role } from '../../../types';
@@ -19,6 +19,7 @@ interface InputProps extends HTMLProps<HTMLInputElement> {
isFocused?: boolean;
disabled?: boolean;
width?: string;
isLoading?: boolean;
onQueryChange: (query?: string) => void;
onOpen: (event: FormEvent<HTMLElement>) => void;
onClose: () => void;
@@ -32,6 +33,7 @@ export const RolePickerInput = ({
query,
showBasicRole,
width,
isLoading,
onOpen,
onClose,
onQueryChange,
@@ -63,6 +65,11 @@ export const RolePickerInput = ({
numberOfRoles={appliedRoles.length}
showBuiltInRole={showBasicRoleOnLabel}
/>
{isLoading && (
<div className={styles.spinner}>
<Spinner size={16} inline />
</div>
)}
</div>
) : (
<div className={styles.wrapper}>
@@ -141,22 +148,22 @@ const getRolePickerInputStyles = (
${styleMixins.focusCss(theme.v1)}
`,
disabled && styles.inputDisabled,
css`
min-width: ${width || ROLE_PICKER_WIDTH + 'px'};
width: ${width};
min-height: 32px;
height: auto;
flex-direction: row;
padding-right: 24px;
max-width: 100%;
align-items: center;
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
position: relative;
box-sizing: border-box;
cursor: default;
`,
css({
minWidth: width || ROLE_PICKER_WIDTH + 'px',
width: width,
minHeight: '32px',
height: 'auto',
flexDirection: 'row',
paddingRight: theme.spacing(1),
maxWidth: '100%',
alignItems: 'center',
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'flex-start',
position: 'relative',
boxSizing: 'border-box',
cursor: 'default',
}),
withPrefix &&
css`
padding-left: 0;
@@ -184,6 +191,11 @@ const getRolePickerInputStyles = (
margin-bottom: ${theme.spacing(0.5)};
}
`,
spinner: css({
display: 'flex',
flexGrow: 1,
justifyContent: 'flex-end',
}),
};
};

View File

@@ -12,6 +12,7 @@ export interface Props {
orgId?: number;
roleOptions: Role[];
disabled?: boolean;
roles?: Role[];
onApplyRoles?: (newRoles: Role[]) => void;
pendingRoles?: Role[];
/**
@@ -28,20 +29,26 @@ export interface Props {
apply?: boolean;
maxWidth?: string | number;
width?: string | number;
isLoading?: boolean;
}
export const TeamRolePicker = ({
teamId,
roleOptions,
disabled,
roles,
onApplyRoles,
pendingRoles,
apply = false,
maxWidth,
width,
isLoading,
}: Props) => {
const [{ loading, value: appliedRoles = [] }, getTeamRoles] = useAsyncFn(async () => {
const [{ loading, value: appliedRoles = roles || [] }, getTeamRoles] = useAsyncFn(async () => {
try {
if (roles) {
return roles;
}
if (apply && Boolean(pendingRoles?.length)) {
return pendingRoles;
}
@@ -53,11 +60,11 @@ export const TeamRolePicker = ({
console.error('Error loading options', e);
}
return [];
}, [teamId, pendingRoles]);
}, [teamId, pendingRoles, roles]);
useEffect(() => {
getTeamRoles();
}, [teamId, getTeamRoles, pendingRoles]);
}, [getTeamRoles]);
const onRolesChange = async (roles: Role[]) => {
if (!apply) {
@@ -78,7 +85,7 @@ export const TeamRolePicker = ({
onRolesChange={onRolesChange}
roleOptions={roleOptions}
appliedRoles={appliedRoles}
isLoading={loading}
isLoading={loading || isLoading}
disabled={disabled}
basicRoleDisabled={true}
canUpdateRoles={canUpdateRoles}

View File

@@ -9,6 +9,7 @@ import { fetchUserRoles, updateUserRoles } from './api';
export interface Props {
basicRole: OrgRole;
roles?: Role[];
userId: number;
orgId?: number;
onBasicRoleChange: (newRole: OrgRole) => void;
@@ -32,10 +33,12 @@ export interface Props {
pendingRoles?: Role[];
maxWidth?: string | number;
width?: string | number;
isLoading?: boolean;
}
export const UserRolePicker = ({
basicRole,
roles,
userId,
orgId,
onBasicRoleChange,
@@ -48,9 +51,13 @@ export const UserRolePicker = ({
pendingRoles,
maxWidth,
width,
isLoading,
}: Props) => {
const [{ loading, value: appliedRoles = [] }, getUserRoles] = useAsyncFn(async () => {
const [{ loading, value: appliedRoles = roles || [] }, getUserRoles] = useAsyncFn(async () => {
try {
if (roles) {
return roles;
}
if (apply && Boolean(pendingRoles?.length)) {
return pendingRoles;
}
@@ -63,14 +70,14 @@ export const UserRolePicker = ({
console.error('Error loading options');
}
return [];
}, [orgId, userId, pendingRoles]);
}, [orgId, userId, pendingRoles, roles]);
useEffect(() => {
// only load roles when there is an Org selected
if (orgId) {
getUserRoles();
}
}, [orgId, getUserRoles, pendingRoles]);
}, [getUserRoles, orgId]);
const onRolesChange = async (roles: Role[]) => {
if (!apply) {
@@ -92,7 +99,7 @@ export const UserRolePicker = ({
onRolesChange={onRolesChange}
onBasicRoleChange={onBasicRoleChange}
roleOptions={roleOptions}
isLoading={loading}
isLoading={loading || isLoading}
disabled={disabled}
basicRoleDisabled={basicRoleDisabled}
basicRoleDisabledMessage={basicRoleDisabledMessage}