Merge pull request #4165 from SDA-SE/feat/ecosystem-explorer
Ecosystem explorer
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/core': patch
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
Add `href` in addition to `onClick` to `ItemCard`. Ensure that the height of a
|
||||
`ItemCard` with and without tags is equal.
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
'@backstage/core': patch
|
||||
'@backstage/plugin-catalog': patch
|
||||
'@backstage/plugin-explore': patch
|
||||
---
|
||||
|
||||
Introduce `TabbedLayout` for creating tabs that are routed.
|
||||
|
||||
```typescript
|
||||
<TabbedLayout>
|
||||
<TabbedLayout.Route path="/example" title="Example tab">
|
||||
<div>This is rendered under /example/anything-here route</div>
|
||||
</TabbedLayout.Route>
|
||||
</TabbedLayout>
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-explore': patch
|
||||
---
|
||||
|
||||
Rework the explore plugin to allow the user to explore things in the ecosystem,
|
||||
including tools and domains.
|
||||
+15
-14
@@ -15,30 +15,30 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
createApp,
|
||||
AlertDisplay,
|
||||
OAuthRequestDialog,
|
||||
SignInPage,
|
||||
createApp,
|
||||
createRouteRef,
|
||||
FlatRoutes,
|
||||
OAuthRequestDialog,
|
||||
SignInPage,
|
||||
} from '@backstage/core';
|
||||
import React from 'react';
|
||||
import Root from './components/Root';
|
||||
import * as plugins from './plugins';
|
||||
import { apis } from './apis';
|
||||
import { hot } from 'react-hot-loader/root';
|
||||
import { providers } from './identityProviders';
|
||||
import { Router as CatalogRouter } from '@backstage/plugin-catalog';
|
||||
import { Router as DocsRouter } from '@backstage/plugin-techdocs';
|
||||
import { Router as ImportComponentRouter } from '@backstage/plugin-catalog-import';
|
||||
import { ExplorePage } from '@backstage/plugin-explore';
|
||||
import { Router as GraphiQLRouter } from '@backstage/plugin-graphiql';
|
||||
import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar';
|
||||
import { Router as LighthouseRouter } from '@backstage/plugin-lighthouse';
|
||||
import { Router as RegisterComponentRouter } from '@backstage/plugin-register-component';
|
||||
import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar';
|
||||
import { Router as DocsRouter } from '@backstage/plugin-techdocs';
|
||||
import { Router as SettingsRouter } from '@backstage/plugin-user-settings';
|
||||
import { Router as ImportComponentRouter } from '@backstage/plugin-catalog-import';
|
||||
import { Route, Navigate } from 'react-router';
|
||||
|
||||
import React from 'react';
|
||||
import { hot } from 'react-hot-loader/root';
|
||||
import { Navigate, Route } from 'react-router';
|
||||
import { apis } from './apis';
|
||||
import { EntityPage } from './components/catalog/EntityPage';
|
||||
import Root from './components/Root';
|
||||
import { providers } from './identityProviders';
|
||||
import * as plugins from './plugins';
|
||||
|
||||
const app = createApp({
|
||||
apis,
|
||||
@@ -78,6 +78,7 @@ const routes = (
|
||||
element={<CatalogRouter EntityPage={EntityPage} />}
|
||||
/>
|
||||
<Route path="/docs" element={<DocsRouter />} />
|
||||
<Route path="/explore" element={<ExplorePage />} />
|
||||
<Route
|
||||
path="/tech-radar"
|
||||
element={<TechRadarRouter width={1500} height={800} />}
|
||||
|
||||
@@ -20,6 +20,7 @@ import HomeIcon from '@material-ui/icons/Home';
|
||||
import ExtensionIcon from '@material-ui/icons/Extension';
|
||||
import RuleIcon from '@material-ui/icons/AssignmentTurnedIn';
|
||||
import MapIcon from '@material-ui/icons/MyLocation';
|
||||
import LayersIcon from '@material-ui/icons/Layers';
|
||||
import LibraryBooks from '@material-ui/icons/LibraryBooks';
|
||||
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
|
||||
import MoneyIcon from '@material-ui/icons/MonetizationOn';
|
||||
@@ -82,6 +83,7 @@ const Root = ({ children }: PropsWithChildren<{}>) => (
|
||||
<SidebarItem icon={HomeIcon} to="/catalog" text="Home" />
|
||||
<SidebarItem icon={ExtensionIcon} to="api-docs" text="APIs" />
|
||||
<SidebarItem icon={LibraryBooks} to="docs" text="Docs" />
|
||||
<SidebarItem icon={LayersIcon} to="explore" text="Explore" />
|
||||
<SidebarItem icon={CreateComponentIcon} to="create" text="Create..." />
|
||||
{/* End global nav */}
|
||||
<SidebarDivider />
|
||||
|
||||
@@ -18,7 +18,7 @@ export { plugin as LighthousePlugin } from '@backstage/plugin-lighthouse';
|
||||
export { plugin as CatalogPlugin } from '@backstage/plugin-catalog';
|
||||
export { plugin as ScaffolderPlugin } from '@backstage/plugin-scaffolder';
|
||||
export { plugin as TechRadar } from '@backstage/plugin-tech-radar';
|
||||
export { plugin as Explore } from '@backstage/plugin-explore';
|
||||
export { explorePlugin } from '@backstage/plugin-explore';
|
||||
export { plugin as Circleci } from '@backstage/plugin-circleci';
|
||||
export { plugin as RegisterComponent } from '@backstage/plugin-register-component';
|
||||
export { plugin as Sentry } from '@backstage/plugin-sentry';
|
||||
|
||||
+9
-9
@@ -13,12 +13,12 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { TabbedLayout } from './TabbedLayout';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Routes, Route } from 'react-router';
|
||||
import { Route, Routes } from 'react-router';
|
||||
import { RoutedTabs } from './RoutedTabs';
|
||||
|
||||
const testRoute1 = {
|
||||
path: '',
|
||||
@@ -31,10 +31,10 @@ const testRoute2 = {
|
||||
children: <div>tabbed-test-content-2</div>,
|
||||
};
|
||||
|
||||
describe('TabbedLayout', () => {
|
||||
describe('RoutedTabs', () => {
|
||||
it('renders simplest case', async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<TabbedLayout routes={[testRoute1]} />,
|
||||
<RoutedTabs routes={[testRoute1]} />,
|
||||
);
|
||||
|
||||
expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument();
|
||||
@@ -46,7 +46,7 @@ describe('TabbedLayout', () => {
|
||||
<Routes>
|
||||
<Route
|
||||
path="/*"
|
||||
element={<TabbedLayout routes={[testRoute1, testRoute2]} />}
|
||||
element={<RoutedTabs routes={[testRoute1, testRoute2]} />}
|
||||
/>
|
||||
</Routes>,
|
||||
);
|
||||
@@ -70,7 +70,7 @@ describe('TabbedLayout', () => {
|
||||
<Route
|
||||
path="/*"
|
||||
element={
|
||||
<TabbedLayout
|
||||
<RoutedTabs
|
||||
routes={[
|
||||
testRoute1,
|
||||
{
|
||||
@@ -122,7 +122,7 @@ describe('TabbedLayout', () => {
|
||||
|
||||
it('shows only one tab contents at a time', async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<TabbedLayout routes={[testRoute1, testRoute2]} />,
|
||||
<RoutedTabs routes={[testRoute1, testRoute2]} />,
|
||||
{ routeEntries: ['/some-other-path'] },
|
||||
);
|
||||
|
||||
@@ -135,7 +135,7 @@ describe('TabbedLayout', () => {
|
||||
|
||||
it('redirects to the top level when no route is matching the url', async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<TabbedLayout routes={[testRoute1, testRoute2]} />,
|
||||
<RoutedTabs routes={[testRoute1, testRoute2]} />,
|
||||
{ routeEntries: ['/non-existing-path'] },
|
||||
);
|
||||
|
||||
+5
-5
@@ -14,9 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { useMemo } from 'react';
|
||||
import { useParams, useNavigate, matchRoutes, useRoutes } from 'react-router';
|
||||
import { HeaderTabs, Content as LayoutContent } from '@backstage/core';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { matchRoutes, useNavigate, useParams, useRoutes } from 'react-router';
|
||||
import { Content, HeaderTabs } from '../../layout';
|
||||
import { SubRoute } from './types';
|
||||
|
||||
export function useSelectedSubRoute(
|
||||
@@ -44,7 +44,7 @@ export function useSelectedSubRoute(
|
||||
};
|
||||
}
|
||||
|
||||
export const TabbedLayout = ({ routes }: { routes: SubRoute[] }) => {
|
||||
export const RoutedTabs = ({ routes }: { routes: SubRoute[] }) => {
|
||||
const navigate = useNavigate();
|
||||
const { index, route, element } = useSelectedSubRoute(routes);
|
||||
const headerTabs = useMemo(
|
||||
@@ -66,10 +66,10 @@ export const TabbedLayout = ({ routes }: { routes: SubRoute[] }) => {
|
||||
selectedIndex={index}
|
||||
onChange={onTabChange}
|
||||
/>
|
||||
<LayoutContent>
|
||||
<Content>
|
||||
<Helmet title={route.title} />
|
||||
{element}
|
||||
</LayoutContent>
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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, { PropsWithChildren } from 'react';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router';
|
||||
import { TabbedLayout } from './TabbedLayout';
|
||||
|
||||
export default {
|
||||
title: 'Navigation/TabbedLayout',
|
||||
component: TabbedLayout,
|
||||
};
|
||||
|
||||
const Wrapper = ({ children }: PropsWithChildren<{}>) => (
|
||||
<MemoryRouter>
|
||||
<Routes>
|
||||
<Route path="/*" element={<>{children}</>} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
export const Default = () => (
|
||||
<Wrapper>
|
||||
<TabbedLayout>
|
||||
<TabbedLayout.Route path="/" title="tabbed-test-title">
|
||||
<div>tabbed-test-content</div>
|
||||
</TabbedLayout.Route>
|
||||
<TabbedLayout.Route path="/some-other-path" title="tabbed-test-title-2">
|
||||
<div>tabbed-test-content-2</div>
|
||||
</TabbedLayout.Route>
|
||||
</TabbedLayout>
|
||||
</Wrapper>
|
||||
);
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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 { renderInTestApp, withLogCollector } from '@backstage/test-utils';
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Route, Routes } from 'react-router';
|
||||
import { TabbedLayout } from './TabbedLayout';
|
||||
|
||||
describe('TabbedLayout', () => {
|
||||
it('renders simplest case', async () => {
|
||||
const { getByText } = await renderInTestApp(
|
||||
<TabbedLayout>
|
||||
<TabbedLayout.Route path="/" title="tabbed-test-title">
|
||||
<div>tabbed-test-content</div>
|
||||
</TabbedLayout.Route>
|
||||
</TabbedLayout>,
|
||||
);
|
||||
|
||||
expect(getByText('tabbed-test-title')).toBeInTheDocument();
|
||||
expect(getByText('tabbed-test-content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('throws if any other component is a child of TabbedLayout', async () => {
|
||||
const { error } = await withLogCollector(async () => {
|
||||
await expect(
|
||||
renderInTestApp(
|
||||
<TabbedLayout>
|
||||
<TabbedLayout.Route path="/" title="tabbed-test-title">
|
||||
<div>tabbed-test-content</div>
|
||||
</TabbedLayout.Route>
|
||||
<div>This will cause app to throw</div>
|
||||
</TabbedLayout>,
|
||||
),
|
||||
).rejects.toThrow(/Child of TabbedLayout must be an TabbedLayout.Route/);
|
||||
});
|
||||
|
||||
expect(error).toEqual([
|
||||
expect.stringMatching(
|
||||
/Child of TabbedLayout must be an TabbedLayout.Route/,
|
||||
),
|
||||
expect.stringMatching(
|
||||
/The above error occurred in the <TabbedLayout> component/,
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
it('navigates when user clicks different tab', async () => {
|
||||
const { getByText, queryByText, queryAllByRole } = await renderInTestApp(
|
||||
<Routes>
|
||||
<Route
|
||||
path="/*"
|
||||
element={
|
||||
<TabbedLayout>
|
||||
<TabbedLayout.Route path="/" title="tabbed-test-title">
|
||||
<div>tabbed-test-content</div>
|
||||
</TabbedLayout.Route>
|
||||
<TabbedLayout.Route
|
||||
path="/some-other-path"
|
||||
title="tabbed-test-title-2"
|
||||
>
|
||||
<div>tabbed-test-content-2</div>
|
||||
</TabbedLayout.Route>
|
||||
</TabbedLayout>
|
||||
}
|
||||
/>
|
||||
</Routes>,
|
||||
);
|
||||
|
||||
const secondTab = queryAllByRole('tab')[1];
|
||||
act(() => {
|
||||
fireEvent.click(secondTab);
|
||||
});
|
||||
|
||||
expect(getByText('tabbed-test-title')).toBeInTheDocument();
|
||||
expect(queryByText('tabbed-test-content')).not.toBeInTheDocument();
|
||||
|
||||
expect(getByText('tabbed-test-title-2')).toBeInTheDocument();
|
||||
expect(queryByText('tabbed-test-content-2')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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 { attachComponentData } from '@backstage/core-api';
|
||||
import React, {
|
||||
Children,
|
||||
Fragment,
|
||||
isValidElement,
|
||||
PropsWithChildren,
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
import { RoutedTabs } from './RoutedTabs';
|
||||
|
||||
type SubRoute = {
|
||||
path: string;
|
||||
title: string;
|
||||
children: JSX.Element;
|
||||
};
|
||||
|
||||
const Route: (props: SubRoute) => null = () => null;
|
||||
|
||||
// This causes all mount points that are discovered within this route to use the path of the route itself
|
||||
attachComponentData(Route, 'core.gatherMountPoints', true);
|
||||
|
||||
export function createSubRoutesFromChildren(children: ReactNode): SubRoute[] {
|
||||
// Directly comparing child.type with Route will not work with in
|
||||
// combination with react-hot-loader in storybook
|
||||
// https://github.com/gaearon/react-hot-loader/issues/304
|
||||
const routeType = (
|
||||
<Route path="" title="">
|
||||
<div />
|
||||
</Route>
|
||||
).type;
|
||||
|
||||
return Children.toArray(children).flatMap(child => {
|
||||
if (!isValidElement(child)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (child.type === Fragment) {
|
||||
return createSubRoutesFromChildren(child.props.children);
|
||||
}
|
||||
|
||||
if (child.type !== routeType) {
|
||||
throw new Error('Child of TabbedLayout must be an TabbedLayout.Route');
|
||||
}
|
||||
|
||||
const { path, title, children } = child.props;
|
||||
return [{ path, title, children }];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* TabbedLayout is a compound component, which allows you to define a layout for
|
||||
* pages using a sub-navigation mechanism.
|
||||
*
|
||||
* Consists of two parts: TabbedLayout and TabbedLayout.Route
|
||||
*
|
||||
* @example
|
||||
* ```jsx
|
||||
* <TabbedLayout>
|
||||
* <TabbedLayout.Route path="/example" title="Example tab">
|
||||
* <div>This is rendered under /example/anything-here route</div>
|
||||
* </TabbedLayout.Route>
|
||||
* </TabbedLayout>
|
||||
* ```
|
||||
*/
|
||||
export const TabbedLayout = ({ children }: PropsWithChildren<{}>) => {
|
||||
const routes = createSubRoutesFromChildren(children);
|
||||
|
||||
return <RoutedTabs routes={routes} />;
|
||||
};
|
||||
|
||||
TabbedLayout.Route = Route;
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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 { TabbedLayout } from './TabbedLayout';
|
||||
@@ -21,10 +21,13 @@ export * from './CodeSnippet';
|
||||
export * from './CopyTextButton';
|
||||
export * from './DependencyGraph';
|
||||
export * from './DismissableBanner';
|
||||
export * from './EmptyState';
|
||||
export * from './FeatureDiscovery';
|
||||
export * from './HeaderIconLinkRow';
|
||||
export * from './HorizontalScrollGrid';
|
||||
export * from './Lifecycle';
|
||||
export * from './Link';
|
||||
export * from './MarkdownContent';
|
||||
export * from './OAuthRequestDialog';
|
||||
export * from './Progress';
|
||||
export * from './ProgressBars';
|
||||
@@ -32,10 +35,8 @@ export * from './SimpleStepper';
|
||||
export * from './Status';
|
||||
export * from './StructuredMetadataTable';
|
||||
export * from './SupportButton';
|
||||
export * from './TabbedLayout';
|
||||
export * from './Table';
|
||||
export * from './Tabs';
|
||||
export * from './TrendLine';
|
||||
export * from './WarningPanel';
|
||||
export * from './EmptyState';
|
||||
export * from './MarkdownContent';
|
||||
export * from './HeaderIconLinkRow';
|
||||
|
||||
@@ -13,9 +13,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { ItemCard } from '.';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router';
|
||||
import { ItemCard } from '.';
|
||||
|
||||
export default {
|
||||
title: 'Layout/Item Card',
|
||||
@@ -63,5 +64,35 @@ export const Tags = () => (
|
||||
label="Button"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={3}>
|
||||
<ItemCard
|
||||
title="Item Card"
|
||||
description="This is the description of an Item Card without Tags"
|
||||
label="Button"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
export const Link = () => (
|
||||
<MemoryRouter>
|
||||
<Grid container spacing={4}>
|
||||
<Grid item xs={12} md={3}>
|
||||
<ItemCard
|
||||
title="Backstage"
|
||||
description="This is the description of an Item Card with link"
|
||||
label="Read More"
|
||||
href="https://backstage.io"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={3}>
|
||||
<ItemCard
|
||||
title="Backstage @ GitHub"
|
||||
description="This is the description of an Item Card with link"
|
||||
label="Read More"
|
||||
href="https://github.com/backstage/backstage"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
@@ -13,8 +13,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Button, Card, Chip, makeStyles, Typography } from '@material-ui/core';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { Button, Card, Chip, Typography, makeStyles } from '@material-ui/core';
|
||||
import { Link } from '../../components';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
header: {
|
||||
@@ -30,6 +32,9 @@ const useStyles = makeStyles(theme => ({
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
withTags: {
|
||||
height: 'calc(175px - 32px - 8px)',
|
||||
},
|
||||
footer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row-reverse',
|
||||
@@ -43,7 +48,9 @@ type ItemCardProps = {
|
||||
type?: string;
|
||||
label: string;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
};
|
||||
|
||||
export const ItemCard = ({
|
||||
description,
|
||||
tags,
|
||||
@@ -51,6 +58,7 @@ export const ItemCard = ({
|
||||
type,
|
||||
label,
|
||||
onClick,
|
||||
href,
|
||||
}: ItemCardProps) => {
|
||||
const classes = useStyles();
|
||||
|
||||
@@ -64,13 +72,27 @@ export const ItemCard = ({
|
||||
{tags?.map((tag, i) => (
|
||||
<Chip label={tag} key={`tag-${i}`} />
|
||||
))}
|
||||
<Typography variant="body2" paragraph className={classes.description}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
paragraph
|
||||
className={clsx(
|
||||
classes.description,
|
||||
tags && tags.length > 0 && classes.withTags,
|
||||
)}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
<div className={classes.footer}>
|
||||
<Button onClick={onClick} color="primary">
|
||||
{label}
|
||||
</Button>
|
||||
{!href && (
|
||||
<Button onClick={onClick} color="primary">
|
||||
{label}
|
||||
</Button>
|
||||
)}
|
||||
{href && (
|
||||
<Button component={Link} to={href} color="primary">
|
||||
{label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
ApiRegistry,
|
||||
} from '@backstage/core';
|
||||
import { catalogApiRef, EntityContext } from '@backstage/plugin-catalog-react';
|
||||
import { renderInTestApp, withLogCollector } from '@backstage/test-utils';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
@@ -59,34 +59,11 @@ describe('EntityLayout', () => {
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
expect(rendered.getByText('my-entity')).toBeInTheDocument();
|
||||
expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument();
|
||||
expect(rendered.getByText('tabbed-test-content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('throws if any other component is a child of TabbedLayout', async () => {
|
||||
const { error } = await withLogCollector(async () => {
|
||||
await expect(
|
||||
renderInTestApp(
|
||||
<EntityLayout>
|
||||
<EntityLayout.Route path="/" title="tabbed-test-title">
|
||||
<div>tabbed-test-content</div>
|
||||
</EntityLayout.Route>
|
||||
<div>This will cause app to throw</div>
|
||||
</EntityLayout>,
|
||||
),
|
||||
).rejects.toThrow(/Child of EntityLayout must be an EntityLayout.Route/);
|
||||
});
|
||||
|
||||
expect(error).toEqual([
|
||||
expect.stringMatching(
|
||||
/Child of EntityLayout must be an EntityLayout.Route/,
|
||||
),
|
||||
expect.stringMatching(
|
||||
/The above error occurred in the <EntityLayout> component/,
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
it('navigates when user clicks different tab', async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<Routes>
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
|
||||
import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model';
|
||||
import {
|
||||
attachComponentData,
|
||||
Content,
|
||||
Header,
|
||||
HeaderLabel,
|
||||
Page,
|
||||
Progress,
|
||||
TabbedLayout,
|
||||
} from '@backstage/core';
|
||||
import {
|
||||
EntityContext,
|
||||
@@ -29,12 +29,9 @@ import {
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { Box } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import React, {
|
||||
Children,
|
||||
Fragment,
|
||||
isValidElement,
|
||||
import {
|
||||
default as React,
|
||||
PropsWithChildren,
|
||||
ReactNode,
|
||||
useContext,
|
||||
useState,
|
||||
} from 'react';
|
||||
@@ -42,37 +39,6 @@ import { useNavigate } from 'react-router';
|
||||
import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu';
|
||||
import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity';
|
||||
import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog';
|
||||
import { TabbedLayout } from './TabbedLayout';
|
||||
|
||||
type SubRoute = {
|
||||
path: string;
|
||||
title: string;
|
||||
children: JSX.Element;
|
||||
};
|
||||
|
||||
const Route: (props: SubRoute) => null = () => null;
|
||||
|
||||
// This causes all mount points that are discovered within this route to use the path of the route itself
|
||||
attachComponentData(Route, 'core.gatherMountPoints', true);
|
||||
|
||||
export function createSubRoutesFromChildren(children: ReactNode): SubRoute[] {
|
||||
return Children.toArray(children).flatMap(child => {
|
||||
if (!isValidElement(child)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (child.type === Fragment) {
|
||||
return createSubRoutesFromChildren(child.props.children);
|
||||
}
|
||||
|
||||
if (child.type !== Route) {
|
||||
throw new Error('Child of EntityLayout must be an EntityLayout.Route');
|
||||
}
|
||||
|
||||
const { path, title, children } = child.props;
|
||||
return [{ path, title, children }];
|
||||
});
|
||||
}
|
||||
|
||||
const EntityLayoutTitle = ({
|
||||
entity,
|
||||
@@ -132,7 +98,6 @@ export const EntityLayout = ({ children }: PropsWithChildren<{}>) => {
|
||||
const { kind, namespace, name } = useEntityCompoundName();
|
||||
const { entity, loading, error } = useContext(EntityContext);
|
||||
|
||||
const routes = createSubRoutesFromChildren(children);
|
||||
const { headerTitle, headerType } = headerProps(
|
||||
kind,
|
||||
namespace,
|
||||
@@ -174,7 +139,7 @@ export const EntityLayout = ({ children }: PropsWithChildren<{}>) => {
|
||||
|
||||
{loading && <Progress />}
|
||||
|
||||
{entity && <TabbedLayout routes={routes} />}
|
||||
{entity && <TabbedLayout>{children}</TabbedLayout>}
|
||||
|
||||
{error && (
|
||||
<Content>
|
||||
@@ -191,4 +156,4 @@ export const EntityLayout = ({ children }: PropsWithChildren<{}>) => {
|
||||
);
|
||||
};
|
||||
|
||||
EntityLayout.Route = Route;
|
||||
EntityLayout.Route = TabbedLayout.Route;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
# explore-react
|
||||
|
||||
This package provides helpers to the `explore` plugin that can be imported by
|
||||
any other plugin or app.
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "@backstage/plugin-explore-react",
|
||||
"version": "0.0.1",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "plugins/explore-react"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"clean": "backstage-cli clean",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.5.0",
|
||||
"@backstage/dev-utils": "^0.1.8",
|
||||
"@backstage/test-utils": "^0.1.6",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^12.0.0",
|
||||
"cross-fetch": "^3.0.6",
|
||||
"msw": "^0.21.2"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -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 './tools';
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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 { exploreToolsConfigRef } from './api';
|
||||
|
||||
describe('api', () => {
|
||||
// This is a dummy test to have at least one test in the package.
|
||||
it('api ref exists', () => {
|
||||
expect(exploreToolsConfigRef.id).toBe('plugin.explore.toolsconfig');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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 { createApiRef } from '@backstage/core';
|
||||
|
||||
export const exploreToolsConfigRef = createApiRef<ExploreToolsConfig>({
|
||||
id: 'plugin.explore.toolsconfig',
|
||||
description: 'Used to configure tools displayed in the explore plugin',
|
||||
});
|
||||
|
||||
export type ExploreTool = {
|
||||
title: string;
|
||||
description?: string;
|
||||
url: string;
|
||||
image: string;
|
||||
tags?: string[];
|
||||
lifecycle?: string;
|
||||
newsTag?: string;
|
||||
};
|
||||
|
||||
export interface ExploreToolsConfig {
|
||||
getTools: () => Promise<ExploreTool[]>;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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 { exploreToolsConfigRef } from './api';
|
||||
export type { ExploreTool, ExploreToolsConfig } from './api';
|
||||
@@ -1,7 +1,32 @@
|
||||
# Title
|
||||
# explore
|
||||
|
||||
Welcome to the explore plugin!
|
||||
This plugin helps to visualize the domains and tools in your ecosystem.
|
||||
|
||||
## Sub-section 1
|
||||
## Getting started
|
||||
|
||||
## Sub-section 2
|
||||
To install the plugin, include the following import your `plugins.ts`:
|
||||
|
||||
```typescript
|
||||
export { explorePlugin } from '@backstage/plugin-explore';
|
||||
```
|
||||
|
||||
Register the route in `App.tsx`:
|
||||
|
||||
```typescript
|
||||
import { ExplorePage } from '@backstage/plugin-explore';
|
||||
|
||||
...
|
||||
|
||||
<Route path="/explore" element={<ExplorePage />} />
|
||||
```
|
||||
|
||||
Add a link to the sidebar in `Root.tsx`:
|
||||
|
||||
```typescript
|
||||
import LayersIcon from '@material-ui/icons/Layers';
|
||||
|
||||
...
|
||||
|
||||
<SidebarItem icon={LayersIcon} to="explore" text="Explore" />
|
||||
```
|
||||
|
||||
@@ -14,7 +14,59 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { createDevApp } from '@backstage/dev-utils';
|
||||
import { plugin } from '../src/plugin';
|
||||
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { exploreToolsConfigRef } from '@backstage/plugin-explore-react';
|
||||
import React from 'react';
|
||||
import { ExplorePage, explorePlugin } from '../src';
|
||||
import { exampleTools } from '../src/util/examples';
|
||||
|
||||
createDevApp().registerPlugin(plugin).render();
|
||||
createDevApp()
|
||||
.registerPlugin(explorePlugin)
|
||||
.registerApi({
|
||||
api: exploreToolsConfigRef,
|
||||
deps: {},
|
||||
factory: () => ({
|
||||
async getTools() {
|
||||
return exampleTools;
|
||||
},
|
||||
}),
|
||||
})
|
||||
.registerApi({
|
||||
api: catalogApiRef,
|
||||
deps: {},
|
||||
factory: () =>
|
||||
({
|
||||
async getEntities() {
|
||||
const domainNames = [
|
||||
'playback',
|
||||
'artists',
|
||||
'payments',
|
||||
'analytics',
|
||||
'songs',
|
||||
'devops',
|
||||
];
|
||||
|
||||
return {
|
||||
items: domainNames.map(
|
||||
(n, i) =>
|
||||
({
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Domain',
|
||||
metadata: {
|
||||
name: n,
|
||||
description: `Everything about ${n}`,
|
||||
tags: i % 2 === 0 ? [n] : undefined,
|
||||
},
|
||||
spec: {
|
||||
owner: `${n}@example.com`,
|
||||
},
|
||||
} as Entity),
|
||||
),
|
||||
};
|
||||
},
|
||||
} as CatalogApi),
|
||||
})
|
||||
.addPage({ element: <ExplorePage />, title: 'Explore' })
|
||||
.render();
|
||||
|
||||
@@ -30,7 +30,10 @@
|
||||
"start": "backstage-cli plugin:serve"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.7.0",
|
||||
"@backstage/core": "^0.5.0",
|
||||
"@backstage/plugin-catalog-react": "^0.0.1",
|
||||
"@backstage/plugin-explore-react": "^0.0.1",
|
||||
"@backstage/theme": "^0.2.2",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
@@ -38,7 +41,7 @@
|
||||
"classnames": "^2.2.6",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router": "6.0.0-beta.0",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 { DomainEntity } from '@backstage/catalog-model';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { DomainCard } from './DomainCard';
|
||||
|
||||
describe('<DomainCard />', () => {
|
||||
it('renders a domain card', () => {
|
||||
const entity: DomainEntity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Domain',
|
||||
metadata: {
|
||||
name: 'artists',
|
||||
description: 'Everything about artists',
|
||||
tags: ['a-tag'],
|
||||
},
|
||||
spec: {
|
||||
owner: 'guest',
|
||||
},
|
||||
};
|
||||
const { getByText } = render(<DomainCard entity={entity} />, {
|
||||
wrapper: MemoryRouter,
|
||||
});
|
||||
|
||||
expect(getByText('artists')).toBeInTheDocument();
|
||||
expect(getByText('Everything about artists')).toBeInTheDocument();
|
||||
expect(getByText('a-tag')).toBeInTheDocument();
|
||||
expect(getByText('Explore').parentElement).toHaveAttribute(
|
||||
'href',
|
||||
'/catalog/default/domain/artists',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2021 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 { DomainEntity } from '@backstage/catalog-model';
|
||||
import { ItemCard } from '@backstage/core';
|
||||
import {
|
||||
entityRoute,
|
||||
entityRouteParams,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import React from 'react';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
|
||||
type DomainCardProps = {
|
||||
entity: DomainEntity;
|
||||
};
|
||||
|
||||
export const DomainCard = ({ entity }: DomainCardProps) => (
|
||||
<ItemCard
|
||||
title={entity.metadata.name}
|
||||
description={entity.metadata.description}
|
||||
tags={entity.metadata.tags}
|
||||
label="Explore"
|
||||
// TODO: Use useRouteRef here to generate the path
|
||||
href={generatePath(
|
||||
`/catalog/${entityRoute.path}`,
|
||||
entityRouteParams(entity),
|
||||
)}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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 { DomainEntity } from '@backstage/catalog-model';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { DomainCardGrid } from './DomainCardGrid';
|
||||
|
||||
describe('<DomainCardGrid />', () => {
|
||||
it('renders a grid of domain cards', () => {
|
||||
const entities: DomainEntity[] = [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Domain',
|
||||
metadata: {
|
||||
name: 'playback',
|
||||
},
|
||||
spec: {
|
||||
owner: 'guest',
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Domain',
|
||||
metadata: {
|
||||
name: 'artists',
|
||||
},
|
||||
spec: {
|
||||
owner: 'guest',
|
||||
},
|
||||
},
|
||||
];
|
||||
const { getByText } = render(<DomainCardGrid entities={entities} />, {
|
||||
wrapper: MemoryRouter,
|
||||
});
|
||||
|
||||
expect(getByText('artists')).toBeInTheDocument();
|
||||
expect(getByText('playback')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2021 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 { DomainEntity } from '@backstage/catalog-model';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { DomainCard } from '.';
|
||||
|
||||
type DomainCardGridProps = {
|
||||
entities: DomainEntity[];
|
||||
};
|
||||
|
||||
export const DomainCardGrid = ({ entities }: DomainCardGridProps) => (
|
||||
<Grid container spacing={4}>
|
||||
{entities.map((e, i) => (
|
||||
<Grid item xs={12} md={3} key={i}>
|
||||
<DomainCard entity={e} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2021 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 { DomainCard } from './DomainCard';
|
||||
export { DomainCardGrid } from './DomainCardGrid';
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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 { DomainEntity } from '@backstage/catalog-model';
|
||||
import { ApiProvider, ApiRegistry } from '@backstage/core';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { DomainExplorerContent } from './DomainExplorerContent';
|
||||
|
||||
describe('<DomainExplorerContent />', () => {
|
||||
const catalogApi: jest.Mocked<typeof catalogApiRef.T> = {
|
||||
addLocation: jest.fn(_a => new Promise(() => {})),
|
||||
getEntities: jest.fn(),
|
||||
getLocationByEntity: jest.fn(),
|
||||
getLocationById: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
getEntityByName: jest.fn(),
|
||||
};
|
||||
|
||||
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
<MemoryRouter>
|
||||
<ApiProvider apis={ApiRegistry.with(catalogApiRef, catalogApi)}>
|
||||
{children}
|
||||
</ApiProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('renders a grid of domains', async () => {
|
||||
const entities: DomainEntity[] = [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Domain',
|
||||
metadata: {
|
||||
name: 'playback',
|
||||
},
|
||||
spec: {
|
||||
owner: 'guest',
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Domain',
|
||||
metadata: {
|
||||
name: 'artists',
|
||||
},
|
||||
spec: {
|
||||
owner: 'guest',
|
||||
},
|
||||
},
|
||||
];
|
||||
catalogApi.getEntities.mockResolvedValue({ items: entities });
|
||||
|
||||
const { getByText } = render(<DomainExplorerContent />, {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('artists')).toBeInTheDocument();
|
||||
expect(getByText('playback')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders empty state', async () => {
|
||||
catalogApi.getEntities.mockResolvedValue({ items: [] });
|
||||
|
||||
const { getByText } = render(<DomainExplorerContent />, {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getByText('No domains to display')).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2021 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 { DomainEntity } from '@backstage/catalog-model';
|
||||
import {
|
||||
Content,
|
||||
ContentHeader,
|
||||
EmptyState,
|
||||
Progress,
|
||||
SupportButton,
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { Button } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { DomainCardGrid } from '../DomainCard';
|
||||
|
||||
export const DomainExplorerContent = () => {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const { value: entities, loading } = useAsync(async () => {
|
||||
const response = await catalogApi.getEntities({
|
||||
filter: { kind: 'domain' },
|
||||
});
|
||||
|
||||
return response.items as DomainEntity[];
|
||||
}, [catalogApi]);
|
||||
|
||||
return (
|
||||
<Content noPadding>
|
||||
<ContentHeader title="Domains">
|
||||
<SupportButton>Discover the domains in your ecosystem.</SupportButton>
|
||||
</ContentHeader>
|
||||
|
||||
{loading && <Progress />}
|
||||
{!loading && (!entities || entities.length === 0) && (
|
||||
<EmptyState
|
||||
missing="info"
|
||||
title="No domains to display"
|
||||
description={`You haven't added any domains yet.`}
|
||||
action={
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
href="https://backstage.io/docs/features/software-catalog/descriptor-format#kind-domain"
|
||||
>
|
||||
Read more
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!loading && entities && <DomainCardGrid entities={entities} />}
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2021 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 { DomainExplorerContent } from './DomainExplorerContent';
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2021 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 { configApiRef, Header, Page, useApi } from '@backstage/core';
|
||||
import React from 'react';
|
||||
import { ExploreTabs } from './ExploreTabs';
|
||||
|
||||
export const ExplorePage = () => {
|
||||
const configApi = useApi(configApiRef);
|
||||
const organizationName =
|
||||
configApi.getOptionalString('organization.name') ?? 'Backstage';
|
||||
return (
|
||||
<Page themeId="home">
|
||||
<Header
|
||||
title={`Explore the ${organizationName} ecosystem`}
|
||||
subtitle="Discover solutions available in your ecosystem"
|
||||
/>
|
||||
|
||||
<ExploreTabs />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2021 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 { TabbedLayout } from '@backstage/core';
|
||||
import React from 'react';
|
||||
import { DomainExplorerContent } from '../DomainExplorerContent';
|
||||
import { ToolExplorerContent } from '../ToolExplorerContent';
|
||||
|
||||
export const ExploreTabs = () => (
|
||||
<TabbedLayout>
|
||||
<TabbedLayout.Route path="domains" title="Domains">
|
||||
<DomainExplorerContent />
|
||||
</TabbedLayout.Route>
|
||||
<TabbedLayout.Route path="tools" title="Tools">
|
||||
<ToolExplorerContent />
|
||||
</TabbedLayout.Route>
|
||||
</TabbedLayout>
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2021 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 { ExplorePage } from './ExplorePage';
|
||||
+15
-22
@@ -14,11 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
|
||||
import ExploreCard from './ExploreCard';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { ToolCard } from './ToolCard';
|
||||
|
||||
const minProps = {
|
||||
card: {
|
||||
@@ -30,37 +29,31 @@ const minProps = {
|
||||
},
|
||||
};
|
||||
|
||||
describe('<ExploreCard />', () => {
|
||||
describe('<ToolCard />', () => {
|
||||
it('renders without exploding', () => {
|
||||
const { getByText } = render(wrapInTestApp(<ExploreCard {...minProps} />));
|
||||
const { getByText } = render(wrapInTestApp(<ToolCard {...minProps} />));
|
||||
expect(getByText('Explore')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders props correctly', () => {
|
||||
const { getByText } = render(wrapInTestApp(<ExploreCard {...minProps} />));
|
||||
const { getByText } = render(wrapInTestApp(<ToolCard {...minProps} />));
|
||||
expect(getByText(minProps.card.title)).toBeInTheDocument();
|
||||
expect(getByText(minProps.card.description)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should link out', () => {
|
||||
const rendered = render(wrapInTestApp(<ExploreCard {...minProps} />));
|
||||
const rendered = render(wrapInTestApp(<ToolCard {...minProps} />));
|
||||
const anchor = rendered.container.querySelector('a');
|
||||
expect(anchor.href).toBe(minProps.card.url);
|
||||
expect(anchor).toHaveAttribute('href', minProps.card.url);
|
||||
});
|
||||
|
||||
it('renders default description when missing', () => {
|
||||
const propsWithoutDescription = {
|
||||
card: {
|
||||
card: {
|
||||
title: 'Title',
|
||||
url: 'http://spotify.com/',
|
||||
image: 'https://developer.spotify.com/assets/WebAPI_intro.png',
|
||||
},
|
||||
},
|
||||
const card = {
|
||||
title: 'Title',
|
||||
url: 'http://spotify.com/',
|
||||
image: 'https://developer.spotify.com/assets/WebAPI_intro.png',
|
||||
};
|
||||
const { getByText } = render(
|
||||
wrapInTestApp(<ExploreCard {...propsWithoutDescription} />),
|
||||
);
|
||||
const { getByText } = render(wrapInTestApp(<ToolCard card={card} />));
|
||||
expect(getByText('Description missing')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -74,13 +67,13 @@ describe('<ExploreCard />', () => {
|
||||
},
|
||||
};
|
||||
const { queryByText } = render(
|
||||
wrapInTestApp(<ExploreCard {...propsWithLifecycle} />),
|
||||
wrapInTestApp(<ToolCard {...propsWithLifecycle} />),
|
||||
);
|
||||
expect(queryByText('GA')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders tags correctly', () => {
|
||||
const { getByText } = render(wrapInTestApp(<ExploreCard {...minProps} />));
|
||||
const { getByText } = render(wrapInTestApp(<ToolCard {...minProps} />));
|
||||
expect(getByText(minProps.card.tags[0])).toBeInTheDocument();
|
||||
expect(getByText(minProps.card.tags[1])).toBeInTheDocument();
|
||||
});
|
||||
+9
-18
@@ -14,8 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ExploreTool } from '@backstage/plugin-explore-react';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -23,10 +23,13 @@ import {
|
||||
CardContent,
|
||||
CardMedia,
|
||||
Chip,
|
||||
Typography,
|
||||
makeStyles,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
// TODO: Align styling between Domain and ToolCard
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
card: {
|
||||
@@ -65,22 +68,12 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
},
|
||||
}));
|
||||
|
||||
export type CardData = {
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
image: string;
|
||||
tags?: string[];
|
||||
lifecycle?: string;
|
||||
newsTag?: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
card: CardData;
|
||||
card: ExploreTool;
|
||||
objectFit?: 'cover' | 'contain';
|
||||
};
|
||||
|
||||
const ExploreCard = ({ card, objectFit }: Props) => {
|
||||
export const ToolCard = ({ card, objectFit }: Props) => {
|
||||
const classes = useStyles();
|
||||
|
||||
const { title, description, url, image, lifecycle, newsTag, tags } = card;
|
||||
@@ -130,5 +123,3 @@ const ExploreCard = ({ card, objectFit }: Props) => {
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreCard;
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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 { ExploreTool } from '@backstage/plugin-explore-react';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import { makeStyles } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { ToolCard } from './ToolCard';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
container: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, 296px)',
|
||||
gridGap: theme.spacing(3),
|
||||
marginBottom: theme.spacing(6),
|
||||
},
|
||||
}));
|
||||
|
||||
type ToolCardGridProps = {
|
||||
tools: ExploreTool[];
|
||||
};
|
||||
|
||||
export const ToolCardGrid = ({ tools }: ToolCardGridProps) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
{tools.map((card: ExploreTool, ix: any) => (
|
||||
<ToolCard card={card} key={ix} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2021 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 { ToolCard } from './ToolCard';
|
||||
export { ToolCardGrid } from './ToolCardGrid';
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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 { ApiProvider, ApiRegistry } from '@backstage/core';
|
||||
import {
|
||||
ExploreTool,
|
||||
exploreToolsConfigRef,
|
||||
} from '@backstage/plugin-explore-react';
|
||||
import { lightTheme } from '@backstage/theme';
|
||||
import { ThemeProvider } from '@material-ui/core';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { ToolExplorerContent } from './ToolExplorerContent';
|
||||
|
||||
describe('<ToolExplorerContent />', () => {
|
||||
const exploreToolsConfigApi: jest.Mocked<typeof exploreToolsConfigRef.T> = {
|
||||
getTools: jest.fn(),
|
||||
};
|
||||
|
||||
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<MemoryRouter>
|
||||
<ApiProvider
|
||||
apis={ApiRegistry.with(exploreToolsConfigRef, exploreToolsConfigApi)}
|
||||
>
|
||||
{children}
|
||||
</ApiProvider>
|
||||
</MemoryRouter>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('renders a grid of tools', async () => {
|
||||
const tools: ExploreTool[] = [
|
||||
{
|
||||
title: 'Lighthouse',
|
||||
description:
|
||||
"Google's Lighthouse tool is a great resource for benchmarking and improving the accessibility, performance, SEO, and best practices of your website.",
|
||||
url: '/lighthouse',
|
||||
image:
|
||||
'https://raw.githubusercontent.com/GoogleChrome/lighthouse/8b3d7f052b2e64dd857e741d7395647f487697e7/assets/lighthouse-logo.png',
|
||||
tags: ['web', 'seo', 'accessibility', 'performance'],
|
||||
},
|
||||
{
|
||||
title: 'Tech Radar',
|
||||
description:
|
||||
'Tech Radar is a list of technologies, complemented by an assessment result, called ring assignment.',
|
||||
url: '/tech-radar',
|
||||
image:
|
||||
'https://storage.googleapis.com/wf-blogs-engineering-media/2018/09/fe13bb32-wf-tech-radar-hero-1024x597.png',
|
||||
tags: ['standards', 'landscape'],
|
||||
},
|
||||
];
|
||||
exploreToolsConfigApi.getTools.mockResolvedValue(tools);
|
||||
|
||||
const { getByText } = render(<ToolExplorerContent />, {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Lighthouse')).toBeInTheDocument();
|
||||
expect(getByText('Tech Radar')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders empty state', async () => {
|
||||
exploreToolsConfigApi.getTools.mockResolvedValue([]);
|
||||
|
||||
const { getByText } = render(<ToolExplorerContent />, {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getByText('No tools to display')).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2021 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 {
|
||||
Content,
|
||||
ContentHeader,
|
||||
EmptyState,
|
||||
Progress,
|
||||
SupportButton,
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
import { exploreToolsConfigRef } from '@backstage/plugin-explore-react';
|
||||
import React from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { ToolCardGrid } from '../ToolCard';
|
||||
|
||||
export const ToolExplorerContent = () => {
|
||||
const exploreToolsConfigApi = useApi(exploreToolsConfigRef);
|
||||
const { value: tools, loading } = useAsync(async () => {
|
||||
return await exploreToolsConfigApi.getTools();
|
||||
}, [exploreToolsConfigApi]);
|
||||
|
||||
return (
|
||||
<Content noPadding>
|
||||
<ContentHeader title="Tools">
|
||||
<SupportButton>Discover the tools in your ecosystem.</SupportButton>
|
||||
</ContentHeader>
|
||||
|
||||
{loading && <Progress />}
|
||||
{!loading && (!tools || tools.length === 0) && (
|
||||
<EmptyState
|
||||
missing="info"
|
||||
title="No tools to display"
|
||||
description={`You haven't added any tools yet.`}
|
||||
/>
|
||||
)}
|
||||
{!loading && tools && <ToolCardGrid tools={tools} />}
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2021 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 { ToolExplorerContent } from './ToolExplorerContent';
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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 { createRoutableExtension } from '@backstage/core';
|
||||
import { explorePlugin } from './plugin';
|
||||
import { exploreRouteRef } from './routes';
|
||||
|
||||
export const ExplorePage = explorePlugin.provide(
|
||||
createRoutableExtension({
|
||||
component: () =>
|
||||
import('./components/ExplorePage').then(m => m.ExplorePage),
|
||||
mountPoint: exploreRouteRef,
|
||||
}),
|
||||
);
|
||||
@@ -14,5 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { plugin } from './plugin';
|
||||
export { Router } from './components/Router';
|
||||
export * from './extensions';
|
||||
export { explorePlugin } from './plugin';
|
||||
export * from './routes';
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { plugin } from './plugin';
|
||||
import { explorePlugin } from './plugin';
|
||||
|
||||
describe('explore', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(plugin).toBeDefined();
|
||||
expect(explorePlugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,9 +14,27 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createPlugin, createRouteRef } from '@backstage/core';
|
||||
import { createApiFactory, createPlugin } from '@backstage/core';
|
||||
import { exploreToolsConfigRef } from '@backstage/plugin-explore-react';
|
||||
import { exploreRouteRef } from './routes';
|
||||
import { exampleTools } from './util/examples';
|
||||
|
||||
export const rootRouteRef = createRouteRef({ path: '', title: 'Explore' });
|
||||
export const plugin = createPlugin({
|
||||
export const explorePlugin = createPlugin({
|
||||
id: 'explore',
|
||||
apis: [
|
||||
// Register a default for exploreToolsConfigRef, you may want to override
|
||||
// the API locally in your app.
|
||||
createApiFactory({
|
||||
api: exploreToolsConfigRef,
|
||||
deps: {},
|
||||
factory: () => ({
|
||||
async getTools() {
|
||||
return exampleTools;
|
||||
},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
routes: {
|
||||
explore: exploreRouteRef,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,13 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Route, Routes } from 'react-router';
|
||||
import { ExplorePluginPage } from './ExplorePluginPage';
|
||||
import { rootRouteRef } from '../plugin';
|
||||
import { createRouteRef } from '@backstage/core';
|
||||
|
||||
export const Router = () => (
|
||||
<Routes>
|
||||
<Route path={`/${rootRouteRef.path}`} element={<ExplorePluginPage />} />
|
||||
</Routes>
|
||||
);
|
||||
const NoIcon = () => null;
|
||||
|
||||
export const exploreRouteRef = createRouteRef({
|
||||
icon: NoIcon,
|
||||
title: 'Explore',
|
||||
});
|
||||
+1
-47
@@ -14,28 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { makeStyles, Typography } from '@material-ui/core';
|
||||
import {
|
||||
Content,
|
||||
ContentHeader,
|
||||
Header,
|
||||
Page,
|
||||
SupportButton,
|
||||
} from '@backstage/core';
|
||||
import ExploreCard, { CardData } from './ExploreCard';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
container: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, 296px)',
|
||||
gridGap: theme.spacing(3),
|
||||
marginBottom: theme.spacing(6),
|
||||
},
|
||||
}));
|
||||
|
||||
const toolsCards = [
|
||||
export const exampleTools = [
|
||||
{
|
||||
title: 'New Relic',
|
||||
description:
|
||||
@@ -112,28 +91,3 @@ const toolsCards = [
|
||||
tags: ['rollbar', 'monitoring', 'errors'],
|
||||
},
|
||||
];
|
||||
|
||||
export const ExplorePluginPage = () => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Page themeId="home">
|
||||
<Header
|
||||
title="Explore"
|
||||
subtitle="Tools and services available in Backstage"
|
||||
/>
|
||||
<Content>
|
||||
<ContentHeader title="Tools">
|
||||
<SupportButton>
|
||||
<Typography>Explore tools available in Backstage</Typography>
|
||||
</SupportButton>
|
||||
</ContentHeader>
|
||||
<div className={classes.container}>
|
||||
{toolsCards.map((card: CardData, ix: any) => (
|
||||
<ExploreCard card={card} key={ix} />
|
||||
))}
|
||||
</div>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -25,13 +25,12 @@ import {
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { generatePath, useNavigate } from 'react-router-dom';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { useAsync } from 'react-use';
|
||||
import { rootDocsRouteRef } from '../../plugin';
|
||||
|
||||
export const TechDocsHome = () => {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { value, loading, error } = useAsync(async () => {
|
||||
const response = await catalogApi.getEntities();
|
||||
@@ -80,15 +79,11 @@ export const TechDocsHome = () => {
|
||||
? value.map((entity, index: number) => (
|
||||
<Grid key={index} item xs={12} sm={6} md={3}>
|
||||
<ItemCard
|
||||
onClick={() =>
|
||||
navigate(
|
||||
generatePath(rootDocsRouteRef.path, {
|
||||
namespace: entity.metadata.namespace ?? 'default',
|
||||
kind: entity.kind,
|
||||
name: entity.metadata.name,
|
||||
}),
|
||||
)
|
||||
}
|
||||
href={generatePath(rootDocsRouteRef.path, {
|
||||
namespace: entity.metadata.namespace ?? 'default',
|
||||
kind: entity.kind,
|
||||
name: entity.metadata.name,
|
||||
})}
|
||||
title={entity.metadata.name}
|
||||
label="Read Docs"
|
||||
description={entity.metadata.description}
|
||||
|
||||
Reference in New Issue
Block a user