Merge branch 'master' of github.com:spotify/backstage into migrate-to-msw

* 'master' of github.com:spotify/backstage:
  Read git auth token from backend config (#2992)
  chore(catalog-model): add the petstore into the default entities
  chore(deps-dev): bump @storybook/addon-actions from 6.0.21 to 6.0.26
  chore(deps): bump eslint-config-prettier from 6.10.0 to 6.14.0
  Add API docs link to About card
  adds a sample organization set of users and groups (#2963)
  Make sure hasCostsWithinTimeframe is a boolean
  fix(catalog-backend): fix codeowners processor to handle users
  feat(catalog): add simple client side paging
  default to last 30 days
  Removed default-branch library since it's no longer used (#3019)
  useRealTimers in  onCssReady test case
  move type dependecy
  remove unused import
  changeset
  simpler query params
This commit is contained in:
blam
2020-10-22 11:02:32 +02:00
35 changed files with 851 additions and 208 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog': minor
---
Add client side paging for catalog table
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Fix CodeOwnersProcessor to handle non team users
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-cost-insights': minor
---
Remove product filters from query parameters
+3
View File
@@ -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:
@@ -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
@@ -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]
@@ -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]
@@ -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]
@@ -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
@@ -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]
@@ -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]
@@ -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]
@@ -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]
@@ -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
@@ -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
@@ -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');
});
@@ -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];
}
@@ -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}`}
/>
<IconLinkVertical
disabled={!entity.spec?.implementsApis}
label="View API"
icon={<BrightnessAutoIcon />}
href={`./${
entity.metadata.namespace || ENTITY_DEFAULT_NAMESPACE
}:${entity.metadata.name}/api`}
/>
</nav>
}
/>
@@ -155,7 +155,8 @@ export const CatalogTable = ({
isLoading={loading}
columns={columns}
options={{
paging: false,
paging: true,
pageSize: 10,
actionsColumnIndex: -1,
loadingType: 'linear',
showEmptyDataSourceMessage: !loading,
+3 -1
View File
@@ -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",
@@ -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`
-1
View File
@@ -20,5 +20,4 @@ export * from './useFilters';
export * from './useCurrency';
export * from './useGroups';
export * from './useLoading';
export * from './useQueryParams';
export * from './useScroll';
+60 -86
View File
@@ -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<PageFilters>,
) => {
// 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<SetStateAction<PageFilters>>;
setProductFilters: Dispatch<SetStateAction<ProductFilters>>;
setPageFilters: Dispatch<SetStateAction<Maybe<PageFilters>>>;
setProductFilters: Dispatch<SetStateAction<Maybe<ProductFilters>>>;
};
export type MapFiltersToProps<T> = (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<Maybe<Error>>(null);
const [pageFilters, setPageFilters] = useState<Maybe<PageFilters>>(null);
const [productFilters, setProductFilters] = useState<Maybe<ProductFilters>>(
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 <Alert severity="error">{error.message}</Alert>;
}
// Wait for filters to load
if (!pageFilters || !productFilters) {
return null;
}
return (
<FilterContext.Provider
value={{
pageFilters: pageFilters,
productFilters: productFilters,
setPageFilters: setPageFilters,
setProductFilters: setProductFilters,
}}
value={{ pageFilters, productFilters, setPageFilters, setProductFilters }}
>
{children}
</FilterContext.Provider>
@@ -132,17 +116,7 @@ export const FilterProvider = ({ children }: FilterProviderProps) => {
export function useFilters<T>(mapFiltersToProps: MapFiltersToProps<T>): 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 {
+30 -21
View File
@@ -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<GroupsContextProps>({
groups: [],
});
export const GroupsProvider = ({ children }: GroupsProviderProps) => {
export const GroupsProvider = ({ children }: PropsWithChildren<{}>) => {
const userId = useApi(identityApiRef).getUserId();
const client = useApi(costInsightsApiRef);
const [error, setError] = useState<Maybe<Error>>(null);
const { dispatchLoadingGroups } = useLoading(mapLoadingToProps);
const [groups, setGroups] = useState<Group[]>([]);
const [groups, setGroups] = useState<Maybe<Group[]>>(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 <Alert severity="error">{error.message}</Alert>;
}
if (!groups) return null;
return (
<GroupsContext.Provider value={{ groups: groups }}>
{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 {
@@ -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<PageFilters>;
productFilters?: ProductFilters;
};
export function useQueryParams(): FilterParams {
const location: Location = useLocation();
const { products: productFilters, ...pageFilters } = parse(location.search);
return {
productFilters: productFilters,
pageFilters: pageFilters,
};
}
@@ -27,8 +27,6 @@ export interface PageFilters {
export type ProductFilters = Array<ProductPeriod>;
export type QueryParams = PageFilters & { products: ProductFilters };
export interface ProductPeriod {
duration: Duration;
productType: string;
@@ -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();
});
});
+38 -3
View File
@@ -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<QueryParams>) =>
const schema = yup
.object()
.shape({
group: yup.string(),
project: yup.string().nullable(),
})
.required();
export const stringify = (queryParams: Partial<PageFilters>) =>
qs.stringify(queryParams, { strictNullHandling: true });
export const parse = (queryString: string): Partial<QueryParams> =>
export const parse = (queryString: string): Partial<PageFilters> =>
qs.parse(queryString, { ignoreQueryPrefix: true, strictNullHandling: true });
export const validate = (queryString: string): Promise<PageFilters> => {
return schema.validate(parse(queryString), {
stripUnknown: true,
strict: true,
}) as Promise<PageFilters>;
};
export const getInitialPageState = (
groups: Group[],
queryParams: Partial<PageFilters> = {},
) => {
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,
}));
+3 -2
View File
@@ -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<SetStateAction<PageFilters>>;
setProductFilters: Dispatch<SetStateAction<ProductFilters>>;
setPageFilters: Dispatch<SetStateAction<Maybe<PageFilters>>>;
setProductFilters: Dispatch<SetStateAction<Maybe<ProductFilters>>>;
children: ReactNode;
};
-1
View File
@@ -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",
+20 -27
View File
@@ -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<string> {
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<string> {
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<string> {
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<string> => {
// 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) {
+116
View File
@@ -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<string | undefined> => {
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;
}
};
+4 -6
View File
@@ -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}`;
}
@@ -25,14 +25,20 @@ import { onCssReady } from '../transformers';
const docStorageUrl: string =
'https://techdocs-mock-sites.storage.googleapis.com';
jest.useFakeTimers();
const fixture = `
<link rel="stylesheet" href="${docStorageUrl}/test.css" />
<link rel="stylesheet" href="http://example.com/test.css" />
`;
describe('onCssReady', () => {
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
});
beforeEach(() => {
mockStylesheetEventListener(100);
});
+172 -18
View File
@@ -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"