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