1
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

221 lines
6.3 KiB

import { cx } from '@emotion/css';
import { Checkbox, Icon, IconButton, LoadingPlaceholder, useStyles2, useTheme2, FadeTransition } from '@grafana/ui';
import React, { useCallback, useEffect, useState } from 'react';
import { Space } from '../Space';
import getStyles from './styles';
import { ResourceRowType, ResourceRow, ResourceRowGroup } from './types';
import { findRow } from './utils';
interface NestedRowsProps {
rows: ResourceRowGroup;
level: number;
selectedRows: ResourceRowGroup;
requestNestedRows: (row: ResourceRow) => Promise<void>;
onRowSelectedChange: (row: ResourceRow, selected: boolean) => void;
}
const NestedRows: React.FC<NestedRowsProps> = ({
rows,
selectedRows,
level,
requestNestedRows,
onRowSelectedChange,
}) => (
<>
{rows.map((row) => (
<NestedRow
key={row.id}
row={row}
selectedRows={selectedRows}
level={level}
requestNestedRows={requestNestedRows}
onRowSelectedChange={onRowSelectedChange}
/>
))}
</>
);
interface NestedRowProps {
row: ResourceRow;
level: number;
selectedRows: ResourceRowGroup;
requestNestedRows: (row: ResourceRow) => Promise<void>;
onRowSelectedChange: (row: ResourceRow, selected: boolean) => void;
}
const NestedRow: React.FC<NestedRowProps> = ({ row, selectedRows, level, requestNestedRows, onRowSelectedChange }) => {
const styles = useStyles2(getStyles);
const initialOpenStatus = row.type === ResourceRowType.Subscription ? 'open' : 'closed';
const [rowStatus, setRowStatus] = useState<'open' | 'closed' | 'loading'>(initialOpenStatus);
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');
await requestNestedRows(row);
setRowStatus('open');
};
// 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}
/>
</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}
/>
)}
<FadeTransition visible={rowStatus === 'loading'}>
<tr>
<td className={cx(styles.cell, styles.loadingCell)} colSpan={3}>
<LoadingPlaceholder text="Loading..." className={styles.spinner} />
</td>
</tr>
</FadeTransition>
</>
);
};
interface EntryIconProps {
entry: ResourceRow;
isOpen: boolean;
}
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;
}
};
interface NestedEntryProps {
level: number;
entry: ResourceRow;
isSelected: boolean;
isOpen: boolean;
isDisabled: boolean;
onToggleCollapse: (row: ResourceRow) => void;
onSelectedChange: (row: ResourceRow, selected: boolean) => void;
}
const NestedEntry: React.FC<NestedEntryProps> = ({
entry,
isSelected,
isDisabled,
isOpen,
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 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}`;
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' : 'Expand'}
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} />
<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>
);
};
export default NestedRows;