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';