mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Access control: Team role picker (#43418)
* Refactor: move fetching from role picker to parent component * Make built in role props optional * Initial team role picker * Add role picker to the teams list * Optimize fetching roles * Add pagination for the teams page * Fix tests * Hide roles if access control not enabled * Fix test snapshots * Refactor: use useAsync() hook * Refactor: simplify input component * Move api calls to separate file * Refactor: use useAsync() hook for user role picker * Tweak role picker input width * Fix pagination * Update test snapshots * Use loading state from useAsync() hook * Fix roles label if no roles assigned
This commit is contained in:
parent
e888a40531
commit
867ca5b59e
@ -1,61 +1,42 @@
|
|||||||
import React, { FormEvent, useCallback, useEffect, useState } from 'react';
|
import React, { FormEvent, useCallback, useEffect, useState } from 'react';
|
||||||
import { ClickOutsideWrapper } from '@grafana/ui';
|
import { ClickOutsideWrapper, HorizontalGroup, Spinner } from '@grafana/ui';
|
||||||
import { RolePickerMenu } from './RolePickerMenu';
|
import { RolePickerMenu } from './RolePickerMenu';
|
||||||
import { RolePickerInput } from './RolePickerInput';
|
import { RolePickerInput } from './RolePickerInput';
|
||||||
import { Role, OrgRole } from 'app/types';
|
import { Role, OrgRole } from 'app/types';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
builtInRole: OrgRole;
|
builtInRole?: OrgRole;
|
||||||
getRoles: () => Promise<Role[]>;
|
appliedRoles: Role[];
|
||||||
getRoleOptions: () => Promise<Role[]>;
|
roleOptions: Role[];
|
||||||
getBuiltinRoles: () => Promise<Record<string, Role[]>>;
|
builtInRoles?: Record<string, Role[]>;
|
||||||
onRolesChange: (newRoles: string[]) => void;
|
isLoading?: boolean;
|
||||||
onBuiltinRoleChange: (newRole: OrgRole) => void;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
builtinRolesDisabled?: boolean;
|
builtinRolesDisabled?: boolean;
|
||||||
|
showBuiltInRole?: boolean;
|
||||||
|
onRolesChange: (newRoles: string[]) => void;
|
||||||
|
onBuiltinRoleChange?: (newRole: OrgRole) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RolePicker = ({
|
export const RolePicker = ({
|
||||||
builtInRole,
|
builtInRole,
|
||||||
getRoles,
|
appliedRoles,
|
||||||
getRoleOptions,
|
roleOptions,
|
||||||
getBuiltinRoles,
|
builtInRoles,
|
||||||
|
disabled,
|
||||||
|
isLoading,
|
||||||
|
builtinRolesDisabled,
|
||||||
|
showBuiltInRole,
|
||||||
onRolesChange,
|
onRolesChange,
|
||||||
onBuiltinRoleChange,
|
onBuiltinRoleChange,
|
||||||
disabled,
|
|
||||||
builtinRolesDisabled,
|
|
||||||
}: Props): JSX.Element | null => {
|
}: Props): JSX.Element | null => {
|
||||||
const [isOpen, setOpen] = useState(false);
|
const [isOpen, setOpen] = useState(false);
|
||||||
const [roleOptions, setRoleOptions] = useState<Role[]>([]);
|
const [selectedRoles, setSelectedRoles] = useState<Role[]>(appliedRoles);
|
||||||
const [appliedRoles, setAppliedRoles] = useState<Role[]>([]);
|
const [selectedBuiltInRole, setSelectedBuiltInRole] = useState<OrgRole | undefined>(builtInRole);
|
||||||
const [selectedRoles, setSelectedRoles] = useState<Role[]>([]);
|
|
||||||
const [selectedBuiltInRole, setSelectedBuiltInRole] = useState<OrgRole>(builtInRole);
|
|
||||||
const [builtInRoles, setBuiltinRoles] = useState<Record<string, Role[]>>({});
|
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchOptions() {
|
setSelectedRoles(appliedRoles);
|
||||||
try {
|
}, [appliedRoles]);
|
||||||
let options = await getRoleOptions();
|
|
||||||
setRoleOptions(options.filter((option) => !option.name?.startsWith('managed:')));
|
|
||||||
|
|
||||||
const builtInRoles = await getBuiltinRoles();
|
|
||||||
setBuiltinRoles(builtInRoles);
|
|
||||||
|
|
||||||
const userRoles = await getRoles();
|
|
||||||
setAppliedRoles(userRoles);
|
|
||||||
setSelectedRoles(userRoles);
|
|
||||||
} catch (e) {
|
|
||||||
// TODO handle error
|
|
||||||
console.error('Error loading options');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchOptions();
|
|
||||||
}, [getRoles, getRoleOptions, getBuiltinRoles, builtInRole]);
|
|
||||||
|
|
||||||
const onOpen = useCallback(
|
const onOpen = useCallback(
|
||||||
(event: FormEvent<HTMLElement>) => {
|
(event: FormEvent<HTMLElement>) => {
|
||||||
@ -94,8 +75,10 @@ export const RolePicker = ({
|
|||||||
setSelectedBuiltInRole(role);
|
setSelectedBuiltInRole(role);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onUpdate = (newBuiltInRole: OrgRole, newRoles: string[]) => {
|
const onUpdate = (newRoles: string[], newBuiltInRole?: OrgRole) => {
|
||||||
|
if (onBuiltinRoleChange && newBuiltInRole) {
|
||||||
onBuiltinRoleChange(newBuiltInRole);
|
onBuiltinRoleChange(newBuiltInRole);
|
||||||
|
}
|
||||||
onRolesChange(newRoles);
|
onRolesChange(newRoles);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
setQuery('');
|
setQuery('');
|
||||||
@ -109,7 +92,12 @@ export const RolePicker = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return null;
|
return (
|
||||||
|
<HorizontalGroup justify="center">
|
||||||
|
<span>Loading...</span>
|
||||||
|
<Spinner size={16} />
|
||||||
|
</HorizontalGroup>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -124,6 +112,7 @@ export const RolePicker = ({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
isFocused={isOpen}
|
isFocused={isOpen}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
showBuiltInRole={showBuiltInRole}
|
||||||
/>
|
/>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<RolePickerMenu
|
<RolePickerMenu
|
||||||
@ -136,6 +125,7 @@ export const RolePicker = ({
|
|||||||
onUpdate={onUpdate}
|
onUpdate={onUpdate}
|
||||||
showGroups={query.length === 0 || query.trim() === ''}
|
showGroups={query.length === 0 || query.trim() === ''}
|
||||||
builtinRolesDisabled={builtinRolesDisabled}
|
builtinRolesDisabled={builtinRolesDisabled}
|
||||||
|
showBuiltInRole={showBuiltInRole}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ClickOutsideWrapper>
|
</ClickOutsideWrapper>
|
||||||
|
@ -9,8 +9,9 @@ const stopPropagation = (event: React.MouseEvent<HTMLDivElement>) => event.stopP
|
|||||||
|
|
||||||
interface InputProps extends HTMLProps<HTMLInputElement> {
|
interface InputProps extends HTMLProps<HTMLInputElement> {
|
||||||
appliedRoles: Role[];
|
appliedRoles: Role[];
|
||||||
builtInRole: string;
|
builtInRole?: string;
|
||||||
query: string;
|
query: string;
|
||||||
|
showBuiltInRole?: boolean;
|
||||||
isFocused?: boolean;
|
isFocused?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onQueryChange: (query?: string) => void;
|
onQueryChange: (query?: string) => void;
|
||||||
@ -24,6 +25,7 @@ export const RolePickerInput = ({
|
|||||||
disabled,
|
disabled,
|
||||||
isFocused,
|
isFocused,
|
||||||
query,
|
query,
|
||||||
|
showBuiltInRole,
|
||||||
onOpen,
|
onOpen,
|
||||||
onClose,
|
onClose,
|
||||||
onQueryChange,
|
onQueryChange,
|
||||||
@ -47,26 +49,12 @@ export const RolePickerInput = ({
|
|||||||
|
|
||||||
return !isFocused ? (
|
return !isFocused ? (
|
||||||
<div className={styles.selectedRoles} onMouseDown={onOpen}>
|
<div className={styles.selectedRoles} onMouseDown={onOpen}>
|
||||||
<ValueContainer>{builtInRole}</ValueContainer>
|
{showBuiltInRole && <ValueContainer>{builtInRole}</ValueContainer>}
|
||||||
{!!numberOfRoles && (
|
<RolesLabel appliedRoles={appliedRoles} numberOfRoles={numberOfRoles} showBuiltInRole={showBuiltInRole} />
|
||||||
<Tooltip
|
|
||||||
content={
|
|
||||||
<div className={styles.tooltip}>
|
|
||||||
{appliedRoles?.map((role) => (
|
|
||||||
<p key={role.uid}>{role.displayName}</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<ValueContainer>{`+${numberOfRoles} role${numberOfRoles > 1 ? 's' : ''}`}</ValueContainer>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<ValueContainer>{builtInRole}</ValueContainer>
|
{showBuiltInRole && <ValueContainer>{builtInRole}</ValueContainer>}
|
||||||
{appliedRoles.map((role) => (
|
{appliedRoles.map((role) => (
|
||||||
<ValueContainer key={role.uid}>{role.displayName}</ValueContainer>
|
<ValueContainer key={role.uid}>{role.displayName}</ValueContainer>
|
||||||
))}
|
))}
|
||||||
@ -92,6 +80,44 @@ export const RolePickerInput = ({
|
|||||||
|
|
||||||
RolePickerInput.displayName = 'RolePickerInput';
|
RolePickerInput.displayName = 'RolePickerInput';
|
||||||
|
|
||||||
|
interface RolesLabelProps {
|
||||||
|
appliedRoles: Role[];
|
||||||
|
showBuiltInRole?: boolean;
|
||||||
|
numberOfRoles: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RolesLabel = ({ showBuiltInRole, numberOfRoles, appliedRoles }: RolesLabelProps): JSX.Element => {
|
||||||
|
const styles = useStyles2((theme) => getTooltipStyles(theme));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!!numberOfRoles ? (
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<div className={styles.tooltip}>
|
||||||
|
{appliedRoles?.map((role) => (
|
||||||
|
<p key={role.uid}>{role.displayName}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<ValueContainer>{`${showBuiltInRole ? '+' : ''}${numberOfRoles} role${
|
||||||
|
numberOfRoles > 1 ? 's' : ''
|
||||||
|
}`}</ValueContainer>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
!showBuiltInRole && (
|
||||||
|
<div>
|
||||||
|
<ValueContainer>No roles assigned</ValueContainer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const getRolePickerInputStyles = (
|
const getRolePickerInputStyles = (
|
||||||
theme: GrafanaTheme2,
|
theme: GrafanaTheme2,
|
||||||
invalid: boolean,
|
invalid: boolean,
|
||||||
@ -111,7 +137,7 @@ const getRolePickerInputStyles = (
|
|||||||
`,
|
`,
|
||||||
disabled && styles.inputDisabled,
|
disabled && styles.inputDisabled,
|
||||||
css`
|
css`
|
||||||
min-width: 520px;
|
width: 520px;
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
height: auto;
|
height: auto;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -154,3 +180,11 @@ const getRolePickerInputStyles = (
|
|||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getTooltipStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
tooltip: css`
|
||||||
|
p {
|
||||||
|
margin-bottom: ${theme.spacing(0.5)};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
@ -30,15 +30,16 @@ const fixedRoleGroupNames: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface RolePickerMenuProps {
|
interface RolePickerMenuProps {
|
||||||
builtInRole: OrgRole;
|
builtInRole?: OrgRole;
|
||||||
builtInRoles: BuiltInRoles;
|
builtInRoles?: BuiltInRoles;
|
||||||
options: Role[];
|
options: Role[];
|
||||||
appliedRoles: Role[];
|
appliedRoles: Role[];
|
||||||
showGroups?: boolean;
|
showGroups?: boolean;
|
||||||
builtinRolesDisabled?: boolean;
|
builtinRolesDisabled?: boolean;
|
||||||
|
showBuiltInRole?: boolean;
|
||||||
onSelect: (roles: Role[]) => void;
|
onSelect: (roles: Role[]) => void;
|
||||||
onBuiltInRoleSelect?: (role: OrgRole) => void;
|
onBuiltInRoleSelect?: (role: OrgRole) => void;
|
||||||
onUpdate: (newBuiltInRole: OrgRole, newRoles: string[]) => void;
|
onUpdate: (newRoles: string[], newBuiltInRole?: OrgRole) => void;
|
||||||
onClear?: () => void;
|
onClear?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,13 +50,14 @@ export const RolePickerMenu = ({
|
|||||||
appliedRoles,
|
appliedRoles,
|
||||||
showGroups,
|
showGroups,
|
||||||
builtinRolesDisabled,
|
builtinRolesDisabled,
|
||||||
|
showBuiltInRole,
|
||||||
onSelect,
|
onSelect,
|
||||||
onBuiltInRoleSelect,
|
onBuiltInRoleSelect,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onClear,
|
onClear,
|
||||||
}: RolePickerMenuProps): JSX.Element => {
|
}: RolePickerMenuProps): JSX.Element => {
|
||||||
const [selectedOptions, setSelectedOptions] = useState<Role[]>(appliedRoles);
|
const [selectedOptions, setSelectedOptions] = useState<Role[]>(appliedRoles);
|
||||||
const [selectedBuiltInRole, setSelectedBuiltInRole] = useState<OrgRole>(builtInRole);
|
const [selectedBuiltInRole, setSelectedBuiltInRole] = useState<OrgRole | undefined>(builtInRole);
|
||||||
const [showSubMenu, setShowSubMenu] = useState(false);
|
const [showSubMenu, setShowSubMenu] = useState(false);
|
||||||
const [openedMenuGroup, setOpenedMenuGroup] = useState('');
|
const [openedMenuGroup, setOpenedMenuGroup] = useState('');
|
||||||
const [subMenuOptions, setSubMenuOptions] = useState<Role[]>([]);
|
const [subMenuOptions, setSubMenuOptions] = useState<Role[]>([]);
|
||||||
@ -71,7 +73,7 @@ export const RolePickerMenu = ({
|
|||||||
}, [selectedOptions, onSelect]);
|
}, [selectedOptions, onSelect]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (onBuiltInRoleSelect) {
|
if (onBuiltInRoleSelect && selectedBuiltInRole) {
|
||||||
onBuiltInRoleSelect(selectedBuiltInRole);
|
onBuiltInRoleSelect(selectedBuiltInRole);
|
||||||
}
|
}
|
||||||
}, [selectedBuiltInRole, onBuiltInRoleSelect]);
|
}, [selectedBuiltInRole, onBuiltInRoleSelect]);
|
||||||
@ -168,13 +170,14 @@ export const RolePickerMenu = ({
|
|||||||
const roleUID = selectedOptions[key]?.uid;
|
const roleUID = selectedOptions[key]?.uid;
|
||||||
selectedCustomRoles.push(roleUID);
|
selectedCustomRoles.push(roleUID);
|
||||||
}
|
}
|
||||||
onUpdate(selectedBuiltInRole, selectedCustomRoles);
|
onUpdate(selectedCustomRoles, selectedBuiltInRole);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx(styles.menu, customStyles.menuWrapper)}>
|
<div className={cx(styles.menu, customStyles.menuWrapper)}>
|
||||||
<div className={customStyles.menu} aria-label="Role picker menu">
|
<div className={customStyles.menu} aria-label="Role picker menu">
|
||||||
<CustomScrollbar autoHide={false} autoHeightMax="300px" hideHorizontalTrack hideVerticalTrack>
|
<CustomScrollbar autoHide={false} autoHeightMax="300px" hideHorizontalTrack hideVerticalTrack>
|
||||||
|
{showBuiltInRole && (
|
||||||
<div className={customStyles.menuSection}>
|
<div className={customStyles.menuSection}>
|
||||||
<div className={customStyles.groupHeader}>Built-in roles</div>
|
<div className={customStyles.groupHeader}>Built-in roles</div>
|
||||||
<RadioButtonGroup
|
<RadioButtonGroup
|
||||||
@ -186,6 +189,7 @@ export const RolePickerMenu = ({
|
|||||||
disabled={builtinRolesDisabled}
|
disabled={builtinRolesDisabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
{!!fixedRoles.length &&
|
{!!fixedRoles.length &&
|
||||||
(showGroups && !!optionGroups.length ? (
|
(showGroups && !!optionGroups.length ? (
|
||||||
<div className={customStyles.menuSection}>
|
<div className={customStyles.menuSection}>
|
||||||
|
42
public/app/core/components/RolePicker/TeamRolePicker.tsx
Normal file
42
public/app/core/components/RolePicker/TeamRolePicker.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React, { FC, useState } from 'react';
|
||||||
|
import { useAsync } from 'react-use';
|
||||||
|
import { Role } from 'app/types';
|
||||||
|
import { RolePicker } from './RolePicker';
|
||||||
|
import { fetchRoleOptions, fetchTeamRoles, updateTeamRoles } from './api';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
teamId: number;
|
||||||
|
orgId?: number;
|
||||||
|
getRoleOptions?: () => Promise<Role[]>;
|
||||||
|
disabled?: boolean;
|
||||||
|
builtinRolesDisabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TeamRolePicker: FC<Props> = ({ teamId, orgId, getRoleOptions, disabled, builtinRolesDisabled }) => {
|
||||||
|
const [roleOptions, setRoleOptions] = useState<Role[]>([]);
|
||||||
|
const [appliedRoles, setAppliedRoles] = useState<Role[]>([]);
|
||||||
|
|
||||||
|
const { loading } = useAsync(async () => {
|
||||||
|
try {
|
||||||
|
let options = await (getRoleOptions ? getRoleOptions() : fetchRoleOptions(orgId));
|
||||||
|
setRoleOptions(options.filter((option) => !option.name?.startsWith('managed:')));
|
||||||
|
|
||||||
|
const teamRoles = await fetchTeamRoles(teamId, orgId);
|
||||||
|
setAppliedRoles(teamRoles);
|
||||||
|
} catch (e) {
|
||||||
|
// TODO handle error
|
||||||
|
console.error('Error loading options');
|
||||||
|
}
|
||||||
|
}, [getRoleOptions, orgId, teamId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RolePicker
|
||||||
|
onRolesChange={(roles) => updateTeamRoles(roles, teamId, orgId)}
|
||||||
|
roleOptions={roleOptions}
|
||||||
|
appliedRoles={appliedRoles}
|
||||||
|
isLoading={loading}
|
||||||
|
disabled={disabled}
|
||||||
|
builtinRolesDisabled={builtinRolesDisabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -1,7 +1,8 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC, useState } from 'react';
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { useAsync } from 'react-use';
|
||||||
import { Role, OrgRole } from 'app/types';
|
import { Role, OrgRole } from 'app/types';
|
||||||
import { RolePicker } from './RolePicker';
|
import { RolePicker } from './RolePicker';
|
||||||
|
import { fetchBuiltinRoles, fetchRoleOptions, fetchUserRoles, updateUserRoles } from './api';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
builtInRole: OrgRole;
|
builtInRole: OrgRole;
|
||||||
@ -24,64 +25,38 @@ export const UserRolePicker: FC<Props> = ({
|
|||||||
disabled,
|
disabled,
|
||||||
builtinRolesDisabled,
|
builtinRolesDisabled,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [roleOptions, setRoleOptions] = useState<Role[]>([]);
|
||||||
|
const [appliedRoles, setAppliedRoles] = useState<Role[]>([]);
|
||||||
|
const [builtInRoles, setBuiltinRoles] = useState<Record<string, Role[]>>({});
|
||||||
|
|
||||||
|
const { loading } = useAsync(async () => {
|
||||||
|
try {
|
||||||
|
let options = await (getRoleOptions ? getRoleOptions() : fetchRoleOptions(orgId));
|
||||||
|
setRoleOptions(options.filter((option) => !option.name?.startsWith('managed:')));
|
||||||
|
|
||||||
|
const builtInRoles = await (getBuiltinRoles ? getBuiltinRoles() : fetchBuiltinRoles(orgId));
|
||||||
|
setBuiltinRoles(builtInRoles);
|
||||||
|
|
||||||
|
const userRoles = await fetchUserRoles(userId, orgId);
|
||||||
|
setAppliedRoles(userRoles);
|
||||||
|
} catch (e) {
|
||||||
|
// TODO handle error
|
||||||
|
console.error('Error loading options');
|
||||||
|
}
|
||||||
|
}, [getBuiltinRoles, getRoleOptions, orgId, userId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RolePicker
|
<RolePicker
|
||||||
builtInRole={builtInRole}
|
builtInRole={builtInRole}
|
||||||
onRolesChange={(roles) => updateUserRoles(roles, userId, orgId)}
|
onRolesChange={(roles) => updateUserRoles(roles, userId, orgId)}
|
||||||
onBuiltinRoleChange={onBuiltinRoleChange}
|
onBuiltinRoleChange={onBuiltinRoleChange}
|
||||||
getRoleOptions={() => (getRoleOptions ? getRoleOptions() : fetchRoleOptions(orgId))}
|
roleOptions={roleOptions}
|
||||||
getRoles={() => fetchUserRoles(userId, orgId)}
|
appliedRoles={appliedRoles}
|
||||||
getBuiltinRoles={() => (getBuiltinRoles ? getBuiltinRoles() : fetchBuiltinRoles(orgId))}
|
builtInRoles={builtInRoles}
|
||||||
|
isLoading={loading}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
builtinRolesDisabled={builtinRolesDisabled}
|
builtinRolesDisabled={builtinRolesDisabled}
|
||||||
|
showBuiltInRole
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchRoleOptions = async (orgId?: number, query?: string): Promise<Role[]> => {
|
|
||||||
let rolesUrl = '/api/access-control/roles?delegatable=true';
|
|
||||||
if (orgId) {
|
|
||||||
rolesUrl += `&targetOrgId=${orgId}`;
|
|
||||||
}
|
|
||||||
const roles = await getBackendSrv().get(rolesUrl);
|
|
||||||
if (!roles || !roles.length) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return roles;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchBuiltinRoles = (orgId?: number): Promise<{ [key: string]: Role[] }> => {
|
|
||||||
let builtinRolesUrl = '/api/access-control/builtin-roles';
|
|
||||||
if (orgId) {
|
|
||||||
builtinRolesUrl += `?targetOrgId=${orgId}`;
|
|
||||||
}
|
|
||||||
return getBackendSrv().get(builtinRolesUrl);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchUserRoles = async (userId: number, orgId?: number): Promise<Role[]> => {
|
|
||||||
let userRolesUrl = `/api/access-control/users/${userId}/roles`;
|
|
||||||
if (orgId) {
|
|
||||||
userRolesUrl += `?targetOrgId=${orgId}`;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const roles = await getBackendSrv().get(userRolesUrl);
|
|
||||||
if (!roles || !roles.length) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return roles;
|
|
||||||
} catch (error) {
|
|
||||||
error.isHandled = true;
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateUserRoles = (roleUids: string[], userId: number, orgId?: number) => {
|
|
||||||
let userRolesUrl = `/api/access-control/users/${userId}/roles`;
|
|
||||||
if (orgId) {
|
|
||||||
userRolesUrl += `?targetOrgId=${orgId}`;
|
|
||||||
}
|
|
||||||
return getBackendSrv().put(userRolesUrl, {
|
|
||||||
orgId,
|
|
||||||
roleUids,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
78
public/app/core/components/RolePicker/api.ts
Normal file
78
public/app/core/components/RolePicker/api.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
|
import { Role } from 'app/types';
|
||||||
|
|
||||||
|
export const fetchRoleOptions = async (orgId?: number, query?: string): Promise<Role[]> => {
|
||||||
|
let rolesUrl = '/api/access-control/roles?delegatable=true';
|
||||||
|
if (orgId) {
|
||||||
|
rolesUrl += `&targetOrgId=${orgId}`;
|
||||||
|
}
|
||||||
|
const roles = await getBackendSrv().get(rolesUrl);
|
||||||
|
if (!roles || !roles.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return roles;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchBuiltinRoles = (orgId?: number): Promise<{ [key: string]: Role[] }> => {
|
||||||
|
let builtinRolesUrl = '/api/access-control/builtin-roles';
|
||||||
|
if (orgId) {
|
||||||
|
builtinRolesUrl += `?targetOrgId=${orgId}`;
|
||||||
|
}
|
||||||
|
return getBackendSrv().get(builtinRolesUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchUserRoles = async (userId: number, orgId?: number): Promise<Role[]> => {
|
||||||
|
let userRolesUrl = `/api/access-control/users/${userId}/roles`;
|
||||||
|
if (orgId) {
|
||||||
|
userRolesUrl += `?targetOrgId=${orgId}`;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const roles = await getBackendSrv().get(userRolesUrl);
|
||||||
|
if (!roles || !roles.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return roles;
|
||||||
|
} catch (error) {
|
||||||
|
error.isHandled = true;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateUserRoles = (roleUids: string[], userId: number, orgId?: number) => {
|
||||||
|
let userRolesUrl = `/api/access-control/users/${userId}/roles`;
|
||||||
|
if (orgId) {
|
||||||
|
userRolesUrl += `?targetOrgId=${orgId}`;
|
||||||
|
}
|
||||||
|
return getBackendSrv().put(userRolesUrl, {
|
||||||
|
orgId,
|
||||||
|
roleUids,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchTeamRoles = async (teamId: number, orgId?: number): Promise<Role[]> => {
|
||||||
|
let teamRolesUrl = `/api/access-control/teams/${teamId}/roles`;
|
||||||
|
if (orgId) {
|
||||||
|
teamRolesUrl += `?targetOrgId=${orgId}`;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const roles = await getBackendSrv().get(teamRolesUrl);
|
||||||
|
if (!roles || !roles.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return roles;
|
||||||
|
} catch (error) {
|
||||||
|
error.isHandled = true;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateTeamRoles = (roleUids: string[], teamId: number, orgId?: number) => {
|
||||||
|
let teamRolesUrl = `/api/access-control/teams/${teamId}/roles`;
|
||||||
|
if (orgId) {
|
||||||
|
teamRolesUrl += `?targetOrgId=${orgId}`;
|
||||||
|
}
|
||||||
|
return getBackendSrv().put(teamRolesUrl, {
|
||||||
|
orgId,
|
||||||
|
roleUids,
|
||||||
|
});
|
||||||
|
};
|
@ -67,6 +67,7 @@ describe('rootReducer', () => {
|
|||||||
expect(resultingState.teams).toEqual({
|
expect(resultingState.teams).toEqual({
|
||||||
hasFetched: true,
|
hasFetched: true,
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
|
searchPage: 1,
|
||||||
teams,
|
teams,
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
@ -81,6 +82,7 @@ describe('rootReducer', () => {
|
|||||||
teams: {
|
teams: {
|
||||||
hasFetched: true,
|
hasFetched: true,
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
|
searchPage: 1,
|
||||||
teams,
|
teams,
|
||||||
},
|
},
|
||||||
} as StoreState;
|
} as StoreState;
|
||||||
|
@ -4,7 +4,8 @@ import { OrgRolePicker } from '../admin/OrgRolePicker';
|
|||||||
import { Button, ConfirmModal } from '@grafana/ui';
|
import { Button, ConfirmModal } from '@grafana/ui';
|
||||||
import { OrgRole } from '@grafana/data';
|
import { OrgRole } from '@grafana/data';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
import { fetchBuiltinRoles, fetchRoleOptions, UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker';
|
import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker';
|
||||||
|
import { fetchBuiltinRoles, fetchRoleOptions } from 'app/core/components/RolePicker/api';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
serviceAccounts: OrgServiceAccount[];
|
serviceAccounts: OrgServiceAccount[];
|
||||||
|
@ -6,7 +6,7 @@ import { getMockTeam, getMultipleMockTeams } from './__mocks__/teamMocks';
|
|||||||
import { contextSrv, User } from 'app/core/services/context_srv';
|
import { contextSrv, User } from 'app/core/services/context_srv';
|
||||||
import { NavModel } from '@grafana/data';
|
import { NavModel } from '@grafana/data';
|
||||||
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
|
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
|
||||||
import { setSearchQuery } from './state/reducers';
|
import { setSearchQuery, setTeamsSearchPage } from './state/reducers';
|
||||||
|
|
||||||
jest.mock('app/core/config', () => {
|
jest.mock('app/core/config', () => {
|
||||||
return {
|
return {
|
||||||
@ -28,7 +28,9 @@ const setup = (propOverrides?: object) => {
|
|||||||
loadTeams: jest.fn(),
|
loadTeams: jest.fn(),
|
||||||
deleteTeam: jest.fn(),
|
deleteTeam: jest.fn(),
|
||||||
setSearchQuery: mockToolkitActionCreator(setSearchQuery),
|
setSearchQuery: mockToolkitActionCreator(setSearchQuery),
|
||||||
|
setTeamsSearchPage: mockToolkitActionCreator(setTeamsSearchPage),
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
|
searchPage: 1,
|
||||||
teamsCount: 0,
|
teamsCount: 0,
|
||||||
hasFetched: false,
|
hasFetched: false,
|
||||||
editorsCanAdmin: false,
|
editorsCanAdmin: false,
|
||||||
|
@ -1,39 +1,62 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import Page from 'app/core/components/Page/Page';
|
import Page from 'app/core/components/Page/Page';
|
||||||
import { DeleteButton, LinkButton, FilterInput } from '@grafana/ui';
|
import { DeleteButton, LinkButton, FilterInput, VerticalGroup, HorizontalGroup, Pagination } from '@grafana/ui';
|
||||||
import { NavModel } from '@grafana/data';
|
import { NavModel } from '@grafana/data';
|
||||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||||
import { AccessControlAction, StoreState, Team } from 'app/types';
|
import { AccessControlAction, Role, StoreState, Team } from 'app/types';
|
||||||
import { deleteTeam, loadTeams } from './state/actions';
|
import { deleteTeam, loadTeams } from './state/actions';
|
||||||
import { getSearchQuery, getTeams, getTeamsCount, isPermissionTeamAdmin } from './state/selectors';
|
import { getSearchQuery, getTeams, getTeamsCount, getTeamsSearchPage, isPermissionTeamAdmin } from './state/selectors';
|
||||||
import { getNavModel } from 'app/core/selectors/navModel';
|
import { getNavModel } from 'app/core/selectors/navModel';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import { contextSrv, User } from 'app/core/services/context_srv';
|
import { contextSrv, User } from 'app/core/services/context_srv';
|
||||||
import { connectWithCleanUp } from '../../core/components/connectWithCleanUp';
|
import { connectWithCleanUp } from '../../core/components/connectWithCleanUp';
|
||||||
import { setSearchQuery } from './state/reducers';
|
import { setSearchQuery, setTeamsSearchPage } from './state/reducers';
|
||||||
|
import { TeamRolePicker } from 'app/core/components/RolePicker/TeamRolePicker';
|
||||||
|
import { fetchRoleOptions } from 'app/core/components/RolePicker/api';
|
||||||
|
|
||||||
|
const pageLimit = 30;
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
navModel: NavModel;
|
navModel: NavModel;
|
||||||
teams: Team[];
|
teams: Team[];
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
|
searchPage: number;
|
||||||
teamsCount: number;
|
teamsCount: number;
|
||||||
hasFetched: boolean;
|
hasFetched: boolean;
|
||||||
loadTeams: typeof loadTeams;
|
loadTeams: typeof loadTeams;
|
||||||
deleteTeam: typeof deleteTeam;
|
deleteTeam: typeof deleteTeam;
|
||||||
setSearchQuery: typeof setSearchQuery;
|
setSearchQuery: typeof setSearchQuery;
|
||||||
|
setTeamsSearchPage: typeof setTeamsSearchPage;
|
||||||
editorsCanAdmin: boolean;
|
editorsCanAdmin: boolean;
|
||||||
signedInUser: User;
|
signedInUser: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TeamList extends PureComponent<Props, any> {
|
export interface State {
|
||||||
|
roleOptions: Role[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TeamList extends PureComponent<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { roleOptions: [] };
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.fetchTeams();
|
this.fetchTeams();
|
||||||
|
if (contextSrv.accessControlEnabled()) {
|
||||||
|
this.fetchRoleOptions();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchTeams() {
|
async fetchTeams() {
|
||||||
await this.props.loadTeams();
|
await this.props.loadTeams();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchRoleOptions() {
|
||||||
|
const roleOptions = await fetchRoleOptions();
|
||||||
|
this.setState({ roleOptions });
|
||||||
|
}
|
||||||
|
|
||||||
deleteTeam = (team: Team) => {
|
deleteTeam = (team: Team) => {
|
||||||
this.props.deleteTeam(team.id);
|
this.props.deleteTeam(team.id);
|
||||||
};
|
};
|
||||||
@ -66,6 +89,11 @@ export class TeamList extends PureComponent<Props, any> {
|
|||||||
<td className="link-td">
|
<td className="link-td">
|
||||||
<a href={teamUrl}>{team.memberCount}</a>
|
<a href={teamUrl}>{team.memberCount}</a>
|
||||||
</td>
|
</td>
|
||||||
|
{contextSrv.accessControlEnabled() && (
|
||||||
|
<td>
|
||||||
|
<TeamRolePicker teamId={team.id} getRoleOptions={async () => this.state.roleOptions} />
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
<td className="text-right">
|
<td className="text-right">
|
||||||
<DeleteButton
|
<DeleteButton
|
||||||
aria-label="Delete team"
|
aria-label="Delete team"
|
||||||
@ -93,11 +121,18 @@ export class TeamList extends PureComponent<Props, any> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPaginatedTeams = (teams: Team[]) => {
|
||||||
|
const offset = (this.props.searchPage - 1) * pageLimit;
|
||||||
|
return teams.slice(offset, offset + pageLimit);
|
||||||
|
};
|
||||||
|
|
||||||
renderTeamList() {
|
renderTeamList() {
|
||||||
const { teams, searchQuery, editorsCanAdmin } = this.props;
|
const { teams, searchQuery, editorsCanAdmin, searchPage, setTeamsSearchPage } = this.props;
|
||||||
const teamAdmin = contextSrv.hasRole('Admin') || (editorsCanAdmin && contextSrv.hasRole('Editor'));
|
const teamAdmin = contextSrv.hasRole('Admin') || (editorsCanAdmin && contextSrv.hasRole('Editor'));
|
||||||
const canCreate = contextSrv.hasAccess(AccessControlAction.ActionTeamsCreate, teamAdmin);
|
const canCreate = contextSrv.hasAccess(AccessControlAction.ActionTeamsCreate, teamAdmin);
|
||||||
const newTeamHref = canCreate ? 'org/teams/new' : '#';
|
const newTeamHref = canCreate ? 'org/teams/new' : '#';
|
||||||
|
const paginatedTeams = this.getPaginatedTeams(teams);
|
||||||
|
const totalPages = Math.ceil(teams.length / pageLimit);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -112,6 +147,7 @@ export class TeamList extends PureComponent<Props, any> {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="admin-list-table">
|
<div className="admin-list-table">
|
||||||
|
<VerticalGroup spacing="md">
|
||||||
<table className="filter-table filter-table--hover form-inline">
|
<table className="filter-table filter-table--hover form-inline">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -119,11 +155,21 @@ export class TeamList extends PureComponent<Props, any> {
|
|||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
<th>Members</th>
|
<th>Members</th>
|
||||||
|
{contextSrv.accessControlEnabled() && <th>Roles</th>}
|
||||||
<th style={{ width: '1%' }} />
|
<th style={{ width: '1%' }} />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>{teams.map((team) => this.renderTeam(team))}</tbody>
|
<tbody>{paginatedTeams.map((team) => this.renderTeam(team))}</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<HorizontalGroup justify="flex-end">
|
||||||
|
<Pagination
|
||||||
|
onNavigate={setTeamsSearchPage}
|
||||||
|
currentPage={searchPage}
|
||||||
|
numberOfPages={totalPages}
|
||||||
|
hideWhenSinglePage={true}
|
||||||
|
/>
|
||||||
|
</HorizontalGroup>
|
||||||
|
</VerticalGroup>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -159,6 +205,7 @@ function mapStateToProps(state: StoreState) {
|
|||||||
navModel: getNavModel(state.navIndex, 'teams'),
|
navModel: getNavModel(state.navIndex, 'teams'),
|
||||||
teams: getTeams(state.teams),
|
teams: getTeams(state.teams),
|
||||||
searchQuery: getSearchQuery(state.teams),
|
searchQuery: getSearchQuery(state.teams),
|
||||||
|
searchPage: getTeamsSearchPage(state.teams),
|
||||||
teamsCount: getTeamsCount(state.teams),
|
teamsCount: getTeamsCount(state.teams),
|
||||||
hasFetched: state.teams.hasFetched,
|
hasFetched: state.teams.hasFetched,
|
||||||
editorsCanAdmin: config.editorsCanAdmin, // this makes the feature toggle mockable/controllable from tests,
|
editorsCanAdmin: config.editorsCanAdmin, // this makes the feature toggle mockable/controllable from tests,
|
||||||
@ -170,6 +217,7 @@ const mapDispatchToProps = {
|
|||||||
loadTeams,
|
loadTeams,
|
||||||
deleteTeam,
|
deleteTeam,
|
||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
|
setTeamsSearchPage,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connectWithCleanUp(mapStateToProps, mapDispatchToProps, (state) => state.teams)(TeamList);
|
export default connectWithCleanUp(mapStateToProps, mapDispatchToProps, (state) => state.teams)(TeamList);
|
||||||
|
@ -56,6 +56,9 @@ exports[`Render should render teams table 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="admin-list-table"
|
className="admin-list-table"
|
||||||
|
>
|
||||||
|
<VerticalGroup
|
||||||
|
spacing="md"
|
||||||
>
|
>
|
||||||
<table
|
<table
|
||||||
className="filter-table filter-table--hover form-inline"
|
className="filter-table filter-table--hover form-inline"
|
||||||
@ -354,6 +357,17 @@ exports[`Render should render teams table 1`] = `
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<HorizontalGroup
|
||||||
|
justify="flex-end"
|
||||||
|
>
|
||||||
|
<Pagination
|
||||||
|
currentPage={1}
|
||||||
|
hideWhenSinglePage={true}
|
||||||
|
numberOfPages={1}
|
||||||
|
onNavigate={[MockFunction]}
|
||||||
|
/>
|
||||||
|
</HorizontalGroup>
|
||||||
|
</VerticalGroup>
|
||||||
</div>
|
</div>
|
||||||
</PageContents>
|
</PageContents>
|
||||||
</Page>
|
</Page>
|
||||||
@ -396,6 +410,9 @@ exports[`Render when feature toggle editorsCanAdmin is turned on and signedin us
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="admin-list-table"
|
className="admin-list-table"
|
||||||
|
>
|
||||||
|
<VerticalGroup
|
||||||
|
spacing="md"
|
||||||
>
|
>
|
||||||
<table
|
<table
|
||||||
className="filter-table filter-table--hover form-inline"
|
className="filter-table filter-table--hover form-inline"
|
||||||
@ -478,6 +495,17 @@ exports[`Render when feature toggle editorsCanAdmin is turned on and signedin us
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<HorizontalGroup
|
||||||
|
justify="flex-end"
|
||||||
|
>
|
||||||
|
<Pagination
|
||||||
|
currentPage={1}
|
||||||
|
hideWhenSinglePage={true}
|
||||||
|
numberOfPages={1}
|
||||||
|
onNavigate={[MockFunction]}
|
||||||
|
/>
|
||||||
|
</HorizontalGroup>
|
||||||
|
</VerticalGroup>
|
||||||
</div>
|
</div>
|
||||||
</PageContents>
|
</PageContents>
|
||||||
</Page>
|
</Page>
|
||||||
@ -520,6 +548,9 @@ exports[`Render when feature toggle editorsCanAdmin is turned on and signedin us
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="admin-list-table"
|
className="admin-list-table"
|
||||||
|
>
|
||||||
|
<VerticalGroup
|
||||||
|
spacing="md"
|
||||||
>
|
>
|
||||||
<table
|
<table
|
||||||
className="filter-table filter-table--hover form-inline"
|
className="filter-table filter-table--hover form-inline"
|
||||||
@ -602,6 +633,17 @@ exports[`Render when feature toggle editorsCanAdmin is turned on and signedin us
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<HorizontalGroup
|
||||||
|
justify="flex-end"
|
||||||
|
>
|
||||||
|
<Pagination
|
||||||
|
currentPage={1}
|
||||||
|
hideWhenSinglePage={true}
|
||||||
|
numberOfPages={1}
|
||||||
|
onNavigate={[MockFunction]}
|
||||||
|
/>
|
||||||
|
</HorizontalGroup>
|
||||||
|
</VerticalGroup>
|
||||||
</div>
|
</div>
|
||||||
</PageContents>
|
</PageContents>
|
||||||
</Page>
|
</Page>
|
||||||
|
@ -2,7 +2,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|||||||
|
|
||||||
import { Team, TeamGroup, TeamMember, TeamsState, TeamState } from 'app/types';
|
import { Team, TeamGroup, TeamMember, TeamsState, TeamState } from 'app/types';
|
||||||
|
|
||||||
export const initialTeamsState: TeamsState = { teams: [], searchQuery: '', hasFetched: false };
|
export const initialTeamsState: TeamsState = { teams: [], searchQuery: '', searchPage: 1, hasFetched: false };
|
||||||
|
|
||||||
const teamsSlice = createSlice({
|
const teamsSlice = createSlice({
|
||||||
name: 'teams',
|
name: 'teams',
|
||||||
@ -12,12 +12,15 @@ const teamsSlice = createSlice({
|
|||||||
return { ...state, hasFetched: true, teams: action.payload };
|
return { ...state, hasFetched: true, teams: action.payload };
|
||||||
},
|
},
|
||||||
setSearchQuery: (state, action: PayloadAction<string>): TeamsState => {
|
setSearchQuery: (state, action: PayloadAction<string>): TeamsState => {
|
||||||
return { ...state, searchQuery: action.payload };
|
return { ...state, searchQuery: action.payload, searchPage: initialTeamsState.searchPage };
|
||||||
|
},
|
||||||
|
setTeamsSearchPage: (state, action: PayloadAction<number>): TeamsState => {
|
||||||
|
return { ...state, searchPage: action.payload };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { teamsLoaded, setSearchQuery } = teamsSlice.actions;
|
export const { teamsLoaded, setSearchQuery, setTeamsSearchPage } = teamsSlice.actions;
|
||||||
|
|
||||||
export const teamsReducer = teamsSlice.reducer;
|
export const teamsReducer = teamsSlice.reducer;
|
||||||
|
|
||||||
|
@ -8,14 +8,14 @@ describe('Teams selectors', () => {
|
|||||||
const mockTeams = getMultipleMockTeams(5);
|
const mockTeams = getMultipleMockTeams(5);
|
||||||
|
|
||||||
it('should return teams if no search query', () => {
|
it('should return teams if no search query', () => {
|
||||||
const mockState: TeamsState = { teams: mockTeams, searchQuery: '', hasFetched: false };
|
const mockState: TeamsState = { teams: mockTeams, searchQuery: '', searchPage: 1, hasFetched: false };
|
||||||
|
|
||||||
const teams = getTeams(mockState);
|
const teams = getTeams(mockState);
|
||||||
expect(teams).toEqual(mockTeams);
|
expect(teams).toEqual(mockTeams);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should filter teams if search query', () => {
|
it('Should filter teams if search query', () => {
|
||||||
const mockState: TeamsState = { teams: mockTeams, searchQuery: '5', hasFetched: false };
|
const mockState: TeamsState = { teams: mockTeams, searchQuery: '5', searchPage: 1, hasFetched: false };
|
||||||
|
|
||||||
const teams = getTeams(mockState);
|
const teams = getTeams(mockState);
|
||||||
expect(teams.length).toEqual(1);
|
expect(teams.length).toEqual(1);
|
||||||
|
@ -5,6 +5,7 @@ export const getSearchQuery = (state: TeamsState) => state.searchQuery;
|
|||||||
export const getSearchMemberQuery = (state: TeamState) => state.searchMemberQuery;
|
export const getSearchMemberQuery = (state: TeamState) => state.searchMemberQuery;
|
||||||
export const getTeamGroups = (state: TeamState) => state.groups;
|
export const getTeamGroups = (state: TeamState) => state.groups;
|
||||||
export const getTeamsCount = (state: TeamsState) => state.teams.length;
|
export const getTeamsCount = (state: TeamsState) => state.teams.length;
|
||||||
|
export const getTeamsSearchPage = (state: TeamsState) => state.searchPage;
|
||||||
|
|
||||||
export const getTeam = (state: TeamState, currentTeamId: any): Team | null => {
|
export const getTeam = (state: TeamState, currentTeamId: any): Team | null => {
|
||||||
if (state.team.id === parseInt(currentTeamId, 10)) {
|
if (state.team.id === parseInt(currentTeamId, 10)) {
|
||||||
|
@ -4,7 +4,8 @@ import { OrgRolePicker } from '../admin/OrgRolePicker';
|
|||||||
import { Button, ConfirmModal } from '@grafana/ui';
|
import { Button, ConfirmModal } from '@grafana/ui';
|
||||||
import { OrgRole } from '@grafana/data';
|
import { OrgRole } from '@grafana/data';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
import { fetchBuiltinRoles, fetchRoleOptions, UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker';
|
import { fetchBuiltinRoles, fetchRoleOptions } from 'app/core/components/RolePicker/api';
|
||||||
|
import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
users: OrgUser[];
|
users: OrgUser[];
|
||||||
|
@ -28,6 +28,7 @@ export interface TeamGroup {
|
|||||||
export interface TeamsState {
|
export interface TeamsState {
|
||||||
teams: Team[];
|
teams: Team[];
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
|
searchPage: number;
|
||||||
hasFetched: boolean;
|
hasFetched: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user