mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
9f8cb17b01
commit
aace0b1e7f
@ -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}
|
||||||
|
@ -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;
|
||||||
|
@ -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};
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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} />
|
||||||
|
@ -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}
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user