mirror of https://github.com/grafana/grafana
Azure Monitor : Add support for the resource picker to be configurable to only select some entry types (#46735)
Co-authored-by: Kevin Yu <kevinwcyu@users.noreply.github.com> Co-authored-by: Andres Martinez Gotor <andres.mgotor@gmail.com> Co-authored-by: Isabella Siu <isabella.siu@grafana.com> Co-authored-by: Sarah Zinger <sarah.zinger@grafana.com>pull/46916/head^2
parent
3c5e68a349
commit
c00f488f89
@ -0,0 +1,31 @@ |
|||||||
|
import { Icon } from '@grafana/ui'; |
||||||
|
import React from 'react'; |
||||||
|
|
||||||
|
import { ResourceRow, ResourceRowType } from './types'; |
||||||
|
|
||||||
|
interface EntryIconProps { |
||||||
|
entry: ResourceRow; |
||||||
|
isOpen: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
export const EntryIcon: React.FC<EntryIconProps> = ({ isOpen, entry: { type } }) => { |
||||||
|
switch (type) { |
||||||
|
case ResourceRowType.Subscription: |
||||||
|
return <Icon name="layer-group" />; |
||||||
|
|
||||||
|
case ResourceRowType.ResourceGroup: |
||||||
|
return <Icon name={isOpen ? 'folder-open' : 'folder'} />; |
||||||
|
|
||||||
|
case ResourceRowType.Resource: |
||||||
|
return <Icon name="cube" />; |
||||||
|
|
||||||
|
case ResourceRowType.VariableGroup: |
||||||
|
return <Icon name="x" />; |
||||||
|
|
||||||
|
case ResourceRowType.Variable: |
||||||
|
return <Icon name="x" />; |
||||||
|
|
||||||
|
default: |
||||||
|
return null; |
||||||
|
} |
||||||
|
}; |
||||||
@ -0,0 +1,29 @@ |
|||||||
|
import { render, screen } from '@testing-library/react'; |
||||||
|
import React from 'react'; |
||||||
|
import { NestedEntry } from './NestedEntry'; |
||||||
|
import { ResourceRowType } from './types'; |
||||||
|
|
||||||
|
const defaultProps = { |
||||||
|
level: 0, |
||||||
|
entry: { id: '123', uri: 'someuri', name: '123', type: ResourceRowType.Resource, typeLabel: '' }, |
||||||
|
isSelected: false, |
||||||
|
isSelectable: false, |
||||||
|
isOpen: false, |
||||||
|
isDisabled: false, |
||||||
|
onToggleCollapse: jest.fn(), |
||||||
|
onSelectedChange: jest.fn(), |
||||||
|
}; |
||||||
|
|
||||||
|
describe('NestedEntry', () => { |
||||||
|
it('should be selectable', () => { |
||||||
|
render(<NestedEntry {...defaultProps} isSelectable={true} />); |
||||||
|
const box = screen.getByRole('checkbox'); |
||||||
|
expect(box).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should not be selectable', () => { |
||||||
|
render(<NestedEntry {...defaultProps} />); |
||||||
|
const box = screen.queryByRole('checkbox'); |
||||||
|
expect(box).not.toBeInTheDocument(); |
||||||
|
}); |
||||||
|
}); |
||||||
@ -0,0 +1,104 @@ |
|||||||
|
import { cx } from '@emotion/css'; |
||||||
|
import { Checkbox, IconButton, useStyles2, useTheme2 } from '@grafana/ui'; |
||||||
|
import React, { useCallback, useEffect } from 'react'; |
||||||
|
|
||||||
|
import { Space } from '../Space'; |
||||||
|
import { EntryIcon } from './EntryIcon'; |
||||||
|
import getStyles from './styles'; |
||||||
|
import { ResourceRow } from './types'; |
||||||
|
|
||||||
|
interface NestedEntryProps { |
||||||
|
level: number; |
||||||
|
entry: ResourceRow; |
||||||
|
isSelected: boolean; |
||||||
|
isSelectable: boolean; |
||||||
|
isOpen: boolean; |
||||||
|
isDisabled: boolean; |
||||||
|
onToggleCollapse: (row: ResourceRow) => void; |
||||||
|
onSelectedChange: (row: ResourceRow, selected: boolean) => void; |
||||||
|
} |
||||||
|
|
||||||
|
export const NestedEntry: React.FC<NestedEntryProps> = ({ |
||||||
|
entry, |
||||||
|
isSelected, |
||||||
|
isDisabled, |
||||||
|
isOpen, |
||||||
|
isSelectable, |
||||||
|
level, |
||||||
|
onToggleCollapse, |
||||||
|
onSelectedChange, |
||||||
|
}) => { |
||||||
|
const theme = useTheme2(); |
||||||
|
const styles = useStyles2(getStyles); |
||||||
|
const hasChildren = !!entry.children; |
||||||
|
// Subscriptions, resource groups, resources, and variables are all selectable, so
|
||||||
|
// the top-level variable group is the only thing that cannot be selected.
|
||||||
|
// const isSelectable = entry.type !== ResourceRowType.VariableGroup;
|
||||||
|
// const isSelectable = selectableEntryTypes?.some((e) => e === entry.type);
|
||||||
|
|
||||||
|
const handleToggleCollapse = useCallback(() => { |
||||||
|
onToggleCollapse(entry); |
||||||
|
}, [onToggleCollapse, entry]); |
||||||
|
|
||||||
|
const handleSelectedChanged = useCallback( |
||||||
|
(ev: React.ChangeEvent<HTMLInputElement>) => { |
||||||
|
const isSelected = ev.target.checked; |
||||||
|
onSelectedChange(entry, isSelected); |
||||||
|
}, |
||||||
|
[entry, onSelectedChange] |
||||||
|
); |
||||||
|
|
||||||
|
const checkboxId = `checkbox_${entry.id}`; |
||||||
|
|
||||||
|
// Scroll to the selected element if it's not in the view
|
||||||
|
// Only do it once, when the component is mounted
|
||||||
|
useEffect(() => { |
||||||
|
if (isSelected) { |
||||||
|
document.getElementById(checkboxId)?.scrollIntoView({ |
||||||
|
behavior: 'smooth', |
||||||
|
block: 'center', |
||||||
|
}); |
||||||
|
} |
||||||
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={styles.nestedEntry} style={{ marginLeft: level * (3 * theme.spacing.gridSize) }}> |
||||||
|
{/* When groups are selectable, I *think* we will want to show a 2-wide space instead |
||||||
|
of the collapse button for leaf rows that have no children to get them to align */} |
||||||
|
|
||||||
|
{hasChildren ? ( |
||||||
|
<IconButton |
||||||
|
className={styles.collapseButton} |
||||||
|
name={isOpen ? 'angle-down' : 'angle-right'} |
||||||
|
aria-label={isOpen ? `Collapse ${entry.name}` : `Expand ${entry.name}`} |
||||||
|
onClick={handleToggleCollapse} |
||||||
|
id={entry.id} |
||||||
|
/> |
||||||
|
) : ( |
||||||
|
<Space layout="inline" h={2} /> |
||||||
|
)} |
||||||
|
|
||||||
|
<Space layout="inline" h={2} /> |
||||||
|
|
||||||
|
{isSelectable && ( |
||||||
|
<> |
||||||
|
<Checkbox |
||||||
|
id={checkboxId} |
||||||
|
onChange={handleSelectedChanged} |
||||||
|
disabled={isDisabled} |
||||||
|
value={isSelected} |
||||||
|
className={styles.nestedRowCheckbox} |
||||||
|
/> |
||||||
|
<Space layout="inline" h={2} /> |
||||||
|
</> |
||||||
|
)} |
||||||
|
|
||||||
|
<EntryIcon entry={entry} isOpen={isOpen} /> |
||||||
|
<Space layout="inline" h={1} /> |
||||||
|
|
||||||
|
<label htmlFor={checkboxId} className={cx(styles.entryContentItem, styles.truncated)}> |
||||||
|
{entry.name} |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
@ -0,0 +1,58 @@ |
|||||||
|
import { render, screen } from '@testing-library/react'; |
||||||
|
import React from 'react'; |
||||||
|
|
||||||
|
import NestedRow from './NestedRow'; |
||||||
|
import { ResourceRowType } from './types'; |
||||||
|
|
||||||
|
const defaultProps = { |
||||||
|
row: { |
||||||
|
id: '1', |
||||||
|
uri: 'some-uri', |
||||||
|
name: '1', |
||||||
|
type: ResourceRowType.Resource, |
||||||
|
typeLabel: '1', |
||||||
|
}, |
||||||
|
level: 0, |
||||||
|
selectedRows: [], |
||||||
|
requestNestedRows: jest.fn(), |
||||||
|
onRowSelectedChange: jest.fn(), |
||||||
|
selectableEntryTypes: [], |
||||||
|
}; |
||||||
|
|
||||||
|
describe('NestedRow', () => { |
||||||
|
it('should not display a checkbox when the type of row is empty', () => { |
||||||
|
render( |
||||||
|
<table> |
||||||
|
<tbody> |
||||||
|
<NestedRow {...defaultProps} /> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
); |
||||||
|
const box = screen.queryByRole('checkbox'); |
||||||
|
expect(box).not.toBeInTheDocument(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should display a checkbox when the type of row is in selectableEntryTypes', () => { |
||||||
|
render( |
||||||
|
<table> |
||||||
|
<tbody> |
||||||
|
<NestedRow {...defaultProps} selectableEntryTypes={[ResourceRowType.Resource]} /> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
); |
||||||
|
const box = screen.queryByRole('checkbox'); |
||||||
|
expect(box).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should not display a checkbox when the type of row is not in selectableEntryTypes', () => { |
||||||
|
render( |
||||||
|
<table> |
||||||
|
<tbody> |
||||||
|
<NestedRow {...defaultProps} selectableEntryTypes={[ResourceRowType.ResourceGroup]} /> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
); |
||||||
|
const box = screen.queryByRole('checkbox'); |
||||||
|
expect(box).not.toBeInTheDocument(); |
||||||
|
}); |
||||||
|
}); |
||||||
@ -0,0 +1,101 @@ |
|||||||
|
import { cx } from '@emotion/css'; |
||||||
|
import { FadeTransition, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; |
||||||
|
import React, { useEffect, useState } from 'react'; |
||||||
|
|
||||||
|
import { NestedEntry } from './NestedEntry'; |
||||||
|
import NestedRows from './NestedRows'; |
||||||
|
import getStyles from './styles'; |
||||||
|
import { ResourceRow, ResourceRowGroup, ResourceRowType } from './types'; |
||||||
|
import { findRow } from './utils'; |
||||||
|
|
||||||
|
interface NestedRowProps { |
||||||
|
row: ResourceRow; |
||||||
|
level: number; |
||||||
|
selectedRows: ResourceRowGroup; |
||||||
|
requestNestedRows: (row: ResourceRow) => Promise<void>; |
||||||
|
onRowSelectedChange: (row: ResourceRow, selected: boolean) => void; |
||||||
|
selectableEntryTypes: ResourceRowType[]; |
||||||
|
} |
||||||
|
|
||||||
|
const NestedRow: React.FC<NestedRowProps> = ({ |
||||||
|
row, |
||||||
|
selectedRows, |
||||||
|
level, |
||||||
|
requestNestedRows, |
||||||
|
onRowSelectedChange, |
||||||
|
selectableEntryTypes, |
||||||
|
}) => { |
||||||
|
const styles = useStyles2(getStyles); |
||||||
|
const [rowStatus, setRowStatus] = useState<'open' | 'closed' | 'loading'>('closed'); |
||||||
|
|
||||||
|
const isSelected = !!selectedRows.find((v) => v.id === row.id); |
||||||
|
const isDisabled = selectedRows.length > 0 && !isSelected; |
||||||
|
const isOpen = rowStatus === 'open'; |
||||||
|
|
||||||
|
const onRowToggleCollapse = async () => { |
||||||
|
if (rowStatus === 'open') { |
||||||
|
setRowStatus('closed'); |
||||||
|
return; |
||||||
|
} |
||||||
|
setRowStatus('loading'); |
||||||
|
requestNestedRows(row) |
||||||
|
.then(() => setRowStatus('open')) |
||||||
|
.catch(() => setRowStatus('closed')); |
||||||
|
}; |
||||||
|
|
||||||
|
// opens the resource group on load of component if there was a previously saved selection
|
||||||
|
useEffect(() => { |
||||||
|
// Assuming we don't have multi-select yet
|
||||||
|
const selectedRow = selectedRows[0]; |
||||||
|
|
||||||
|
const containsChild = selectedRow && !!findRow(row.children ?? [], selectedRow.id); |
||||||
|
|
||||||
|
if (containsChild) { |
||||||
|
setRowStatus('open'); |
||||||
|
} |
||||||
|
}, [selectedRows, row]); |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<tr className={cx(styles.row, isDisabled && styles.disabledRow)} key={row.id}> |
||||||
|
<td className={styles.cell}> |
||||||
|
<NestedEntry |
||||||
|
level={level} |
||||||
|
isSelected={isSelected} |
||||||
|
isDisabled={isDisabled} |
||||||
|
isOpen={isOpen} |
||||||
|
entry={row} |
||||||
|
onToggleCollapse={onRowToggleCollapse} |
||||||
|
onSelectedChange={onRowSelectedChange} |
||||||
|
isSelectable={selectableEntryTypes.some((type) => type === row.type)} |
||||||
|
/> |
||||||
|
</td> |
||||||
|
|
||||||
|
<td className={styles.cell}>{row.typeLabel}</td> |
||||||
|
|
||||||
|
<td className={styles.cell}>{row.location ?? '-'}</td> |
||||||
|
</tr> |
||||||
|
|
||||||
|
{isOpen && row.children && Object.keys(row.children).length > 0 && ( |
||||||
|
<NestedRows |
||||||
|
rows={row.children} |
||||||
|
selectedRows={selectedRows} |
||||||
|
level={level + 1} |
||||||
|
requestNestedRows={requestNestedRows} |
||||||
|
onRowSelectedChange={onRowSelectedChange} |
||||||
|
selectableEntryTypes={selectableEntryTypes} |
||||||
|
/> |
||||||
|
)} |
||||||
|
|
||||||
|
<FadeTransition visible={rowStatus === 'loading'}> |
||||||
|
<tr> |
||||||
|
<td className={cx(styles.cell, styles.loadingCell)} colSpan={3}> |
||||||
|
<LoadingPlaceholder text="Loading..." className={styles.spinner} /> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</FadeTransition> |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default NestedRow; |
||||||
Loading…
Reference in new issue