|
| 1 | +/** |
| 2 | + * Copyright 2004-present Facebook. All Rights Reserved. |
| 3 | + * |
| 4 | + * This source code is licensed under the BSD-style license found in the |
| 5 | + * LICENSE file in the root directory of this source tree. |
| 6 | + * |
| 7 | + * @flow |
| 8 | + * @format |
| 9 | + */ |
| 10 | +import typeof SvgIcon from '@material-ui/core/@@SvgIcon'; |
| 11 | + |
| 12 | +import Card from '@material-ui/core/Card'; |
| 13 | +import CardHeader from '@material-ui/core/CardHeader'; |
| 14 | +import Collapse from '@material-ui/core/Collapse'; |
| 15 | +import Divider from '@material-ui/core/Divider'; |
| 16 | +import ExpandLess from '@material-ui/icons/ExpandLess'; |
| 17 | +import ExpandMore from '@material-ui/icons/ExpandMore'; |
| 18 | +import Grid from '@material-ui/core/Grid'; |
| 19 | +import IconButton from '@material-ui/core/IconButton'; |
| 20 | +import Input from '@material-ui/core/Input'; |
| 21 | +import InputAdornment from '@material-ui/core/InputAdornment'; |
| 22 | +import List from '@material-ui/core/List'; |
| 23 | +import ListItem from '@material-ui/core/ListItem'; |
| 24 | +import React from 'react'; |
| 25 | +import Visibility from '@material-ui/icons/Visibility'; |
| 26 | +import VisibilityOff from '@material-ui/icons/VisibilityOff'; |
| 27 | + |
| 28 | +import {colors} from './Colors'; |
| 29 | +import {makeStyles} from '@material-ui/styles'; |
| 30 | + |
| 31 | +const useStyles = makeStyles(theme => ({ |
| 32 | + dataHeaderBlock: { |
| 33 | + display: 'flex', |
| 34 | + alignItems: 'center', |
| 35 | + padding: 0, |
| 36 | + }, |
| 37 | + dataHeaderContent: { |
| 38 | + display: 'flex', |
| 39 | + alignItems: 'center', |
| 40 | + }, |
| 41 | + dataHeaderIcon: { |
| 42 | + fill: colors.primary.comet, |
| 43 | + marginRight: theme.spacing(1), |
| 44 | + }, |
| 45 | + dataBlock: { |
| 46 | + boxShadow: `0 0 0 1px ${colors.primary.concrete}`, |
| 47 | + }, |
| 48 | + dataLabel: { |
| 49 | + color: colors.primary.comet, |
| 50 | + whiteSpace: 'nowrap', |
| 51 | + overflow: 'hidden', |
| 52 | + textOverflow: 'ellipsis', |
| 53 | + }, |
| 54 | + dataValue: { |
| 55 | + color: colors.primary.brightGray, |
| 56 | + whiteSpace: 'nowrap', |
| 57 | + overflow: 'hidden', |
| 58 | + textOverflow: 'ellipsis', |
| 59 | + width: props => |
| 60 | + props.hasStatus |
| 61 | + ? 'calc(100% - 16px)' |
| 62 | + : props.hasIcon |
| 63 | + ? 'calc(100% - 32px)' |
| 64 | + : '100%', |
| 65 | + }, |
| 66 | + dataObscuredValue: { |
| 67 | + color: colors.primary.brightGray, |
| 68 | + width: '100%', |
| 69 | + |
| 70 | + '& input': { |
| 71 | + whiteSpace: 'nowrap', |
| 72 | + overflow: 'hidden', |
| 73 | + textOverflow: 'ellipsis', |
| 74 | + }, |
| 75 | + }, |
| 76 | + dataBox: { |
| 77 | + width: '100%', |
| 78 | + padding: props => (props.collapsed ? '0' : null), |
| 79 | + |
| 80 | + '& > div': { |
| 81 | + width: '100%', |
| 82 | + }, |
| 83 | + }, |
| 84 | + dataIcon: { |
| 85 | + display: 'flex', |
| 86 | + alignItems: 'center', |
| 87 | + |
| 88 | + '& svg': { |
| 89 | + fill: colors.primary.comet, |
| 90 | + marginRight: theme.spacing(1), |
| 91 | + }, |
| 92 | + }, |
| 93 | + list: { |
| 94 | + padding: 0, |
| 95 | + }, |
| 96 | +})); |
| 97 | + |
| 98 | +// Data Icon adds an icon to the left of the value |
| 99 | +function DataIcon(icon: SvgIcon, val: string) { |
| 100 | + const props = {hasIcon: true}; |
| 101 | + const classes = useStyles(props); |
| 102 | + const Icon = icon; |
| 103 | + return ( |
| 104 | + <Grid container alignItems="center"> |
| 105 | + <Grid item className={classes.dataIcon}> |
| 106 | + <Icon /> |
| 107 | + </Grid> |
| 108 | + <Grid item className={classes.dataValue}> |
| 109 | + {val} |
| 110 | + </Grid> |
| 111 | + </Grid> |
| 112 | + ); |
| 113 | +} |
| 114 | + |
| 115 | +// Data Obscure makes the field into a password type filed with a visibility toggle for more sensitive fields. |
| 116 | +function DataObscure(value: number | string, category: ?string) { |
| 117 | + const [showPassword, setShowPassword] = React.useState(false); |
| 118 | + return ( |
| 119 | + <Input |
| 120 | + type={showPassword ? 'text' : 'password'} |
| 121 | + fullWidth={true} |
| 122 | + value={value} |
| 123 | + disableUnderline={true} |
| 124 | + readOnly={true} |
| 125 | + data-testid={`${category ?? value} obscure`} |
| 126 | + endAdornment={ |
| 127 | + <InputAdornment position="end"> |
| 128 | + <IconButton |
| 129 | + aria-label="toggle password visibility" |
| 130 | + onClick={() => setShowPassword(!showPassword)} |
| 131 | + onMouseDown={event => event.preventDefault()}> |
| 132 | + {showPassword ? <Visibility /> : <VisibilityOff />} |
| 133 | + </IconButton> |
| 134 | + </InputAdornment> |
| 135 | + } |
| 136 | + /> |
| 137 | + ); |
| 138 | +} |
| 139 | + |
| 140 | +function DataCollapse(data: Data) { |
| 141 | + const props = {collapsed: true}; |
| 142 | + const classes = useStyles(props); |
| 143 | + const [open, setOpen] = React.useState(true); |
| 144 | + const dataEntryValue = data.value + (data.unit ?? ''); |
| 145 | + return ( |
| 146 | + <List |
| 147 | + key={`${data.category ?? data.value}Collapse`} |
| 148 | + className={classes.list}> |
| 149 | + <ListItem button onClick={() => setOpen(!open)}> |
| 150 | + <CardHeader |
| 151 | + data-testid={data.category} |
| 152 | + title={data.category} |
| 153 | + className={classes.dataBox} |
| 154 | + subheader={ |
| 155 | + data.icon |
| 156 | + ? DataIcon(data.icon, dataEntryValue) |
| 157 | + : data.obscure === true |
| 158 | + ? DataObscure(data.value, data.category) |
| 159 | + : dataEntryValue |
| 160 | + } |
| 161 | + titleTypographyProps={{ |
| 162 | + variant: 'caption', |
| 163 | + className: classes.dataLabel, |
| 164 | + title: data.category, |
| 165 | + }} |
| 166 | + subheaderTypographyProps={{ |
| 167 | + variant: 'body1', |
| 168 | + className: classes.dataValue, |
| 169 | + title: data.tooltip ?? dataEntryValue, |
| 170 | + }} |
| 171 | + /> |
| 172 | + {open ? <ExpandLess /> : <ExpandMore />} |
| 173 | + </ListItem> |
| 174 | + <Divider /> |
| 175 | + <Collapse key={data.value} in={open} timeout="auto" unmountOnExit> |
| 176 | + {data.collapse ?? <></>} |
| 177 | + </Collapse> |
| 178 | + </List> |
| 179 | + ); |
| 180 | +} |
| 181 | + |
| 182 | +type Data = { |
| 183 | + icon?: SvgIcon, |
| 184 | + category?: string, |
| 185 | + value: number | string, |
| 186 | + obscure?: boolean, |
| 187 | + //$FlowFixMe TODO: Needs a ComponentType argument |
| 188 | + collapse?: ComponentType | boolean, |
| 189 | + unit?: string, |
| 190 | + statusCircle?: boolean, |
| 191 | + statusInactive?: boolean, |
| 192 | + status?: boolean, |
| 193 | + tooltip?: string, |
| 194 | +}; |
| 195 | + |
| 196 | +export type DataRows = Data[]; |
| 197 | + |
| 198 | +type Props = {data: DataRows[], testID?: string}; |
| 199 | + |
| 200 | +export default function DataGrid(props: Props) { |
| 201 | + const classes = useStyles(); |
| 202 | + const dataGrid = props.data.map((row, i) => ( |
| 203 | + <Grid key={i} container direction="row"> |
| 204 | + {row.map((data, j) => { |
| 205 | + const dataEntryValue = data.value + (data.unit ?? ''); |
| 206 | + |
| 207 | + return ( |
| 208 | + <React.Fragment key={`data-${i}-${j}`}> |
| 209 | + <Grid |
| 210 | + item |
| 211 | + container |
| 212 | + alignItems="center" |
| 213 | + xs={12} |
| 214 | + md |
| 215 | + key={`data-${i}-${j}`} |
| 216 | + zeroMinWidth |
| 217 | + className={classes.dataBlock}> |
| 218 | + <Grid item xs={12}> |
| 219 | + {data.collapse !== undefined && data.collapse !== false ? ( |
| 220 | + DataCollapse(data) |
| 221 | + ) : ( |
| 222 | + <CardHeader |
| 223 | + data-testid={data.category} |
| 224 | + className={classes.dataBox} |
| 225 | + title={data.category} |
| 226 | + titleTypographyProps={{ |
| 227 | + variant: 'caption', |
| 228 | + className: classes.dataLabel, |
| 229 | + title: data.category, |
| 230 | + }} |
| 231 | + subheaderTypographyProps={{ |
| 232 | + variant: 'body1', |
| 233 | + className: |
| 234 | + data.obscure === true |
| 235 | + ? classes.dataObscuredValue |
| 236 | + : classes.dataValue, |
| 237 | + title: data.tooltip ?? dataEntryValue, |
| 238 | + }} |
| 239 | + subheader={ |
| 240 | + data.icon |
| 241 | + ? DataIcon(data.icon, dataEntryValue) |
| 242 | + : data.obscure === true |
| 243 | + ? DataObscure(data.value, data.category) |
| 244 | + : dataEntryValue |
| 245 | + } |
| 246 | + /> |
| 247 | + )} |
| 248 | + </Grid> |
| 249 | + </Grid> |
| 250 | + </React.Fragment> |
| 251 | + ); |
| 252 | + })} |
| 253 | + </Grid> |
| 254 | + )); |
| 255 | + return ( |
| 256 | + <Card elevation={0}> |
| 257 | + <Grid |
| 258 | + container |
| 259 | + alignItems="center" |
| 260 | + justify="center" |
| 261 | + data-testid={props.testID ?? null}> |
| 262 | + {dataGrid} |
| 263 | + </Grid> |
| 264 | + </Card> |
| 265 | + ); |
| 266 | +} |
0 commit comments