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 { 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 {
basicRole?: OrgRole;
@ -22,6 +22,7 @@ export interface Props {
* Set {@link RolePickerMenu}'s button to display either `Apply` (apply=true) or `Update` (apply=false)
*/
apply?: boolean;
maxWidth?: string | number;
}
export const RolePicker = ({
@ -36,6 +37,7 @@ export const RolePicker = ({
onBasicRoleChange,
canUpdateRoles = true,
apply = false,
maxWidth = ROLE_PICKER_WIDTH,
}: Props): JSX.Element | null => {
const [isOpen, setOpen] = useState(false);
const [selectedRoles, setSelectedRoles] = useState<Role[]>(appliedRoles);
@ -54,7 +56,7 @@ export const RolePicker = ({
if (!dimensions || !isOpen) {
return;
}
const { bottom, top, left, right } = dimensions;
const { bottom, top, left, right, width: currentRolePickerWidth } = dimensions;
const distance = window.innerHeight - bottom;
const offsetVertical = bottom - top + 10; // Add extra 10px to offset to account for border and outline
const offsetHorizontal = right - left;
@ -65,7 +67,17 @@ export const RolePicker = ({
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;
}
@ -140,7 +152,14 @@ export const RolePicker = ({
}
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}>
<RolePickerInput
basicRole={selectedBuiltInRole}

View File

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

View File

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

View File

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

View File

@ -29,6 +29,7 @@ export interface Props {
apply?: boolean;
onApplyRoles?: (newRoles: Role[], userId: number, orgId: number | undefined) => void;
pendingRoles?: Role[];
maxWidth?: string | number;
}
export const UserRolePicker: FC<Props> = ({
@ -42,6 +43,7 @@ export const UserRolePicker: FC<Props> = ({
apply = false,
onApplyRoles,
pendingRoles,
maxWidth,
}) => {
const [{ loading, value: appliedRoles = [] }, getUserRoles] = useAsyncFn(async () => {
try {
@ -92,6 +94,7 @@ export const UserRolePicker: FC<Props> = ({
showBasicRole
apply={apply}
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 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}
onApplyRoles={onPendingRolesUpdate}
pendingRoles={pendingRoles}
maxWidth="100%"
/>
) : (
<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>
</td>
{contextSrv.licensedAccessControlEnabled() ? (
<td className="width-25" colSpan={3}>
<td colSpan={3}>
<UserRolePicker
userId={serviceAccount.id}
orgId={serviceAccount.orgId}

View File

@ -45,7 +45,7 @@ export const CreateTeam = (): JSX.Element => {
{({ register, errors }) => (
<FieldSet label="New Team">
<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>
{contextSrv.licensedAccessControlEnabled() && (
<Field label="Role">
@ -56,6 +56,7 @@ export const CreateTeam = (): JSX.Element => {
apply={true}
onApplyRoles={setPendingRoles}
pendingRoles={pendingRoles}
maxWidth="100%"
/>
</Field>
)}
@ -63,7 +64,7 @@ export const CreateTeam = (): JSX.Element => {
label={'Email'}
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>
<div className="gf-form-button-row">
<Button type="submit" variant="primary">

View File

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