From 5938544c46f3e7bcb371eabb2b0e7275a1baecb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Mon, 13 Apr 2020 14:52:24 +0200 Subject: [PATCH 01/11] Port over StructuredMetadataTable --- packages/core/package.json | 1 + .../StructuredMetadataTable/MetadataTable.js | 88 ++++++++++++ .../StructuredMetadataTable/README.md | 62 +++++++++ .../StructuredMetadataTable.js | 130 ++++++++++++++++++ .../StructuredMetadataTable.stories.tsx | 58 ++++++++ .../StructuredMetadataTable.test.js | 98 +++++++++++++ .../StructuredMetadataTable/index.js | 17 +++ yarn.lock | 2 +- 8 files changed, 455 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/components/StructuredMetadataTable/MetadataTable.js create mode 100644 packages/core/src/components/StructuredMetadataTable/README.md create mode 100644 packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.js create mode 100644 packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx create mode 100644 packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.test.js create mode 100644 packages/core/src/components/StructuredMetadataTable/index.js diff --git a/packages/core/package.json b/packages/core/package.json index 3dc8150f83..a41f82a0fc 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", diff --git a/packages/core/src/components/StructuredMetadataTable/MetadataTable.js b/packages/core/src/components/StructuredMetadataTable/MetadataTable.js new file mode 100644 index 0000000000..dce6b63946 --- /dev/null +++ b/packages/core/src/components/StructuredMetadataTable/MetadataTable.js @@ -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 }) => ( + + {!dense && ( + + + + + )} + {children} +
+); + +export const MetadataTableItem = ({ title, children, ...rest }) => ( + + {title && {title}} + + {children} + + +); + +export const MetadataList = withStyles(listStyles)(({ classes, children }) => ( + +)); + +export const MetadataListItem = withStyles( + listItemStyles, +)(({ classes, children }) =>
  • {children}
  • ); diff --git a/packages/core/src/components/StructuredMetadataTable/README.md b/packages/core/src/components/StructuredMetadataTable/README.md new file mode 100644 index 0000000000..452d32490f --- /dev/null +++ b/packages/core/src/components/StructuredMetadataTable/README.md @@ -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. + +``` + +``` + +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 `` with its values as formatted key/value pairs. + +### Arrays + +Arrays are displayed similarly to objects, its values in a ``. + +### 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 contact me@email.com + +but if you wanted this to be a mailto you could inject that react into your map: + +``` +{ + contact: me@email.com +} +``` + +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 `` and exposes the `dense` for when that is necessary. diff --git a/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.js b/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.js new file mode 100644 index 0000000000..60516a56f1 --- /dev/null +++ b/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.js @@ -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) => ( + {toValue(item)} + )); + return nested ? ( + {values} + ) : ( + {values} + ); +} + +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 ( + + {`${fmtKey}: `} + {value} + + ); + }); + + return nested ? ( + {values} + ) : ( + {values} + ); +} + +function toValue(value, options, nested) { + if (React.isValidElement(value)) { + return {value}; + } + + if (typeof value === 'object' && !Array.isArray(value)) { + return renderMap(value, options, nested); + } + + if (Array.isArray(value)) { + return renderList(value, options, nested); + } + + return {value}; +} + +function mapToItems(info, options) { + return Object.keys(info).map(key => ( + + )); +} + +// Sub Components +const StyledList = withStyles(listStyle)(({ classes, children }) => ( + {children} +)); +const StyledNestedList = withStyles( + nestedListStyle, +)(({ classes, children }) => ( + {children} +)); +const ItemValue = ({ value, options }) => ( + {toValue(value, options)} +); +const TableItem = ({ title, value, options }) => { + return ( + + + + ); +}; + +export default class StructuredMetadataTable extends Component { + render() { + const { metadata, dense, options } = this.props; + const metadataItems = mapToItems(metadata, options || {}); + + return {metadataItems}; + } +} diff --git a/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx b/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx new file mode 100644 index 0000000000..5a04520dd1 --- /dev/null +++ b/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx @@ -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 }) => ( + + {children} + +); + +export const Default = () => ( + + +
    + +
    +
    +
    +); diff --git a/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.test.js b/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.test.js new file mode 100644 index 0000000000..b9aa962969 --- /dev/null +++ b/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.test.js @@ -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('', () => { + it('renders without exploding', () => { + const metadata = { hello: 'world' }; + const { getByText } = render( + , + ); + 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( + , + ); + 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( + , + ); + + 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( + , + ); + 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:
    field
    }; + const { getByText } = render( + , + ); + + expect(getByText('field')).toBeInTheDocument(); + }); + + it('Supports object elements', () => { + const metadata = { config: { a: 1, b: 2 } }; + const { getByText } = render( + , + ); + + 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(); + }); + }); + }); +}); diff --git a/packages/core/src/components/StructuredMetadataTable/index.js b/packages/core/src/components/StructuredMetadataTable/index.js new file mode 100644 index 0000000000..588f5ef170 --- /dev/null +++ b/packages/core/src/components/StructuredMetadataTable/index.js @@ -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'; diff --git a/yarn.lock b/yarn.lock index acf9de9c97..03f14af1bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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== From 8d82d2e530e0581f4aa2a9bcf9b580cd94c08a79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Mon, 13 Apr 2020 17:05:31 +0200 Subject: [PATCH 02/11] Add CopyTextButton --- .../CopyTextButton/CopyTextButton.js | 109 ++++++++++++++++++ .../CopyTextButton/CopyTextButton.stories.tsx | 34 ++++++ .../CopyTextButton/CopyTextButton.test.js | 45 ++++++++ .../src/components/CopyTextButton/index.ts | 17 +++ packages/core/src/index.ts | 1 + 5 files changed, 206 insertions(+) create mode 100644 packages/core/src/components/CopyTextButton/CopyTextButton.js create mode 100644 packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx create mode 100644 packages/core/src/components/CopyTextButton/CopyTextButton.test.js create mode 100644 packages/core/src/components/CopyTextButton/index.ts diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.js b/packages/core/src/components/CopyTextButton/CopyTextButton.js new file mode 100644 index 0000000000..49489319b3 --- /dev/null +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.js @@ -0,0 +1,109 @@ +/* + * 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, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { IconButton, Tooltip, withStyles } from '@material-ui/core'; +import CopyIcon from '@material-ui/icons/FileCopy'; + +const buttonStyles = theme => ({ + button: { + '&:hover': { + backgroundColor: theme.palette.highlight, + cursor: 'pointer', + }, + }, +}); + +/** + * Copy text button with visual feedback in the form of + * - a hover color + * - click ripple + * - Tooltip shown when user has clicked + * + * Properties: + * - text: the text to be copied + * - tooltipDelay: Number os ms to show the tooltip, default: 1000ms + * - tooltipText: Text to show in the tooltip when user has clicked the button, default: "Text + * copied to clipboard" + * + * Example: + * + */ +class CopyTextButton extends React.Component { + static propTypes = { + text: PropTypes.string.isRequired, + tooltipDelay: PropTypes.number, + tooltipText: PropTypes.string, + }; + + static defaultProps = { + tooltipDelay: 1000, + tooltipText: 'Text copied to clipboard', + }; + + state = { + open: false, + }; + + handleTooltipClose = () => { + this.setState({ open: false }); + }; + + handleTooltipOpen = () => { + this.setState({ open: true }); + }; + + handleCopyClick = e => { + e.stopPropagation(); + this.handleTooltipOpen(); + try { + this.clipboardInput.select(); + document.execCommand('copy'); + } catch (error) { + // console.error(error); + } + }; + + render() { + const { classes, text, tooltipDelay, tooltipText } = this.props; + + return ( + + (this.clipboardInput = el)} + type="text" + style={{ position: 'absolute', top: '-9999px', left: '-9999px' }} + defaultValue={text} + /> + + + + + + + ); + } +} + +export default withStyles(buttonStyles)(CopyTextButton); diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx b/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx new file mode 100644 index 0000000000..9f5618ad0e --- /dev/null +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx @@ -0,0 +1,34 @@ +/* + * 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 CopyTextButton from '.'; + +export default { + title: 'CopyTextButton', + component: CopyTextButton, +}; + +export const Default = () => ( + +); + +export const WithTooltip = () => ( + +); diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.test.js b/packages/core/src/components/CopyTextButton/CopyTextButton.test.js new file mode 100644 index 0000000000..c1c2212be7 --- /dev/null +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.test.js @@ -0,0 +1,45 @@ +/* + * 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 { wrapInThemedTestApp } from '@backstage/test-utils'; +import CopyTextButton from 'shared/components/CopyTextButton'; + +const props = { + text: 'mockText', + tooltipDelay: 2, + tooltipText: 'mockTooltip', +}; + +describe('', () => { + it('renders without exploding', () => { + const { getByDisplayValue } = render( + wrapInThemedTestApp(), + ); + getByDisplayValue('mockText'); + }); + + it('displays tooltip on click', () => { + const spy = jest.fn(); + Object.defineProperty(document, 'execCommand', { value: spy }); + const rendered = render(wrapInThemedTestApp()); + const button = rendered.getByTitle('mockTooltip'); + button.click(); + expect(spy).toHaveBeenCalled(); + rendered.getByText('mockTooltip'); + }); +}); diff --git a/packages/core/src/components/CopyTextButton/index.ts b/packages/core/src/components/CopyTextButton/index.ts new file mode 100644 index 0000000000..22fd7f05bb --- /dev/null +++ b/packages/core/src/components/CopyTextButton/index.ts @@ -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 './CopyTextButton'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 740a4cc803..c94068b556 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -27,6 +27,7 @@ export * from './layout/Sidebar'; export { default as HorizontalScrollGrid } from './components/HorizontalScrollGrid'; export { default as ProgressCard } from './components/ProgressCard'; export { default as CircleProgress } from './components/CircleProgress'; +export { default as CopyTextButton } from './components/CopyTextButton'; export { default as Progress } from './components/Progress'; export { AlphaLabel, BetaLabel } from './components/Lifecycle'; export { default as SupportButton } from './components/SupportButton'; From 6a1ca051aa2b54b2e58af4a9b5e1eb7907bef76a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Mon, 13 Apr 2020 17:08:23 +0200 Subject: [PATCH 03/11] Update index.ts --- packages/core/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c94068b556..3e2cfd1018 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -32,5 +32,6 @@ export { default as Progress } from './components/Progress'; export { AlphaLabel, BetaLabel } from './components/Lifecycle'; export { default as SupportButton } from './components/SupportButton'; export { default as SortableTable } from './components/SortableTable'; +export { default as StructuredMetadataTable } from './components/StructuredMetadataTable'; export { FeatureCalloutCircular } from './components/FeatureDiscovery/FeatureCalloutCircular'; export * from './components/Status'; From 71a30200dc4fa25bd3232e5c691fe7d85e52e94b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Mon, 13 Apr 2020 17:13:18 +0200 Subject: [PATCH 04/11] Add extra story --- .../components/CopyTextButton/CopyTextButton.stories.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx b/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx index 9f5618ad0e..92db2073d3 100644 --- a/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx @@ -32,3 +32,11 @@ export const WithTooltip = () => ( tooltipText="Custom tooltip shown on button click" /> ); + +export const LongerTooltipDelay = () => ( + +); From 223a752bc0a9fce4296e0a48f286aaa0b9da2eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Mon, 13 Apr 2020 17:41:13 +0200 Subject: [PATCH 05/11] Add HorizontalProgress --- .../CopyTextButton/CopyTextButton.test.js | 2 +- .../{ => ProgressBars}/CircleProgress.test.js | 0 .../{ => ProgressBars}/CircleProgress.tsx | 0 .../HorizontalProgress.stories.tsx | 43 ++++++++++++++++ .../ProgressBars/HorizontalProgress.tsx | 51 +++++++++++++++++++ .../ProgressCard.stories.tsx | 0 .../{ => ProgressBars}/ProgressCard.test.js | 0 .../{ => ProgressBars}/ProgressCard.tsx | 0 packages/core/src/index.ts | 5 +- 9 files changed, 98 insertions(+), 3 deletions(-) rename packages/core/src/components/{ => ProgressBars}/CircleProgress.test.js (100%) rename packages/core/src/components/{ => ProgressBars}/CircleProgress.tsx (100%) create mode 100644 packages/core/src/components/ProgressBars/HorizontalProgress.stories.tsx create mode 100644 packages/core/src/components/ProgressBars/HorizontalProgress.tsx rename packages/core/src/components/{ => ProgressBars}/ProgressCard.stories.tsx (100%) rename packages/core/src/components/{ => ProgressBars}/ProgressCard.test.js (100%) rename packages/core/src/components/{ => ProgressBars}/ProgressCard.tsx (100%) diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.test.js b/packages/core/src/components/CopyTextButton/CopyTextButton.test.js index c1c2212be7..0d267ae5c9 100644 --- a/packages/core/src/components/CopyTextButton/CopyTextButton.test.js +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.test.js @@ -17,7 +17,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { wrapInThemedTestApp } from '@backstage/test-utils'; -import CopyTextButton from 'shared/components/CopyTextButton'; +import CopyTextButton from './CopyTextButton'; const props = { text: 'mockText', diff --git a/packages/core/src/components/CircleProgress.test.js b/packages/core/src/components/ProgressBars/CircleProgress.test.js similarity index 100% rename from packages/core/src/components/CircleProgress.test.js rename to packages/core/src/components/ProgressBars/CircleProgress.test.js diff --git a/packages/core/src/components/CircleProgress.tsx b/packages/core/src/components/ProgressBars/CircleProgress.tsx similarity index 100% rename from packages/core/src/components/CircleProgress.tsx rename to packages/core/src/components/ProgressBars/CircleProgress.tsx diff --git a/packages/core/src/components/ProgressBars/HorizontalProgress.stories.tsx b/packages/core/src/components/ProgressBars/HorizontalProgress.stories.tsx new file mode 100644 index 0000000000..d027bcedc5 --- /dev/null +++ b/packages/core/src/components/ProgressBars/HorizontalProgress.stories.tsx @@ -0,0 +1,43 @@ +/* + * 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 HorizontalProgress from './HorizontalProgress'; + +const containerStyle = { width: 300 }; + +export default { + title: 'HorizontalProgress', + component: HorizontalProgress, +}; + +export const Default = () => ( +
    + +
    +); + +export const MediumProgress = () => ( +
    + +
    +); + +export const LowProgress = () => ( +
    + +
    +); diff --git a/packages/core/src/components/ProgressBars/HorizontalProgress.tsx b/packages/core/src/components/ProgressBars/HorizontalProgress.tsx new file mode 100644 index 0000000000..505610aa08 --- /dev/null +++ b/packages/core/src/components/ProgressBars/HorizontalProgress.tsx @@ -0,0 +1,51 @@ +/* + * 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, { FC } from 'react'; +import { Tooltip } from '@material-ui/core'; +// @ts-ignore +import { Line } from 'rc-progress'; +import { getProgressColor } from './CircleProgress'; + +type Props = { + /** + * Progress value between 0.0 - 1.0. + */ + value: number; +}; + +const HorizontalProgress: FC = ({ value }) => { + if (isNaN(value)) { + return null; + } + let percent = Math.round(value * 100 * 100) / 100; + if (percent > 100) { + percent = 100; + } + const strokeColor = getProgressColor(percent, false, 100); + return ( + + + + ); +}; + +export default HorizontalProgress; diff --git a/packages/core/src/components/ProgressCard.stories.tsx b/packages/core/src/components/ProgressBars/ProgressCard.stories.tsx similarity index 100% rename from packages/core/src/components/ProgressCard.stories.tsx rename to packages/core/src/components/ProgressBars/ProgressCard.stories.tsx diff --git a/packages/core/src/components/ProgressCard.test.js b/packages/core/src/components/ProgressBars/ProgressCard.test.js similarity index 100% rename from packages/core/src/components/ProgressCard.test.js rename to packages/core/src/components/ProgressBars/ProgressCard.test.js diff --git a/packages/core/src/components/ProgressCard.tsx b/packages/core/src/components/ProgressBars/ProgressCard.tsx similarity index 100% rename from packages/core/src/components/ProgressCard.tsx rename to packages/core/src/components/ProgressBars/ProgressCard.tsx diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3e2cfd1018..40a4bd452e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -25,8 +25,9 @@ export { default as InfoCard } from './layout/InfoCard'; export { default as ErrorBoundary } from './layout/ErrorBoundary'; export * from './layout/Sidebar'; export { default as HorizontalScrollGrid } from './components/HorizontalScrollGrid'; -export { default as ProgressCard } from './components/ProgressCard'; -export { default as CircleProgress } from './components/CircleProgress'; +export { default as ProgressCard } from './components/ProgressBars/ProgressCard'; +export { default as CircleProgress } from './components/ProgressBars/CircleProgress'; +export { default as HorizontalProgress } from './components/ProgressBars/HorizontalProgress'; export { default as CopyTextButton } from './components/CopyTextButton'; export { default as Progress } from './components/Progress'; export { AlphaLabel, BetaLabel } from './components/Lifecycle'; From ce067bf2bc9262a4e72ad2f2981dbef331b729cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Mon, 13 Apr 2020 20:38:01 +0200 Subject: [PATCH 06/11] Disable failing test --- .../core/src/components/CopyTextButton/CopyTextButton.js | 6 +++--- .../src/components/CopyTextButton/CopyTextButton.test.js | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.js b/packages/core/src/components/CopyTextButton/CopyTextButton.js index 49489319b3..c888a55205 100644 --- a/packages/core/src/components/CopyTextButton/CopyTextButton.js +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.js @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { Fragment } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { IconButton, Tooltip, withStyles } from '@material-ui/core'; import CopyIcon from '@material-ui/icons/FileCopy'; @@ -82,7 +82,7 @@ class CopyTextButton extends React.Component { const { classes, text, tooltipDelay, tooltipText } = this.props; return ( - + <> (this.clipboardInput = el)} type="text" @@ -101,7 +101,7 @@ class CopyTextButton extends React.Component { - + ); } } diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.test.js b/packages/core/src/components/CopyTextButton/CopyTextButton.test.js index 0d267ae5c9..81d2f9a863 100644 --- a/packages/core/src/components/CopyTextButton/CopyTextButton.test.js +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.test.js @@ -33,7 +33,9 @@ describe('', () => { getByDisplayValue('mockText'); }); - it('displays tooltip on click', () => { + // stefanalund: FIXME: cannot figure out wht this test fails. + // eslint-disable-next-line jest/no-disabled-tests + it.skip('displays tooltip on click', () => { const spy = jest.fn(); Object.defineProperty(document, 'execCommand', { value: spy }); const rendered = render(wrapInThemedTestApp()); From e5d6ff1b5ce67799a001a1caf6d46759b16caf57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Wed, 15 Apr 2020 08:03:59 +0200 Subject: [PATCH 07/11] Use errorApi --- .../core/src/components/CopyTextButton/CopyTextButton.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.js b/packages/core/src/components/CopyTextButton/CopyTextButton.js index c888a55205..6ba37ae5ae 100644 --- a/packages/core/src/components/CopyTextButton/CopyTextButton.js +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.js @@ -18,6 +18,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { IconButton, Tooltip, withStyles } from '@material-ui/core'; import CopyIcon from '@material-ui/icons/FileCopy'; +import { errorApiRef, useApi } from '../api'; const buttonStyles = theme => ({ button: { @@ -74,7 +75,8 @@ class CopyTextButton extends React.Component { this.clipboardInput.select(); document.execCommand('copy'); } catch (error) { - // console.error(error); + const errorApi = useApi(errorApiRef); + errorApi.post(error); } }; @@ -86,7 +88,7 @@ class CopyTextButton extends React.Component { (this.clipboardInput = el)} type="text" - style={{ position: 'absolute', top: '-9999px', left: '-9999px' }} + style={{ position: 'absolute', top: -9999, left: 9999 }} defaultValue={text} /> Date: Wed, 15 Apr 2020 22:14:23 +0200 Subject: [PATCH 08/11] Fixed link --- packages/core/src/components/ProgressBars/ProgressCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/components/ProgressBars/ProgressCard.tsx b/packages/core/src/components/ProgressBars/ProgressCard.tsx index dc3ee7aa35..9b4a0d79e0 100644 --- a/packages/core/src/components/ProgressBars/ProgressCard.tsx +++ b/packages/core/src/components/ProgressBars/ProgressCard.tsx @@ -17,7 +17,7 @@ import React, { FC } from 'react'; import { makeStyles } from '@material-ui/core'; import InfoCard from 'layout/InfoCard'; -import { Props as BottomLinkProps } from '../layout/BottomLink'; +import { Props as BottomLinkProps } from '../../layout/BottomLink'; import CircleProgress from './CircleProgress'; type Props = { From 09411b066123974b311460f7b90eb75dbc9821eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Fri, 17 Apr 2020 12:30:54 +0200 Subject: [PATCH 09/11] Update APIs --- .../core/src/components/CopyTextButton/CopyTextButton.js | 2 +- .../core/src/components/ProgressBars/HorizontalProgress.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.js b/packages/core/src/components/CopyTextButton/CopyTextButton.js index 6ba37ae5ae..b6b5edc993 100644 --- a/packages/core/src/components/CopyTextButton/CopyTextButton.js +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.js @@ -18,7 +18,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { IconButton, Tooltip, withStyles } from '@material-ui/core'; import CopyIcon from '@material-ui/icons/FileCopy'; -import { errorApiRef, useApi } from '../api'; +import { errorApiRef, useApi } from '../../api'; const buttonStyles = theme => ({ button: { diff --git a/packages/core/src/components/ProgressBars/HorizontalProgress.tsx b/packages/core/src/components/ProgressBars/HorizontalProgress.tsx index 505610aa08..7575c5a9b2 100644 --- a/packages/core/src/components/ProgressBars/HorizontalProgress.tsx +++ b/packages/core/src/components/ProgressBars/HorizontalProgress.tsx @@ -15,9 +15,10 @@ */ import React, { FC } from 'react'; -import { Tooltip } from '@material-ui/core'; +import { Tooltip, useTheme } from '@material-ui/core'; // @ts-ignore import { Line } from 'rc-progress'; +import { BackstageTheme } from '@backstage/theme'; import { getProgressColor } from './CircleProgress'; type Props = { @@ -35,7 +36,8 @@ const HorizontalProgress: FC = ({ value }) => { if (percent > 100) { percent = 100; } - const strokeColor = getProgressColor(percent, false, 100); + const theme = useTheme(); + const strokeColor = getProgressColor(theme.palette, percent, false, 100); return ( Date: Sat, 18 Apr 2020 18:20:11 +0200 Subject: [PATCH 10/11] packages/core/components/CopyTextButton: refactor to functional component + provide errorApi in storybook --- .../CopyTextButton/CopyTextButton.js | 106 ++++++++---------- .../CopyTextButton/CopyTextButton.stories.tsx | 18 +++ .../CopyTextButton/CopyTextButton.test.js | 26 ++++- 3 files changed, 89 insertions(+), 61 deletions(-) diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.js b/packages/core/src/components/CopyTextButton/CopyTextButton.js index b6b5edc993..2df978ab2d 100644 --- a/packages/core/src/components/CopyTextButton/CopyTextButton.js +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.js @@ -14,20 +14,20 @@ * limitations under the License. */ -import React from 'react'; +import React, { useState, useRef } from 'react'; import PropTypes from 'prop-types'; -import { IconButton, Tooltip, withStyles } from '@material-ui/core'; +import { IconButton, Tooltip, makeStyles } from '@material-ui/core'; import CopyIcon from '@material-ui/icons/FileCopy'; -import { errorApiRef, useApi } from '../../api'; +import { errorApiRef, useApi } from 'api'; -const buttonStyles = theme => ({ +const useStyles = makeStyles(theme => ({ button: { '&:hover': { backgroundColor: theme.palette.highlight, cursor: 'pointer', }, }, -}); +})); /** * Copy text button with visual feedback in the form of @@ -44,68 +44,56 @@ const buttonStyles = theme => ({ * Example: * */ -class CopyTextButton extends React.Component { - static propTypes = { - text: PropTypes.string.isRequired, - tooltipDelay: PropTypes.number, - tooltipText: PropTypes.string, - }; +const CopyTextButton = ({ + text, + tooltipDelay = 1000, + tooltipText = 'Text copied to clipboard', +}) => { + const classes = useStyles(); + const errorApi = useApi(errorApiRef); + const inputRef = useRef(); + const [open, setOpen] = useState(false); - static defaultProps = { - tooltipDelay: 1000, - tooltipText: 'Text copied to clipboard', - }; - - state = { - open: false, - }; - - handleTooltipClose = () => { - this.setState({ open: false }); - }; - - handleTooltipOpen = () => { - this.setState({ open: true }); - }; - - handleCopyClick = e => { + const handleCopyClick = e => { e.stopPropagation(); - this.handleTooltipOpen(); + setOpen(true); + try { - this.clipboardInput.select(); + inputRef.current.select(); document.execCommand('copy'); } catch (error) { - const errorApi = useApi(errorApiRef); errorApi.post(error); } }; - render() { - const { classes, text, tooltipDelay, tooltipText } = this.props; + return ( + <> + + setOpen(false)} + open={open} + > + + + + + + ); +}; - return ( - <> - (this.clipboardInput = el)} - type="text" - style={{ position: 'absolute', top: -9999, left: 9999 }} - defaultValue={text} - /> - - - - - - - ); - } -} +CopyTextButton.propTypes = { + text: PropTypes.string.isRequired, + tooltipDelay: PropTypes.number, + tooltipText: PropTypes.string, +}; -export default withStyles(buttonStyles)(CopyTextButton); +export default CopyTextButton; diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx b/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx index 92db2073d3..7fd78f8057 100644 --- a/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx @@ -16,10 +16,28 @@ import React from 'react'; import CopyTextButton from '.'; +import { ApiProvider, errorApiRef, ApiRegistry } from 'api'; export default { title: 'CopyTextButton', component: CopyTextButton, + decorators: [ + storyFn => { + // TODO: move this to common storybook config, requires core package to be separate from components + const registry = ApiRegistry.from([ + [ + errorApiRef, + { + post(error) { + // eslint-disable-next-line no-alert + window.alert(`Component posted error, ${error}`); + }, + }, + ], + ]); + return ; + }, + ], }; export const Default = () => ( diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.test.js b/packages/core/src/components/CopyTextButton/CopyTextButton.test.js index 81d2f9a863..ad8a9ce5cb 100644 --- a/packages/core/src/components/CopyTextButton/CopyTextButton.test.js +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.test.js @@ -18,6 +18,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { wrapInThemedTestApp } from '@backstage/test-utils'; import CopyTextButton from './CopyTextButton'; +import { ApiRegistry, errorApiRef, ApiProvider } from 'api'; const props = { text: 'mockText', @@ -25,10 +26,25 @@ const props = { tooltipText: 'mockTooltip', }; +const apiRegistry = ApiRegistry.from([ + [ + errorApiRef, + { + post(error) { + throw error; + }, + }, + ], +]); + describe('', () => { it('renders without exploding', () => { const { getByDisplayValue } = render( - wrapInThemedTestApp(), + wrapInThemedTestApp( + + + , + ), ); getByDisplayValue('mockText'); }); @@ -38,7 +54,13 @@ describe('', () => { it.skip('displays tooltip on click', () => { const spy = jest.fn(); Object.defineProperty(document, 'execCommand', { value: spy }); - const rendered = render(wrapInThemedTestApp()); + const rendered = render( + wrapInThemedTestApp( + + + , + ), + ); const button = rendered.getByTitle('mockTooltip'); button.click(); expect(spy).toHaveBeenCalled(); From 23beb805798e1d94e17c269808f909f63f0d0d80 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 18 Apr 2020 19:49:44 +0200 Subject: [PATCH 11/11] packages/core/components/CopyTextButton: fix disabled test --- .../CopyTextButton/CopyTextButton.test.js | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.test.js b/packages/core/src/components/CopyTextButton/CopyTextButton.test.js index ad8a9ce5cb..2abf9d0622 100644 --- a/packages/core/src/components/CopyTextButton/CopyTextButton.test.js +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.test.js @@ -20,6 +20,17 @@ import { wrapInThemedTestApp } from '@backstage/test-utils'; import CopyTextButton from './CopyTextButton'; import { ApiRegistry, errorApiRef, ApiProvider } from 'api'; +jest.mock('popper.js', () => { + const PopperJS = jest.requireActual('popper.js'); + + return class { + static placements = PopperJS.placements; + update() {} + destroy() {} + scheduleUpdate() {} + }; +}); + const props = { text: 'mockText', tooltipDelay: 2, @@ -49,11 +60,8 @@ describe('', () => { getByDisplayValue('mockText'); }); - // stefanalund: FIXME: cannot figure out wht this test fails. - // eslint-disable-next-line jest/no-disabled-tests - it.skip('displays tooltip on click', () => { - const spy = jest.fn(); - Object.defineProperty(document, 'execCommand', { value: spy }); + it('displays tooltip on click', async () => { + document.execCommand = jest.fn(); const rendered = render( wrapInThemedTestApp( @@ -63,7 +71,7 @@ describe('', () => { ); const button = rendered.getByTitle('mockTooltip'); button.click(); - expect(spy).toHaveBeenCalled(); + expect(document.execCommand).toHaveBeenCalled(); rendered.getByText('mockTooltip'); }); });