diff --git a/packages/core/package.json b/packages/core/package.json index 6aea91be7d..e07307d05c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -42,6 +42,7 @@ "@material-ui/icons": "^4.9.1", "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/CopyTextButton/CopyTextButton.js b/packages/core/src/components/CopyTextButton/CopyTextButton.js new file mode 100644 index 0000000000..2df978ab2d --- /dev/null +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.js @@ -0,0 +1,99 @@ +/* + * 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, { useState, useRef } from 'react'; +import PropTypes from 'prop-types'; +import { IconButton, Tooltip, makeStyles } from '@material-ui/core'; +import CopyIcon from '@material-ui/icons/FileCopy'; +import { errorApiRef, useApi } from 'api'; + +const useStyles = makeStyles(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: + * + */ +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); + + const handleCopyClick = e => { + e.stopPropagation(); + setOpen(true); + + try { + inputRef.current.select(); + document.execCommand('copy'); + } catch (error) { + errorApi.post(error); + } + }; + + return ( + <> + + setOpen(false)} + open={open} + > + + + + + + ); +}; + +CopyTextButton.propTypes = { + text: PropTypes.string.isRequired, + tooltipDelay: PropTypes.number, + tooltipText: PropTypes.string, +}; + +export default 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..7fd78f8057 --- /dev/null +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx @@ -0,0 +1,60 @@ +/* + * 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 '.'; +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 = () => ( + +); + +export const WithTooltip = () => ( + +); + +export const LongerTooltipDelay = () => ( + +); 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..2abf9d0622 --- /dev/null +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.test.js @@ -0,0 +1,77 @@ +/* + * 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 './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, + tooltipText: 'mockTooltip', +}; + +const apiRegistry = ApiRegistry.from([ + [ + errorApiRef, + { + post(error) { + throw error; + }, + }, + ], +]); + +describe('', () => { + it('renders without exploding', () => { + const { getByDisplayValue } = render( + wrapInThemedTestApp( + + + , + ), + ); + getByDisplayValue('mockText'); + }); + + it('displays tooltip on click', async () => { + document.execCommand = jest.fn(); + const rendered = render( + wrapInThemedTestApp( + + + , + ), + ); + const button = rendered.getByTitle('mockTooltip'); + button.click(); + expect(document.execCommand).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/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..7575c5a9b2 --- /dev/null +++ b/packages/core/src/components/ProgressBars/HorizontalProgress.tsx @@ -0,0 +1,53 @@ +/* + * 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, useTheme } from '@material-ui/core'; +// @ts-ignore +import { Line } from 'rc-progress'; +import { BackstageTheme } from '@backstage/theme'; +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 theme = useTheme(); + const strokeColor = getProgressColor(theme.palette, 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 95% rename from packages/core/src/components/ProgressCard.tsx rename to packages/core/src/components/ProgressBars/ProgressCard.tsx index dc3ee7aa35..9b4a0d79e0 100644 --- a/packages/core/src/components/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 = { 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 }) => ( +
    {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/packages/core/src/index.ts b/packages/core/src/index.ts index a13146991c..af5789ef92 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -25,12 +25,15 @@ 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'; export { default as SupportButton } from './components/SupportButton'; export { default as SortableTable } from './components/SortableTable'; +export { default as StructuredMetadataTable } from './components/StructuredMetadataTable'; export { default as TrendLine } from './components/TrendLine'; export { FeatureCalloutCircular } from './components/FeatureDiscovery/FeatureCalloutCircular'; export * from './components/Status';