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