diff --git a/packages/cli/config/eslint.js b/packages/cli/config/eslint.js index c169e354d3..983a423a57 100644 --- a/packages/cli/config/eslint.js +++ b/packages/cli/config/eslint.js @@ -51,6 +51,10 @@ module.exports = { bundledDependencies: true, }, ], + '@typescript-eslint/no-unused-vars': [ + 'warn', + { vars: 'all', args: 'after-used', ignoreRestSiblings: true }, + ], }, overrides: [ { diff --git a/packages/core/src/components/ProgressCard.tsx b/packages/core/src/components/ProgressCard.tsx index 1964f382e9..dc3ee7aa35 100644 --- a/packages/core/src/components/ProgressCard.tsx +++ b/packages/core/src/components/ProgressCard.tsx @@ -16,9 +16,8 @@ import React, { FC } from 'react'; import { makeStyles } from '@material-ui/core'; - import InfoCard from 'layout/InfoCard'; -import { Props as BottomLinkProps } from 'layout/InfoCard/BottomLink'; +import { Props as BottomLinkProps } from '../layout/BottomLink'; import CircleProgress from './CircleProgress'; type Props = { diff --git a/packages/core/src/layout/InfoCard/BottomLink.test.tsx b/packages/core/src/layout/BottomLink/BottomLink.test.tsx similarity index 100% rename from packages/core/src/layout/InfoCard/BottomLink.test.tsx rename to packages/core/src/layout/BottomLink/BottomLink.test.tsx diff --git a/packages/core/src/layout/InfoCard/BottomLink.tsx b/packages/core/src/layout/BottomLink/BottomLink.tsx similarity index 100% rename from packages/core/src/layout/InfoCard/BottomLink.tsx rename to packages/core/src/layout/BottomLink/BottomLink.tsx index 6721eda0f1..9250685504 100644 --- a/packages/core/src/layout/InfoCard/BottomLink.tsx +++ b/packages/core/src/layout/BottomLink/BottomLink.tsx @@ -14,18 +14,18 @@ * limitations under the License. */ +import React, { FC } from 'react'; import { - Divider, Link, ListItem, ListItemIcon, + Divider, ListItemText, makeStyles, } from '@material-ui/core'; -import Box from '@material-ui/core/Box'; -import grey from '@material-ui/core/colors/grey'; import ArrowIcon from '@material-ui/icons/ArrowForward'; -import React, { FC } from 'react'; +import grey from '@material-ui/core/colors/grey'; +import Box from '@material-ui/core/Box'; const useStyles = makeStyles(theme => ({ root: { diff --git a/packages/core/src/layout/BottomLink/index.ts b/packages/core/src/layout/BottomLink/index.ts new file mode 100644 index 0000000000..5d4845064b --- /dev/null +++ b/packages/core/src/layout/BottomLink/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, Props } from './BottomLink'; diff --git a/packages/core/src/layout/InfoCard/InfoCard.tsx b/packages/core/src/layout/InfoCard/InfoCard.tsx index caa5f16513..f3d03d29c8 100644 --- a/packages/core/src/layout/InfoCard/InfoCard.tsx +++ b/packages/core/src/layout/InfoCard/InfoCard.tsx @@ -25,7 +25,7 @@ import { makeStyles, } from '@material-ui/core'; import ErrorBoundary from 'layout/ErrorBoundary/ErrorBoundary'; -import BottomLink, { Props as BottomLinkProps } from './BottomLink'; +import BottomLink, { Props as BottomLinkProps } from '../BottomLink'; const useStyles = makeStyles(theme => ({ header: { diff --git a/packages/core/src/layout/TabbedCard/TabbedCard.stories.tsx b/packages/core/src/layout/TabbedCard/TabbedCard.stories.tsx new file mode 100644 index 0000000000..d670b0b908 --- /dev/null +++ b/packages/core/src/layout/TabbedCard/TabbedCard.stories.tsx @@ -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, { useState } from 'react'; +import { TabbedCard, CardTab } from '.'; + +export default { + title: 'Tabbed Card', + component: TabbedCard, +}; + +export const Default = () => { + return ( + + some content 1 + some content 2 + some content 3 + some content 4 + + ); +}; + +const linkInfo = { title: 'Go to XYZ Location', link: '#' }; + +export const WithFooterLink = () => { + return ( + + some content 1 + some content 2 + some content 3 + some content 4 + + ); +}; + +export const WithControlledTabValue = () => { + const [selectedTab, setSelectedTab] = useState('one'); + + const handleChange = (_ev, newSelectedTab) => setSelectedTab(newSelectedTab); + + return ( + <> + Selected tab is {selectedTab} + + + + some content 1 + + + some content 2 + + + some content 3 + + + some content 4 + + + + ); +}; diff --git a/packages/core/src/layout/TabbedCard/TabbedCard.test.tsx b/packages/core/src/layout/TabbedCard/TabbedCard.test.tsx new file mode 100644 index 0000000000..ceeae8d49d --- /dev/null +++ b/packages/core/src/layout/TabbedCard/TabbedCard.test.tsx @@ -0,0 +1,105 @@ +/* + * 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, fireEvent } from '@testing-library/react'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { TabbedCard, CardTab } from '.'; + +const minProps = { + title: 'Some title', + deepLink: { + title: 'A deepLink title', + link: '/mocked', + }, +}; + +describe('', () => { + it('renders without exploding', () => { + const rendered = render( + wrapInTestApp( + + Test Content + Test Content + , + ), + ); + expect(rendered.getByText('Some title')).toBeInTheDocument(); + }); + + it('renders a deepLink when prop is set', () => { + const rendered = render( + wrapInTestApp( + + Test Content + Test Content + , + ), + ); + expect(rendered.getByText('A deepLink title')).toBeInTheDocument(); + }); + + it('switches tabs when clicking', () => { + const rendered = render( + wrapInTestApp( + + Test Content 1 + Test Content 2 + , + ), + ); + expect(rendered.getByText('Test Content 1')).toBeInTheDocument(); + + fireEvent.click(rendered.getByText('Test 2')); + expect(rendered.getByText('Test Content 2')).toBeInTheDocument(); + }); + + it('switches tabs when clicking in controlled mode', () => { + let selectedTab = 'one'; + + const handleTabChange = jest.fn( + (_ev, newSelectedTab) => (selectedTab = newSelectedTab), + ); + + const rendered = render( + wrapInTestApp( + + + Test Content 1 + + + Test Content 2 + + , + ), + ); + expect(rendered.getByText('Test Content 1')).toBeInTheDocument(); + + fireEvent.click(rendered.getByText('Test 2')); + expect(handleTabChange.mock.calls.length).toBe(1); + rendered.rerender( + + + Test Content 1 + + + Test Content 2 + + , + ); + expect(rendered.getByText('Test Content 2')).toBeInTheDocument(); + }); +}); diff --git a/packages/core/src/layout/TabbedCard/TabbedCard.tsx b/packages/core/src/layout/TabbedCard/TabbedCard.tsx new file mode 100644 index 0000000000..bd7443e924 --- /dev/null +++ b/packages/core/src/layout/TabbedCard/TabbedCard.tsx @@ -0,0 +1,127 @@ +/* + * 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, useState, ReactElement, ReactNode } from 'react'; +import { + Card, + CardContent, + CardHeader, + Divider, + withStyles, + makeStyles, + Tabs, + Tab, + TabProps, +} from '@material-ui/core'; +import BottomLink, { Props as BottomLinkProps } from '../BottomLink'; +import ErrorBoundary from '../ErrorBoundary/ErrorBoundary'; + +const useTabsStyles = makeStyles(theme => ({ + root: { + padding: theme.spacing(0, 2, 0, 2.5), + minHeight: theme.spacing(3), + }, + indicator: { + backgroundColor: theme.palette.info.main, + height: theme.spacing(0.3), + }, +})); + +const BoldHeader = withStyles(theme => ({ + root: { padding: theme.spacing(2, 2, 2, 2.5), display: 'inline-block' }, + title: { fontWeight: 700 }, + subheader: { paddingTop: theme.spacing(1) }, +}))(CardHeader); + +type Props = { + slackChannel?: string; + children?: ReactElement[]; + onChange?: (event: React.ChangeEvent<{}>, value: number | string) => void; + title?: string; + value?: number | string; + deepLink?: BottomLinkProps; +}; + +const TabbedCard: FC = ({ + slackChannel = '#backstage', + children, + title, + deepLink, + value, + onChange, +}) => { + const tabsClasses = useTabsStyles(); + const [selectedIndex, selectIndex] = useState(0); + + const handleChange = onChange + ? onChange + : (_ev, newSelectedIndex: number) => selectIndex(newSelectedIndex); + + let selectedTabContent: ReactNode; + if (!value) { + React.Children.map(children, (child, index) => { + if (index === selectedIndex) selectedTabContent = child?.props.children; + }); + } else { + React.Children.map(children, child => { + if (child?.props.value === value) + selectedTabContent = child?.props.children; + }); + } + + return ( + + + {title && } + + {children} + + + {selectedTabContent} + {deepLink && } + + + ); +}; + +const useCardTabStyles = makeStyles(theme => ({ + root: { + minWidth: theme.spacing(6), + minHeight: theme.spacing(3), + margin: theme.spacing(0, 2, 0, 0), + padding: theme.spacing(0.5, 0, 0.5, 0), + textTransform: 'none', + }, + selected: { + fontWeight: 'bold', + }, +})); + +type CardTabProps = TabProps & { + children: ReactNode; +}; + +const CardTab: FC = ({ children, ...props }) => { + const classes = useCardTabStyles(); + + return ; +}; + +export { TabbedCard, CardTab }; diff --git a/packages/core/src/layout/TabbedCard/index.ts b/packages/core/src/layout/TabbedCard/index.ts new file mode 100644 index 0000000000..f5019f2451 --- /dev/null +++ b/packages/core/src/layout/TabbedCard/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 * from './TabbedCard';