From bb09898b51e403c3b25c3f668118772b104ce304 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Mon, 15 Jun 2020 12:50:35 +0200 Subject: [PATCH 01/52] feat(catalog): extract useEntitiesStore --- .../CatalogFilter/CatalogFilter.tsx | 7 +- .../components/CatalogFilter/OwnedCount.tsx | 23 ++++++ .../components/CatalogFilter/StarredCount.tsx | 4 +- .../components/CatalogPage/CatalogPage.tsx | 42 ++++------ plugins/catalog/src/data/filters.ts | 14 +++- plugins/catalog/src/hooks/useEntitiesStore.ts | 82 +++++++++++++++++++ 6 files changed, 136 insertions(+), 36 deletions(-) create mode 100644 plugins/catalog/src/components/CatalogFilter/OwnedCount.tsx create mode 100644 plugins/catalog/src/hooks/useEntitiesStore.ts diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx index 5335b33ffa..0abb5f817a 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React from 'react'; +import React, { FC } from 'react'; import { Card, List, @@ -27,11 +27,12 @@ import { } from '@material-ui/core'; import type { IconComponent } from '@backstage/core'; import { EntityFilterType } from '../../data/filters'; + export type CatalogFilterItem = { id: EntityFilterType; label: string; icon?: IconComponent; - count?: number | React.FC; + count?: number | FC; }; export type CatalogFilterGroup = { @@ -71,7 +72,7 @@ const useStyles = makeStyles(theme => ({ }, })); -export const CatalogFilter: React.FC = ({ +export const CatalogFilter: FC = ({ groups, selectedId, onSelectedChange, diff --git a/plugins/catalog/src/components/CatalogFilter/OwnedCount.tsx b/plugins/catalog/src/components/CatalogFilter/OwnedCount.tsx new file mode 100644 index 0000000000..a48019c48a --- /dev/null +++ b/plugins/catalog/src/components/CatalogFilter/OwnedCount.tsx @@ -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 React, { FC } from 'react'; +import { useStarredEntities } from '../../hooks/useStarredEntites'; + +export const StarredCount: FC<{}> = () => { + const { starredEntities } = useStarredEntities(); + return {starredEntities.size}; +}; diff --git a/plugins/catalog/src/components/CatalogFilter/StarredCount.tsx b/plugins/catalog/src/components/CatalogFilter/StarredCount.tsx index 5e79682783..a48019c48a 100644 --- a/plugins/catalog/src/components/CatalogFilter/StarredCount.tsx +++ b/plugins/catalog/src/components/CatalogFilter/StarredCount.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import React from 'react'; +import React, { FC } from 'react'; import { useStarredEntities } from '../../hooks/useStarredEntites'; -export const StarredCount: React.FC<{}> = () => { +export const StarredCount: FC<{}> = () => { const { starredEntities } = useStarredEntities(); return {starredEntities.size}; }; diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 3ba63188c1..87e25f8f82 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -21,7 +21,6 @@ import { DismissableBanner, HeaderTabs, SupportButton, - useApi, } from '@backstage/core'; import CatalogLayout from './CatalogLayout'; import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder'; @@ -30,18 +29,13 @@ import Edit from '@material-ui/icons/Edit'; import GitHub from '@material-ui/icons/GitHub'; import Star from '@material-ui/icons/Star'; import StarOutline from '@material-ui/icons/StarBorder'; -import React, { FC, useCallback, useState } from 'react'; +import React, { FC, useCallback } from 'react'; import { Link as RouterLink } from 'react-router-dom'; -import { catalogApiRef } from '../..'; -import { defaultFilter, entityFilters, filterGroups } from '../../data/filters'; -import { findLocationForEntityMeta } from '../../data/utils'; -import { useStarredEntities } from '../../hooks/useStarredEntites'; -import { - CatalogFilter, - CatalogFilterItem, -} from '../CatalogFilter/CatalogFilter'; +import { CatalogFilter } from '../CatalogFilter/CatalogFilter'; import { CatalogTable } from '../CatalogTable/CatalogTable'; -import useStaleWhileRevalidate from 'swr'; +import { useEntitiesStore } from '../../hooks/useEntitiesStore'; +import { findLocationForEntityMeta } from '../../data/utils'; +import { filterGroups } from '../../data/filters'; const useStyles = makeStyles(theme => ({ contentWrapper: { @@ -57,26 +51,18 @@ const useStyles = makeStyles(theme => ({ })); export const CatalogPage: FC<{}> = () => { - const catalogApi = useApi(catalogApiRef); - const { toggleStarredEntity, isStarredEntity } = useStarredEntities(); - - const [selectedFilter, setSelectedFilter] = useState( - defaultFilter, - ); - - const { data: entities, error } = useStaleWhileRevalidate( - ['catalog/all', entityFilters[selectedFilter.id]], - async () => catalogApi.getEntities(), - ); - - const data = - entities?.filter(e => - entityFilters[selectedFilter.id](e, { isStarred: isStarredEntity(e) }), - ) ?? []; + const { + filteredEntities: data, + selectedFilter, + setSelectedFilter, + error, + toggleStarredEntity, + isStarredEntity, + } = useEntitiesStore(); const onFilterSelected = useCallback( selected => setSelectedFilter(selected), - [], + [setSelectedFilter], ); const styles = useStyles(); diff --git a/plugins/catalog/src/data/filters.ts b/plugins/catalog/src/data/filters.ts index 71efa923c6..c20f6b6b14 100644 --- a/plugins/catalog/src/data/filters.ts +++ b/plugins/catalog/src/data/filters.ts @@ -63,14 +63,22 @@ export const filterGroups: CatalogFilterGroup[] = [ type EntityFilter = (entity: Entity, options: EntityFilterOptions) => boolean; -type EntityFilterOptions = { +type EntityFilterOptions = Partial<{ isStarred: boolean; + userId: string; +}>; + +type Owned = { + owner: string; }; export const entityFilters: Record = { - [EntityFilterType.OWNED]: () => false, + [EntityFilterType.OWNED]: (e, { userId }) => { + const owner = (e.spec! as Owned).owner; + return owner === userId; + }, [EntityFilterType.ALL]: () => true, - [EntityFilterType.STARRED]: (_, { isStarred }) => isStarred, + [EntityFilterType.STARRED]: (_, { isStarred }) => isStarred ?? false, }; export const defaultFilter: CatalogFilterItem = filterGroups[0].items[0]; diff --git a/plugins/catalog/src/hooks/useEntitiesStore.ts b/plugins/catalog/src/hooks/useEntitiesStore.ts new file mode 100644 index 0000000000..21ba283dfe --- /dev/null +++ b/plugins/catalog/src/hooks/useEntitiesStore.ts @@ -0,0 +1,82 @@ +/* + * 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 { useState, useEffect } from 'react'; +import { CatalogFilterItem } from '../components/CatalogFilter/CatalogFilter'; +import { + defaultFilter, + EntityFilterType, + entityFilters, +} from '../data/filters'; +import { useApi } from '@backstage/core'; +import { catalogApiRef } from '..'; +import { useStarredEntities } from './useStarredEntites'; +import { Entity } from '@backstage/catalog-model'; +import useStaleWhileRevalidate from 'swr'; + +export const useEntitiesStore = () => { + const [selectedFilter, setSelectedFilter] = useState( + defaultFilter, + ); + const catalogApi = useApi(catalogApiRef); + const { toggleStarredEntity, isStarredEntity } = useStarredEntities(); + const { data: entities, error } = useStaleWhileRevalidate( + ['catalog/all', entityFilters[selectedFilter.id]], + async () => catalogApi.getEntities(), + ); + const useUser = () => { + const [user] = useState('tools@example.com'); + return user; + }; + const userId = useUser(); + + const [filteredEntities, setFilteredEntities] = useState([]); + const [entitiesByFilter] = useState>( + // Create an empty result for every filter by default + Object.keys(EntityFilterType).reduce( + (res, key) => ({ ...res, [key]: [] }), + {}, + ), + ); + useEffect(() => { + // const dataByFilter = Object.keys(EntityFilterType).reduce( + + // ,{}); + + const data = + entities?.filter((e: Entity) => + entityFilters[selectedFilter.id](e, { + isStarred: isStarredEntity(e), + userId, + }), + ) ?? []; + setFilteredEntities(data); + }, [ + entities, + selectedFilter.id, + isStarredEntity, + userId, + setFilteredEntities, + ]); + return { + filteredEntities, + selectedFilter, + setSelectedFilter, + error, + toggleStarredEntity, + isStarredEntity, + entitiesByFilter, + }; +}; From c8797d6d7a8a7a0d4059e61a8f6da0672389c0b1 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Mon, 15 Jun 2020 17:45:05 +0200 Subject: [PATCH 02/52] fix(catalog): starred entities count --- .../CatalogFilter/AllServicesCount.tsx | 4 +- .../CatalogFilter/CatalogFilter.tsx | 32 ++++---- .../components/CatalogPage/CatalogPage.tsx | 34 ++++---- plugins/catalog/src/data/filters.ts | 11 +++ plugins/catalog/src/hooks/useEntitiesStore.ts | 82 ------------------- 5 files changed, 46 insertions(+), 117 deletions(-) delete mode 100644 plugins/catalog/src/hooks/useEntitiesStore.ts diff --git a/plugins/catalog/src/components/CatalogFilter/AllServicesCount.tsx b/plugins/catalog/src/components/CatalogFilter/AllServicesCount.tsx index 5ccac8f772..c528b40882 100644 --- a/plugins/catalog/src/components/CatalogFilter/AllServicesCount.tsx +++ b/plugins/catalog/src/components/CatalogFilter/AllServicesCount.tsx @@ -14,13 +14,13 @@ * limitations under the License. */ -import React from 'react'; +import React, { FC } from 'react'; import { useApi } from '@backstage/core'; import { catalogApiRef } from '../../api/types'; import { useAsync } from 'react-use'; import { CircularProgress, useTheme } from '@material-ui/core'; -export const AllServicesCount: React.FC<{}> = () => { +export const AllServicesCount: FC<{}> = () => { const theme = useTheme(); const catalogApi = useApi(catalogApiRef); const { value, loading } = useAsync(() => catalogApi.getEntities()); diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx index 0abb5f817a..e815d470e1 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx @@ -26,7 +26,8 @@ import { makeStyles, } from '@material-ui/core'; import type { IconComponent } from '@backstage/core'; -import { EntityFilterType } from '../../data/filters'; +import { EntityFilterType, filterGroups } from '../../data/filters'; +import { EntitiesByFilter } from '../../hooks/useEntities'; export type CatalogFilterItem = { id: EntityFilterType; @@ -40,12 +41,6 @@ export type CatalogFilterGroup = { items: CatalogFilterItem[]; }; -export type CatalogFilterProps = { - groups: CatalogFilterGroup[]; - selectedId?: string; - onSelectedChange?: (item: CatalogFilterItem) => void; -}; - const useStyles = makeStyles(theme => ({ root: { backgroundColor: 'rgba(0, 0, 0, .11)', @@ -72,11 +67,16 @@ const useStyles = makeStyles(theme => ({ }, })); -export const CatalogFilter: FC = ({ - groups, - selectedId, - onSelectedChange, +export const CatalogFilter: FC<{ + selectedFilter: EntityFilterType; + onFilterChange: (type: EntityFilterType) => void; + entitiesByFilter: EntitiesByFilter; +}> = ({ + selectedFilter: selectedId, + onFilterChange: setSelectedFilter, + entitiesByFilter, }) => { + const groups = filterGroups; const classes = useStyles(); return ( @@ -92,7 +92,9 @@ export const CatalogFilter: FC = ({ key={item.id} button divider - onClick={() => onSelectedChange?.(item)} + onClick={() => { + setSelectedFilter(item.id); + }} selected={item.id === selectedId} className={classes.menuItem} > @@ -106,11 +108,7 @@ export const CatalogFilter: FC = ({ {item.label} - {typeof item.count === 'function' ? ( - - ) : ( - item.count - )} + {entitiesByFilter[item.id]?.length ?? 0} ))} diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 87e25f8f82..0ab4e9c7d4 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -29,13 +29,16 @@ import Edit from '@material-ui/icons/Edit'; import GitHub from '@material-ui/icons/GitHub'; import Star from '@material-ui/icons/Star'; import StarOutline from '@material-ui/icons/StarBorder'; -import React, { FC, useCallback } from 'react'; +import React, { FC } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import { CatalogFilter } from '../CatalogFilter/CatalogFilter'; import { CatalogTable } from '../CatalogTable/CatalogTable'; -import { useEntitiesStore } from '../../hooks/useEntitiesStore'; +import { useEntities } from '../../hooks/useEntities'; import { findLocationForEntityMeta } from '../../data/utils'; -import { filterGroups } from '../../data/filters'; +import { + getCatalogFilterItemByType, + EntityFilterType, +} from '../../data/filters'; const useStyles = makeStyles(theme => ({ contentWrapper: { @@ -52,19 +55,15 @@ const useStyles = makeStyles(theme => ({ export const CatalogPage: FC<{}> = () => { const { - filteredEntities: data, - selectedFilter, - setSelectedFilter, + entitiesByFilter, + selectedFilter: selectedId, error, toggleStarredEntity, isStarredEntity, - } = useEntitiesStore(); - - const onFilterSelected = useCallback( - selected => setSelectedFilter(selected), - [setSelectedFilter], - ); + setSelectedFilter, + } = useEntities(); + const data = entitiesByFilter[selectedId ?? EntityFilterType.ALL]; const styles = useStyles(); const actions = [ @@ -172,13 +171,16 @@ export const CatalogPage: FC<{}> = () => {
{ + for (const group of filterGroups) { + for (const filter of group.items) { + if (filter.id === filterType) { + return filter; + } + } + } + return null; +}; + type EntityFilter = (entity: Entity, options: EntityFilterOptions) => boolean; type EntityFilterOptions = Partial<{ diff --git a/plugins/catalog/src/hooks/useEntitiesStore.ts b/plugins/catalog/src/hooks/useEntitiesStore.ts deleted file mode 100644 index 21ba283dfe..0000000000 --- a/plugins/catalog/src/hooks/useEntitiesStore.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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 { useState, useEffect } from 'react'; -import { CatalogFilterItem } from '../components/CatalogFilter/CatalogFilter'; -import { - defaultFilter, - EntityFilterType, - entityFilters, -} from '../data/filters'; -import { useApi } from '@backstage/core'; -import { catalogApiRef } from '..'; -import { useStarredEntities } from './useStarredEntites'; -import { Entity } from '@backstage/catalog-model'; -import useStaleWhileRevalidate from 'swr'; - -export const useEntitiesStore = () => { - const [selectedFilter, setSelectedFilter] = useState( - defaultFilter, - ); - const catalogApi = useApi(catalogApiRef); - const { toggleStarredEntity, isStarredEntity } = useStarredEntities(); - const { data: entities, error } = useStaleWhileRevalidate( - ['catalog/all', entityFilters[selectedFilter.id]], - async () => catalogApi.getEntities(), - ); - const useUser = () => { - const [user] = useState('tools@example.com'); - return user; - }; - const userId = useUser(); - - const [filteredEntities, setFilteredEntities] = useState([]); - const [entitiesByFilter] = useState>( - // Create an empty result for every filter by default - Object.keys(EntityFilterType).reduce( - (res, key) => ({ ...res, [key]: [] }), - {}, - ), - ); - useEffect(() => { - // const dataByFilter = Object.keys(EntityFilterType).reduce( - - // ,{}); - - const data = - entities?.filter((e: Entity) => - entityFilters[selectedFilter.id](e, { - isStarred: isStarredEntity(e), - userId, - }), - ) ?? []; - setFilteredEntities(data); - }, [ - entities, - selectedFilter.id, - isStarredEntity, - userId, - setFilteredEntities, - ]); - return { - filteredEntities, - selectedFilter, - setSelectedFilter, - error, - toggleStarredEntity, - isStarredEntity, - entitiesByFilter, - }; -}; From 6ef5f53fdec4c3e53c96d212e07a230ec45e66e9 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Mon, 15 Jun 2020 18:00:09 +0200 Subject: [PATCH 03/52] chore(catalog): clean up code --- .../CatalogFilter/AllServicesCount.tsx | 2 +- .../CatalogFilter/CatalogFilter.tsx | 2 +- .../components/CatalogFilter/OwnedCount.tsx | 23 ----- .../CatalogFilter/StarredCount.test.tsx | 35 -------- .../components/CatalogFilter/StarredCount.tsx | 23 ----- plugins/catalog/src/data/filters.ts | 5 -- plugins/catalog/src/hooks/useEntities.ts | 88 +++++++++++++++++++ 7 files changed, 90 insertions(+), 88 deletions(-) delete mode 100644 plugins/catalog/src/components/CatalogFilter/OwnedCount.tsx delete mode 100644 plugins/catalog/src/components/CatalogFilter/StarredCount.test.tsx delete mode 100644 plugins/catalog/src/components/CatalogFilter/StarredCount.tsx create mode 100644 plugins/catalog/src/hooks/useEntities.ts diff --git a/plugins/catalog/src/components/CatalogFilter/AllServicesCount.tsx b/plugins/catalog/src/components/CatalogFilter/AllServicesCount.tsx index c528b40882..b4d90c249a 100644 --- a/plugins/catalog/src/components/CatalogFilter/AllServicesCount.tsx +++ b/plugins/catalog/src/components/CatalogFilter/AllServicesCount.tsx @@ -29,5 +29,5 @@ export const AllServicesCount: FC<{}> = () => { return ; } - return {value?.length ?? '-'}; + return {value ?? length ?? '-'}; }; diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx index e815d470e1..f11494079f 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx @@ -108,7 +108,7 @@ export const CatalogFilter: FC<{ {item.label} - {entitiesByFilter[item.id]?.length ?? 0} + {entitiesByFilter[item.id]?.length ?? '-'} ))} diff --git a/plugins/catalog/src/components/CatalogFilter/OwnedCount.tsx b/plugins/catalog/src/components/CatalogFilter/OwnedCount.tsx deleted file mode 100644 index a48019c48a..0000000000 --- a/plugins/catalog/src/components/CatalogFilter/OwnedCount.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { FC } from 'react'; -import { useStarredEntities } from '../../hooks/useStarredEntites'; - -export const StarredCount: FC<{}> = () => { - const { starredEntities } = useStarredEntities(); - return {starredEntities.size}; -}; diff --git a/plugins/catalog/src/components/CatalogFilter/StarredCount.test.tsx b/plugins/catalog/src/components/CatalogFilter/StarredCount.test.tsx deleted file mode 100644 index 546d00e854..0000000000 --- a/plugins/catalog/src/components/CatalogFilter/StarredCount.test.tsx +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import { render } from '@testing-library/react'; -import { wrapInTestApp } from '@backstage/test-utils'; -import { StarredCount } from './StarredCount'; -import * as Hooks from '../../hooks/useStarredEntites'; - -describe('Starred Count', () => { - it('should render the count returned from the hook', async () => { - jest.spyOn(Hooks, 'useStarredEntities').mockReturnValue({ - starredEntities: new Set(['id1', 'id2', 'id3', 'id4']), - isStarredEntity: () => false, - toggleStarredEntity: () => undefined, - }); - - const { findByText } = render(wrapInTestApp()); - - expect(await findByText('4')).toBeInTheDocument(); - }); -}); diff --git a/plugins/catalog/src/components/CatalogFilter/StarredCount.tsx b/plugins/catalog/src/components/CatalogFilter/StarredCount.tsx deleted file mode 100644 index a48019c48a..0000000000 --- a/plugins/catalog/src/components/CatalogFilter/StarredCount.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { FC } from 'react'; -import { useStarredEntities } from '../../hooks/useStarredEntites'; - -export const StarredCount: FC<{}> = () => { - const { starredEntities } = useStarredEntities(); - return {starredEntities.size}; -}; diff --git a/plugins/catalog/src/data/filters.ts b/plugins/catalog/src/data/filters.ts index 226ccbc683..6230b3c8fc 100644 --- a/plugins/catalog/src/data/filters.ts +++ b/plugins/catalog/src/data/filters.ts @@ -17,12 +17,10 @@ import { Entity } from '@backstage/catalog-model'; import SettingsIcon from '@material-ui/icons/Settings'; import StarIcon from '@material-ui/icons/Star'; -import { AllServicesCount } from '../components/CatalogFilter/AllServicesCount'; import { CatalogFilterGroup, CatalogFilterItem, } from '../components/CatalogFilter/CatalogFilter'; -import { StarredCount } from '../components/CatalogFilter/StarredCount'; export enum EntityFilterType { ALL = 'ALL', @@ -37,13 +35,11 @@ export const filterGroups: CatalogFilterGroup[] = [ { id: EntityFilterType.OWNED, label: 'Owned', - count: 0, icon: SettingsIcon, }, { id: EntityFilterType.STARRED, label: 'Starred', - count: StarredCount, icon: StarIcon, }, ], @@ -55,7 +51,6 @@ export const filterGroups: CatalogFilterGroup[] = [ { id: EntityFilterType.ALL, label: 'All Services', - count: AllServicesCount, }, ], }, diff --git a/plugins/catalog/src/hooks/useEntities.ts b/plugins/catalog/src/hooks/useEntities.ts new file mode 100644 index 0000000000..8dca72f095 --- /dev/null +++ b/plugins/catalog/src/hooks/useEntities.ts @@ -0,0 +1,88 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useState, useMemo } from 'react'; +import { EntityFilterType, entityFilters } from '../data/filters'; +import { useApi } from '@backstage/core'; +import { catalogApiRef } from '..'; +import { useStarredEntities } from './useStarredEntites'; +import { Entity } from '@backstage/catalog-model'; +import useStaleWhileRevalidate from 'swr'; + +export type EntitiesByFilter = Record; + +type UseEntities = { + selectedFilter: EntityFilterType | undefined; + setSelectedFilter: (f: EntityFilterType) => void; + error: Error | null; + toggleStarredEntity: any; + isStarredEntity: (e: Entity) => boolean; + entitiesByFilter: EntitiesByFilter; +}; + +export const useEntities = (): UseEntities => { + const [selectedFilter, setSelectedFilter] = useState< + EntityFilterType | undefined + >(); + const catalogApi = useApi(catalogApiRef); + const { toggleStarredEntity, isStarredEntity } = useStarredEntities(); + const { data: entities, error } = useStaleWhileRevalidate( + ['catalog/all', entityFilters[selectedFilter ?? EntityFilterType.ALL]], + async () => catalogApi.getEntities(), + ); + const useUser = () => { + const [user] = useState('tools@example.com'); + return user; + }; + const userId = useUser(); + + const entitiesByFilter = useMemo(() => { + const filterEntities = ( + ents: Entity[] | undefined, + filterId: EntityFilterType, + isStarred: (e: Entity) => boolean, + user: string, + ) => { + return ents?.filter((e: Entity) => + entityFilters[filterId](e, { + isStarred: isStarred(e), + userId: user, + }), + ); + }; + const data = Object.keys(EntityFilterType).reduce( + (res, key) => ({ + ...res, + [key]: filterEntities( + entities, + key as EntityFilterType, + isStarredEntity, + userId, + ), + }), + {} as EntitiesByFilter, + ); + return data; + }, [entities, isStarredEntity, userId]); + + return { + selectedFilter, + setSelectedFilter, + error, + toggleStarredEntity, + isStarredEntity, + entitiesByFilter, + }; +}; From 806790ea8907f5bc423e13d7dd46eb50fa7e194c Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Tue, 16 Jun 2020 09:24:32 +0200 Subject: [PATCH 04/52] feat(catalog): use identity API for filtering owned entites --- plugins/catalog/src/hooks/useEntities.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/plugins/catalog/src/hooks/useEntities.ts b/plugins/catalog/src/hooks/useEntities.ts index 8dca72f095..e23ec72910 100644 --- a/plugins/catalog/src/hooks/useEntities.ts +++ b/plugins/catalog/src/hooks/useEntities.ts @@ -15,7 +15,7 @@ */ import { useState, useMemo } from 'react'; import { EntityFilterType, entityFilters } from '../data/filters'; -import { useApi } from '@backstage/core'; +import { useApi, identifyApiRef } from '@backstage/core'; import { catalogApiRef } from '..'; import { useStarredEntities } from './useStarredEntites'; import { Entity } from '@backstage/catalog-model'; @@ -42,11 +42,9 @@ export const useEntities = (): UseEntities => { ['catalog/all', entityFilters[selectedFilter ?? EntityFilterType.ALL]], async () => catalogApi.getEntities(), ); - const useUser = () => { - const [user] = useState('tools@example.com'); - return user; - }; - const userId = useUser(); + + const indentityApi = useApi(identifyApiRef); + const userId = indentityApi.getUserId(); const entitiesByFilter = useMemo(() => { const filterEntities = ( From 3b19d7f37abcce4233ea18d17e9e0c23b50df65e Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Tue, 16 Jun 2020 11:05:17 +0200 Subject: [PATCH 05/52] feat(catalog): add placeholder for identity API implementation --- packages/app/src/apis.ts | 4 +++ .../src/apis/definitions/IdentityApi.ts | 2 +- .../implementations/IdentityApi/Identity.ts | 25 +++++++++++++++++++ .../apis/implementations/IdentityApi/index.ts | 16 ++++++++++++ .../src/apis/implementations/index.ts | 1 + packages/storybook/.storybook/apis.js | 4 +++ .../CatalogPage/CatalogPage.test.tsx | 2 ++ plugins/catalog/src/hooks/useEntities.ts | 4 +-- 8 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 packages/core-api/src/apis/implementations/IdentityApi/Identity.ts create mode 100644 packages/core-api/src/apis/implementations/IdentityApi/index.ts diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index 9bfc618216..a6d9987819 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -32,6 +32,8 @@ import { githubAuthApiRef, storageApiRef, WebStorage, + identityApiRef, + MockIdentity, } from '@backstage/core'; import { @@ -52,6 +54,8 @@ export const apis = (config: ConfigApi) => { const builder = ApiRegistry.builder(); + builder.add(identityApiRef, new MockIdentity()); + const alertApi = builder.add(alertApiRef, new AlertApiForwarder()); const errorApi = builder.add( errorApiRef, diff --git a/packages/core-api/src/apis/definitions/IdentityApi.ts b/packages/core-api/src/apis/definitions/IdentityApi.ts index bbfab0ef35..4c9ccfdbf6 100644 --- a/packages/core-api/src/apis/definitions/IdentityApi.ts +++ b/packages/core-api/src/apis/definitions/IdentityApi.ts @@ -40,7 +40,7 @@ export type IdentityApi = { // TODO: getProfile(): Promise - We want this to be async when added, but needs more work. }; -export const identifyApiRef = createApiRef({ +export const identityApiRef = createApiRef({ id: 'core.identity', description: 'Provides access to the identity of the signed in user', }); diff --git a/packages/core-api/src/apis/implementations/IdentityApi/Identity.ts b/packages/core-api/src/apis/implementations/IdentityApi/Identity.ts new file mode 100644 index 0000000000..6f2035d1b8 --- /dev/null +++ b/packages/core-api/src/apis/implementations/IdentityApi/Identity.ts @@ -0,0 +1,25 @@ +/* + * 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 { IdentityApi } from '../..'; + +export class MockIdentity implements IdentityApi { + getUserId(): string { + return ''; + } + getIdToken(): string | undefined { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/core-api/src/apis/implementations/IdentityApi/index.ts b/packages/core-api/src/apis/implementations/IdentityApi/index.ts new file mode 100644 index 0000000000..1e57036685 --- /dev/null +++ b/packages/core-api/src/apis/implementations/IdentityApi/index.ts @@ -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 * from './Identity'; diff --git a/packages/core-api/src/apis/implementations/index.ts b/packages/core-api/src/apis/implementations/index.ts index e6d23fee21..885082ce09 100644 --- a/packages/core-api/src/apis/implementations/index.ts +++ b/packages/core-api/src/apis/implementations/index.ts @@ -26,3 +26,4 @@ export * from './ConfigApi'; export * from './ErrorApi'; export * from './OAuthRequestApi'; export * from './StorageApi'; +export * from './IdentityApi'; diff --git a/packages/storybook/.storybook/apis.js b/packages/storybook/.storybook/apis.js index d3150400cf..9dc1e2bad2 100644 --- a/packages/storybook/.storybook/apis.js +++ b/packages/storybook/.storybook/apis.js @@ -11,6 +11,8 @@ import { ErrorAlerter, GoogleAuth, GithubAuth, + identityApiRef, + MockIdentity, } from '@backstage/core'; const builder = ApiRegistry.builder(); @@ -19,6 +21,8 @@ const alertApi = builder.add(alertApiRef, new AlertApiForwarder()); builder.add(errorApiRef, new ErrorAlerter(alertApi, new ErrorApiForwarder())); +builder.add(identityApiRef, new MockIdentity()); + const oauthRequestApi = builder.add( oauthRequestApiRef, new OAuthRequestManager(), diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index 2e1989323c..4cd0cae7af 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -20,6 +20,8 @@ import { errorApiRef, storageApiRef, WebStorage, + IdentityApi, + identityApiRef, } from '@backstage/core'; import { MockErrorApi, wrapInTestApp } from '@backstage/test-utils'; import { render } from '@testing-library/react'; diff --git a/plugins/catalog/src/hooks/useEntities.ts b/plugins/catalog/src/hooks/useEntities.ts index e23ec72910..1530be6f7c 100644 --- a/plugins/catalog/src/hooks/useEntities.ts +++ b/plugins/catalog/src/hooks/useEntities.ts @@ -15,7 +15,7 @@ */ import { useState, useMemo } from 'react'; import { EntityFilterType, entityFilters } from '../data/filters'; -import { useApi, identifyApiRef } from '@backstage/core'; +import { useApi, identityApiRef } from '@backstage/core'; import { catalogApiRef } from '..'; import { useStarredEntities } from './useStarredEntites'; import { Entity } from '@backstage/catalog-model'; @@ -43,7 +43,7 @@ export const useEntities = (): UseEntities => { async () => catalogApi.getEntities(), ); - const indentityApi = useApi(identifyApiRef); + const indentityApi = useApi(identityApiRef); const userId = indentityApi.getUserId(); const entitiesByFilter = useMemo(() => { From 92629f5f5de6aa423e31f1126f16cdeb8b9db63d Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Wed, 17 Jun 2020 03:00:30 +0200 Subject: [PATCH 06/52] fix(catalog): tests and rebase on master --- plugins/catalog/package.json | 2 +- .../CatalogFilter/CatalogFilter.test.tsx | 84 +++++++++++++++---- .../CatalogFilter/CatalogFilter.tsx | 5 +- .../CatalogPage/CatalogPage.test.tsx | 45 ++++++++-- .../components/CatalogPage/CatalogPage.tsx | 2 + yarn.lock | 25 +++++- 6 files changed, 140 insertions(+), 23 deletions(-) diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index d7329e4b2f..500adaf0b5 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -43,7 +43,7 @@ "@backstage/dev-utils": "^0.1.1-alpha.8", "@backstage/test-utils": "^0.1.1-alpha.8", "@testing-library/jest-dom": "^5.7.0", - "@testing-library/react": "^9.3.2", + "@testing-library/react": "^10.2.1", "@testing-library/react-hooks": "^3.3.0", "@testing-library/user-event": "^10.2.4", "@types/jest": "^25.2.2", diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx index b78450d549..a19459b236 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx @@ -15,19 +15,60 @@ */ import React from 'react'; -import { render, fireEvent } from '@testing-library/react'; +import { render, fireEvent, waitFor, screen } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; import { CatalogFilter, CatalogFilterGroup } from './CatalogFilter'; import { EntityFilterType } from '../../data/filters'; describe('Catalog Filter', () => { + const comp1 = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component-1', + }, + spec: { + owner: 'team', + }, + }; + const comp2 = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component-2', + }, + spec: { + owner: 'team', + }, + }; + const comp3 = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component-3', + }, + spec: { + owner: '', + }, + }; + const defaultFilterProps = { + selectedFilter: EntityFilterType.ALL, + onFilterChange: (type: EntityFilterType) => type, + entitiesByFilter: { + [EntityFilterType.ALL]: [comp1, comp2, comp3], + [EntityFilterType.STARRED]: [comp1], + [EntityFilterType.OWNED]: [comp1], + }, + }; it('should render the different groups', async () => { const mockGroups: CatalogFilterGroup[] = [ { name: 'Test Group 1', items: [] }, { name: 'Test Group 2', items: [] }, ]; const { findByText } = render( - wrapInTestApp(), + wrapInTestApp( + , + ), ); for (const group of mockGroups) { @@ -53,7 +94,9 @@ describe('Catalog Filter', () => { ]; const { findByText } = render( - wrapInTestApp(), + wrapInTestApp( + , + ), ); const [group] = mockGroups; @@ -70,24 +113,34 @@ describe('Catalog Filter', () => { { id: EntityFilterType.ALL, label: 'First Label', - count: 100, + count: 3, }, { id: EntityFilterType.STARRED, label: 'Second Label', - count: 400, + count: 1, }, ], }, ]; - const { findByText } = render( - wrapInTestApp(), + render( + wrapInTestApp( + , + ), ); - const [group] = mockGroups; - for (const item of group.items) { - expect(await findByText(item.count!.toString())).toBeInTheDocument(); + for (const key of Object.keys(defaultFilterProps.entitiesByFilter)) { + await waitFor(() => + screen.getAllByText( + new RegExp( + `(${ + defaultFilterProps.entitiesByFilter[key as EntityFilterType] + .length + })`, + ), + ), + ); } }); @@ -115,8 +168,9 @@ describe('Catalog Filter', () => { const { findByText } = render( wrapInTestApp( , ), ); @@ -127,7 +181,7 @@ describe('Catalog Filter', () => { fireEvent.click(element); - expect(onSelectedChangeHandler).toHaveBeenCalledWith(item); + expect(onSelectedChangeHandler).toHaveBeenCalledWith(item.id); }); it('should render a component when a function is passed to the count component', async () => { @@ -149,9 +203,11 @@ describe('Catalog Filter', () => { }, ]; const { findByText } = render( - wrapInTestApp(), + wrapInTestApp( + , + ), ); - expect(await findByText('BACKSTAGE!')).toBeInTheDocument(); + expect(await findByText('Test Group 1')).toBeInTheDocument(); }); }); diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx index f11494079f..cc3d269e61 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx @@ -26,7 +26,7 @@ import { makeStyles, } from '@material-ui/core'; import type { IconComponent } from '@backstage/core'; -import { EntityFilterType, filterGroups } from '../../data/filters'; +import { EntityFilterType } from '../../data/filters'; import { EntitiesByFilter } from '../../hooks/useEntities'; export type CatalogFilterItem = { @@ -71,12 +71,13 @@ export const CatalogFilter: FC<{ selectedFilter: EntityFilterType; onFilterChange: (type: EntityFilterType) => void; entitiesByFilter: EntitiesByFilter; + groups: CatalogFilterGroup[]; }> = ({ selectedFilter: selectedId, onFilterChange: setSelectedFilter, entitiesByFilter, + groups, }) => { - const groups = filterGroups; const classes = useStyles(); return ( diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index 4cd0cae7af..4a9319ea3b 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -24,7 +24,7 @@ import { identityApiRef, } from '@backstage/core'; import { MockErrorApi, wrapInTestApp } from '@backstage/test-utils'; -import { render } from '@testing-library/react'; +import { screen, render, fireEvent, waitFor } from '@testing-library/react'; import React from 'react'; import { catalogApiRef } from '../..'; import { CatalogApi } from '../../api/types'; @@ -42,31 +42,66 @@ describe('CatalogPage', () => { }, apiVersion: 'backstage.io/v1alpha1', kind: 'Component', + spec: { + owner: 'tools@example.com', + }, + }, + { + metadata: { + name: 'Entity2', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + spec: { + owner: 'not-tools@example.com', + }, }, ] as Entity[]), getLocationByEntity: () => Promise.resolve({ id: 'id', type: 'github', target: 'url' }), }; + const mockIndentityApi: Partial = { + getUserId: () => 'tools@example.com', + }; // this test right now causes some red lines in the log output when running tests // related to some theme issues in mui-table // https://github.com/mbrn/material-table/issues/1293 it('should render', async () => { - const rendered = render( + render( wrapInTestApp( , ), ); - expect( - await rendered.findByText('Backstage Service Catalog'), - ).toBeInTheDocument(); + await waitFor(() => screen.getByText(/All Services \(2\)/)); + expect(screen.getByText(/All Services \(2\)/)).toBeInTheDocument(); + }); + it('should filter by owner', async () => { + render( + wrapInTestApp( + + + , + ), + ); + fireEvent.click(screen.getByText(/Owned/)); + await waitFor(() => screen.getByText(/Owned \(1\)/)); + expect(screen.getByText(/Owned \(1\)/)).toBeInTheDocument(); }); }); diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 0ab4e9c7d4..1722dbb9ac 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -36,6 +36,7 @@ import { CatalogTable } from '../CatalogTable/CatalogTable'; import { useEntities } from '../../hooks/useEntities'; import { findLocationForEntityMeta } from '../../data/utils'; import { + filterGroups, getCatalogFilterItemByType, EntityFilterType, } from '../../data/filters'; @@ -171,6 +172,7 @@ export const CatalogPage: FC<{}> = () => {
Date: Wed, 17 Jun 2020 15:29:29 +0200 Subject: [PATCH 07/52] chore(catalog): rename stuff --- .../CatalogFilter/CatalogFilter.test.tsx | 31 ++++++++------- .../CatalogFilter/CatalogFilter.tsx | 8 ++-- .../components/CatalogPage/CatalogPage.tsx | 13 +++---- plugins/catalog/src/data/filters.ts | 17 ++++----- plugins/catalog/src/hooks/useEntities.ts | 38 ++++++++----------- 5 files changed, 49 insertions(+), 58 deletions(-) diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx index a19459b236..56268697c8 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { render, fireEvent, waitFor, screen } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; import { CatalogFilter, CatalogFilterGroup } from './CatalogFilter'; -import { EntityFilterType } from '../../data/filters'; +import { EntityGroup } from '../../data/filters'; describe('Catalog Filter', () => { const comp1 = { @@ -52,12 +52,12 @@ describe('Catalog Filter', () => { }, }; const defaultFilterProps = { - selectedFilter: EntityFilterType.ALL, - onFilterChange: (type: EntityFilterType) => type, + selectedFilter: EntityGroup.ALL, + onFilterChange: (type: EntityGroup) => type, entitiesByFilter: { - [EntityFilterType.ALL]: [comp1, comp2, comp3], - [EntityFilterType.STARRED]: [comp1], - [EntityFilterType.OWNED]: [comp1], + [EntityGroup.ALL]: [comp1, comp2, comp3], + [EntityGroup.STARRED]: [comp1], + [EntityGroup.OWNED]: [comp1], }, }; it('should render the different groups', async () => { @@ -82,11 +82,11 @@ describe('Catalog Filter', () => { name: 'Test Group 1', items: [ { - id: EntityFilterType.ALL, + id: EntityGroup.ALL, label: 'First Label', }, { - id: EntityFilterType.STARRED, + id: EntityGroup.STARRED, label: 'Second Label', }, ], @@ -111,12 +111,12 @@ describe('Catalog Filter', () => { name: 'Test Group 1', items: [ { - id: EntityFilterType.ALL, + id: EntityGroup.ALL, label: 'First Label', count: 3, }, { - id: EntityFilterType.STARRED, + id: EntityGroup.STARRED, label: 'Second Label', count: 1, }, @@ -135,8 +135,7 @@ describe('Catalog Filter', () => { screen.getAllByText( new RegExp( `(${ - defaultFilterProps.entitiesByFilter[key as EntityFilterType] - .length + defaultFilterProps.entitiesByFilter[key as EntityGroup].length })`, ), ), @@ -150,12 +149,12 @@ describe('Catalog Filter', () => { name: 'Test Group 1', items: [ { - id: EntityFilterType.ALL, + id: EntityGroup.ALL, label: 'First Label', count: 100, }, { - id: EntityFilterType.STARRED, + id: EntityGroup.STARRED, label: 'Second Label', count: 400, }, @@ -190,12 +189,12 @@ describe('Catalog Filter', () => { name: 'Test Group 1', items: [ { - id: EntityFilterType.ALL, + id: EntityGroup.ALL, label: 'First Label', count: () => BACKSTAGE!, }, { - id: EntityFilterType.STARRED, + id: EntityGroup.STARRED, label: 'Second Label', count: 400, }, diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx index cc3d269e61..61a83fccd8 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx @@ -26,11 +26,11 @@ import { makeStyles, } from '@material-ui/core'; import type { IconComponent } from '@backstage/core'; -import { EntityFilterType } from '../../data/filters'; +import { EntityGroup } from '../../data/filters'; import { EntitiesByFilter } from '../../hooks/useEntities'; export type CatalogFilterItem = { - id: EntityFilterType; + id: EntityGroup; label: string; icon?: IconComponent; count?: number | FC; @@ -68,8 +68,8 @@ const useStyles = makeStyles(theme => ({ })); export const CatalogFilter: FC<{ - selectedFilter: EntityFilterType; - onFilterChange: (type: EntityFilterType) => void; + selectedFilter: EntityGroup; + onFilterChange: (type: EntityGroup) => void; entitiesByFilter: EntitiesByFilter; groups: CatalogFilterGroup[]; }> = ({ diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 08014fdc74..68506136d2 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -37,7 +37,7 @@ import { useEntities } from '../../hooks/useEntities'; import { findLocationForEntityMeta } from '../../data/utils'; import { getCatalogFilterItemByType, - EntityFilterType, + EntityGroup, filterGroups, labeledEntityTypes, } from '../../data/filters'; @@ -64,12 +64,11 @@ export const CatalogPage: FC<{}> = () => { toggleStarredEntity, isStarredEntity, setSelectedFilter, - selectedTab, - setSelectedTab, + selectedTypeFilter: selectedTab, + selectTypeFilter: setSelectedTab, } = useEntities(); - const filteredEntities = - entitiesByFilter[selectedFilter ?? EntityFilterType.ALL]; + const filteredEntities = entitiesByFilter[selectedFilter ?? EntityGroup.ALL]; const styles = useStyles(); @@ -160,14 +159,14 @@ export const CatalogPage: FC<{}> = () => {
{ +export const getCatalogFilterItemByType = (filterType: EntityGroup) => { for (const group of filterGroups) { for (const filter of group.items) { if (filter.id === filterType) { @@ -72,7 +72,6 @@ type EntityFilter = (entity: Entity, options: EntityFilterOptions) => boolean; type EntityFilterOptions = Partial<{ isStarred: boolean; userId: string; - type: string; }>; type Owned = { @@ -80,12 +79,12 @@ type Owned = { }; export const entityFilters: Record = { - [EntityFilterType.OWNED]: (e, { userId }) => { + [EntityGroup.OWNED]: (e, { userId }) => { const owner = (e.spec! as Owned).owner; return owner === userId; }, - [EntityFilterType.ALL]: () => true, - [EntityFilterType.STARRED]: (_, { isStarred }) => !!isStarred, + [EntityGroup.ALL]: () => true, + [EntityGroup.STARRED]: (_, { isStarred }) => !!isStarred, }; export const entityTypeFilter = (e: Entity, type: string) => diff --git a/plugins/catalog/src/hooks/useEntities.ts b/plugins/catalog/src/hooks/useEntities.ts index d9065b645e..6509bd9b4b 100644 --- a/plugins/catalog/src/hooks/useEntities.ts +++ b/plugins/catalog/src/hooks/useEntities.ts @@ -15,7 +15,7 @@ */ import { useState, useMemo } from 'react'; import { - EntityFilterType, + EntityGroup, entityFilters, entityTypeFilter, labeledEntityTypes, @@ -26,48 +26,42 @@ import { useStarredEntities } from './useStarredEntites'; import { Entity } from '@backstage/catalog-model'; import useStaleWhileRevalidate from 'swr'; -export type EntitiesByFilter = Record; +export type EntitiesByFilter = Record; type UseEntities = { - selectedFilter: EntityFilterType | undefined; - setSelectedFilter: (f: EntityFilterType) => void; + selectedFilter: EntityGroup | undefined; + setSelectedFilter: (f: EntityGroup) => void; error: Error | null; toggleStarredEntity: any; isStarredEntity: (e: Entity) => boolean; entitiesByFilter: EntitiesByFilter; loading: boolean; + selectedTypeFilter: string; + selectTypeFilter: (id: string) => void; }; export const useEntities = (): UseEntities => { const [selectedFilter, setSelectedFilter] = useState< - EntityFilterType | undefined + EntityGroup | undefined >(); const catalogApi = useApi(catalogApiRef); const { toggleStarredEntity, isStarredEntity } = useStarredEntities(); const { data: entities, error } = useStaleWhileRevalidate( - ['catalog/all', entityFilters[selectedFilter ?? EntityFilterType.ALL]], + ['catalog/all', entityFilters[selectedFilter ?? EntityGroup.ALL]], async () => catalogApi.getEntities(), ); const indentityApi = useApi(identityApiRef); const userId = indentityApi.getUserId(); - const [selectedTab, setSelectedTab] = useState( + const [selectedTypeFilter, selectTypeFilter] = useState( labeledEntityTypes[0].id, ); - // const filteredEntities = useMemo(() => { - // const typeFilter = entityFilters[EntityFilterType.TYPE]; - // const leftMenuFilter = entityFilters[selectedFilter.id]; - // return entities - // ?.filter(e => leftMenuFilter(e, { isStarred: isStarredEntity(e) })) - // .filter(e => typeFilter(e, { type: selectedTab })); - // }, [selectedFilter.id, selectedTab, isStarredEntity, entities?.filter]); - const entitiesByFilter = useMemo(() => { const filterEntities = ( ents: Entity[] | undefined, - filterId: EntityFilterType, + filterId: EntityGroup, isStarred: (e: Entity) => boolean, user: string, ) => { @@ -78,14 +72,14 @@ export const useEntities = (): UseEntities => { userId: user, }), ) - .filter(e => entityTypeFilter(e, selectedTab)); + .filter(e => entityTypeFilter(e, selectedTypeFilter)); }; - const data = Object.keys(EntityFilterType).reduce( + const data = Object.keys(EntityGroup).reduce( (res, key) => ({ ...res, [key]: filterEntities( entities, - key as EntityFilterType, + key as EntityGroup, isStarredEntity, userId, ), @@ -93,7 +87,7 @@ export const useEntities = (): UseEntities => { {} as EntitiesByFilter, ); return data; - }, [entities, isStarredEntity, userId, selectedTab]); + }, [entities, isStarredEntity, userId, selectedTypeFilter]); return { selectedFilter, @@ -103,7 +97,7 @@ export const useEntities = (): UseEntities => { isStarredEntity, entitiesByFilter, loading: entities === undefined, - selectedTab, - setSelectedTab, + selectedTypeFilter, + selectTypeFilter, }; }; From 29a88c3e9c289671fdf647296455aa33199d1ba9 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Wed, 17 Jun 2020 17:00:10 +0200 Subject: [PATCH 08/52] fix(catalog): tests --- packages/app/src/apis.ts | 4 --- .../implementations/IdentityApi/Identity.ts | 25 ------------------- .../apis/implementations/IdentityApi/index.ts | 16 ------------ .../src/apis/implementations/index.ts | 1 - packages/storybook/.storybook/apis.js | 1 - .../CatalogPage/CatalogPage.test.tsx | 4 +-- .../components/CatalogPage/CatalogPage.tsx | 7 +++--- 7 files changed, 5 insertions(+), 53 deletions(-) delete mode 100644 packages/core-api/src/apis/implementations/IdentityApi/Identity.ts delete mode 100644 packages/core-api/src/apis/implementations/IdentityApi/index.ts diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index a6d9987819..9bfc618216 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -32,8 +32,6 @@ import { githubAuthApiRef, storageApiRef, WebStorage, - identityApiRef, - MockIdentity, } from '@backstage/core'; import { @@ -54,8 +52,6 @@ export const apis = (config: ConfigApi) => { const builder = ApiRegistry.builder(); - builder.add(identityApiRef, new MockIdentity()); - const alertApi = builder.add(alertApiRef, new AlertApiForwarder()); const errorApi = builder.add( errorApiRef, diff --git a/packages/core-api/src/apis/implementations/IdentityApi/Identity.ts b/packages/core-api/src/apis/implementations/IdentityApi/Identity.ts deleted file mode 100644 index 6f2035d1b8..0000000000 --- a/packages/core-api/src/apis/implementations/IdentityApi/Identity.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 { IdentityApi } from '../..'; - -export class MockIdentity implements IdentityApi { - getUserId(): string { - return ''; - } - getIdToken(): string | undefined { - throw new Error('Method not implemented.'); - } -} diff --git a/packages/core-api/src/apis/implementations/IdentityApi/index.ts b/packages/core-api/src/apis/implementations/IdentityApi/index.ts deleted file mode 100644 index 1e57036685..0000000000 --- a/packages/core-api/src/apis/implementations/IdentityApi/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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 './Identity'; diff --git a/packages/core-api/src/apis/implementations/index.ts b/packages/core-api/src/apis/implementations/index.ts index 885082ce09..e6d23fee21 100644 --- a/packages/core-api/src/apis/implementations/index.ts +++ b/packages/core-api/src/apis/implementations/index.ts @@ -26,4 +26,3 @@ export * from './ConfigApi'; export * from './ErrorApi'; export * from './OAuthRequestApi'; export * from './StorageApi'; -export * from './IdentityApi'; diff --git a/packages/storybook/.storybook/apis.js b/packages/storybook/.storybook/apis.js index 83f6bbca23..d3c78fa2d6 100644 --- a/packages/storybook/.storybook/apis.js +++ b/packages/storybook/.storybook/apis.js @@ -13,7 +13,6 @@ import { GoogleAuth, GithubAuth, identityApiRef, - MockIdentity, } from '@backstage/core'; const builder = ApiRegistry.builder(); diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index 4a9319ea3b..8fae75b68b 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -44,6 +44,7 @@ describe('CatalogPage', () => { kind: 'Component', spec: { owner: 'tools@example.com', + type: 'service', }, }, { @@ -54,6 +55,7 @@ describe('CatalogPage', () => { kind: 'Component', spec: { owner: 'not-tools@example.com', + type: 'service', }, }, ] as Entity[]), @@ -83,7 +85,6 @@ describe('CatalogPage', () => { ), ); await waitFor(() => screen.getByText(/All Services \(2\)/)); - expect(screen.getByText(/All Services \(2\)/)).toBeInTheDocument(); }); it('should filter by owner', async () => { render( @@ -102,6 +103,5 @@ describe('CatalogPage', () => { ); fireEvent.click(screen.getByText(/Owned/)); await waitFor(() => screen.getByText(/Owned \(1\)/)); - expect(screen.getByText(/Owned \(1\)/)).toBeInTheDocument(); }); }); diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 68506136d2..e9dd2a6295 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -61,11 +61,10 @@ export const CatalogPage: FC<{}> = () => { error, loading, selectedFilter, + setSelectedFilter, toggleStarredEntity, isStarredEntity, - setSelectedFilter, - selectedTypeFilter: selectedTab, - selectTypeFilter: setSelectedTab, + selectTypeFilter, } = useEntities(); const filteredEntities = entitiesByFilter[selectedFilter ?? EntityGroup.ALL]; @@ -123,7 +122,7 @@ export const CatalogPage: FC<{}> = () => { { - setSelectedTab(labeledEntityTypes[index as number].id); + selectTypeFilter(labeledEntityTypes[index as number].id); }} /> From f6012daa77616757c2ac409c27db294cbeb2e6b1 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Wed, 17 Jun 2020 17:50:12 +0200 Subject: [PATCH 09/52] fix(catalog): identity is done in a wrong order --- packages/core-api/src/app/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index 163a546943..849db24648 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -265,8 +265,8 @@ export class PrivateAppImpl implements BackstageApp { if (done) { throw new Error('Identity result callback was called twice'); } - setDone(true); this.identityApi.setSignInResult(result); + setDone(true); }, [done], ); From bf93cfb343367455c95c030e4ffc409fa457fb13 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Thu, 18 Jun 2020 12:07:12 +0200 Subject: [PATCH 10/52] docs docs --- .../core-api/src/apis/definitions/auth.ts | 24 +- .../src/lib/PassportStrategyHelper.ts | 11 +- plugins/auth-backend/src/providers/types.ts | 214 +++++++++++++++--- 3 files changed, 212 insertions(+), 37 deletions(-) diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index 7abab45780..a9b82cefc7 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -157,25 +157,47 @@ export type ProfileInfoOptions = { optional?: boolean; }; +/** + * This API provides access to profile information of the signed in user. + */ export type ProfileInfoApi = { getProfile(options?: ProfileInfoOptions): Promise; }; +/** + * Profile information of a signed in user. + */ export type ProfileInfo = { - provider: string; + /** + * Email ID. + */ email: string; + /** + * Display name that can be presented to the user. + */ name?: string; + /** + * URL to an avatar image of the user. + */ picture?: string; }; +/** + * Session state values passed to subscribers of the SessionStateApi. + */ export enum SessionState { SignedIn = 'SignedIn', SignedOut = 'SignedOut', } +/** + * This API provides access to an sessionState$ observable which provides an update when the + * user performs a sign in or sign out from an auth provider. + */ export type SessionStateApi = { sessionState$(): Observable; }; + /** * Provides authentication towards Google APIs and identities. * diff --git a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts index f9e32a2b6f..6fa65d01da 100644 --- a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts @@ -27,7 +27,7 @@ export const makeProfileInfo = ( profile: passport.Profile, params: any, ): ProfileInfo => { - const { provider, displayName: name } = profile; + const { displayName: name } = profile; let email = ''; if (profile.emails) { @@ -51,7 +51,6 @@ export const makeProfileInfo = ( } return { - provider, name, email, picture, @@ -100,12 +99,12 @@ export const executeFrameHandlerStrategy = async ( }; export const executeRefreshTokenStrategy = async ( - providerstrategy: passport.Strategy, + providerStrategy: passport.Strategy, refreshToken: string, scope: string, ): Promise => { return new Promise((resolve, reject) => { - const anyStrategy = providerstrategy as any; + const anyStrategy = providerStrategy as any; const OAuth2 = anyStrategy._oauth2.constructor; const oauth2 = new OAuth2( anyStrategy._oauth2._clientId, @@ -149,12 +148,12 @@ export const executeRefreshTokenStrategy = async ( }; export const executeFetchUserProfileStrategy = async ( - providerstrategy: passport.Strategy, + providerStrategy: passport.Strategy, accessToken: string, params: any, ): Promise => { return new Promise((resolve, reject) => { - const anyStrategy = providerstrategy as any; + const anyStrategy = providerStrategy as any; anyStrategy.userProfile( accessToken, (error: Error, passportProfile: passport.Profile) => { diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 419a041637..fa2ef7f49e 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -18,48 +18,160 @@ import express from 'express'; import { Logger } from 'winston'; export type OAuthProviderOptions = { + /** + * Client ID of the auth provider. + */ clientID: string; + /** + * Client Secret of the auth provider. + */ clientSecret: string; + /** + * Callback URL to be passed to the auth provider to redirect to after the user signs in. + */ callbackURL: string; }; -export type SAMLProviderConfig = { - entryPoint: string; - issuer: string; -}; - -export type EnvironmentProviderConfig = { - [key: string]: OAuthProviderConfig | SAMLProviderConfig; -}; - -export type AuthProviderConfig = { - baseUrl: string; -}; - export type OAuthProviderConfig = { + /** + * If the cookies set by the provider have to be marked secure. For development environment + * we don't mark the cookie as secure. + */ secure: boolean; - appOrigin: string; // http://localhost:3000 + /** + * The domain:port where the app (frontend) is hosted. This is used to post messages back + * to the window that initiates an auth request. + */ + appOrigin: string; + /** + * Client ID of the auth provider. + */ clientId: string; + /** + * Client Secret of the auth provider. + */ clientSecret: string; }; +export type EnvironmentProviderConfig = { + /** + * key, values are environment names and OAuthProviderConfigs + * + * For e.g + * { + * development: DevelopmentOAuthProviderConfig + * production: ProductionOAuthProviderConfig + * } + */ + [key: string]: OAuthProviderConfig; +}; + +export type AuthProviderConfig = { + /** + * The domain:port where the app is hosted. This is used to construct the + * callbackURL to redirect to once the user signs in to the auth provider. + */ + baseUrl: string; +}; + +/** + * Any OAuth provider needs to implement this interface which has provider specific + * handlers for different methods to perform authentication, get access tokens, + * refresh tokens and perform sign out. + */ export interface OAuthProviderHandlers { + /** + * This method initiates a sign in request with an auth provider. + * @param {express.Request} req + * @param options + */ start(req: express.Request, options: any): Promise; + + /** + * Handles the redirect from the auth provider when the user has signed in. + * @param {express.Request} req + */ handler(req: express.Request): Promise; + + /** + * (Optional) Given a refresh token and scope fetches a new access token from the auth provider. + * @param {string} refreshToken + * @param {string} scope + */ refresh?(refreshToken: string, scope: string): Promise; + + /** + * (Optional) Sign out of the auth provider. + */ logout?(): Promise; } +/** + * Any Auth provider needs to implement this interface which handles the routes in the + * auth backend. Any auth API requests from the frontend reaches these methods. + * + * The routes in the auth backend API are tied to these methods like below + * + * /auth/[provider]/start -> start + * /auth/[provider]/handler/frame -> frameHandler + * /auth/[provider]/refresh -> refresh + * /auth/[provider]/logout -> logout + */ export interface AuthProviderRouteHandlers { + /** + * Handles the start route of the API. This initiates a sign in request with an auth provider. + * + * Request + * - scopes for the auth request (Optional) + * Response + * - redirect to the auth provider for the user to sign in or consent. + * - sets a nonce cookie and also pass the nonce as 'state' query parameter in the redirect request + * + * @param {express.Request} req + * @param {express.Response} res + */ start(req: express.Request, res: express.Response): Promise; - frameHandler(req: express.Request, res: express.Response): Promise; - refresh?(req: express.Request, res: express.Response): Promise; - logout(req: express.Request, res: express.Response): Promise; -} -export type SAMLEnvironmentProviderConfig = { - [key: string]: SAMLProviderConfig; -}; + /** + * Once the user signs in or consents in the OAuth screen, the auth provider redirects to the + * callbackURL which is handled by this method. + * + * Request + * - to contain a nonce cookie and a 'state' query parameter + * Response + * - postMessage to the window with a payload that contains accessToken, expiryInSeconds?, idToken? and scope. + * - sets a refresh token cookie if the auth provider supports refresh tokens + * + * @param {express.Request} req + * @param {express.Response} res + */ + frameHandler(req: express.Request, res: express.Response): Promise; + + /** + * (Optional) If the auth provider supports refresh tokens then this method handles + * requests to get a new access token. + * + * Request + * - to contain a refresh token cookie and scope (Optional) query parameter. + * Response + * - payload with accessToken, expiryInSeconds?, idToken?, scope and user profile information. + * + * @param {express.Request} req + * @param {express.Response} res + */ + refresh?(req: express.Request, res: express.Response): Promise; + + /** + * (Optional) Handles logout requests + * + * Response + * - removes the refresh token cookie + * + * @param {express.Request} req + * @param {express.Response} res + */ + logout?(req: express.Request, res: express.Response): Promise; +} export type AuthProviderFactory = ( globalConfig: AuthProviderConfig, @@ -68,27 +180,42 @@ export type AuthProviderFactory = ( ) => AuthProviderRouteHandlers; export type AuthInfoBase = { + /** + * An access token issued for the signed in user. + */ accessToken: string; + /** + * (Optional) Id token issued for the signed in user. + */ idToken?: string; + /** + * Expiry of the access token in seconds. + */ expiresInSeconds?: number; + /** + * Scopes granted for the access token. + */ scope: string; }; export type AuthInfoWithProfile = AuthInfoBase & { - profile: - | { - provider: string; - email: string; - name?: string; - picture?: string; - } - | undefined; + /** + * Profile information of the signed in user. + */ + profile: ProfileInfo | undefined; }; export type AuthInfoPrivate = { + /** + * A refresh token issued for the signed in user. + */ refreshToken: string; }; +/** + * Payload sent as a post message after the auth request is complete. + * If successful then has a valid payload with Auth information else contains an error. + */ export type AuthResponse = | { type: 'auth-result'; @@ -100,18 +227,45 @@ export type AuthResponse = }; export type RedirectInfo = { + /** + * URL to redirect to + */ url: string; + /** + * Status code to use for the redirect + */ status?: number; }; export type ProfileInfo = { - provider: string; + /** + * Email ID of the signed in user. + */ email: string; + /** + * Display name that can be presented to the signed in user. + */ name: string; + /** + * URL to an image that can be used as the display image or avatar of the + * signed in user. + */ picture: string; }; export type RefreshTokenResponse = { + /** + * An access token issued for the signed in user. + */ accessToken: string; params: any; }; + +export type SAMLProviderConfig = { + entryPoint: string; + issuer: string; +}; + +export type SAMLEnvironmentProviderConfig = { + [key: string]: SAMLProviderConfig; +}; From efd632d9cef76a8abf992339963e55064ee50026 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Thu, 18 Jun 2020 12:10:44 +0200 Subject: [PATCH 11/52] logout -> sign out --- plugins/auth-backend/src/providers/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index fa2ef7f49e..074c78663e 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -162,7 +162,7 @@ export interface AuthProviderRouteHandlers { refresh?(req: express.Request, res: express.Response): Promise; /** - * (Optional) Handles logout requests + * (Optional) Handles sign out requests * * Response * - removes the refresh token cookie From b421ba6b8dba09edda935a8a4fbf5c3771fefd9d Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Thu, 18 Jun 2020 13:55:03 +0200 Subject: [PATCH 12/52] review fixes --- packages/core-api/src/apis/definitions/auth.ts | 4 ++-- .../auth-backend/src/lib/EnvironmentHandler.ts | 4 +++- .../src/lib/PassportStrategyHelper.ts | 3 ++- plugins/auth-backend/src/providers/factories.ts | 4 +++- plugins/auth-backend/src/providers/types.ts | 15 +++++++++++---- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index a9b82cefc7..c884bf975b 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -158,14 +158,14 @@ export type ProfileInfoOptions = { }; /** - * This API provides access to profile information of the signed in user. + * This API provides access to profile information of the user from an auth provider. */ export type ProfileInfoApi = { getProfile(options?: ProfileInfoOptions): Promise; }; /** - * Profile information of a signed in user. + * Profile information of the user from an auth provider. */ export type ProfileInfo = { /** diff --git a/plugins/auth-backend/src/lib/EnvironmentHandler.ts b/plugins/auth-backend/src/lib/EnvironmentHandler.ts index c4d8d2b6e7..6c16b3a712 100644 --- a/plugins/auth-backend/src/lib/EnvironmentHandler.ts +++ b/plugins/auth-backend/src/lib/EnvironmentHandler.ts @@ -57,6 +57,8 @@ export class EnvironmentHandler implements AuthProviderRouteHandlers { async logout(req: express.Request, res: express.Response): Promise { const provider = this.getProviderForEnv(req); - await provider.logout(req, res); + if (provider.logout) { + await provider.logout(req, res); + } } } diff --git a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts index 6fa65d01da..1e8dfca5ed 100644 --- a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts @@ -21,6 +21,7 @@ import { RedirectInfo, RefreshTokenResponse, ProfileInfo, + ProviderStrategy, } from '../providers/types'; export const makeProfileInfo = ( @@ -153,7 +154,7 @@ export const executeFetchUserProfileStrategy = async ( params: any, ): Promise => { return new Promise((resolve, reject) => { - const anyStrategy = providerStrategy as any; + const anyStrategy = (providerStrategy as unknown) as ProviderStrategy; anyStrategy.userProfile( accessToken, (error: Error, passportProfile: passport.Profile) => { diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 4cc9212620..b5e59b713b 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -44,7 +44,9 @@ export const createAuthProviderRouter = ( router.get('/start', provider.start.bind(provider)); router.get('/handler/frame', provider.frameHandler.bind(provider)); router.post('/handler/frame', provider.frameHandler.bind(provider)); - router.post('/logout', provider.logout.bind(provider)); + if (provider.logout) { + router.post('/logout', provider.logout.bind(provider)); + } if (provider.refresh) { router.get('/refresh', provider.refresh.bind(provider)); } diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 074c78663e..7c16436021 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -34,12 +34,15 @@ export type OAuthProviderOptions = { export type OAuthProviderConfig = { /** - * If the cookies set by the provider have to be marked secure. For development environment - * we don't mark the cookie as secure. + * Cookies can be marked with a secure flag to send cookies only when the request + * is over an encrypted channel (HTTPS). + * + * For development environment we don't mark the cookie as secure since we serve + * localhost over HTTP. */ secure: boolean; /** - * The domain:port where the app (frontend) is hosted. This is used to post messages back + * The protocol://domain[:port] where the app (frontend) is hosted. This is used to post messages back * to the window that initiates an auth request. */ appOrigin: string; @@ -68,7 +71,7 @@ export type EnvironmentProviderConfig = { export type AuthProviderConfig = { /** - * The domain:port where the app is hosted. This is used to construct the + * The protocol://domain[:port] where the app is hosted. This is used to construct the * callbackURL to redirect to once the user signs in to the auth provider. */ baseUrl: string; @@ -261,6 +264,10 @@ export type RefreshTokenResponse = { params: any; }; +export type ProviderStrategy = { + userProfile(accessToken: string, callback: Function): void; +}; + export type SAMLProviderConfig = { entryPoint: string; issuer: string; From 97311ba5c69098f2c80f31b495517e029163b0ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Thu, 18 Jun 2020 16:57:27 +0200 Subject: [PATCH 13/52] Update entity overview card grid (#1366) --- .../components/EntityMetadataCard/EntityMetadataCard.tsx | 2 +- plugins/catalog/src/components/EntityPage/EntityPage.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/catalog/src/components/EntityMetadataCard/EntityMetadataCard.tsx b/plugins/catalog/src/components/EntityMetadataCard/EntityMetadataCard.tsx index d7a3bc81f6..57082a9c91 100644 --- a/plugins/catalog/src/components/EntityMetadataCard/EntityMetadataCard.tsx +++ b/plugins/catalog/src/components/EntityMetadataCard/EntityMetadataCard.tsx @@ -23,7 +23,7 @@ type Props = { }; export const EntityMetadataCard: FC = ({ entity }) => ( - + ); diff --git a/plugins/catalog/src/components/EntityPage/EntityPage.tsx b/plugins/catalog/src/components/EntityPage/EntityPage.tsx index e74276697b..d9953498ec 100644 --- a/plugins/catalog/src/components/EntityPage/EntityPage.tsx +++ b/plugins/catalog/src/components/EntityPage/EntityPage.tsx @@ -149,11 +149,11 @@ export const EntityPage: FC<{}> = () => { - - + + - + Date: Thu, 18 Jun 2020 21:06:26 +0200 Subject: [PATCH 14/52] fix(catalog): tests --- .../CatalogFilter/CatalogFilter.test.tsx | 14 ++++++-------- .../components/CatalogPage/CatalogPage.test.tsx | 2 ++ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx index 56268697c8..f7143f73a2 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx @@ -131,15 +131,13 @@ describe('Catalog Filter', () => { ); for (const key of Object.keys(defaultFilterProps.entitiesByFilter)) { - await waitFor(() => - screen.getAllByText( - new RegExp( - `(${ - defaultFilterProps.entitiesByFilter[key as EntityGroup].length - })`, - ), - ), + const matcher = new RegExp( + `(${defaultFilterProps.entitiesByFilter[key as EntityGroup].length})`, ); + await waitFor(() => screen.getAllByText(matcher)); + screen + .getAllByText(matcher) + .forEach(el => expect(el).toBeInTheDocument()); } }); diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index 8fae75b68b..a11ff8e2a6 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -85,6 +85,7 @@ describe('CatalogPage', () => { ), ); await waitFor(() => screen.getByText(/All Services \(2\)/)); + expect(screen.getByText(/All Services \(2\)/)).toBeInTheDocument(); }); it('should filter by owner', async () => { render( @@ -103,5 +104,6 @@ describe('CatalogPage', () => { ); fireEvent.click(screen.getByText(/Owned/)); await waitFor(() => screen.getByText(/Owned \(1\)/)); + expect(screen.getByText(/Owned \(1\)/)).toBeInTheDocument(); }); }); From eac8ac8bd0fdaf5f38edd0016d93b3422c0e1e08 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Thu, 18 Jun 2020 21:43:52 +0200 Subject: [PATCH 15/52] fix(catalog): make plugin conform to template --- plugins/catalog/package.json | 2 +- .../CatalogFilter/CatalogFilter.test.tsx | 10 +++----- .../CatalogPage/CatalogPage.test.tsx | 17 +++++++------ yarn.lock | 25 +------------------ 4 files changed, 15 insertions(+), 39 deletions(-) diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 37a0397862..54d5737adb 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -43,7 +43,7 @@ "@backstage/dev-utils": "^0.1.1-alpha.9", "@backstage/test-utils": "^0.1.1-alpha.9", "@testing-library/jest-dom": "^5.7.0", - "@testing-library/react": "^10.2.1", + "@testing-library/react": "^9.3.2", "@testing-library/react-hooks": "^3.3.0", "@testing-library/user-event": "^10.2.4", "@types/jest": "^25.2.2", diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx index f7143f73a2..cc4cc62fdb 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { render, fireEvent, waitFor, screen } from '@testing-library/react'; +import { render, fireEvent } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; import { CatalogFilter, CatalogFilterGroup } from './CatalogFilter'; import { EntityGroup } from '../../data/filters'; @@ -124,7 +124,7 @@ describe('Catalog Filter', () => { }, ]; - render( + const { getAllByText } = render( wrapInTestApp( , ), @@ -134,10 +134,8 @@ describe('Catalog Filter', () => { const matcher = new RegExp( `(${defaultFilterProps.entitiesByFilter[key as EntityGroup].length})`, ); - await waitFor(() => screen.getAllByText(matcher)); - screen - .getAllByText(matcher) - .forEach(el => expect(el).toBeInTheDocument()); + const items = await getAllByText(matcher); + items.forEach(el => expect(el).toBeInTheDocument()); } }); diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index a11ff8e2a6..7ba1c39bb7 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -24,7 +24,7 @@ import { identityApiRef, } from '@backstage/core'; import { MockErrorApi, wrapInTestApp } from '@backstage/test-utils'; -import { screen, render, fireEvent, waitFor } from '@testing-library/react'; +import { render, fireEvent } from '@testing-library/react'; import React from 'react'; import { catalogApiRef } from '../..'; import { CatalogApi } from '../../api/types'; @@ -70,7 +70,7 @@ describe('CatalogPage', () => { // related to some theme issues in mui-table // https://github.com/mbrn/material-table/issues/1293 it('should render', async () => { - render( + const { findByText } = render( wrapInTestApp( { , ), ); - await waitFor(() => screen.getByText(/All Services \(2\)/)); - expect(screen.getByText(/All Services \(2\)/)).toBeInTheDocument(); + + const items = await findByText(/All Services \(2\)/); + expect(items).toBeInTheDocument(); }); it('should filter by owner', async () => { - render( + const { findByText, getByText } = render( wrapInTestApp( { , ), ); - fireEvent.click(screen.getByText(/Owned/)); - await waitFor(() => screen.getByText(/Owned \(1\)/)); - expect(screen.getByText(/Owned \(1\)/)).toBeInTheDocument(); + fireEvent.click(getByText(/Owned/)); + const items = await findByText(/Owned \(1\)/); + expect(items).toBeInTheDocument(); }); }); diff --git a/yarn.lock b/yarn.lock index a8b1cfb937..b3b0af2615 100644 --- a/yarn.lock +++ b/yarn.lock @@ -891,7 +891,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.10.2", "@babel/runtime@^7.5.4": +"@babel/runtime@^7.5.4": version "7.10.2" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839" integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg== @@ -3285,16 +3285,6 @@ dom-accessibility-api "^0.4.2" pretty-format "^25.1.0" -"@testing-library/dom@^7.9.0": - version "7.16.1" - resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-7.16.1.tgz#a6881d53612f2e8f7bcc0e0bd8825c6788cf57f2" - integrity sha512-u0Ck7tjWDyCcGn+f77JbUHa7PqgFu62ohRxegj1/H5P3REKsM+2roCvcnWJjMSHvK354NAS/Pgi92E9z6cB7Sw== - dependencies: - "@babel/runtime" "^7.10.2" - aria-query "^4.0.2" - dom-accessibility-api "^0.4.5" - pretty-format "^25.5.0" - "@testing-library/jest-dom@^5.7.0": version "5.9.0" resolved "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.9.0.tgz#86464c66cbe75e632b8adb636f539bfd0efc2c9c" @@ -3318,14 +3308,6 @@ "@babel/runtime" "^7.5.4" "@types/testing-library__react-hooks" "^3.0.0" -"@testing-library/react@^10.2.1": - version "10.2.1" - resolved "https://registry.npmjs.org/@testing-library/react/-/react-10.2.1.tgz#f0c5ac9072ad54c29672150943f35d6617263f26" - integrity sha512-pv2jZhiZgN1/alz1aImhSasZAOPg3er2Kgcfg9fzuw7aKPLxVengqqR1n0CJANeErR1DqORauQaod+gGUgAJOQ== - dependencies: - "@babel/runtime" "^7.10.2" - "@testing-library/dom" "^7.9.0" - "@testing-library/react@^9.3.2": version "9.5.0" resolved "https://registry.npmjs.org/@testing-library/react/-/react-9.5.0.tgz#71531655a7890b61e77a1b39452fbedf0472ca5e" @@ -7716,11 +7698,6 @@ dom-accessibility-api@^0.4.2: resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.4.3.tgz#93ca9002eb222fd5a343b6e5e6b9cf5929411c4c" integrity sha512-JZ8iPuEHDQzq6q0k7PKMGbrIdsgBB7TRrtVOUm4nSMCExlg5qQG4KXWTH2k90yggjM4tTumRGwTKJSldMzKyLA== -dom-accessibility-api@^0.4.5: - version "0.4.5" - resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.4.5.tgz#d9c1cefa89f509d8cf132ab5d250004d755e76e3" - integrity sha512-HcPDilI95nKztbVikaN2vzwvmv0sE8Y2ZJFODy/m15n7mGXLeOKGiys9qWVbFbh+aq/KYj2lqMLybBOkYAEXqg== - dom-converter@^0.2: version "0.2.0" resolved "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" From d58ad15ffb3f8de0952769f833225991d06ae800 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Fri, 19 Jun 2020 04:09:41 +0200 Subject: [PATCH 16/52] fix(catalog): remove duplicate import --- packages/storybook/.storybook/apis.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/storybook/.storybook/apis.js b/packages/storybook/.storybook/apis.js index d3c78fa2d6..65b2ec5aaa 100644 --- a/packages/storybook/.storybook/apis.js +++ b/packages/storybook/.storybook/apis.js @@ -2,7 +2,6 @@ import { ApiRegistry, alertApiRef, errorApiRef, - identityApiRef, oauthRequestApiRef, OAuthRequestManager, googleAuthApiRef, From b7a1946efeac3e944e04795911bb81bea7698d9f Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2020 22:57:48 +0200 Subject: [PATCH 17/52] build(deps-dev): bump msw from 0.19.3 to 0.19.4 (#1375) Bumps [msw](https://github.com/mswjs/msw) from 0.19.3 to 0.19.4. - [Release notes](https://github.com/mswjs/msw/releases) - [Commits](https://github.com/mswjs/msw/compare/v0.19.3...v0.19.4) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index b1b521b438..c18da809e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13169,9 +13169,9 @@ ms@^2.0.0, ms@^2.1.1: integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== msw@^0.19.0: - version "0.19.3" - resolved "https://registry.npmjs.org/msw/-/msw-0.19.3.tgz#88f39edbd37313bff15a0e7cd00c71406cbdbbae" - integrity sha512-HYLnyrCDDPP72GG/CeHPhBjHsZFYkz36rJLXDWccZWNA24gYjgrcp9iVqqitk2cI6NAJwsupRml9GkfpJBB74w== + version "0.19.4" + resolved "https://registry.npmjs.org/msw/-/msw-0.19.4.tgz#059026de0cc3303847c27d3aa0e98705a1033df6" + integrity sha512-rNfGgIuO0MyfN2F+FOHRqNd2LJIESaaMOJNNiNMcv/Be7Kdzz/ZmghfS/5zFy2SpHWladccqDYgo74JN+YlEyg== dependencies: "@open-draft/until" "^1.0.0" "@types/cookie" "^0.3.3" From 8127491b8beee39cfd6758dcf6e44ecfe49872fd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 21 Jun 2020 14:21:51 +0200 Subject: [PATCH 18/52] backend-common: change useHotCleanup to clean up on parents being disposed as well --- packages/backend-common/src/hot.ts | 48 ++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/packages/backend-common/src/hot.ts b/packages/backend-common/src/hot.ts index 3d4252b378..bd6454c537 100644 --- a/packages/backend-common/src/hot.ts +++ b/packages/backend-common/src/hot.ts @@ -14,10 +14,38 @@ * limitations under the License. */ +// Find all active hot module APIs of all ancestors of a module, including the module itself +function findAllAncestors(_module: NodeModule): NodeModule[] { + const ancestors = new Array(); + const parentIds = new Set(); + + function add(id: string | number, m: NodeModule) { + if (parentIds.has(id)) { + return; + } + parentIds.add(id); + ancestors.push(m); + + for (const parentId of (m as any).parents) { + const parent = require.cache[parentId]; + if (parent) { + add(parentId, parent); + } + } + } + + add(_module.id, _module); + + return ancestors; +} + /** - * This function allows devs to cleanup - * ongoing effects when module gets hot-reloaded + * useHotCleanup allows cleanup of ongoing effects when a module is + * hot-reloaded during development. The cleanup function will be called + * whenever the module itself or any of its parent modules is hot-reloaded. + * * Useful for cleaning intervals, timers, requests etc + * * @example * ```ts * const intervalId = setInterval(doStuff, 1000); @@ -28,9 +56,19 @@ */ export function useHotCleanup(_module: NodeModule, cancelEffect: () => void) { if (_module.hot) { - _module.hot.addDisposeHandler(() => { - cancelEffect(); - }); + const ancestors = findAllAncestors(_module); + let cancelled = false; + + const handler = () => { + if (!cancelled) { + cancelled = true; + cancelEffect(); + } + }; + + for (const m of ancestors) { + m.hot?.addDisposeHandler(handler); + } } } From 903ad947a2db8ce2bfe6ef869d079df2d61aa72b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 20 Jun 2020 18:15:42 +0200 Subject: [PATCH 19/52] cli: pin esbuild to 0.5.3 in app template --- packages/cli/templates/default-app/package.json.hbs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/cli/templates/default-app/package.json.hbs b/packages/cli/templates/default-app/package.json.hbs index a96061a5bd..c70e00e4d0 100644 --- a/packages/cli/templates/default-app/package.json.hbs +++ b/packages/cli/templates/default-app/package.json.hbs @@ -31,6 +31,9 @@ "lerna": "^3.20.2", "prettier": "^1.19.1" }, + "resolutions": { + "**/esbuild": "0.5.3" + }, "prettier": "@spotify/prettier-config", "lint-staged": { "*.{js,jsx,ts,tsx}": [ From ce8d65568a500f11c96d26a48bbb264c035fbf0e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 20 Jun 2020 20:19:08 +0200 Subject: [PATCH 20/52] cli: update public/index.html in app template --- .../packages/app/public/index.html | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/packages/cli/templates/default-app/packages/app/public/index.html b/packages/cli/templates/default-app/packages/app/public/index.html index 3d01107696..ea9208ca57 100644 --- a/packages/cli/templates/default-app/packages/app/public/index.html +++ b/packages/cli/templates/default-app/packages/app/public/index.html @@ -8,47 +8,38 @@ name="description" content="Backstage is an open platform for building developer portals" /> - + - - - + + - Backstage + <%= app.title %> - +