mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Nested folders: move permissions to a drawer (#68476)
* move permissions to a drawer when nested folders is enabled * only show count when resource is folder * Extract descendant count out into its own component * remove label
This commit is contained in:
parent
3ffff632be
commit
e27e71ee59
@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { Button, Form, HorizontalGroup, Select } from '@grafana/ui';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { Button, Form, Select } from '@grafana/ui';
|
||||
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
|
||||
import { TeamPicker } from 'app/core/components/Select/TeamPicker';
|
||||
import { UserPicker } from 'app/core/components/Select/UserPicker';
|
||||
@ -16,7 +17,7 @@ export interface Props {
|
||||
onAdd: (state: SetPermission) => void;
|
||||
}
|
||||
|
||||
export const AddPermission = ({ title = 'Add Permission For', permissions, assignments, onAdd, onCancel }: Props) => {
|
||||
export const AddPermission = ({ title = 'Add permission for', permissions, assignments, onAdd, onCancel }: Props) => {
|
||||
const [target, setPermissionTarget] = useState<PermissionTarget>(PermissionTarget.None);
|
||||
const [teamId, setTeamId] = useState(0);
|
||||
const [userId, setUserId] = useState(0);
|
||||
@ -59,35 +60,32 @@ export const AddPermission = ({ title = 'Add Permission For', permissions, assig
|
||||
onSubmit={() => onAdd({ userId, teamId, builtInRole, permission, target })}
|
||||
>
|
||||
{() => (
|
||||
<HorizontalGroup>
|
||||
<Stack gap={1} direction="row">
|
||||
<Select
|
||||
aria-label="Role to add new permission to"
|
||||
value={target}
|
||||
options={targetOptions}
|
||||
onChange={(v) => setPermissionTarget(v.value!)}
|
||||
disabled={targetOptions.length === 0}
|
||||
width="auto"
|
||||
/>
|
||||
|
||||
{target === PermissionTarget.User && (
|
||||
<UserPicker onSelected={(u) => setUserId(u.value || 0)} className={'width-20'} />
|
||||
)}
|
||||
{target === PermissionTarget.User && <UserPicker onSelected={(u) => setUserId(u?.value || 0)} />}
|
||||
|
||||
{target === PermissionTarget.Team && (
|
||||
<TeamPicker onSelected={(t) => setTeamId(t.value?.id || 0)} className={'width-20'} />
|
||||
)}
|
||||
{target === PermissionTarget.Team && <TeamPicker onSelected={(t) => setTeamId(t.value?.id || 0)} />}
|
||||
|
||||
{target === PermissionTarget.BuiltInRole && (
|
||||
<Select
|
||||
aria-label={'Built-in role picker'}
|
||||
options={Object.values(OrgRole).map((r) => ({ value: r, label: r }))}
|
||||
onChange={(r) => setBuiltinRole(r.value || '')}
|
||||
width={40}
|
||||
width="auto"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Select
|
||||
aria-label="Permission Level"
|
||||
width={25}
|
||||
width="auto"
|
||||
value={permissions.find((p) => p === permission)}
|
||||
options={permissions.map((p) => ({ label: p, value: p }))}
|
||||
onChange={(v) => setPermission(v.value || '')}
|
||||
@ -95,7 +93,7 @@ export const AddPermission = ({ title = 'Add Permission For', permissions, assig
|
||||
<Button type="submit" disabled={!isValid()}>
|
||||
Save
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
|
@ -14,19 +14,16 @@ interface Props {
|
||||
|
||||
export const PermissionListItem = ({ item, permissionLevels, canSet, onRemove, onChange }: Props) => (
|
||||
<tr>
|
||||
<td style={{ width: '1%' }}>{getAvatar(item)}</td>
|
||||
<td style={{ width: '90%' }}>{getDescription(item)}</td>
|
||||
<td>{getAvatar(item)}</td>
|
||||
<td>{getDescription(item)}</td>
|
||||
<td>{item.isInherited && <em className="muted no-wrap">Inherited from folder</em>}</td>
|
||||
<td>
|
||||
<div className="gf-form">
|
||||
<Select
|
||||
className="width-20"
|
||||
disabled={!canSet || !item.isManaged}
|
||||
onChange={(p) => onChange(item, p.value!)}
|
||||
value={permissionLevels.find((p) => p === item.permission)}
|
||||
options={permissionLevels.map((p) => ({ value: p, label: p }))}
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
disabled={!canSet || !item.isManaged}
|
||||
onChange={(p) => onChange(item, p.value!)}
|
||||
value={permissionLevels.find((p) => p === item.permission)}
|
||||
options={permissionLevels.map((p) => ({ value: p, label: p }))}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Tooltip content={getPermissionInfo(item)}>
|
||||
|
@ -1,9 +1,13 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { sortBy } from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { Button } from '@grafana/ui';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Button, useStyles2 } from '@grafana/ui';
|
||||
import { SlideDown } from 'app/core/components/Animations/SlideDown';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { DescendantCount } from 'app/features/browse-dashboards/components/BrowseActions/DescendantCount';
|
||||
|
||||
import { AddPermission } from './AddPermission';
|
||||
import { PermissionList } from './PermissionList';
|
||||
@ -42,6 +46,7 @@ export const Permissions = ({
|
||||
canSetPermissions,
|
||||
addPermissionTitle,
|
||||
}: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const [isAdding, setIsAdding] = useState(false);
|
||||
const [items, setItems] = useState<ResourcePermission[]>([]);
|
||||
const [desc, setDesc] = useState(INITIAL_DESCRIPTION);
|
||||
@ -127,63 +132,74 @@ export const Permissions = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="page-action-bar">
|
||||
<h3 className="page-sub-heading">{title}</h3>
|
||||
<div className="page-action-bar__spacer" />
|
||||
{canSetPermissions && (
|
||||
<Button variant={'primary'} key="add-permission" onClick={() => setIsAdding(true)}>
|
||||
{buttonLabel}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<SlideDown in={isAdding}>
|
||||
<AddPermission
|
||||
title={addPermissionTitle}
|
||||
onAdd={onAdd}
|
||||
permissions={desc.permissions}
|
||||
assignments={desc.assignments}
|
||||
onCancel={() => setIsAdding(false)}
|
||||
{config.featureToggles.nestedFolders && resource === 'folders' && (
|
||||
<>
|
||||
This will change permissions for this folder and all its descendants. In total, this will affect:
|
||||
<DescendantCount
|
||||
selectedItems={{
|
||||
folder: { [resourceId]: true },
|
||||
dashboard: {},
|
||||
panel: {},
|
||||
$all: false,
|
||||
}}
|
||||
/>
|
||||
</SlideDown>
|
||||
{items.length === 0 && (
|
||||
<table className="filter-table gf-form-group">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{emptyLabel}</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
<PermissionList
|
||||
title="Role"
|
||||
items={builtInRoles}
|
||||
compareKey={'builtInRole'}
|
||||
permissionLevels={desc.permissions}
|
||||
onChange={onChange}
|
||||
onRemove={onRemove}
|
||||
canSet={canSetPermissions}
|
||||
</>
|
||||
)}
|
||||
{canSetPermissions && (
|
||||
<Button
|
||||
className={styles.addPermissionButton}
|
||||
variant={'primary'}
|
||||
key="add-permission"
|
||||
onClick={() => setIsAdding(true)}
|
||||
>
|
||||
{buttonLabel}
|
||||
</Button>
|
||||
)}
|
||||
<SlideDown in={isAdding}>
|
||||
<AddPermission
|
||||
title={addPermissionTitle}
|
||||
onAdd={onAdd}
|
||||
permissions={desc.permissions}
|
||||
assignments={desc.assignments}
|
||||
onCancel={() => setIsAdding(false)}
|
||||
/>
|
||||
<PermissionList
|
||||
title="User"
|
||||
items={users}
|
||||
compareKey={'userLogin'}
|
||||
permissionLevels={desc.permissions}
|
||||
onChange={onChange}
|
||||
onRemove={onRemove}
|
||||
canSet={canSetPermissions}
|
||||
/>
|
||||
<PermissionList
|
||||
title="Team"
|
||||
items={teams}
|
||||
compareKey={'team'}
|
||||
permissionLevels={desc.permissions}
|
||||
onChange={onChange}
|
||||
onRemove={onRemove}
|
||||
canSet={canSetPermissions}
|
||||
/>
|
||||
</div>
|
||||
</SlideDown>
|
||||
{items.length === 0 && (
|
||||
<table className="filter-table gf-form-group">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{emptyLabel}</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
<PermissionList
|
||||
title="Role"
|
||||
items={builtInRoles}
|
||||
compareKey={'builtInRole'}
|
||||
permissionLevels={desc.permissions}
|
||||
onChange={onChange}
|
||||
onRemove={onRemove}
|
||||
canSet={canSetPermissions}
|
||||
/>
|
||||
<PermissionList
|
||||
title="User"
|
||||
items={users}
|
||||
compareKey={'userLogin'}
|
||||
permissionLevels={desc.permissions}
|
||||
onChange={onChange}
|
||||
onRemove={onRemove}
|
||||
canSet={canSetPermissions}
|
||||
/>
|
||||
<PermissionList
|
||||
title="Team"
|
||||
items={teams}
|
||||
compareKey={'team'}
|
||||
permissionLevels={desc.permissions}
|
||||
onChange={onChange}
|
||||
onRemove={onRemove}
|
||||
canSet={canSetPermissions}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -217,3 +233,14 @@ const setPermission = (
|
||||
permission: string
|
||||
): Promise<void> =>
|
||||
getBackendSrv().post(`/api/access-control/${resource}/${resourceId}/${type}/${typeId}`, { permission });
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
breakdown: css({
|
||||
...theme.typography.bodySmall,
|
||||
color: theme.colors.text.secondary,
|
||||
marginBottom: theme.spacing(2),
|
||||
}),
|
||||
addPermissionButton: css({
|
||||
marginBottom: theme.spacing(2),
|
||||
}),
|
||||
});
|
||||
|
@ -17,6 +17,7 @@ import { BrowseActions } from './components/BrowseActions/BrowseActions';
|
||||
import { BrowseFilters } from './components/BrowseFilters';
|
||||
import { BrowseView } from './components/BrowseView';
|
||||
import { CreateNewButton } from './components/CreateNewButton';
|
||||
import { FolderActionsButton } from './components/FolderActionsButton';
|
||||
import { SearchView } from './components/SearchView';
|
||||
import { getFolderPermissions } from './permissions';
|
||||
import { setAllSelection, useHasSelection } from './state';
|
||||
@ -82,13 +83,16 @@ const BrowseDashboardsPage = memo(({ match }: Props) => {
|
||||
navId="dashboards/browse"
|
||||
pageNav={navModel}
|
||||
actions={
|
||||
(canCreateDashboards || canCreateFolder) && (
|
||||
<CreateNewButton
|
||||
inFolder={folderUID}
|
||||
canCreateDashboard={canCreateDashboards}
|
||||
canCreateFolder={canCreateFolder}
|
||||
/>
|
||||
)
|
||||
<>
|
||||
{folderDTO && <FolderActionsButton folder={folderDTO} />}
|
||||
{(canCreateDashboards || canCreateFolder) && (
|
||||
<CreateNewButton
|
||||
inFolder={folderUID}
|
||||
canCreateDashboard={canCreateDashboards}
|
||||
canCreateFolder={canCreateFolder}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Page.Contents className={styles.pageContents}>
|
||||
|
@ -2,12 +2,11 @@ import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Alert, ConfirmModal, Spinner, useStyles2 } from '@grafana/ui';
|
||||
import { ConfirmModal, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { useGetAffectedItemsQuery } from '../../api/browseDashboardsAPI';
|
||||
import { DashboardTreeSelection } from '../../types';
|
||||
|
||||
import { buildBreakdownString } from './utils';
|
||||
import { DescendantCount } from './DescendantCount';
|
||||
|
||||
export interface Props {
|
||||
isOpen: boolean;
|
||||
@ -18,7 +17,6 @@ export interface Props {
|
||||
|
||||
export const DeleteModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { data, isFetching, isLoading, error } = useGetAffectedItemsQuery(selectedItems);
|
||||
|
||||
const onDelete = () => {
|
||||
onConfirm();
|
||||
@ -30,13 +28,7 @@ export const DeleteModal = ({ onConfirm, onDismiss, selectedItems, ...props }: P
|
||||
body={
|
||||
<div className={styles.modalBody}>
|
||||
This action will delete the following content:
|
||||
<div className={styles.breakdown}>
|
||||
<>
|
||||
{data && buildBreakdownString(data.folder, data.dashboard, data.libraryPanel, data.alertRule)}
|
||||
{(isFetching || isLoading) && <Spinner size={12} />}
|
||||
{error && <Alert severity="error" title="Unable to retrieve descendant information" />}
|
||||
</>
|
||||
</div>
|
||||
<DescendantCount selectedItems={selectedItems} />
|
||||
</div>
|
||||
}
|
||||
confirmationText="Delete"
|
||||
@ -50,11 +42,6 @@ export const DeleteModal = ({ onConfirm, onDismiss, selectedItems, ...props }: P
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
breakdown: css({
|
||||
...theme.typography.bodySmall,
|
||||
color: theme.colors.text.secondary,
|
||||
marginBottom: theme.spacing(2),
|
||||
}),
|
||||
modalBody: css({
|
||||
...theme.typography.body,
|
||||
}),
|
||||
|
@ -0,0 +1,37 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Alert, Spinner, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { useGetAffectedItemsQuery } from '../../api/browseDashboardsAPI';
|
||||
import { DashboardTreeSelection } from '../../types';
|
||||
|
||||
import { buildBreakdownString } from './utils';
|
||||
|
||||
export interface Props {
|
||||
selectedItems: DashboardTreeSelection;
|
||||
}
|
||||
|
||||
export const DescendantCount = ({ selectedItems }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { data, isFetching, isLoading, error } = useGetAffectedItemsQuery(selectedItems);
|
||||
|
||||
return (
|
||||
<div className={styles.breakdown}>
|
||||
<>
|
||||
{data && buildBreakdownString(data.folder, data.dashboard, data.libraryPanel, data.alertRule)}
|
||||
{(isFetching || isLoading) && <Spinner size={12} />}
|
||||
{error && <Alert severity="error" title="Unable to retrieve descendant information" />}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
breakdown: css({
|
||||
...theme.typography.bodySmall,
|
||||
color: theme.colors.text.secondary,
|
||||
marginBottom: theme.spacing(2),
|
||||
}),
|
||||
});
|
@ -1,14 +1,11 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Alert, Button, Field, Modal, Spinner, useStyles2 } from '@grafana/ui';
|
||||
import { Alert, Button, Field, Modal } from '@grafana/ui';
|
||||
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
||||
|
||||
import { useGetAffectedItemsQuery } from '../../api/browseDashboardsAPI';
|
||||
import { DashboardTreeSelection } from '../../types';
|
||||
|
||||
import { buildBreakdownString } from './utils';
|
||||
import { DescendantCount } from './DescendantCount';
|
||||
|
||||
export interface Props {
|
||||
isOpen: boolean;
|
||||
@ -19,9 +16,7 @@ export interface Props {
|
||||
|
||||
export const MoveModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Props) => {
|
||||
const [moveTarget, setMoveTarget] = useState<string>();
|
||||
const styles = useStyles2(getStyles);
|
||||
const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]);
|
||||
const { data, isFetching, isLoading, error } = useGetAffectedItemsQuery(selectedItems);
|
||||
|
||||
const onMove = () => {
|
||||
if (moveTarget !== undefined) {
|
||||
@ -34,13 +29,7 @@ export const MoveModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Pro
|
||||
<Modal title="Move" onDismiss={onDismiss} {...props}>
|
||||
{selectedFolders.length > 0 && <Alert severity="warning" title="Moving this item may change its permissions." />}
|
||||
This action will move the following content:
|
||||
<div className={styles.breakdown}>
|
||||
<>
|
||||
{data && buildBreakdownString(data.folder, data.dashboard, data.libraryPanel, data.alertRule)}
|
||||
{(isFetching || isLoading) && <Spinner size={12} />}
|
||||
{error && <Alert severity="error" title="Unable to retrieve descendant information" />}
|
||||
</>
|
||||
</div>
|
||||
<DescendantCount selectedItems={selectedItems} />
|
||||
<Field label="Folder name">
|
||||
<FolderPicker allowEmpty onChange={({ uid }) => setMoveTarget(uid)} />
|
||||
</Field>
|
||||
@ -55,11 +44,3 @@ export const MoveModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Pro
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
breakdown: css({
|
||||
...theme.typography.bodySmall,
|
||||
color: theme.colors.text.secondary,
|
||||
marginBottom: theme.spacing(2),
|
||||
}),
|
||||
});
|
||||
|
@ -0,0 +1,42 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui';
|
||||
import { Permissions } from 'app/core/components/AccessControl';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { AccessControlAction, FolderDTO } from 'app/types';
|
||||
|
||||
interface Props {
|
||||
folder: FolderDTO;
|
||||
}
|
||||
|
||||
export function FolderActionsButton({ folder }: Props) {
|
||||
const [showPermissionsDrawer, setShowPermissionsDrawer] = useState(false);
|
||||
const canSetPermissions = contextSrv.hasPermission(AccessControlAction.FoldersPermissionsWrite);
|
||||
const menu = (
|
||||
<Menu>
|
||||
<MenuItem onClick={() => setShowPermissionsDrawer(true)} label="Set permissions" />
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown overlay={menu}>
|
||||
<Button variant="secondary">
|
||||
Folder actions
|
||||
<Icon name="angle-down" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
{showPermissionsDrawer && (
|
||||
<Drawer
|
||||
title="Permissions"
|
||||
subtitle={folder.title}
|
||||
scrollableContent
|
||||
onClose={() => setShowPermissionsDrawer(false)}
|
||||
size="md"
|
||||
>
|
||||
<Permissions resource="folders" resourceId={folder.uid} canSetPermissions={canSetPermissions} />
|
||||
</Drawer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -51,14 +51,16 @@ export function buildNavModel(folder: FolderDTO, parents = folder.parents): NavM
|
||||
});
|
||||
}
|
||||
|
||||
if (folder.canAdmin) {
|
||||
model.children!.push({
|
||||
active: false,
|
||||
icon: 'lock',
|
||||
id: getPermissionsTabID(folder.uid),
|
||||
text: 'Permissions',
|
||||
url: `${folder.url}/permissions`,
|
||||
});
|
||||
if (!config.featureToggles.nestedFolders) {
|
||||
if (folder.canAdmin) {
|
||||
model.children!.push({
|
||||
active: false,
|
||||
icon: 'lock',
|
||||
id: getPermissionsTabID(folder.uid),
|
||||
text: 'Permissions',
|
||||
url: `${folder.url}/permissions`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (folder.canSave) {
|
||||
|
@ -153,7 +153,7 @@ export function getAppRoutes(): RouteDescriptor[] {
|
||||
() => import(/* webpackChunkName: "NewDashboardsFolder"*/ 'app/features/folders/components/NewDashboardsFolder')
|
||||
),
|
||||
},
|
||||
{
|
||||
!config.featureToggles.nestedFolders && {
|
||||
path: '/dashboards/f/:uid/:slug/permissions',
|
||||
component: config.rbacEnabled
|
||||
? SafeDynamicImport(
|
||||
|
Loading…
Reference in New Issue
Block a user