diff --git a/.changeset/blue-donkeys-exercise.md b/.changeset/blue-donkeys-exercise.md new file mode 100644 index 0000000000..4f714e3c97 --- /dev/null +++ b/.changeset/blue-donkeys-exercise.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': minor +--- + +Add client side paging for catalog table diff --git a/.changeset/great-apples-flash.md b/.changeset/great-apples-flash.md new file mode 100644 index 0000000000..78ad89dee0 --- /dev/null +++ b/.changeset/great-apples-flash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fix CodeOwnersProcessor to handle non team users diff --git a/.changeset/selfish-mugs-itch.md b/.changeset/selfish-mugs-itch.md new file mode 100644 index 0000000000..39ed236352 --- /dev/null +++ b/.changeset/selfish-mugs-itch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': minor +--- + +Remove product filters from query parameters diff --git a/app-config.yaml b/app-config.yaml index 715d564feb..6e49e765cd 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -146,6 +146,9 @@ catalog: # Backstage example templates - type: url target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/all-templates.yaml + # Backstage example groups and users + - type: url + target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/acme-corp.yaml scaffolder: github: diff --git a/packages/catalog-model/examples/acme-corp.yaml b/packages/catalog-model/examples/acme-corp.yaml new file mode 100644 index 0000000000..7e5c5ebc61 --- /dev/null +++ b/packages/catalog-model/examples/acme-corp.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Location +metadata: + name: acme-corp + description: A collection of all Backstage example Groups +spec: + type: github + targets: + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/acme/org.yaml diff --git a/packages/catalog-model/examples/acme/backstage-group.yaml b/packages/catalog-model/examples/acme/backstage-group.yaml new file mode 100644 index 0000000000..f992d155bc --- /dev/null +++ b/packages/catalog-model/examples/acme/backstage-group.yaml @@ -0,0 +1,11 @@ +apiVersion: backstage.io/v1alpha1 +kind: Group +metadata: + name: backstage + description: The backstage sub-department +spec: + type: sub-department + parent: infrastructure + ancestors: [infrastructure, acme-corp] + children: [team-a, team-b] + descendants: [team-a, team-b] diff --git a/packages/catalog-model/examples/acme/boxoffice-group.yaml b/packages/catalog-model/examples/acme/boxoffice-group.yaml new file mode 100644 index 0000000000..0be1fe58cb --- /dev/null +++ b/packages/catalog-model/examples/acme/boxoffice-group.yaml @@ -0,0 +1,11 @@ +apiVersion: backstage.io/v1alpha1 +kind: Group +metadata: + name: boxoffice + description: The boxoffice sub-department +spec: + type: sub-department + parent: infrastructure + ancestors: [infrastructure, acme-corp] + children: [team-c, team-d] + descendants: [team-c, team-d] diff --git a/packages/catalog-model/examples/acme/infrastructure-group.yaml b/packages/catalog-model/examples/acme/infrastructure-group.yaml new file mode 100644 index 0000000000..2341782944 --- /dev/null +++ b/packages/catalog-model/examples/acme/infrastructure-group.yaml @@ -0,0 +1,11 @@ +apiVersion: backstage.io/v1alpha1 +kind: Group +metadata: + name: infrastructure + description: The infra department +spec: + type: department + parent: acme-corp + ancestors: [acme-corp] + children: [backstage, boxoffice] + descendants: [backstage, boxoffice, team-a, team-b, team-c, team-d] diff --git a/packages/catalog-model/examples/acme/org.yaml b/packages/catalog-model/examples/acme/org.yaml new file mode 100644 index 0000000000..2d6a6a2666 --- /dev/null +++ b/packages/catalog-model/examples/acme/org.yaml @@ -0,0 +1,27 @@ +apiVersion: backstage.io/v1alpha1 +kind: Group +metadata: + name: acme-corp + description: The acme-corp organization +spec: + type: organization + ancestors: [] + children: [infrastructure] + descendants: + [infrastructure, backstage, boxoffice, team-a, team-b, team-c, team-d] +--- +apiVersion: backstage.io/v1alpha1 +kind: Location +metadata: + name: example-groups + description: A collection of all Backstage example Groups +spec: + type: github + targets: + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/acme/infrastructure-group.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/acme/boxoffice-group.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/acme/backstage-group.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/acme/team-a-group.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/acme/team-b-group.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/acme/team-c-group.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/acme/team-d-group.yaml diff --git a/packages/catalog-model/examples/acme/team-a-group.yaml b/packages/catalog-model/examples/acme/team-a-group.yaml new file mode 100644 index 0000000000..dacc4c6c3c --- /dev/null +++ b/packages/catalog-model/examples/acme/team-a-group.yaml @@ -0,0 +1,44 @@ +apiVersion: backstage.io/v1alpha1 +kind: Group +metadata: + name: team-a + description: Team A +spec: + type: team + parent: backstage + ancestors: [backstage, infrastructure, acme-corp] + children: [] + descendants: [] +--- +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: breanna.davison +spec: + profile: + displayName: Breanna Davison + email: breanna-davison@example.com + picture: https://example.com/staff/breanna.jpeg + memberOf: [team-a] +--- +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: janelle.dawe +spec: + profile: + displayName: Janelle Dawe + email: janelle-dawe@example.com + picture: https://example.com/staff/janelle.jpeg + memberOf: [team-a] +--- +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: nigel.manning +spec: + profile: + displayName: Nigel Manning + email: nigel-manning@example.com + picture: https://example.com/staff/nigel.jpeg + memberOf: [team-a] diff --git a/packages/catalog-model/examples/acme/team-b-group.yaml b/packages/catalog-model/examples/acme/team-b-group.yaml new file mode 100644 index 0000000000..00e9e41d80 --- /dev/null +++ b/packages/catalog-model/examples/acme/team-b-group.yaml @@ -0,0 +1,66 @@ +apiVersion: backstage.io/v1alpha1 +kind: Group +metadata: + name: team-b + description: Team B +spec: + type: team + parent: backstage + ancestors: [backstage, infrastructure, acme-corp] + children: [] + descendants: [] +--- +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: amelia.park +spec: + profile: + displayName: Amelia Park + email: amelia-park@example.com + picture: https://example.com/staff/amelia.jpeg + memberOf: [team-b] +--- +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: colette.brock +spec: + profile: + displayName: Colette Brock + email: colette-brock@example.com + picture: https://example.com/staff/colette.jpeg + memberOf: [team-b] +--- +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: jenny.doe +spec: + profile: + displayName: Jenny Doe + email: jenny-doe@example.com + picture: https://example.com/staff/jenny.jpeg + memberOf: [team-b] +--- +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: jonathon.page +spec: + profile: + displayName: Jonathon Page + email: jonathon-page@example.com + picture: https://example.com/staff/jonathon.jpeg + memberOf: [team-b] +--- +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: justine.barrow +spec: + profile: + displayName: Justine Barrow + email: justine-barrow@example.com + picture: https://example.com/staff/justine.jpeg + memberOf: [team-b] diff --git a/packages/catalog-model/examples/acme/team-c-group.yaml b/packages/catalog-model/examples/acme/team-c-group.yaml new file mode 100644 index 0000000000..0f97f69cb3 --- /dev/null +++ b/packages/catalog-model/examples/acme/team-c-group.yaml @@ -0,0 +1,66 @@ +apiVersion: backstage.io/v1alpha1 +kind: Group +metadata: + name: team-c + description: Team C +spec: + type: team + parent: boxoffice + ancestors: [boxoffice, infrastructure, acme-corp] + children: [] + descendants: [] +--- +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: calum.leavy +spec: + profile: + displayName: Calum Leavy + email: calum-leavy@example.com + picture: https://example.com/staff/calum.jpeg + memberOf: [team-c] +--- +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: frank.tiernan +spec: + profile: + displayName: Frank Tiernan + email: frank-tiernan@example.com + picture: https://example.com/staff/frank.jpeg + memberOf: [team-c] +--- +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: peadar.macmahon +spec: + profile: + displayName: Peadar MacMahon + email: peadar-macmahon@example.com + picture: https://example.com/staff/peadar.jpeg + memberOf: [team-c] +--- +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: sarah.gilroy +spec: + profile: + displayName: Sarah Gilroy + email: sarah-gilroy@example.com + picture: https://example.com/staff/sarah.jpeg + memberOf: [team-c] +--- +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: tara.macgovern +spec: + profile: + displayName: Tara MacGovern + email: tara-macgovern@example.com + picture: https://example.com/staff/tara.jpeg + memberOf: [team-c] diff --git a/packages/catalog-model/examples/acme/team-d-group.yaml b/packages/catalog-model/examples/acme/team-d-group.yaml new file mode 100644 index 0000000000..5a1a53a2e6 --- /dev/null +++ b/packages/catalog-model/examples/acme/team-d-group.yaml @@ -0,0 +1,33 @@ +apiVersion: backstage.io/v1alpha1 +kind: Group +metadata: + name: team-d + description: Team D +spec: + type: team + parent: boxoffice + ancestors: [boxoffice, infrastructure, acme-corp] + children: [] + descendants: [] +--- +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: eva.macdowell +spec: + profile: + displayName: Eva MacDowell + email: eva-macdowell@example.com + picture: https://example.com/staff/eva.jpeg + memberOf: [team-d] +--- +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: lucy.sheehan +spec: + profile: + displayName: Lucy Sheehan + email: lucy-sheehan@example.com + picture: https://example.com/staff/lucy.jpeg + memberOf: [team-d] diff --git a/packages/catalog-model/examples/all-apis.yaml b/packages/catalog-model/examples/all-apis.yaml index 5e8937f82b..64586fdddd 100644 --- a/packages/catalog-model/examples/all-apis.yaml +++ b/packages/catalog-model/examples/all-apis.yaml @@ -7,6 +7,7 @@ spec: type: github targets: - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/apis/hello-world-api.yaml - - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/apis/streetlights-api.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/apis/petstore-api.yaml - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/apis/spotify-api.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/apis/streetlights-api.yaml - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/apis/swapi-graphql.yaml diff --git a/packages/catalog-model/examples/all-components.yaml b/packages/catalog-model/examples/all-components.yaml index 902b40e008..bef38df0b1 100644 --- a/packages/catalog-model/examples/all-components.yaml +++ b/packages/catalog-model/examples/all-components.yaml @@ -7,6 +7,7 @@ spec: type: github targets: - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/components/artist-lookup-component.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/components/petstore-component.yaml - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/components/playback-order-component.yaml - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/components/podcast-api-component.yaml - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/components/queue-proxy-component.yaml diff --git a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.test.ts index 622af9ea59..e65714bcfd 100644 --- a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.test.ts @@ -123,6 +123,10 @@ describe('CodeOwnersProcessor', () => { }); describe('normalizeCodeOwner', () => { + it('should remove the @ symbol', () => { + expect(normalizeCodeOwner('@yoda')).toBe('yoda'); + }); + it('should remove org from org/team format', () => { expect(normalizeCodeOwner('@acme/foo')).toBe('foo'); }); diff --git a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts index bc78b6e34a..f186a9425c 100644 --- a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts @@ -127,6 +127,8 @@ export function findPrimaryCodeOwner( export function normalizeCodeOwner(owner: string) { if (owner.match(/^@.*\/.*/)) { return owner.split('/')[1]; + } else if (owner.match(/^@.*/)) { + return owner.substring(1); } else if (owner.match(/^.*@.*\..*$/)) { return owner.split('@')[0]; } diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index 266d5fa843..3214ea3385 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -26,6 +26,7 @@ import { makeStyles, Typography, } from '@material-ui/core'; +import BrightnessAutoIcon from '@material-ui/icons/BrightnessAuto'; import DocsIcon from '@material-ui/icons/Description'; import EditIcon from '@material-ui/icons/Edit'; import GitHubIcon from '@material-ui/icons/GitHub'; @@ -120,6 +121,14 @@ export function AboutCard({ entity, variant }: AboutCardProps) { entity.metadata.namespace || ENTITY_DEFAULT_NAMESPACE }/${entity.kind}/${entity.metadata.name}`} /> + } + href={`./${ + entity.metadata.namespace || ENTITY_DEFAULT_NAMESPACE + }:${entity.metadata.name}/api`} + /> } /> diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index f5885145b3..4e63c6099b 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -155,7 +155,8 @@ export const CatalogTable = ({ isLoading={loading} columns={columns} options={{ - paging: false, + paging: true, + pageSize: 10, actionsColumnIndex: -1, loadingType: 'linear', showEmptyDataSourceMessage: !loading, diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 3d548402be..e79c4ab962 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -40,7 +40,8 @@ "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", "react-use": "^15.3.3", - "recharts": "^1.8.5" + "recharts": "^1.8.5", + "yup": "^0.29.3" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.25", @@ -50,6 +51,7 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", + "@types/yup": "^0.29.8", "@types/recharts": "^1.8.14", "msw": "^0.21.2", "cross-fetch": "^3.0.6", diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx index c9790a5bfa..13bb990740 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx @@ -51,7 +51,7 @@ const ProductInsightsCard = ({ product }: ProductInsightsCardProps) => { const dispatchLoadingProduct = useCallback(dispatchLoading, [product.kind]); const amount = resource?.entities?.length || 0; - const hasCostsWithinTimeframe = resource?.change && amount; + const hasCostsWithinTimeframe = resource?.change && !!amount; const subheader = amount ? `${amount} ${pluralOf(amount, 'entity', 'entities')}, sorted by cost` diff --git a/plugins/cost-insights/src/hooks/index.ts b/plugins/cost-insights/src/hooks/index.ts index 7065bcf91b..8998a10e2c 100644 --- a/plugins/cost-insights/src/hooks/index.ts +++ b/plugins/cost-insights/src/hooks/index.ts @@ -20,5 +20,4 @@ export * from './useFilters'; export * from './useCurrency'; export * from './useGroups'; export * from './useLoading'; -export * from './useQueryParams'; export * from './useScroll'; diff --git a/plugins/cost-insights/src/hooks/useFilters.tsx b/plugins/cost-insights/src/hooks/useFilters.tsx index 7dcf8ddb26..22ef735186 100644 --- a/plugins/cost-insights/src/hooks/useFilters.tsx +++ b/plugins/cost-insights/src/hooks/useFilters.tsx @@ -16,114 +16,98 @@ import React, { Dispatch, - ReactNode, + PropsWithChildren, SetStateAction, useContext, useEffect, - useRef, useState, } from 'react'; -import { - getDefaultPageFilters, - PageFilters, - ProductFilters, - Duration, - Group, -} from '../types'; +import { Alert } from '@material-ui/lab'; +import { Maybe, PageFilters, ProductFilters } from '../types'; import { useLocation, useNavigate } from 'react-router-dom'; -import { useQueryParams } from './useQueryParams'; -import { stringify } from '../utils/history'; +import { + stringify, + validate, + getInitialPageState, + getInitialProductState, +} from '../utils/history'; import { useGroups } from './useGroups'; import { useConfig } from './useConfig'; -const getInitialPageState = ( - groups: Group[], - queryParams?: Partial, -) => { - // The group is written initially to queryParams as null, since user groups are asynchronously - // loaded. We preserve nulls in queryParams for other parameters where null is meaningful; for - // group, avoid overwriting the default with null after groups are loaded. - const { group, ...otherParams } = queryParams || {}; - return { - ...getDefaultPageFilters(groups), - ...otherParams, - ...(group ? { group: group } : {}), - }; -}; - export type FilterContextProps = { pageFilters: PageFilters; productFilters: ProductFilters; - setPageFilters: Dispatch>; - setProductFilters: Dispatch>; + setPageFilters: Dispatch>>; + setProductFilters: Dispatch>>; }; export type MapFiltersToProps = (props: FilterContextProps) => T; -export type FilterProviderProps = { - children: ReactNode; -}; - export const FilterContext = React.createContext< FilterContextProps | undefined >(undefined); -export const FilterProvider = ({ children }: FilterProviderProps) => { +export const FilterProvider = ({ children }: PropsWithChildren<{}>) => { const config = useConfig(); const navigate = useNavigate(); const location = useLocation(); - const queryParams = useQueryParams(); - const qsRef = useRef(''); const groups = useGroups(); - - const defaultProductFilters = config.products.map(product => ({ - productType: product.kind, - duration: Duration.P1M, - })); - - const getInitialProductState = (productFilters?: ProductFilters) => { - if (!productFilters) return defaultProductFilters; - return defaultProductFilters.map(product => { - return ( - productFilters.find( - param => param.productType === product.productType, - ) || product - ); - }); - }; - - const [productFilters, setProductFilters] = useState( - getInitialProductState(queryParams.productFilters), - ); - const [pageFilters, setPageFilters] = useState( - getInitialPageState(groups, queryParams.pageFilters), + const [error, setError] = useState>(null); + const [pageFilters, setPageFilters] = useState>(null); + const [productFilters, setProductFilters] = useState>( + null, ); - // TODO: Figure out why pageFilters doesn't get updated by the above when groups are loaded. useEffect(() => { - const initialState = getInitialPageState(groups, queryParams.pageFilters); - const defaultMetric = config.metrics.find(m => m.default); - setPageFilters({ ...initialState, metric: defaultMetric?.kind ?? null }); + async function setPageFiltersFromLocation() { + try { + // strip extraneous parameters, validate and transform + const queryParams = await validate(location.search); + const defaultMetric = config.metrics.find(m => m.default)?.kind ?? null; + + // Group or project parameters should override defaults + const initialPageState = getInitialPageState(groups, queryParams); + const initialProductState = getInitialProductState(config); + + setProductFilters(initialProductState); + setPageFilters({ ...initialPageState, metric: defaultMetric }); + } catch (e) { + setError(e); + } + } + + setPageFiltersFromLocation(); }, [groups]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { - const queryString = stringify({ ...pageFilters, products: productFilters }); - if (queryString === qsRef.current) return; - qsRef.current = queryString; - // TODO Remove workaround once issue is resolved in react-router - // (https://github.com/ReactTraining/react-router/issues/7496) - // navigate({ ...location, search: queryString }); - navigate({ ...location, search: `?${queryString}` }); - }, [pageFilters, productFilters]); // eslint-disable-line react-hooks/exhaustive-deps + function setLocationFromPageFilters(filters: PageFilters) { + const queryString = stringify({ + group: filters.group, + ...(filters.project ? { project: filters.project } : {}), + }); + // TODO Remove workaround once issue is resolved in react-router + // (https://github.com/ReactTraining/react-router/issues/7496) + // navigate({ ...location, search: queryString }); + navigate({ ...location, search: `?${queryString}` }); + } + + if (pageFilters) { + setLocationFromPageFilters(pageFilters); + } + }, [pageFilters]); // eslint-disable-line react-hooks/exhaustive-deps + + if (error) { + return {error.message}; + } + + // Wait for filters to load + if (!pageFilters || !productFilters) { + return null; + } return ( {children} @@ -132,17 +116,7 @@ export const FilterProvider = ({ children }: FilterProviderProps) => { export function useFilters(mapFiltersToProps: MapFiltersToProps): T { const context = useContext(FilterContext); - - if (!context) { - assertNever(); - } - - return mapFiltersToProps({ - pageFilters: context.pageFilters, - productFilters: context.productFilters, - setPageFilters: context.setPageFilters, - setProductFilters: context.setProductFilters, - }); + return context ? mapFiltersToProps(context) : assertNever(); } function assertNever(): never { diff --git a/plugins/cost-insights/src/hooks/useGroups.tsx b/plugins/cost-insights/src/hooks/useGroups.tsx index 02c32e7920..8f11c88e60 100644 --- a/plugins/cost-insights/src/hooks/useGroups.tsx +++ b/plugins/cost-insights/src/hooks/useGroups.tsx @@ -14,11 +14,17 @@ * limitations under the License. */ -import React, { ReactNode, useContext, useEffect, useState } from 'react'; +import React, { + PropsWithChildren, + useContext, + useEffect, + useState, +} from 'react'; +import { Alert } from '@material-ui/lab'; import { useApi, identityApiRef } from '@backstage/core'; import { costInsightsApiRef } from '../api'; import { MapLoadingToProps, useLoading } from './useLoading'; -import { DefaultLoadingAction, Group } from '../types'; +import { DefaultLoadingAction, Group, Maybe } from '../types'; type GroupsProviderLoadingProps = { dispatchLoadingGroups: (isLoading: boolean) => void; @@ -35,33 +41,41 @@ type GroupsContextProps = { groups: Group[]; }; -export type GroupsProviderProps = { - children: ReactNode; -}; +export const GroupsContext = React.createContext< + GroupsContextProps | undefined +>(undefined); -export const GroupsContext = React.createContext({ - groups: [], -}); - -export const GroupsProvider = ({ children }: GroupsProviderProps) => { +export const GroupsProvider = ({ children }: PropsWithChildren<{}>) => { const userId = useApi(identityApiRef).getUserId(); const client = useApi(costInsightsApiRef); + const [error, setError] = useState>(null); const { dispatchLoadingGroups } = useLoading(mapLoadingToProps); - const [groups, setGroups] = useState([]); + const [groups, setGroups] = useState>(null); useEffect(() => { dispatchLoadingGroups(true); async function getUserGroups() { - const g = await client.getUserGroups(userId); - setGroups(g); - dispatchLoadingGroups(false); + try { + const g = await client.getUserGroups(userId); + setGroups(g); + } catch (e) { + setError(e); + } finally { + dispatchLoadingGroups(false); + } } getUserGroups(); }, [userId, client]); // eslint-disable-line react-hooks/exhaustive-deps + if (error) { + return {error.message}; + } + + if (!groups) return null; + return ( {children} @@ -70,13 +84,8 @@ export const GroupsProvider = ({ children }: GroupsProviderProps) => { }; export function useGroups(): Group[] { - const { groups } = useContext(GroupsContext); - - if (!groups) { - assertNever(); - } - - return groups; + const context = useContext(GroupsContext); + return context ? context.groups : assertNever(); } function assertNever(): never { diff --git a/plugins/cost-insights/src/hooks/useQueryParams.tsx b/plugins/cost-insights/src/hooks/useQueryParams.tsx deleted file mode 100644 index 1e6ce8f0fd..0000000000 --- a/plugins/cost-insights/src/hooks/useQueryParams.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 { useLocation } from 'react-router-dom'; -import { Location } from 'history'; -import { PageFilters, ProductFilters } from '../types'; -import { parse } from '../utils/history'; - -export type FilterParams = { - pageFilters?: Partial; - productFilters?: ProductFilters; -}; - -export function useQueryParams(): FilterParams { - const location: Location = useLocation(); - const { products: productFilters, ...pageFilters } = parse(location.search); - - return { - productFilters: productFilters, - pageFilters: pageFilters, - }; -} diff --git a/plugins/cost-insights/src/types/Filters.ts b/plugins/cost-insights/src/types/Filters.ts index f90b9b4c78..b1fbbb2848 100644 --- a/plugins/cost-insights/src/types/Filters.ts +++ b/plugins/cost-insights/src/types/Filters.ts @@ -27,8 +27,6 @@ export interface PageFilters { export type ProductFilters = Array; -export type QueryParams = PageFilters & { products: ProductFilters }; - export interface ProductPeriod { duration: Duration; productType: string; diff --git a/plugins/cost-insights/src/utils/history.test.ts b/plugins/cost-insights/src/utils/history.test.ts new file mode 100644 index 0000000000..e4310e13c6 --- /dev/null +++ b/plugins/cost-insights/src/utils/history.test.ts @@ -0,0 +1,80 @@ +/* + * 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 { validate, getInitialPageState } from './history'; + +describe('getInitialPageState', () => { + describe('groups', () => { + it('should set defaults if params or group is not provided', () => { + const initialState = getInitialPageState([]); + expect(initialState.group).toBe(null); + }); + + it('should set defaults if a group is fetched but no group is present on query params', () => { + const initialState = getInitialPageState([{ id: 'group' }]); + expect(initialState.group).toMatch(/group/); + }); + + it('group param should always override fetched group', () => { + const initialState = getInitialPageState( + [{ id: 'group' }, { id: 'second-group' }], + { group: 'other-group' }, + ); + expect(initialState.group).toMatch(/other-group/); + }); + + it('first group should be set as default group if user belongs to multiple groups', () => { + const initialState = getInitialPageState([ + { id: 'group' }, + { id: 'other-group' }, + ]); + expect(initialState.group).toMatch(/group/); + }); + }); + + describe('projects', () => { + it("should set defaults if project param doesn't exist", () => { + const initialState = getInitialPageState([], {}); + expect(initialState.project).toBeNull(); + }); + + it('should override defaults if project param exists', () => { + const initialState = getInitialPageState([], { project: 'some-project' }); + expect(initialState.project).toMatch(/some-project/); + }); + }); +}); + +describe.each` + params | expected + ${''} | ${{}} + ${'?foo=bar'} | ${{}} + ${'?project'} | ${{ project: null }} + ${'?group=some-group'} | ${{ group: 'some-group' }} + ${'?group=some-group&project'} | ${{ group: 'some-group', project: null }} + ${'?group=some-group&project=some-project'} | ${{ group: 'some-group', project: 'some-project' }} + ${'?group=some-group&project=some-project&foo=bar'} | ${{ group: 'some-group', project: 'some-project' }} +`('validate', ({ params, expected }) => { + it(`should validate ${params}`, async () => { + const pageFilters = await validate(params); + expect(pageFilters).toMatchObject(expected); + }); +}); + +describe('invalidate', () => { + it("should throw an error if param values don't match schema", async () => { + await expect(validate('?group')).rejects.toThrowError(); + }); +}); diff --git a/plugins/cost-insights/src/utils/history.ts b/plugins/cost-insights/src/utils/history.ts index 71e8e2c219..90df574acf 100644 --- a/plugins/cost-insights/src/utils/history.ts +++ b/plugins/cost-insights/src/utils/history.ts @@ -15,9 +15,44 @@ */ import qs from 'qs'; -import { QueryParams } from '../types'; +import * as yup from 'yup'; +import { Group, PageFilters, getDefaultPageFilters, Duration } from '../types'; +import { ConfigContextProps } from '../hooks/useConfig'; -export const stringify = (queryParams: Partial) => +const schema = yup + .object() + .shape({ + group: yup.string(), + project: yup.string().nullable(), + }) + .required(); + +export const stringify = (queryParams: Partial) => qs.stringify(queryParams, { strictNullHandling: true }); -export const parse = (queryString: string): Partial => + +export const parse = (queryString: string): Partial => qs.parse(queryString, { ignoreQueryPrefix: true, strictNullHandling: true }); + +export const validate = (queryString: string): Promise => { + return schema.validate(parse(queryString), { + stripUnknown: true, + strict: true, + }) as Promise; +}; + +export const getInitialPageState = ( + groups: Group[], + queryParams: Partial = {}, +) => { + return { + ...getDefaultPageFilters(groups), + ...(queryParams.project ? { project: queryParams.project } : {}), + ...(queryParams.group ? { group: queryParams.group } : {}), + }; +}; + +export const getInitialProductState = (config: ConfigContextProps) => + config.products.map(product => ({ + productType: product.kind, + duration: Duration.P30D, + })); diff --git a/plugins/cost-insights/src/utils/tests.tsx b/plugins/cost-insights/src/utils/tests.tsx index 807232555b..552c524afe 100644 --- a/plugins/cost-insights/src/utils/tests.tsx +++ b/plugins/cost-insights/src/utils/tests.tsx @@ -16,6 +16,7 @@ import React, { Dispatch, ReactNode, SetStateAction } from 'react'; import { getDefaultPageFilters, + Maybe, PageFilters, ProductFilters, Group, @@ -29,8 +30,8 @@ import { MockProductFilters } from './mockData'; export const MockGroups: Group[] = [{ id: 'tech' }, { id: 'mock-group' }]; type MockFilterProviderProps = { - setPageFilters: Dispatch>; - setProductFilters: Dispatch>; + setPageFilters: Dispatch>>; + setProductFilters: Dispatch>>; children: ReactNode; }; diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index a286157e08..60c7c8f084 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -26,7 +26,6 @@ "@types/dockerode": "^2.5.34", "@types/express": "^4.17.6", "command-exists-promise": "^2.0.2", - "default-branch": "^1.0.8", "dockerode": "^3.2.1", "express": "^4.17.1", "express-promise-router": "^3.0.3", diff --git a/plugins/techdocs-backend/src/default-branch.ts b/plugins/techdocs-backend/src/default-branch.ts index 6ed12348ac..2997a2acbf 100644 --- a/plugins/techdocs-backend/src/default-branch.ts +++ b/plugins/techdocs-backend/src/default-branch.ts @@ -17,6 +17,13 @@ import fetch from 'cross-fetch'; import parseGitUrl from 'git-url-parse'; import { Config } from '@backstage/config'; import { getRootLogger, loadBackendConfig } from '@backstage/backend-common'; +import { + getAzureHostToken, + getGitHost, + getGithubHostToken, + getGitlabHostToken, + getGitRepoType, +} from './git-auth'; interface IGitlabBranch { name: string; @@ -73,15 +80,12 @@ function getAzureApiUrl(url: string): URL { ); } -function getGithubRequestOptions(config: Config): RequestInit { +function getGithubRequestOptions(config: Config, host: string): RequestInit { const headers: HeadersInit = { Accept: 'application/vnd.github.v3.raw', }; - const token = - config.getOptionalString('catalog.processors.github.privateToken') ?? - config.getOptionalString('catalog.processors.githubApi.privateToken') ?? - process.env.GITHUB_TOKEN; + const token = getGithubHostToken(config, host); if (token) { headers.Authorization = `token ${token}`; @@ -92,16 +96,12 @@ function getGithubRequestOptions(config: Config): RequestInit { }; } -function getGitlabRequestOptions(config: Config): RequestInit { +function getGitlabRequestOptions(config: Config, host: string): RequestInit { const headers: HeadersInit = { 'PRIVATE-TOKEN': '', }; - const token = - config.getOptionalString('catalog.processors.gitlab.privateToken') ?? - config.getOptionalString('catalog.processors.gitlabApi.privateToken') ?? - process.env.GITLAB_TOKEN; - + const token = getGitlabHostToken(config, host); if (token) { headers['PRIVATE-TOKEN'] = token; } @@ -111,12 +111,10 @@ function getGitlabRequestOptions(config: Config): RequestInit { }; } -function getAzureRequestOptions(config: Config): RequestInit { +function getAzureRequestOptions(config: Config, host: string): RequestInit { const headers: HeadersInit = {}; - const token = - config.getOptionalString('catalog.processors.azureApi.privateToken') ?? - process.env.AZURE_TOKEN; + const token = getAzureHostToken(config, host); if (token !== '') { headers.Authorization = `Basic ${Buffer.from(`:${token}`, 'utf8').toString( @@ -136,7 +134,8 @@ async function getGithubDefaultBranch( config: Config, ): Promise { const path = getGithubApiUrl(config, repositoryUrl).toString(); - const options = getGithubRequestOptions(config); + const host = getGitHost(repositoryUrl); + const options = getGithubRequestOptions(config, host); try { const raw = await fetch(path, options); @@ -165,7 +164,8 @@ async function getGitlabDefaultBranch( ): Promise { const path = getGitlabApiUrl(repositoryUrl).toString(); - const options = getGitlabRequestOptions(config); + const gitlabHost = getGitHost(repositoryUrl); + const options = getGitlabRequestOptions(config, gitlabHost); try { const raw = await fetch(path, options); @@ -196,8 +196,8 @@ async function getAzureDefaultBranch( config: Config, ): Promise { const path = getAzureApiUrl(repositoryUrl).toString(); - - const options = getAzureRequestOptions(config); + const host = getGitHost(repositoryUrl); + const options = getAzureRequestOptions(config, host); try { const urlResponse = await fetch(path, options); @@ -232,14 +232,7 @@ export const getDefaultBranch = async ( ): Promise => { // TODO(Rugvip): Config should not be loaded here, pass it in instead const config = await loadBackendConfig({ logger: getRootLogger() }); - const typeMapping = [ - { url: /github*/g, type: 'github' }, - { url: /gitlab*/g, type: 'gitlab' }, - { url: /azure*/g, type: 'azure/api' }, - ]; - - const type = typeMapping.filter(item => item.url.test(repositoryUrl))[0] - ?.type; + const type = getGitRepoType(repositoryUrl); try { switch (type) { diff --git a/plugins/techdocs-backend/src/git-auth.ts b/plugins/techdocs-backend/src/git-auth.ts new file mode 100644 index 0000000000..d77596929f --- /dev/null +++ b/plugins/techdocs-backend/src/git-auth.ts @@ -0,0 +1,116 @@ +/* + * 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 parseGitUrl from 'git-url-parse'; +import { Config } from '@backstage/config'; +import { getRootLogger, loadBackendConfig } from '@backstage/backend-common'; + +export function getGitHost(url: string): string { + const { resource } = parseGitUrl(url); + return resource; +} + +export function getGitRepoType(url: string): string { + const typeMapping = [ + { url: /github*/g, type: 'github' }, + { url: /gitlab*/g, type: 'gitlab' }, + { url: /azure*/g, type: 'azure/api' }, + ]; + + const type = typeMapping.filter(item => item.url.test(url))[0]?.type; + + return type; +} + +export function getGithubHostToken( + config: Config, + host: string, +): string | undefined { + const providerConfigs = + config.getOptionalConfigArray('integrations.github') ?? []; + + const hostConfig = providerConfigs.filter( + providerConfig => providerConfig.getOptionalString('host') === host, + ); + const token = + hostConfig[0]?.getOptionalString('token') ?? + config.getOptionalString('catalog.processors.github.privateToken') ?? + config.getOptionalString('catalog.processors.githubApi.privateToken') ?? + process.env.GITHUB_TOKEN; + + return token; +} + +export function getGitlabHostToken( + config: Config, + host: string, +): string | undefined { + const providerConfigs = + config.getOptionalConfigArray('integrations.gitlab') ?? []; + + const hostConfig = providerConfigs.filter( + providerConfig => providerConfig.getOptionalString('host') === host, + ); + const token = + hostConfig[0]?.getOptionalString('token') ?? + config.getOptionalString('catalog.processors.gitlab.privateToken') ?? + config.getOptionalString('catalog.processors.gitlabApi.privateToken') ?? + process.env.GITLAB_TOKEN; + + return token; +} + +export function getAzureHostToken( + config: Config, + host: string, +): string | undefined { + const providerConfigs = + config.getOptionalConfigArray('integrations.azure') ?? []; + + const hostConfig = providerConfigs.filter( + providerConfig => providerConfig.getOptionalString('host') === host, + ); + const token = + hostConfig[0]?.getOptionalString('token') ?? + config.getOptionalString('catalog.processors.azureApi.privateToken') ?? + process.env.AZURE_TOKEN; + + return token; +} + +export const getTokenForGitRepo = async ( + repositoryUrl: string, +): Promise => { + const config = await loadBackendConfig({ logger: getRootLogger() }); + + const host = getGitHost(repositoryUrl); + const type = getGitRepoType(repositoryUrl); + + try { + switch (type) { + case 'github': + return getGithubHostToken(config, host); + case 'gitlab': + return getGitlabHostToken(config, host); + case 'azure/api': + return getAzureHostToken(config, host); + + default: + throw new Error('Failed to get repository type'); + } + } catch (error) { + throw error; + } +}; diff --git a/plugins/techdocs-backend/src/helpers.ts b/plugins/techdocs-backend/src/helpers.ts index b2dcd1e8e8..4a21800b07 100644 --- a/plugins/techdocs-backend/src/helpers.ts +++ b/plugins/techdocs-backend/src/helpers.ts @@ -20,6 +20,7 @@ import parseGitUrl from 'git-url-parse'; import NodeGit, { Clone, Repository } from 'nodegit'; import fs from 'fs-extra'; import { getDefaultBranch } from './default-branch'; +import { getTokenForGitRepo } from './git-auth'; import { Entity } from '@backstage/catalog-model'; import { InputError } from '@backstage/backend-common'; import { RemoteProtocol } from './techdocs/stages/prepare/types'; @@ -127,11 +128,8 @@ export const checkoutGitRepository = async ( process.env.GITLAB_PRIVATE_TOKEN_USER || process.env.AZURE_PRIVATE_TOKEN_USER || ''; - const token = - process.env.GITHUB_TOKEN || - process.env.GITLAB_PRIVATE_TOKEN_USER || - process.env.AZURE_TOKEN || - ''; + + const token = await getTokenForGitRepo(repoUrl); if (fs.existsSync(repositoryTmpPath)) { try { @@ -153,7 +151,7 @@ export const checkoutGitRepository = async ( } } - if (user && token) { + if (token) { parsedGitLocation.token = `${user}:${token}`; } diff --git a/plugins/techdocs/src/reader/transformers/onCssReady.test.ts b/plugins/techdocs/src/reader/transformers/onCssReady.test.ts index c83a94f35d..67ae38c56b 100644 --- a/plugins/techdocs/src/reader/transformers/onCssReady.test.ts +++ b/plugins/techdocs/src/reader/transformers/onCssReady.test.ts @@ -25,14 +25,20 @@ import { onCssReady } from '../transformers'; const docStorageUrl: string = 'https://techdocs-mock-sites.storage.googleapis.com'; -jest.useFakeTimers(); - const fixture = ` `; describe('onCssReady', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + beforeEach(() => { mockStylesheetEventListener(100); }); diff --git a/yarn.lock b/yarn.lock index 524f37b63d..7e15bfdf28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3830,16 +3830,16 @@ integrity sha512-so8w32ZV42CHWxOEXcBtbNO/hLXFrQNXVmhfzhUI6dVB9cq2xjRaiqu8GjFj8LvKbWpPj+S+KwTIS4aDVWqrFQ== "@storybook/addon-actions@^6.0.21": - version "6.0.21" - resolved "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.0.21.tgz#0de1d109d4b1eb99f644bbe84e74c25cfd2b1b6b" - integrity sha512-9y3ve+3GK1TsxQ5pxDjhB7E/XJXY+WqcSNlOX8Mb+XbS6AAgpFbkZCw1q8CGzyEUclHsQ6UK2+lo+IRGs4TLpA== + version "6.0.26" + resolved "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.0.26.tgz#d0de9e4d78a8f8f5bf8730c04d0b6d1741c29273" + integrity sha512-9tWbAqSwzWWVz8zwAndZFusZYjIcRYgZUC0LqC8QlH79DgF3ASjw9y97+w1VTTAzdb6LYnsMuSpX6+8m5hrR4g== dependencies: - "@storybook/addons" "6.0.21" - "@storybook/api" "6.0.21" - "@storybook/client-api" "6.0.21" - "@storybook/components" "6.0.21" - "@storybook/core-events" "6.0.21" - "@storybook/theming" "6.0.21" + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/theming" "6.0.26" core-js "^3.0.1" fast-deep-equal "^3.1.1" global "^4.3.2" @@ -3892,7 +3892,7 @@ react-syntax-highlighter "^12.2.1" regenerator-runtime "^0.13.3" -"@storybook/addons@6.0.21", "@storybook/addons@^6.0.21": +"@storybook/addons@6.0.21": version "6.0.21" resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.0.21.tgz#bd5229652102c3aed59b78ef6920ff6b482b4d78" integrity sha512-yDttNLc3vXqBxwK795ykgzTC6MpvuXDQuF4LHSlHZQe6wsMu1m3fljnbYdafJWdx6cNZwUblU3KYcR11PqhkPg== @@ -3907,6 +3907,21 @@ global "^4.3.2" regenerator-runtime "^0.13.3" +"@storybook/addons@6.0.26", "@storybook/addons@^6.0.21": + version "6.0.26" + resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.0.26.tgz#343cbea3eee2d39413b80bc2d66535a7f61488fc" + integrity sha512-OhAApFKgsj9an7FLYfHI4cJQuZ4Zm6yoGOpaxhOvKQMw7dXUPsLvbCyw/6dZOLvaFhjJjQiXtbxtZG+UjR8nvA== + dependencies: + "@storybook/api" "6.0.26" + "@storybook/channels" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/router" "6.0.26" + "@storybook/theming" "6.0.26" + core-js "^3.0.1" + global "^4.3.2" + regenerator-runtime "^0.13.3" + "@storybook/api@6.0.21": version "6.0.21" resolved "https://registry.npmjs.org/@storybook/api/-/api-6.0.21.tgz#a25a1eb4d07dc43500e03c856db43baba46726f1" @@ -3933,6 +3948,32 @@ ts-dedent "^1.1.1" util-deprecate "^1.0.2" +"@storybook/api@6.0.26": + version "6.0.26" + resolved "https://registry.npmjs.org/@storybook/api/-/api-6.0.26.tgz#c45222c132eb8bc2e383536adfebbeb7a89867d0" + integrity sha512-aszDoz1c6T+eRtTUwWvySoyd3gRXmQxsingD084NnEp4VfFLA5H7VS/0sre0ZvU5GWh8d9COxY0DS2Ry/QSKvw== + dependencies: + "@reach/router" "^1.3.3" + "@storybook/channels" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/csf" "0.0.1" + "@storybook/router" "6.0.26" + "@storybook/semver" "^7.3.2" + "@storybook/theming" "6.0.26" + "@types/reach__router" "^1.3.5" + core-js "^3.0.1" + fast-deep-equal "^3.1.1" + global "^4.3.2" + lodash "^4.17.15" + memoizerific "^1.11.3" + react "^16.8.3" + regenerator-runtime "^0.13.3" + store2 "^2.7.1" + telejson "^5.0.2" + ts-dedent "^1.1.1" + util-deprecate "^1.0.2" + "@storybook/channel-postmessage@6.0.21": version "6.0.21" resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.0.21.tgz#97e8f43c1b66f84c7b8271e447d45d4f66d355d1" @@ -3946,6 +3987,19 @@ qs "^6.6.0" telejson "^5.0.2" +"@storybook/channel-postmessage@6.0.26": + version "6.0.26" + resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.0.26.tgz#a98a0132d6bdf06741afac2607e9feabe34ab98b" + integrity sha512-FT6lC8M5JlNBxPT0rYfmF1yl9mBv04nfYs82TZpp1CzpLxf7wxdCBZ8SSRmvWIVBoNwGZPDhIk5+6JWyDEISBg== + dependencies: + "@storybook/channels" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/core-events" "6.0.26" + core-js "^3.0.1" + global "^4.3.2" + qs "^6.6.0" + telejson "^5.0.2" + "@storybook/channels@6.0.21": version "6.0.21" resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.0.21.tgz#bc0951efacbaa5f8827693fba4fe7c2290b5772c" @@ -3955,6 +4009,15 @@ ts-dedent "^1.1.1" util-deprecate "^1.0.2" +"@storybook/channels@6.0.26": + version "6.0.26" + resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.0.26.tgz#3e8678b4b40085081257a39b9e85fab13a19943c" + integrity sha512-H0iUorayYqS+zfhbjd+cYRzAdRLGLWUeWFu2Aa+oJ4/zeAQNL+DafWboHc567RQ4Vb5KqE5QZoCFskWUUYqJYA== + dependencies: + core-js "^3.0.1" + ts-dedent "^1.1.1" + util-deprecate "^1.0.2" + "@storybook/client-api@6.0.21": version "6.0.21" resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.0.21.tgz#6a652dea67d219a31d18af0e05b9f17ba6c7c316" @@ -3978,6 +4041,29 @@ ts-dedent "^1.1.1" util-deprecate "^1.0.2" +"@storybook/client-api@6.0.26": + version "6.0.26" + resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.0.26.tgz#ac9334ba86834e5cb23fc4fb577de60bda66164d" + integrity sha512-Qd5wR5b5lio/EchuJMhAmmJAE1pfvnEyu+JnyFGwMZLV9mN9NSspz+YsqbSCCDZsYcP5ewvPEnumIWqmj/wagQ== + dependencies: + "@storybook/addons" "6.0.26" + "@storybook/channel-postmessage" "6.0.26" + "@storybook/channels" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/csf" "0.0.1" + "@types/qs" "^6.9.0" + "@types/webpack-env" "^1.15.2" + core-js "^3.0.1" + global "^4.3.2" + lodash "^4.17.15" + memoizerific "^1.11.3" + qs "^6.6.0" + stable "^0.1.8" + store2 "^2.7.1" + ts-dedent "^1.1.1" + util-deprecate "^1.0.2" + "@storybook/client-logger@6.0.21": version "6.0.21" resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.0.21.tgz#20369addf9eb79fc0c85a2e0dcb48f5a1a544532" @@ -3986,6 +4072,14 @@ core-js "^3.0.1" global "^4.3.2" +"@storybook/client-logger@6.0.26": + version "6.0.26" + resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.0.26.tgz#e3d28bd8dc02ec2c53a9d69773a68189590b746f" + integrity sha512-VNoL6/oehVhn3hZi9vrTNT+C/3oAZKV+smfZFnPtsCR/Fq7CKbmsBd0pGPL57f81RU8e8WygwrIlAGJTDSNIjw== + dependencies: + core-js "^3.0.1" + global "^4.3.2" + "@storybook/components@6.0.21": version "6.0.21" resolved "https://registry.npmjs.org/@storybook/components/-/components-6.0.21.tgz#2f355370f993e0b7b9062094a03dffc2cdda91db" @@ -4014,6 +4108,34 @@ react-textarea-autosize "^8.1.1" ts-dedent "^1.1.1" +"@storybook/components@6.0.26": + version "6.0.26" + resolved "https://registry.npmjs.org/@storybook/components/-/components-6.0.26.tgz#e1f6e16aae850a71c9ac7bdd1d44a068ec9cfdc1" + integrity sha512-8wigI1pDFJO1m1IQWPguOK+nOsaAVRWkVdu+2te/rDcIR9QNvMzzou0+Lhfp3zKSVT4E6mEoGB/TWXXF5Iq0sQ== + dependencies: + "@storybook/client-logger" "6.0.26" + "@storybook/csf" "0.0.1" + "@storybook/theming" "6.0.26" + "@types/overlayscrollbars" "^1.9.0" + "@types/react-color" "^3.0.1" + "@types/react-syntax-highlighter" "11.0.4" + core-js "^3.0.1" + fast-deep-equal "^3.1.1" + global "^4.3.2" + lodash "^4.17.15" + markdown-to-jsx "^6.11.4" + memoizerific "^1.11.3" + overlayscrollbars "^1.10.2" + polished "^3.4.4" + popper.js "^1.14.7" + react "^16.8.3" + react-color "^2.17.0" + react-dom "^16.8.3" + react-popper-tooltip "^2.11.0" + react-syntax-highlighter "^12.2.1" + react-textarea-autosize "^8.1.1" + ts-dedent "^1.1.1" + "@storybook/core-events@6.0.21": version "6.0.21" resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.0.21.tgz#2ce51e6d7524e7543dbb29571beac1dbeb4e5f40" @@ -4021,6 +4143,13 @@ dependencies: core-js "^3.0.1" +"@storybook/core-events@6.0.26": + version "6.0.26" + resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.0.26.tgz#61181c9a8610d26cc85d47f133a563879044ca2d" + integrity sha512-nWjS/+kMiw31OPgeJQaiFsJk9ZJJo3/d4c+kc6GOl2iC1H3Q4/5cm3NvJBn/7bUtKHmSFwfbDouj+XjUk5rZbQ== + dependencies: + core-js "^3.0.1" + "@storybook/core@6.0.21": version "6.0.21" resolved "https://registry.npmjs.org/@storybook/core/-/core-6.0.21.tgz#105c2b90ab27e7b478cb1b7d10e9fe5aba5e0708" @@ -4180,6 +4309,18 @@ memoizerific "^1.11.3" qs "^6.6.0" +"@storybook/router@6.0.26": + version "6.0.26" + resolved "https://registry.npmjs.org/@storybook/router/-/router-6.0.26.tgz#5b991001afa7d7eb5e40c53cd4c58266b6f9edfd" + integrity sha512-kQ1LF/2gX3IkjS1wX7CsoeBc9ptHQzOsyax16rUyJa769DT5vMNtFtQxjNXMqSiSapPg2yrXJFKQNaoWvKgQEQ== + dependencies: + "@reach/router" "^1.3.3" + "@types/reach__router" "^1.3.5" + core-js "^3.0.1" + global "^4.3.2" + memoizerific "^1.11.3" + qs "^6.6.0" + "@storybook/semver@^7.3.2": version "7.3.2" resolved "https://registry.npmjs.org/@storybook/semver/-/semver-7.3.2.tgz#f3b9c44a1c9a0b933c04e66d0048fcf2fa10dac0" @@ -4222,6 +4363,24 @@ resolve-from "^5.0.0" ts-dedent "^1.1.1" +"@storybook/theming@6.0.26": + version "6.0.26" + resolved "https://registry.npmjs.org/@storybook/theming/-/theming-6.0.26.tgz#e5b545fb2653dfd1b043b567197d490b1c3c0da3" + integrity sha512-9yon2ofb9a+RT1pdvn8Njydy7XRw0qXcIsMqGsJRKoZecmRRozqB6DxH9Gbdf1vRSbM9gYUUDjbiMDFz7+4RiQ== + dependencies: + "@emotion/core" "^10.0.20" + "@emotion/is-prop-valid" "^0.8.6" + "@emotion/styled" "^10.0.17" + "@storybook/client-logger" "6.0.26" + core-js "^3.0.1" + deep-object-diff "^1.1.0" + emotion-theming "^10.0.19" + global "^4.3.2" + memoizerific "^1.11.3" + polished "^3.4.4" + resolve-from "^5.0.0" + ts-dedent "^1.1.1" + "@storybook/ui@6.0.21": version "6.0.21" resolved "https://registry.npmjs.org/@storybook/ui/-/ui-6.0.21.tgz#5dac2b68a30f5dba5457e0315f58977e07138968" @@ -9780,11 +9939,6 @@ deepmerge@4.2.2, deepmerge@^4.2.2: resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== -default-branch@^1.0.8: - version "1.0.8" - resolved "https://registry.npmjs.org/default-branch/-/default-branch-1.0.8.tgz#0e2f36a90e3b0d9f73cdf8e02841364ed35b8b54" - integrity sha512-pViUZEnaxd/Hbu880MEXF7XqV8RJ1ssDlkvzx+woQhcKW8px3BrVDvwBGn09zRiKZ+gOLipK7ft5x3no+3vb8A== - default-gateway@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" @@ -10661,9 +10815,9 @@ escodegen@^1.14.1, escodegen@^1.9.1: source-map "~0.6.1" eslint-config-prettier@^6.0.0: - version "6.10.0" - resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.10.0.tgz#7b15e303bf9c956875c948f6b21500e48ded6a7f" - integrity sha512-AtndijGte1rPILInUdHjvKEGbIV06NuvPrqlIEaEaWtbtvJh464mDeyGMdZEQMsGvC0ZVkiex1fSNcC4HAbRGg== + version "6.14.0" + resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.14.0.tgz#390e7863a8ae99970981933826476169285b3a27" + integrity sha512-DbVwh0qZhAC7CNDWcq8cBdK6FcVHiMTKmCypOPWeZkp9hJ8xYwTaWSa6bb6cjfi8KOeJy0e9a8Izxyx+O4+gCQ== dependencies: get-stdin "^6.0.0"