diff --git a/app-config.yaml b/app-config.yaml index 8a0199e30a..6202d48560 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -50,6 +50,18 @@ newrelic: baseUrl: 'https://api.newrelic.com/v2' key: NEW_RELIC_REST_API_KEY +catalog: + exampleEntityLocations: + github: + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-order-component.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/podcast-api-component.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/queue-proxy-component.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/searcher-component.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-lib-component.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/www-artist-component.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/shuffle-api-component.yaml + auth: providers: google: @@ -124,7 +136,7 @@ auth: env: AUTH_AUTH0_DOMAIN microsoft: development: - appOrigin: "http://localhost:3000/" + appOrigin: 'http://localhost:3000/' secure: false clientId: $secret: @@ -134,4 +146,4 @@ auth: env: AUTH_MICROSOFT_CLIENT_SECRET tenantId: $secret: - env: AUTH_MICROSOFT_TENANT_ID \ No newline at end of file + env: AUTH_MICROSOFT_TENANT_ID diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 6a3d7362aa..db490cf542 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -15,27 +15,29 @@ */ import { + configApiRef, Content, ContentHeader, + errorApiRef, identityApiRef, SupportButton, - configApiRef, useApi, } from '@backstage/core'; import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder'; import { Button, makeStyles } from '@material-ui/core'; import SettingsIcon from '@material-ui/icons/Settings'; import StarIcon from '@material-ui/icons/Star'; -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; +import { catalogApiRef } from '../../api/types'; import { EntityFilterGroupsProvider, useFilteredEntities } from '../../filter'; import { useStarredEntities } from '../../hooks/useStarredEntites'; -import { CatalogFilter, ButtonGroup } from '../CatalogFilter/CatalogFilter'; +import { ButtonGroup, CatalogFilter } from '../CatalogFilter/CatalogFilter'; import { CatalogTable } from '../CatalogTable/CatalogTable'; +import { ResultsFilter } from '../ResultsFilter/ResultsFilter'; import CatalogLayout from './CatalogLayout'; import { CatalogTabs, LabeledComponentType } from './CatalogTabs'; import { WelcomeBanner } from './WelcomeBanner'; -import { ResultsFilter } from '../ResultsFilter/ResultsFilter'; const useStyles = makeStyles(theme => ({ contentWrapper: { @@ -51,15 +53,34 @@ const CatalogPageContents = () => { const { loading, error, + reload, matchingEntities, availableTags, } = useFilteredEntities(); + const configApi = useApi(configApiRef); + const catalogApi = useApi(catalogApiRef); + const errorApi = useApi(errorApiRef); const { isStarredEntity } = useStarredEntities(); const userId = useApi(identityApiRef).getUserId(); const [selectedTab, setSelectedTab] = useState(); const [selectedSidebarItem, setSelectedSidebarItem] = useState(); - const orgName = - useApi(configApiRef).getOptionalString('organization.name') ?? 'Company'; + const orgName = configApi.getOptionalString('organization.name') ?? 'Company'; + + const addMockData = useCallback(async () => { + try { + const promises: Promise[] = []; + const root = configApi.getConfig('catalog.exampleEntityLocations'); + for (const type of root.keys()) { + for (const target of root.getStringArray(type)) { + promises.push(catalogApi.addLocation(type, target)); + } + } + await Promise.all(promises); + await reload(); + } catch (err) { + errorApi.post(err); + } + }, [catalogApi, configApi, errorApi, reload]); const tabs = useMemo( () => [ @@ -153,6 +174,7 @@ const CatalogPageContents = () => { entities={matchingEntities} loading={loading} error={error} + onAddMockData={addMockData} /> diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx index 707213f3bb..d1eef2a005 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx @@ -15,12 +15,11 @@ */ import { Entity } from '@backstage/catalog-model'; -import { wrapInTestApp } from '@backstage/test-utils'; -import { render } from '@testing-library/react'; +import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; import * as React from 'react'; import { CatalogTable } from './CatalogTable'; -const entites: Entity[] = [ +const entities: Entity[] = [ { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', @@ -40,13 +39,14 @@ const entites: Entity[] = [ describe('CatalogTable component', () => { it('should render error message when error is passed in props', async () => { - const rendered = render( + const rendered = await renderWithEffects( wrapInTestApp( {}} />, ), ); @@ -57,12 +57,13 @@ describe('CatalogTable component', () => { }); it('should display entity names when loading has finished and no error occurred', async () => { - const rendered = render( + const rendered = await renderWithEffects( wrapInTestApp( {}} />, ), ); diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index eed8998fb7..c68eb10e66 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -15,11 +15,12 @@ */ import { Entity, LocationSpec } from '@backstage/catalog-model'; import { Table, TableColumn, TableProps } from '@backstage/core'; -import { Link, Chip } from '@material-ui/core'; +import { Chip, Link } from '@material-ui/core'; +import Add from '@material-ui/icons/Add'; import Edit from '@material-ui/icons/Edit'; import GitHub from '@material-ui/icons/GitHub'; import { Alert } from '@material-ui/lab'; -import React from 'react'; +import React, { Dispatch } from 'react'; import { generatePath, Link as RouterLink } from 'react-router-dom'; import { findLocationForEntityMeta } from '../../data/utils'; import { useStarredEntities } from '../../hooks/useStarredEntites'; @@ -74,7 +75,12 @@ const columns: TableColumn[] = [ <> {entity.metadata.tags && entity.metadata.tags.map(t => ( - + ))} ), @@ -86,6 +92,7 @@ type CatalogTableProps = { titlePreamble: string; loading: boolean; error?: any; + onAddMockData: Dispatch; }; export const CatalogTable = ({ @@ -93,6 +100,7 @@ export const CatalogTable = ({ loading, error, titlePreamble, + onAddMockData, }: CatalogTableProps) => { const { isStarredEntity, toggleStarredEntity } = useStarredEntities(); @@ -148,6 +156,13 @@ export const CatalogTable = ({ onClick: () => toggleStarredEntity(rowData), }; }, + { + icon: () => , + tooltip: 'Add example components', + isFreeAction: true, + onClick: onAddMockData, + hidden: !(entities && entities.length === 0), + }, ]; return ( diff --git a/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx b/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx index db8e21e071..94bc428a45 100644 --- a/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx +++ b/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx @@ -16,8 +16,8 @@ import { Entity } from '@backstage/catalog-model'; import { useApi } from '@backstage/core'; -import React, { useCallback, useRef, useState } from 'react'; -import { useAsync } from 'react-use'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useAsyncFn } from 'react-use'; import { catalogApiRef } from '../api/types'; import { filterGroupsContext, FilterGroupsContext } from './context'; import { @@ -46,7 +46,9 @@ export const EntityFilterGroupsProvider = ({ // The hook that implements the actual context building function useProvideEntityFilters(): FilterGroupsContext { const catalogApi = useApi(catalogApiRef); - const { value: entities, error } = useAsync(() => catalogApi.getEntities()); + const [{ value: entities, error }, doReload] = useAsyncFn(() => + catalogApi.getEntities(), + ); const filterGroups = useRef<{ [filterGroupId: string]: FilterGroup; @@ -61,6 +63,10 @@ function useProvideEntityFilters(): FilterGroupsContext { const [matchingEntities, setMatchingEntities] = useState([]); const [availableTags, setAvailableTags] = useState([]); + useEffect(() => { + doReload(); + }, [doReload]); + const rebuild = useCallback(() => { setFilterGroupStates( buildStates( @@ -122,11 +128,16 @@ function useProvideEntityFilters(): FilterGroupsContext { [rebuild], ); + const reload = useCallback(async () => { + await doReload(); + }, [doReload]); + return { register, unregister, setGroupSelectedFilters, setSelectedTags, + reload, loading: !error && !entities, error, filterGroupStates, diff --git a/plugins/catalog/src/filter/context.ts b/plugins/catalog/src/filter/context.ts index 7f16107f09..838ec8d532 100644 --- a/plugins/catalog/src/filter/context.ts +++ b/plugins/catalog/src/filter/context.ts @@ -27,6 +27,7 @@ export type FilterGroupsContext = { unregister: (filterGroupId: string) => void; setGroupSelectedFilters: (filterGroupId: string, filterIds: string[]) => void; setSelectedTags: (tags: string[]) => void; + reload: () => Promise; loading: boolean; error?: Error; filterGroupStates: { [filterGroupId: string]: FilterGroupStates }; diff --git a/plugins/catalog/src/filter/useEntityFilterGroup.test.tsx b/plugins/catalog/src/filter/useEntityFilterGroup.test.tsx index 60e7d4f412..acf4790ebf 100644 --- a/plugins/catalog/src/filter/useEntityFilterGroup.test.tsx +++ b/plugins/catalog/src/filter/useEntityFilterGroup.test.tsx @@ -51,12 +51,12 @@ describe('useEntityFilterGroup', () => { it('works for an empty set of filters', async () => { catalogApi.getEntities.mockResolvedValue([]); const group: FilterGroup = { filters: {} }; - const { result, wait } = renderHook( + const { result, waitFor } = renderHook( () => useEntityFilterGroup('g1', group), { wrapper }, ); - await wait(() => expect(result.current.state.type).toBe('ready')); + await waitFor(() => expect(result.current.state.type).toBe('ready')); }); it('works for a single group', async () => { @@ -73,12 +73,12 @@ describe('useEntityFilterGroup', () => { f2: e => e.metadata.name !== 'n', }, }; - const { result, wait } = renderHook( + const { result, waitFor } = renderHook( () => useEntityFilterGroup('g1', group), { wrapper }, ); - await wait(() => expect(result.current.state.type).toEqual('ready')); + await waitFor(() => expect(result.current.state.type).toEqual('ready')); let state = result.current.state as FilterGroupStatesReady; expect(state.state.filters.f1).toEqual({ isSelected: false, @@ -91,7 +91,7 @@ describe('useEntityFilterGroup', () => { act(() => result.current.setSelectedFilters(['f1'])); - await wait(() => expect(result.current.state.type).toEqual('ready')); + await waitFor(() => expect(result.current.state.type).toEqual('ready')); state = result.current.state as FilterGroupStatesReady; expect(state.state.filters.f1).toEqual({ isSelected: true, @@ -104,7 +104,7 @@ describe('useEntityFilterGroup', () => { act(() => result.current.setSelectedFilters(['f2'])); - await wait(() => expect(result.current.state.type).toEqual('ready')); + await waitFor(() => expect(result.current.state.type).toEqual('ready')); state = result.current.state as FilterGroupStatesReady; expect(state.state.filters.f1).toEqual({ isSelected: false, diff --git a/plugins/catalog/src/filter/useFilteredEntities.ts b/plugins/catalog/src/filter/useFilteredEntities.ts index 3f70c0bd85..138a7547c0 100644 --- a/plugins/catalog/src/filter/useFilteredEntities.ts +++ b/plugins/catalog/src/filter/useFilteredEntities.ts @@ -31,5 +31,6 @@ export function useFilteredEntities() { error: context.error, matchingEntities: context.matchingEntities, availableTags: context.availableTags, + reload: context.reload, }; }