Port over StructuredMetadataTable

This commit is contained in:
Stefan Ålund
2020-04-13 14:52:24 +02:00
parent 6ca4626b78
commit 5938544c46
8 changed files with 455 additions and 1 deletions
+1
View File
@@ -31,6 +31,7 @@
"@types/node": "^12.0.0",
"classnames": "^2.2.6",
"clsx": "^1.1.0",
"lodash": "^4.17.15",
"prop-types": "^15.7.2",
"rc-progress": "^2.5.2",
"react": "^16.12.0",
@@ -0,0 +1,88 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import {
Table,
TableBody,
TableCell,
TableRow,
withStyles,
} from '@material-ui/core';
const tableTitleCellStyles = theme => ({
root: {
fontWeight: 'bolder',
whiteSpace: 'nowrap',
paddingRight: theme.spacing(4),
border: '0',
verticalAlign: 'top',
},
});
const tableContentCellStyles = {
root: {
color: 'rgba(0, 0, 0, 0.6)',
border: '0',
verticalAlign: 'top',
},
};
const listStyles = theme => ({
root: {
listStyle: 'none',
margin: theme.spacing(0, 0, -1, 0),
padding: '0',
},
});
const listItemStyles = theme => ({
root: {
padding: theme.spacing(0, 0, 1, 0),
},
});
const TitleCell = withStyles(tableTitleCellStyles)(TableCell);
const ContentCell = withStyles(tableContentCellStyles)(TableCell);
export const MetadataTable = ({ dense, children }) => (
<Table>
{!dense && (
<colgroup>
<col style={{ width: 'auto' }} />
<col style={{ width: '100%' }} />
</colgroup>
)}
<TableBody>{children}</TableBody>
</Table>
);
export const MetadataTableItem = ({ title, children, ...rest }) => (
<TableRow>
{title && <TitleCell>{title}</TitleCell>}
<ContentCell colSpan={title ? 1 : 2} {...rest}>
{children}
</ContentCell>
</TableRow>
);
export const MetadataList = withStyles(listStyles)(({ classes, children }) => (
<ul className={classes.root}>{children}</ul>
));
export const MetadataListItem = withStyles(
listItemStyles,
)(({ classes, children }) => <li className={classes.root}>{children}</li>);
@@ -0,0 +1,62 @@
# Structured MetadataTable
The `Strucuted MetadataTable` staple is a staple component for displaying basic JSON metadata.
# API
There is a very lightweight API around this component
| property | value |
| :------- | :---------: |
| metadata | object/JSON |
| dense | bool |
## Metadata
The Metadata property takes in JSON and iterates over it to display the tabled information.
The component itself only handles the display area, so you can use standard JS to construct an object that fits your desired outcome. No need to configure deeper within the staple.
```
<StructuredMetadataTable metadata={json} />
```
This will step through each of the keys and based on their types display them in a logical way.
### Primatives
Any non complex value will be displayed using `{value}` which will just output the value as text.
### Objects/Maps
JSON / Maps are displayed in a `<MetadataItemList>` with its values as formatted key/value pairs.
### Arrays
Arrays are displayed similarly to objects, its values in a `<MetadataItemList>`.
### Custom
If you want to customize the rendering of your value you can just replace it with a React Element.
```
{
contact: me@email.com
}
```
Would display as <b>contact</b> <span>me@email.com</span>
but if you wanted this to be a mailto you could inject that react into your map:
```
{
contact: <Link email="me@email.com">me@email.com</Link>
}
```
Then it would be displayed using the react element.
# Usage
For best usage drop this component inside another card. It can be used similarly to the `<MetadataTable>` and exposes the `dense` for when that is necessary.
@@ -0,0 +1,130 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { Component, Fragment } from 'react';
import { withStyles } from '@material-ui/core';
import startCase from 'lodash/startCase';
import {
MetadataTable,
MetadataTableItem,
MetadataList,
MetadataListItem,
} from './MetadataTable';
const listStyle = {
root: {
margin: '0 0',
listStyleType: 'none',
},
};
const nestedListStyle = {
root: {
...listStyle.root,
paddingLeft: '8px',
},
};
function renderList(list, options, nested) {
const values = list.map((item, index) => (
<MetadataListItem key={index}>{toValue(item)}</MetadataListItem>
));
return nested ? (
<StyledNestedList>{values}</StyledNestedList>
) : (
<StyledList>{values}</StyledList>
);
}
function renderMap(map, options, nested) {
const values = Object.keys(map).map(key => {
const value = toValue(map[key], true);
const fmtKey =
options && options.titleFormat
? options.titleFormat(key)
: startCase(key);
return (
<MetadataListItem key={key}>
{`${fmtKey}: `}
{value}
</MetadataListItem>
);
});
return nested ? (
<StyledNestedList>{values}</StyledNestedList>
) : (
<StyledList>{values}</StyledList>
);
}
function toValue(value, options, nested) {
if (React.isValidElement(value)) {
return <Fragment>{value}</Fragment>;
}
if (typeof value === 'object' && !Array.isArray(value)) {
return renderMap(value, options, nested);
}
if (Array.isArray(value)) {
return renderList(value, options, nested);
}
return <Fragment>{value}</Fragment>;
}
function mapToItems(info, options) {
return Object.keys(info).map(key => (
<TableItem key={key} title={key} value={info[key]} options={options} />
));
}
// Sub Components
const StyledList = withStyles(listStyle)(({ classes, children }) => (
<MetadataList classes={classes}>{children}</MetadataList>
));
const StyledNestedList = withStyles(
nestedListStyle,
)(({ classes, children }) => (
<MetadataList classes={classes}>{children}</MetadataList>
));
const ItemValue = ({ value, options }) => (
<Fragment>{toValue(value, options)}</Fragment>
);
const TableItem = ({ title, value, options }) => {
return (
<MetadataTableItem
title={
options && options.titleFormat
? options.titleFormat(title)
: startCase(title)
}
>
<ItemValue value={value} options={options} />
</MetadataTableItem>
);
};
export default class StructuredMetadataTable extends Component {
render() {
const { metadata, dense, options } = this.props;
const metadataItems = mapToItems(metadata, options || {});
return <MetadataTable dense={dense}>{metadataItems}</MetadataTable>;
}
}
@@ -0,0 +1,58 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import InfoCard from '../../layout/InfoCard';
import { Grid } from '@material-ui/core';
import StructuredMetadataTable from '.';
const cardContentStyle = { heightX: 200, width: 500 };
const metadata = {
description:
'This is a long description of what this is doing (and some additional info too). \n It has new lines and extra text to make it especially annoying to render. But it just ignores them.',
something: 'Yes',
owner: 'squad',
'longer key name': ['v1', 'v2', 'v3'],
rules: {
'permit missing partitions': 'No',
'max partition finish time': '19 hours',
Support: {
'office hours': 'Contact goalie',
'after hours': 'trigger PD alert',
},
},
};
export default {
title: 'Structured Metadata Table',
component: StructuredMetadataTable,
};
const Wrapper = ({ children }) => (
<Grid container spacing={4}>
<Grid item>{children}</Grid>
</Grid>
);
export const Default = () => (
<Wrapper>
<InfoCard title="Structured Metadata Table" subheader="Wrapped in InfoCard">
<div style={cardContentStyle}>
<StructuredMetadataTable metadata={metadata} />
</div>
</InfoCard>
</Wrapper>
);
@@ -0,0 +1,98 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { render } from '@testing-library/react';
import StructuredMetadataTable from './StructuredMetadataTable';
import { startCase } from 'lodash';
describe('<StructuredMetadataTable />', () => {
it('renders without exploding', () => {
const metadata = { hello: 'world' };
const { getByText } = render(
<StructuredMetadataTable metadata={metadata} />,
);
expect(getByText(metadata.hello)).toBeInTheDocument();
});
describe('Item Mappings', () => {
it('Iterates over and displays every field in the map', () => {
const metadata = { field1: 'one', field2: 'two', field3: 'three' };
const { getByText } = render(
<StructuredMetadataTable metadata={metadata} />,
);
const keys = Object.keys(metadata);
keys.forEach(value => {
expect(getByText(startCase(value))).toBeInTheDocument();
expect(getByText(metadata[value])).toBeInTheDocument();
});
});
it('Supports primative value fields', () => {
const metadata = { strField: 'my field', intField: 1 };
const { getByText } = render(
<StructuredMetadataTable metadata={metadata} />,
);
const keys = Object.keys(metadata);
keys.forEach(value => {
expect(getByText(startCase(value))).toBeInTheDocument();
expect(getByText(metadata[value].toString())).toBeInTheDocument();
});
});
it('Supports array fields', () => {
const metadata = { arrayField: ['arrVal1', 'arrVal2'] };
const { getByText } = render(
<StructuredMetadataTable metadata={metadata} />,
);
const keys = Object.keys(metadata);
keys.forEach(value => {
expect(getByText(startCase(value))).toBeInTheDocument();
});
metadata.arrayField.forEach(value => {
expect(getByText(value)).toBeInTheDocument();
});
});
it('Supports react elements', () => {
const metadata = { react: <div id="findMe"> field </div> };
const { getByText } = render(
<StructuredMetadataTable metadata={metadata} />,
);
expect(getByText('field')).toBeInTheDocument();
});
it('Supports object elements', () => {
const metadata = { config: { a: 1, b: 2 } };
const { getByText } = render(
<StructuredMetadataTable metadata={metadata} />,
);
const keys = Object.keys(metadata.config);
keys.forEach(value => {
expect(
getByText(startCase(value), { exact: false }),
).toBeInTheDocument();
expect(
getByText(metadata.config[value].toString(), { exact: false }),
).toBeInTheDocument();
});
});
});
});
@@ -0,0 +1,17 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { default } from './StructuredMetadataTable';
+1 -1
View File
@@ -7071,7 +7071,7 @@ cyclist@^1.0.1:
resolved "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
cypress@*, cypress@4.2.0, cypress@^4.2.0:
cypress@*, cypress@^4.2.0:
version "4.2.0"
resolved "https://registry.npmjs.org/cypress/-/cypress-4.2.0.tgz#45673fb648b1a77b9a78d73e58b89ed05212d243"
integrity sha512-8LdreL91S/QiTCLYLNbIjLL8Ht4fJmu/4HGLxUI20Tc7JSfqEfCmXELrRfuPT0kjosJwJJZacdSji9XSRkPKUw==