Scaffolder: Cleanup

This commit is contained in:
Oscar Hernandez
2021-02-19 16:26:55 -06:00
parent 385cb44b3f
commit d0760ecdf3
14 changed files with 27 additions and 249 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-catalog': minor
'@backstage/plugin-catalog-react': minor
'@backstage/plugin-scaffolder': minor
---
Moved common useStarredEntities hook to plugin-catalog-react
+1
View File
@@ -16,3 +16,4 @@
export { EntityContext, useEntity, useEntityFromUrl } from './useEntity';
export { useEntityCompoundName } from './useEntityCompoundName';
export { useRelatedEntities } from './useRelatedEntities';
export { useStarredEntities } from './useStarredEntities';
@@ -23,14 +23,18 @@ import {
useApi,
useRouteRef,
} from '@backstage/core';
import { catalogApiRef, isOwnerOf } from '@backstage/plugin-catalog-react';
import {
catalogApiRef,
isOwnerOf,
useStarredEntities,
} from '@backstage/plugin-catalog-react';
import { Button, makeStyles } from '@material-ui/core';
import SettingsIcon from '@material-ui/icons/Settings';
import StarIcon from '@material-ui/icons/Star';
import React, { useCallback, useMemo, useState } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { EntityFilterGroupsProvider, useFilteredEntities } from '../../filter';
import { useStarredEntities } from '../../hooks/useStarredEntities';
import { createComponentRouteRef } from '../../routes';
import {
ButtonGroup,
@@ -32,13 +32,14 @@ import {
EntityRefLinks,
formatEntityRefTitle,
getEntityRelations,
useStarredEntities,
} from '@backstage/plugin-catalog-react';
import { Chip } from '@material-ui/core';
import Edit from '@material-ui/icons/Edit';
import OpenInNew from '@material-ui/icons/OpenInNew';
import React from 'react';
import { findLocationForEntityMeta } from '../../data/utils';
import { useStarredEntities } from '../../hooks/useStarredEntities';
import { createEditLink } from '../createEditLink';
import {
favouriteEntityIcon,
@@ -15,10 +15,10 @@
*/
import React, { ComponentProps } from 'react';
import { useStarredEntities } from '@backstage/plugin-catalog-react';
import { IconButton, Tooltip, withStyles } from '@material-ui/core';
import StarBorder from '@material-ui/icons/StarBorder';
import Star from '@material-ui/icons/Star';
import { useStarredEntities } from '../../hooks/useStarredEntities';
import { Entity } from '@backstage/catalog-model';
type Props = ComponentProps<typeof IconButton> & { entity: Entity };
@@ -1,121 +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, { PropsWithChildren } from 'react';
import { renderHook, act } from '@testing-library/react-hooks';
import { useStarredEntities } from './useStarredEntities';
import {
ApiProvider,
ApiRegistry,
storageApiRef,
WebStorage,
StorageApi,
} from '@backstage/core';
import { MockErrorApi } from '@backstage/test-utils';
import { Entity } from '@backstage/catalog-model';
describe('useStarredEntities', () => {
let mockStorage: StorageApi | undefined;
const mockEntity: Entity = {
apiVersion: '1',
kind: 'Component',
metadata: {
name: 'mock',
},
};
const secondMockEntity: Entity = {
apiVersion: '1',
kind: 'Component',
metadata: {
namespace: 'test',
name: 'mock2',
},
};
const wrapper = ({ children }: PropsWithChildren<{}>) => {
return (
<ApiProvider apis={ApiRegistry.with(storageApiRef, mockStorage)}>
{children}
</ApiProvider>
);
};
beforeEach(() => {
mockStorage = new WebStorage('@backstage', new MockErrorApi()).forBucket(
Date.now().toString(), // TODO(blam): need something that changes every test run for now until the MockStorage is implemented
);
});
it('should return an empty set for when there is no items in storage', async () => {
const { result } = renderHook(() => useStarredEntities(), { wrapper });
expect(result.current.starredEntities.size).toBe(0);
});
it('should return a set with the current items when there is items in storage', async () => {
const expectedIds = ['i', 'am', 'some', 'test', 'ids'];
const store = mockStorage?.forBucket('settings');
await store?.set('starredEntities', expectedIds);
const { result } = renderHook(() => useStarredEntities(), { wrapper });
for (const item of expectedIds) {
expect(result.current.starredEntities.has(item)).toBeTruthy();
}
});
it('should listen to changes when the storage is set elsewhere', async () => {
const { result, waitForNextUpdate } = renderHook(
() => useStarredEntities(),
{ wrapper },
);
expect(result.current.starredEntities.size).toBe(0);
expect(result.current.isStarredEntity(mockEntity)).toBeFalsy();
// Make this happen after awaiting for the next update so we can
// catch when the hook re-renders with the latest data
setTimeout(() => result.current.toggleStarredEntity(mockEntity), 1);
await waitForNextUpdate();
expect(result.current.starredEntities.size).toBe(1);
expect(result.current.isStarredEntity(mockEntity)).toBeTruthy();
});
it('should write new entries to the local store when adding a togglging entity', async () => {
const { result } = renderHook(() => useStarredEntities(), { wrapper });
act(() => {
result.current.toggleStarredEntity(mockEntity);
});
expect(result.current.isStarredEntity(mockEntity)).toBeTruthy();
expect(result.current.isStarredEntity(secondMockEntity)).toBeFalsy();
});
it('should remove an existing entity when toggling entries', async () => {
const { result } = renderHook(() => useStarredEntities(), { wrapper });
act(() => {
result.current.toggleStarredEntity(mockEntity);
result.current.toggleStarredEntity(secondMockEntity);
result.current.toggleStarredEntity(mockEntity);
});
expect(result.current.isStarredEntity(mockEntity)).toBeFalsy();
expect(result.current.isStarredEntity(secondMockEntity)).toBeTruthy();
});
});
@@ -25,7 +25,7 @@ import {
Theme,
Typography,
} from '@material-ui/core';
import React, { useCallback, useContext, useState } from 'react';
import React, { useContext } from 'react';
import { filterGroupsContext } from '../../filter/context';
const useStyles = makeStyles<Theme>(theme => ({
@@ -59,20 +59,12 @@ type Props = {
export const ResultsFilter = ({ availableCategories }: Props) => {
const classes = useStyles();
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const context = useContext(filterGroupsContext);
if (!context) {
throw new Error(`Must be used inside an EntityFilterGroupsProvider`);
}
const setSelectedCatgoriesFilter = context?.setSelectedCategories;
const updateSelectedCategories = useCallback(
(categories: string[]) => {
setSelectedCategories(categories);
setSelectedCatgoriesFilter(categories);
},
[setSelectedCategories, setSelectedCatgoriesFilter],
);
const { selectedCategories, setSelectedCategories } = context;
return (
<>
@@ -80,7 +72,7 @@ export const ResultsFilter = ({ availableCategories }: Props) => {
<Typography variant="subtitle2" className={classes.filterBoxTitle}>
Refine Results
</Typography>{' '}
<Button onClick={() => updateSelectedCategories([])}>Clear</Button>
<Button onClick={() => setSelectedCategories([])}>Clear</Button>
</div>
<Divider />
<Typography variant="subtitle2" className={classes.title}>
@@ -95,7 +87,7 @@ export const ResultsFilter = ({ availableCategories }: Props) => {
dense
button
onClick={() =>
updateSelectedCategories(
setSelectedCategories(
selectedCategories.includes(category)
? selectedCategories.filter(
selectedCategory => selectedCategory !== category,
@@ -1,33 +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 { useApi } from '@backstage/core';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { CircularProgress, useTheme } from '@material-ui/core';
import React from 'react';
import { useAsync } from 'react-use';
export const AllServicesCount = () => {
const theme = useTheme();
const catalogApi = useApi(catalogApiRef);
const { value, loading } = useAsync(() => catalogApi.getEntities());
if (loading) {
return <CircularProgress size={theme.spacing(2)} />;
}
return <span>{value ?? length ?? '-'}</span>;
};
@@ -15,6 +15,7 @@
*/
import React, { useEffect, useMemo, useState } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { EntityMeta, TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import {
configApiRef,
@@ -28,15 +29,14 @@ import {
useApi,
WarningPanel,
} from '@backstage/core';
import { useStarredEntities } from '@backstage/plugin-catalog-react';
import { Button, Grid, Link, makeStyles, Typography } from '@material-ui/core';
import { Link as RouterLink } from 'react-router-dom';
import StarIcon from '@material-ui/icons/Star';
import { EntityFilterGroupsProvider, useFilteredEntities } from '../../filter';
import { TemplateCard, TemplateCardProps } from '../TemplateCard';
import { ResultsFilter } from '../ResultsFilter/ResultsFilter';
import { ScaffolderFilter } from '../ScaffolderFilter';
import { ButtonGroup } from '../ScaffolderFilter/ScaffolderFilter';
import StarIcon from '@material-ui/icons/Star';
import { useStarredEntities } from '../../hooks/useStarredEntities';
import SearchToolbar from '../SearchToolbar/SearchToolbar';
const useStyles = makeStyles(theme => ({
@@ -104,7 +104,7 @@ export const ScaffolderPageContents = () => {
);
const matchesQuery = (metadata: EntityMeta, query: string) =>
`${metadata.title}`.toUpperCase().indexOf(query) !== -1 ||
`${metadata.title}`.toUpperCase().includes(query) ||
metadata.tags?.join('').toUpperCase().indexOf(query) !== -1;
useEffect(() => {
@@ -145,6 +145,7 @@ function useProvideEntityFilters(): FilterGroupsContext {
setGroupSelectedFilters,
setSelectedCategories,
reload,
selectedCategories: selectedCategories.current,
loading: !error && !entities,
error,
filterGroupStates,
+1
View File
@@ -28,6 +28,7 @@ export type FilterGroupsContext = {
setGroupSelectedFilters: (filterGroupId: string, filterIds: string[]) => void;
setSelectedCategories: (categories: string[]) => void;
reload: () => Promise<void>;
selectedCategories: string[];
loading: boolean;
error?: Error;
filterGroupStates: { [filterGroupId: string]: FilterGroupStates };
@@ -1,75 +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 { Entity } from '@backstage/catalog-model';
import { storageApiRef, useApi } from '@backstage/core';
import { useCallback, useEffect, useState } from 'react';
import { useObservable } from 'react-use';
const buildEntityKey = (component: Entity) =>
`entity:${component.kind}:${component.metadata.namespace ?? 'default'}:${
component.metadata.name
}`;
export const useStarredEntities = () => {
const storageApi = useApi(storageApiRef);
const settingsStore = storageApi.forBucket('settings');
const rawStarredEntityKeys =
settingsStore.get<string[]>('starredEntities') ?? [];
const [starredEntities, setStarredEntities] = useState(
new Set(rawStarredEntityKeys),
);
const observedItems = useObservable(
settingsStore.observe$<string[]>('starredEntities'),
);
useEffect(() => {
if (observedItems?.newValue) {
const currentValue = observedItems?.newValue ?? [];
setStarredEntities(new Set(currentValue));
}
}, [observedItems?.newValue]);
const toggleStarredEntity = useCallback(
(entity: Entity) => {
const entityKey = buildEntityKey(entity);
if (starredEntities.has(entityKey)) {
starredEntities.delete(entityKey);
} else {
starredEntities.add(entityKey);
}
settingsStore.set('starredEntities', Array.from(starredEntities));
},
[starredEntities, settingsStore],
);
const isStarredEntity = useCallback(
(entity: Entity) => {
const entityKey = buildEntityKey(entity);
return starredEntities.has(entityKey);
},
[starredEntities],
);
return {
starredEntities,
toggleStarredEntity,
isStarredEntity,
};
};