Admin: Create/Edit Team/ServiceAccount UI changes (#53889)

* RolePicker: Handle inherited with

* Small ammendment to Create Service Account layout

* RolePicker: introduce maxWidth prop

* Clean up

* Change VerticalGroup spacing to large on Team Settings page

* Introduce constant for submenu width

* Update public/app/core/components/RolePicker/RolePicker.tsx

Simplify style parameter

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* Add description to the improved calculation

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
This commit is contained in:
Mihály Gyöngyösi 2022-08-25 13:30:11 +02:00 committed by GitHub
parent 9f8cb17b01
commit aace0b1e7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 87 additions and 60 deletions

View File

@ -5,7 +5,7 @@ import { Role, OrgRole } from 'app/types';
import { RolePickerInput } from './RolePickerInput'; import { RolePickerInput } from './RolePickerInput';
import { RolePickerMenu } from './RolePickerMenu'; import { RolePickerMenu } from './RolePickerMenu';
import { MENU_MAX_HEIGHT, ROLE_PICKER_WIDTH } from './constants'; import { MENU_MAX_HEIGHT, ROLE_PICKER_SUBMENU_MIN_WIDTH, ROLE_PICKER_WIDTH } from './constants';
export interface Props { export interface Props {
basicRole?: OrgRole; basicRole?: OrgRole;
@ -22,6 +22,7 @@ export interface Props {
* Set {@link RolePickerMenu}'s button to display either `Apply` (apply=true) or `Update` (apply=false) * Set {@link RolePickerMenu}'s button to display either `Apply` (apply=true) or `Update` (apply=false)
*/ */
apply?: boolean; apply?: boolean;
maxWidth?: string | number;
} }
export const RolePicker = ({ export const RolePicker = ({
@ -36,6 +37,7 @@ export const RolePicker = ({
onBasicRoleChange, onBasicRoleChange,
canUpdateRoles = true, canUpdateRoles = true,
apply = false, apply = false,
maxWidth = ROLE_PICKER_WIDTH,
}: Props): JSX.Element | null => { }: Props): JSX.Element | null => {
const [isOpen, setOpen] = useState(false); const [isOpen, setOpen] = useState(false);
const [selectedRoles, setSelectedRoles] = useState<Role[]>(appliedRoles); const [selectedRoles, setSelectedRoles] = useState<Role[]>(appliedRoles);
@ -54,7 +56,7 @@ export const RolePicker = ({
if (!dimensions || !isOpen) { if (!dimensions || !isOpen) {
return; return;
} }
const { bottom, top, left, right } = dimensions; const { bottom, top, left, right, width: currentRolePickerWidth } = dimensions;
const distance = window.innerHeight - bottom; const distance = window.innerHeight - bottom;
const offsetVertical = bottom - top + 10; // Add extra 10px to offset to account for border and outline const offsetVertical = bottom - top + 10; // Add extra 10px to offset to account for border and outline
const offsetHorizontal = right - left; const offsetHorizontal = right - left;
@ -65,7 +67,17 @@ export const RolePicker = ({
vertical = offsetVertical; vertical = offsetVertical;
} }
if (window.innerWidth - right < ROLE_PICKER_WIDTH) { /*
* This expression calculates whether there is enough place
* on the right of the RolePicker input to show/fit the role picker menu and its sub menu AND
* whether there is enough place under the RolePicker input to show/fit
* both (the role picker menu and its sub menu) aligned to the left edge of the input.
* Otherwise, it aligns the role picker menu to the right.
*/
if (
window.innerWidth - right < currentRolePickerWidth &&
currentRolePickerWidth < 2 * ROLE_PICKER_SUBMENU_MIN_WIDTH
) {
horizontal = offsetHorizontal; horizontal = offsetHorizontal;
} }
@ -140,7 +152,14 @@ export const RolePicker = ({
} }
return ( return (
<div data-testid="role-picker" style={{ position: 'relative', width: ROLE_PICKER_WIDTH }} ref={ref}> <div
data-testid="role-picker"
style={{
position: 'relative',
maxWidth,
}}
ref={ref}
>
<ClickOutsideWrapper onClick={onClickOutside}> <ClickOutsideWrapper onClick={onClickOutside}>
<RolePickerInput <RolePickerInput
basicRole={selectedBuiltInRole} basicRole={selectedBuiltInRole}

View File

@ -135,7 +135,7 @@ const getRolePickerInputStyles = (
`, `,
disabled && styles.inputDisabled, disabled && styles.inputDisabled,
css` css`
width: ${ROLE_PICKER_WIDTH}px; min-width: ${ROLE_PICKER_WIDTH}px;
min-height: 32px; min-height: 32px;
height: auto; height: auto;
flex-direction: row; flex-direction: row;

View File

@ -17,7 +17,7 @@ import {
import { getSelectStyles } from '@grafana/ui/src/components/Select/getSelectStyles'; import { getSelectStyles } from '@grafana/ui/src/components/Select/getSelectStyles';
import { OrgRole, Role } from 'app/types'; import { OrgRole, Role } from 'app/types';
import { MENU_MAX_HEIGHT } from './constants'; import { MENU_MAX_HEIGHT, ROLE_PICKER_SUBMENU_MIN_WIDTH } from './constants';
enum GroupType { enum GroupType {
fixed = 'fixed', fixed = 'fixed',
@ -588,7 +588,7 @@ export const getStyles = (theme: GrafanaTheme2) => {
min-width: auto; min-width: auto;
`, `,
menu: css` menu: css`
min-width: 260px; min-width: ${ROLE_PICKER_SUBMENU_MIN_WIDTH}px;
& > div { & > div {
padding-top: ${theme.spacing(1)}; padding-top: ${theme.spacing(1)};
@ -600,7 +600,7 @@ export const getStyles = (theme: GrafanaTheme2) => {
`, `,
subMenu: css` subMenu: css`
height: 100%; height: 100%;
min-width: 260px; min-width: ${ROLE_PICKER_SUBMENU_MIN_WIDTH}px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-left: 1px solid ${theme.components.input.borderColor}; border-left: 1px solid ${theme.components.input.borderColor};

View File

@ -26,6 +26,7 @@ export interface Props {
* @default false * @default false
*/ */
apply?: boolean; apply?: boolean;
maxWidth?: string | number;
} }
export const TeamRolePicker: FC<Props> = ({ export const TeamRolePicker: FC<Props> = ({
@ -35,6 +36,7 @@ export const TeamRolePicker: FC<Props> = ({
onApplyRoles, onApplyRoles,
pendingRoles, pendingRoles,
apply = false, apply = false,
maxWidth,
}) => { }) => {
const [{ loading, value: appliedRoles = [] }, getTeamRoles] = useAsyncFn(async () => { const [{ loading, value: appliedRoles = [] }, getTeamRoles] = useAsyncFn(async () => {
try { try {
@ -78,6 +80,7 @@ export const TeamRolePicker: FC<Props> = ({
disabled={disabled} disabled={disabled}
basicRoleDisabled={true} basicRoleDisabled={true}
canUpdateRoles={canUpdateRoles} canUpdateRoles={canUpdateRoles}
maxWidth={maxWidth}
/> />
); );
}; };

View File

@ -29,6 +29,7 @@ export interface Props {
apply?: boolean; apply?: boolean;
onApplyRoles?: (newRoles: Role[], userId: number, orgId: number | undefined) => void; onApplyRoles?: (newRoles: Role[], userId: number, orgId: number | undefined) => void;
pendingRoles?: Role[]; pendingRoles?: Role[];
maxWidth?: string | number;
} }
export const UserRolePicker: FC<Props> = ({ export const UserRolePicker: FC<Props> = ({
@ -42,6 +43,7 @@ export const UserRolePicker: FC<Props> = ({
apply = false, apply = false,
onApplyRoles, onApplyRoles,
pendingRoles, pendingRoles,
maxWidth,
}) => { }) => {
const [{ loading, value: appliedRoles = [] }, getUserRoles] = useAsyncFn(async () => { const [{ loading, value: appliedRoles = [] }, getUserRoles] = useAsyncFn(async () => {
try { try {
@ -92,6 +94,7 @@ export const UserRolePicker: FC<Props> = ({
showBasicRole showBasicRole
apply={apply} apply={apply}
canUpdateRoles={canUpdateRoles} canUpdateRoles={canUpdateRoles}
maxWidth={maxWidth}
/> />
); );
}; };

View File

@ -1,2 +1,3 @@
export const MENU_MAX_HEIGHT = 300; // max height for the picker's dropdown menu export const MENU_MAX_HEIGHT = 300; // max height for the picker's dropdown menu
export const ROLE_PICKER_WIDTH = 360; export const ROLE_PICKER_WIDTH = 360;
export const ROLE_PICKER_SUBMENU_MIN_WIDTH = 260;

View File

@ -127,6 +127,7 @@ export const ServiceAccountCreatePage = ({}: Props): JSX.Element => {
roleOptions={roleOptions} roleOptions={roleOptions}
onApplyRoles={onPendingRolesUpdate} onApplyRoles={onPendingRolesUpdate}
pendingRoles={pendingRoles} pendingRoles={pendingRoles}
maxWidth="100%"
/> />
) : ( ) : (
<OrgRolePicker aria-label="Role" value={serviceAccount.role} onChange={onRoleChange} /> <OrgRolePicker aria-label="Role" value={serviceAccount.role} onChange={onRoleChange} />

View File

@ -23,7 +23,7 @@ export const ServiceAccountRoleRow = ({ label, serviceAccount, roleOptions, onRo
<Label htmlFor={inputId}>{label}</Label> <Label htmlFor={inputId}>{label}</Label>
</td> </td>
{contextSrv.licensedAccessControlEnabled() ? ( {contextSrv.licensedAccessControlEnabled() ? (
<td className="width-25" colSpan={3}> <td colSpan={3}>
<UserRolePicker <UserRolePicker
userId={serviceAccount.id} userId={serviceAccount.id}
orgId={serviceAccount.orgId} orgId={serviceAccount.orgId}

View File

@ -45,7 +45,7 @@ export const CreateTeam = (): JSX.Element => {
{({ register, errors }) => ( {({ register, errors }) => (
<FieldSet label="New Team"> <FieldSet label="New Team">
<Field label="Name" required invalid={!!errors.name} error="Team name is required"> <Field label="Name" required invalid={!!errors.name} error="Team name is required">
<Input {...register('name', { required: true })} id="team-name" width={60} /> <Input {...register('name', { required: true })} id="team-name" />
</Field> </Field>
{contextSrv.licensedAccessControlEnabled() && ( {contextSrv.licensedAccessControlEnabled() && (
<Field label="Role"> <Field label="Role">
@ -56,6 +56,7 @@ export const CreateTeam = (): JSX.Element => {
apply={true} apply={true}
onApplyRoles={setPendingRoles} onApplyRoles={setPendingRoles}
pendingRoles={pendingRoles} pendingRoles={pendingRoles}
maxWidth="100%"
/> />
</Field> </Field>
)} )}
@ -63,7 +64,7 @@ export const CreateTeam = (): JSX.Element => {
label={'Email'} label={'Email'}
description={'This is optional and is primarily used for allowing custom team avatars.'} description={'This is optional and is primarily used for allowing custom team avatars.'}
> >
<Input {...register('email')} type="email" id="team-email" placeholder="email@test.com" width={60} /> <Input {...register('email')} type="email" id="team-email" placeholder="email@test.com" />
</Field> </Field>
<div className="gf-form-button-row"> <div className="gf-form-button-row">
<Button type="submit" variant="primary"> <Button type="submit" variant="primary">

View File

@ -34,57 +34,56 @@ export const TeamSettings: FC<Props> = ({ team, updateTeam }) => {
contextSrv.hasPermission(AccessControlAction.ActionUserRolesRemove); contextSrv.hasPermission(AccessControlAction.ActionUserRolesRemove);
return ( return (
<VerticalGroup> <VerticalGroup spacing="lg">
<FieldSet label="Team details"> <Form
<Form defaultValues={{ ...team }}
defaultValues={{ ...team }} onSubmit={async (formTeam: Team) => {
onSubmit={async (formTeam: Team) => { if (contextSrv.licensedAccessControlEnabled() && canUpdateRoles) {
if (contextSrv.licensedAccessControlEnabled() && canUpdateRoles) { await updateTeamRoles(pendingRoles, team.id);
await updateTeamRoles(pendingRoles, team.id); }
} updateTeam(formTeam.name, formTeam.email);
updateTeam(formTeam.name, formTeam.email); }}
}} disabled={!canWriteTeamSettings}
disabled={!canWriteTeamSettings} >
> {({ register, errors }) => (
{({ register, errors }) => ( <FieldSet label="Team details">
<> <Field
<Field label="Name"
label="Name" disabled={!canWriteTeamSettings}
disabled={!canWriteTeamSettings} required
required invalid={!!errors.name}
invalid={!!errors.name} error="Name is required"
error="Name is required" >
> <Input {...register('name', { required: true })} id="name-input" />
<Input {...register('name', { required: true })} id="name-input" /> </Field>
</Field>
{contextSrv.licensedAccessControlEnabled() && ( {contextSrv.licensedAccessControlEnabled() && (
<Field label="Role"> <Field label="Role">
<TeamRolePicker <TeamRolePicker
teamId={team.id} teamId={team.id}
roleOptions={roleOptions} roleOptions={roleOptions}
disabled={false} disabled={false}
apply={true} apply={true}
onApplyRoles={setPendingRoles} onApplyRoles={setPendingRoles}
pendingRoles={pendingRoles} pendingRoles={pendingRoles}
/> maxWidth="100%"
</Field> />
)}
<Field
label="Email"
description="This is optional and is primarily used to set the team profile avatar (via gravatar service)."
disabled={!canWriteTeamSettings}
>
<Input {...register('email')} placeholder="team@email.com" type="email" id="email-input" />
</Field> </Field>
<Button type="submit" disabled={!canWriteTeamSettings}> )}
Update
</Button> <Field
</> label="Email"
)} description="This is optional and is primarily used to set the team profile avatar (via gravatar service)."
</Form> disabled={!canWriteTeamSettings}
</FieldSet> >
<Input {...register('email')} placeholder="team@email.com" type="email" id="email-input" />
</Field>
<Button type="submit" disabled={!canWriteTeamSettings}>
Update
</Button>
</FieldSet>
)}
</Form>
<SharedPreferences resourceUri={`teams/${team.id}`} disabled={!canWriteTeamSettings} /> <SharedPreferences resourceUri={`teams/${team.id}`} disabled={!canWriteTeamSettings} />
</VerticalGroup> </VerticalGroup>
); );