diff --git a/.changeset/dull-seals-march.md b/.changeset/dull-seals-march.md index 714c5c5cad..49e1f7f2e6 100644 --- a/.changeset/dull-seals-march.md +++ b/.changeset/dull-seals-march.md @@ -3,4 +3,4 @@ '@backstage/core': patch --- -Add `BackstageRoutes` component to replace the top-level `Routes` component from `react-router` within apps, removing the need for manually appending `/*` to paths or sorting routes. +Add `FlatRoutes` component to replace the top-level `Routes` component from `react-router` within apps, removing the need for manually appending `/*` to paths or sorting routes. diff --git a/.changeset/loud-days-breathe.md b/.changeset/loud-days-breathe.md new file mode 100644 index 0000000000..f8eb561f31 --- /dev/null +++ b/.changeset/loud-days-breathe.md @@ -0,0 +1,5 @@ +--- +'@backstage/dev-utils': patch +--- + +Add new `addPage` method for use with extensions, as well as an `EntityGridItem` to easily create different test cases for entity overview cards. diff --git a/.changeset/polite-glasses-occur.md b/.changeset/polite-glasses-occur.md new file mode 100644 index 0000000000..bec533caad --- /dev/null +++ b/.changeset/polite-glasses-occur.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Add new `EntityProvider` component, which can be used to provide an entity for the `useEntity` hook. diff --git a/.changeset/polite-turtles-prove.md b/.changeset/polite-turtles-prove.md new file mode 100644 index 0000000000..094617026c --- /dev/null +++ b/.changeset/polite-turtles-prove.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-api': patch +--- + +Deprecate `RouteRef` path parameter and member, and remove deprecated `routeRef.createSubRouteRef`. diff --git a/.changeset/purple-turtles-float.md b/.changeset/purple-turtles-float.md new file mode 100644 index 0000000000..8736a7315f --- /dev/null +++ b/.changeset/purple-turtles-float.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-api': patch +--- + +Delay auth loginPopup close to avoid race condition with callers of authFlowHelpers. diff --git a/.changeset/warm-pets-sin.md b/.changeset/warm-pets-sin.md new file mode 100644 index 0000000000..3a9197e7f2 --- /dev/null +++ b/.changeset/warm-pets-sin.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +fix bug in token expiration date diff --git a/packages/cli/src/lib/bundler/LinkedPackageResolvePlugin.test.ts b/packages/cli/src/lib/bundler/LinkedPackageResolvePlugin.test.ts index 53512567eb..b4580ea281 100644 --- a/packages/cli/src/lib/bundler/LinkedPackageResolvePlugin.test.ts +++ b/packages/cli/src/lib/bundler/LinkedPackageResolvePlugin.test.ts @@ -14,20 +14,27 @@ * limitations under the License. */ +import * as os from 'os'; +import * as path from 'path'; import { LinkedPackageResolvePlugin } from './LinkedPackageResolvePlugin'; describe('LinkedPackageResolvePlugin', () => { + const root = os.platform() === 'win32' ? 'C:\\root' : '/root'; + it('should re-write paths for external packages', () => { - const plugin = new LinkedPackageResolvePlugin('/root/repo/node_modules', [ - { - name: 'a', - location: '/root/external-a', - }, - { - name: '@s/b', - location: '/root/external-b', - }, - ]); + const plugin = new LinkedPackageResolvePlugin( + path.resolve(root, 'repo/node_modules'), + [ + { + name: 'a', + location: path.resolve(root, 'external-a'), + }, + { + name: '@s/b', + location: path.resolve(root, 'external-b'), + }, + ], + ); const tapAsync = jest.fn(); const doResolve = jest.fn(); @@ -50,10 +57,10 @@ describe('LinkedPackageResolvePlugin', () => { const callbackX = jest.fn(); tap( { - request: '/root/repo/package/x/src/module.ts', - path: '/root/repo/package/x/src', + request: path.resolve(root, 'repo/package/x/src/module.ts'), + path: path.resolve(root, 'repo/package/x/src'), context: { - issuer: '/root/repo/package/x/src/index.ts', + issuer: path.resolve(root, 'repo/package/x/src/index.ts'), }, }, 'some-context', @@ -81,10 +88,10 @@ describe('LinkedPackageResolvePlugin', () => { const callbackA = jest.fn(); tap( { - request: '/root/external-a/src/module.ts', - path: '/root/external-a/src', + request: path.resolve(root, 'external-a/src/module.ts'), + path: path.resolve(root, 'external-a/src'), context: { - issuer: '/root/external-a/src/index.ts', + issuer: path.resolve(root, 'external-a/src/index.ts'), }, }, 'some-context', @@ -95,13 +102,16 @@ describe('LinkedPackageResolvePlugin', () => { expect(doResolve).toHaveBeenCalledWith( resolver.hooks.resolve, { - request: '/root/external-a/src/module.ts', - path: '/root/repo/node_modules/a/src', + request: path.resolve(root, 'external-a/src/module.ts'), + path: path.resolve(root, 'repo/node_modules/a/src'), context: { - issuer: '/root/repo/node_modules/a/src/index.ts', + issuer: path.resolve(root, 'repo/node_modules/a/src/index.ts'), }, }, - 'resolve /root/external-a/src/module.ts in /root/repo/node_modules/a', + `resolve ${path.resolve( + root, + 'external-a/src/module.ts', + )} in ${path.resolve(root, 'repo/node_modules/a')}`, 'some-context', callbackA, ); @@ -110,8 +120,8 @@ describe('LinkedPackageResolvePlugin', () => { const callbackB = jest.fn(); tap( { - request: '/root/external-b/src/module.ts', - path: '/root/external-b/src', + request: path.resolve(root, 'external-b/src/module.ts'), + path: path.resolve(root, 'external-b/src'), context: { issuer: false, }, @@ -124,13 +134,16 @@ describe('LinkedPackageResolvePlugin', () => { expect(doResolve).toHaveBeenLastCalledWith( resolver.hooks.resolve, { - request: '/root/external-b/src/module.ts', - path: '/root/repo/node_modules/@s/b/src', + request: path.resolve(root, 'external-b/src/module.ts'), + path: path.resolve(root, 'repo/node_modules/@s/b/src'), context: { issuer: false, }, }, - 'resolve /root/external-b/src/module.ts in /root/repo/node_modules/@s/b', + `resolve ${path.resolve( + root, + 'external-b/src/module.ts', + )} in ${path.resolve(root, 'repo/node_modules/@s/b')}`, 'some-context', callbackB, ); diff --git a/packages/core-api/src/routing/BackstageRoutes.tsx b/packages/core-api/src/routing/FlatRoutes.tsx similarity index 94% rename from packages/core-api/src/routing/BackstageRoutes.tsx rename to packages/core-api/src/routing/FlatRoutes.tsx index 6c042af27d..a6783964a7 100644 --- a/packages/core-api/src/routing/BackstageRoutes.tsx +++ b/packages/core-api/src/routing/FlatRoutes.tsx @@ -66,13 +66,11 @@ function createRoutesFromChildren(children: ReactNode): RouteObject[] { }); } -type BackstageRoutesProps = { +type FlatRoutesProps = { children: ReactNode; }; -export const BackstageRoutes = ( - props: BackstageRoutesProps, -): JSX.Element | null => { +export const FlatRoutes = (props: FlatRoutesProps): JSX.Element | null => { const routes = createRoutesFromChildren(props.children); return useRoutes(routes); }; diff --git a/packages/core-api/src/routing/RouteRef.ts b/packages/core-api/src/routing/RouteRef.ts index f89634e0ec..4c2da34098 100644 --- a/packages/core-api/src/routing/RouteRef.ts +++ b/packages/core-api/src/routing/RouteRef.ts @@ -25,25 +25,15 @@ export class AbsoluteRouteRef { // TODO(Rugvip): Remove this, routes are looked up via the registry instead get path() { - return this.config.path; + return this.config.path ?? ''; } get title() { return this.config.title; } - /** - * This function should not be used, create a separate RouteRef instead - * @deprecated - */ - createSubRoute(): any { - throw new Error( - 'This method should not be called, create a separate RouteRef instead', - ); - } - toString() { - return `routeRef{path=${this.path}}`; + return `routeRef{title=${this.title}}`; } } @@ -54,13 +44,7 @@ export function createRouteRef< return new AbsoluteRouteRef(config); } -const create = Symbol('create-external-route-ref'); - export class ExternalRouteRef { - static [create]() { - return new ExternalRouteRef(); - } - private constructor() {} toString() { @@ -69,5 +53,5 @@ export class ExternalRouteRef { } export function createExternalRouteRef(): ExternalRouteRef { - return ExternalRouteRef[create](); + return new ((ExternalRouteRef as unknown) as { new (): ExternalRouteRef })(); } diff --git a/packages/core-api/src/routing/index.ts b/packages/core-api/src/routing/index.ts index 2bdf0aa5e8..9564b3b225 100644 --- a/packages/core-api/src/routing/index.ts +++ b/packages/core-api/src/routing/index.ts @@ -21,6 +21,6 @@ export type { ConcreteRoute, MutableRouteRef, } from './types'; -export { BackstageRoutes } from './BackstageRoutes'; +export { FlatRoutes } from './FlatRoutes'; export { createRouteRef } from './RouteRef'; export { useRouteRef } from './hooks'; diff --git a/packages/core-api/src/routing/types.ts b/packages/core-api/src/routing/types.ts index c6d79fd991..99a20c31e9 100644 --- a/packages/core-api/src/routing/types.ts +++ b/packages/core-api/src/routing/types.ts @@ -19,14 +19,10 @@ import { IconComponent } from '../icons'; // @ts-ignore, we're just embedding the Params type for usage in other places export type RouteRef = { // TODO(Rugvip): Remove path, look up via registry instead + /** @deprecated paths are no longer accessed directly from RouteRefs, use useRouteRef instead */ path: string; icon?: IconComponent; title: string; - /** - * This function should not be used, create a separate RouteRef instead - * @deprecated - */ - createSubRoute(): any; }; export type AnyRouteRef = RouteRef; @@ -51,7 +47,8 @@ export type MutableRouteRef = RouteRef<{}>; export type RouteRefConfig = { params?: Array; - path: string; + /** @deprecated Route refs no longer decide their own path */ + path?: string; icon?: IconComponent; title: string; }; diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index d52e14a728..1e0304a5a9 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -30,6 +30,8 @@ }, "dependencies": { "@backstage/core": "^0.4.0", + "@backstage/catalog-model": "^0.6.0", + "@backstage/plugin-catalog": "^0.2.8", "@backstage/test-utils": "^0.1.5", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", diff --git a/packages/dev-utils/src/components/EntityGridItem/EntityGridItem.tsx b/packages/dev-utils/src/components/EntityGridItem/EntityGridItem.tsx new file mode 100644 index 0000000000..077a8f85e8 --- /dev/null +++ b/packages/dev-utils/src/components/EntityGridItem/EntityGridItem.tsx @@ -0,0 +1,49 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { Grid, GridProps, makeStyles } from '@material-ui/core'; +import { Entity } from '@backstage/catalog-model'; +import { EntityProvider } from '@backstage/plugin-catalog'; +import { BackstageTheme } from '@backstage/theme'; + +const useStyles = makeStyles(theme => ({ + root: ({ entity }) => ({ + position: 'relative', + + '&::before': { + content: `"${entity.metadata.name}"`, + top: -theme.typography.fontSize + 4, + display: 'block', + position: 'absolute', + color: theme.palette.textSubtle, + }, + }), +})); + +export const EntityGridItem = ({ + entity, + classes, + ...rest +}: Omit & { entity: Entity }): JSX.Element => { + const itemClasses = useStyles({ entity }); + + return ( + + + + ); +}; diff --git a/packages/dev-utils/src/components/EntityGridItem/index.ts b/packages/dev-utils/src/components/EntityGridItem/index.ts new file mode 100644 index 0000000000..e0f07b13ba --- /dev/null +++ b/packages/dev-utils/src/components/EntityGridItem/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { EntityGridItem } from './EntityGridItem'; diff --git a/packages/dev-utils/src/components/index.ts b/packages/dev-utils/src/components/index.ts new file mode 100644 index 0000000000..34224f09fd --- /dev/null +++ b/packages/dev-utils/src/components/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './EntityGridItem'; diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index 86a8b2f7cb..f1a6d30b4c 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { hot } from 'react-hot-loader/root'; +import { hot } from 'react-hot-loader'; import React, { ComponentType, ReactNode } from 'react'; import ReactDOM from 'react-dom'; import BookmarkIcon from '@material-ui/icons/Bookmark'; @@ -29,9 +29,25 @@ import { AlertDisplay, OAuthRequestDialog, AnyApiFactory, + IconComponent, + FlatRoutes, + attachComponentData, } from '@backstage/core'; import SentimentDissatisfiedIcon from '@material-ui/icons/SentimentDissatisfied'; -import { Routes } from 'react-router'; +import { Outlet } from 'react-router'; + +const GatheringRoute: (props: { + path: string; + children: JSX.Element; +}) => JSX.Element = () => ; + +attachComponentData(GatheringRoute, 'core.gatherMountPoints', true); + +type RegisterPageOptions = { + element: JSX.Element; + title?: string; + icon?: IconComponent; +}; // TODO(rugvip): export proper plugin type from core that isn't the plugin class type BackstagePlugin = ReturnType; @@ -44,6 +60,8 @@ class DevAppBuilder { private readonly plugins = new Array(); private readonly apis = new Array(); private readonly rootChildren = new Array(); + private readonly routes = new Array(); + private readonly sidebarItems = new Array(); /** * Register one or more plugins to render in the dev app @@ -75,6 +93,21 @@ class DevAppBuilder { return this; } + addPage({ element, title, icon }: RegisterPageOptions): DevAppBuilder { + const path = `/page-${this.routes.length + 1}`; + this.sidebarItems.push( + , + ); + this.routes.push( + , + ); + return this; + } /** * Build a DevApp component using the resources registered so far */ @@ -100,7 +133,10 @@ class DevAppBuilder { {sidebar} - {deprecatedAppRoutes} + + {this.routes} + {deprecatedAppRoutes} + @@ -114,7 +150,12 @@ class DevAppBuilder { * Build and render directory to #root element, with react hot loading. */ render(): void { - const DevApp = hot(this.build()); + const hotModule = + require.cache['./dev/index.tsx'] ?? + require.cache['./dev/index.ts'] ?? + module; + + const DevApp = hot(hotModule)(this.build()); const paths = this.findPluginPaths(this.plugins); @@ -166,6 +207,7 @@ class DevAppBuilder { return ( + {this.sidebarItems} {sidebarItems} ); @@ -199,7 +241,7 @@ class DevAppBuilder { // this to provide their own plugin dev wrappers. /** - * Creates a dev app for rendering one or more plugins and exposing the touchpoints of the plugin. + * Creates a dev app for rendering one or more plugins and exposing the touch points of the plugin. */ export function createDevApp() { return new DevAppBuilder(); diff --git a/packages/dev-utils/src/index.ts b/packages/dev-utils/src/index.ts index c05e67ddbc..97add5dd86 100644 --- a/packages/dev-utils/src/index.ts +++ b/packages/dev-utils/src/index.ts @@ -13,4 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +export * from './components'; export * from './devApp'; diff --git a/packages/techdocs-common/src/stages/publish/helpers.test.ts b/packages/techdocs-common/src/stages/publish/helpers.test.ts index 3b01f5ac43..1fae66f3d2 100644 --- a/packages/techdocs-common/src/stages/publish/helpers.test.ts +++ b/packages/techdocs-common/src/stages/publish/helpers.test.ts @@ -14,6 +14,8 @@ * limitations under the License. */ import mockFs from 'mock-fs'; +import * as os from 'os'; +import * as path from 'path'; import { getFileTreeRecursively, getHeadersForFileExtension } from './helpers'; describe('getHeadersForFileExtension', () => { @@ -39,9 +41,11 @@ describe('getHeadersForFileExtension', () => { }); describe('getFileTreeRecursively', () => { + const root = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; + beforeEach(() => { mockFs({ - '/rootDir': { + [root]: { file1: '', subDirA: { file2: '', @@ -57,9 +61,9 @@ describe('getFileTreeRecursively', () => { }); it('returns complete file tree of a path', async () => { - const fileList = await getFileTreeRecursively('/rootDir'); + const fileList = await getFileTreeRecursively(root); expect(fileList.length).toBe(2); - expect(fileList).toContain('/rootDir/file1'); - expect(fileList).toContain('/rootDir/subDirA/file2'); + expect(fileList).toContain(path.resolve(root, 'file1')); + expect(fileList).toContain(path.resolve(root, 'subDirA/file2')); }); }); diff --git a/plugins/auth-backend/src/identity/TokenFactory.test.ts b/plugins/auth-backend/src/identity/TokenFactory.test.ts index 4303401ee6..8b719799b1 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.test.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.test.ts @@ -87,7 +87,7 @@ describe('TokenFactory', () => { iat: expect.any(Number), exp: expect.any(Number), }); - expect(payload.exp).toBe(payload.iat + keyDurationSeconds * 1000); + expect(payload.exp).toBe(payload.iat + keyDurationSeconds); }); it('should generate new signing keys when the current one expires', async () => { diff --git a/plugins/auth-backend/src/identity/TokenFactory.ts b/plugins/auth-backend/src/identity/TokenFactory.ts index c4c259ed22..36622332e0 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.ts @@ -69,7 +69,7 @@ export class TokenFactory implements TokenIssuer { const sub = params.claims.sub; const aud = 'backstage'; const iat = Math.floor(Date.now() / MS_IN_S); - const exp = iat + this.keyDurationSeconds * MS_IN_S; + const exp = iat + this.keyDurationSeconds; this.logger.info(`Issuing token for ${sub}`); diff --git a/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts b/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts index 22ffc7af88..e70aa0bfee 100644 --- a/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts +++ b/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts @@ -56,7 +56,9 @@ export const postMessageResponse = ( var originInfo = {'type': 'config_info', 'targetOrigin': origin}; (window.opener || window.parent).postMessage(originInfo, '*'); (window.opener || window.parent).postMessage(JSON.parse(authResponse), origin); - window.close(); + setTimeout(() => { + window.close(); + }, 100); // same as the interval of the core-api lib/loginPopup.ts (to address race conditions) `; const hash = crypto.createHash('sha256').update(script).digest('base64'); diff --git a/plugins/catalog/src/components/CatalogEntityPage/CatalogEntityPage.tsx b/plugins/catalog/src/components/CatalogEntityPage/CatalogEntityPage.tsx index e0a314a391..bf5fb1d9d6 100644 --- a/plugins/catalog/src/components/CatalogEntityPage/CatalogEntityPage.tsx +++ b/plugins/catalog/src/components/CatalogEntityPage/CatalogEntityPage.tsx @@ -16,12 +16,12 @@ import React from 'react'; import { Outlet } from 'react-router'; -import { EntityProvider } from '../EntityProvider'; +import { EntityLoaderProvider } from '../EntityLoaderProvider'; export const CatalogEntityPage = () => { return ( - + - + ); }; diff --git a/plugins/catalog/src/components/EntityLoaderProvider/EntityLoaderProvider.tsx b/plugins/catalog/src/components/EntityLoaderProvider/EntityLoaderProvider.tsx new file mode 100644 index 0000000000..98b8295047 --- /dev/null +++ b/plugins/catalog/src/components/EntityLoaderProvider/EntityLoaderProvider.tsx @@ -0,0 +1,27 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { ReactNode } from 'react'; +import { useEntityFromUrl, EntityContext } from '../../hooks/useEntity'; + +export const EntityLoaderProvider = ({ children }: { children: ReactNode }) => { + const { entity, loading, error } = useEntityFromUrl(); + + return ( + + {children} + + ); +}; diff --git a/plugins/catalog/src/components/EntityLoaderProvider/index.ts b/plugins/catalog/src/components/EntityLoaderProvider/index.ts new file mode 100644 index 0000000000..925c927ec9 --- /dev/null +++ b/plugins/catalog/src/components/EntityLoaderProvider/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { EntityLoaderProvider } from './EntityLoaderProvider'; diff --git a/plugins/catalog/src/components/EntityProvider/EntityProvider.tsx b/plugins/catalog/src/components/EntityProvider/EntityProvider.tsx index e328b64d44..9135b54638 100644 --- a/plugins/catalog/src/components/EntityProvider/EntityProvider.tsx +++ b/plugins/catalog/src/components/EntityProvider/EntityProvider.tsx @@ -13,15 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Entity } from '@backstage/catalog-model'; import React, { ReactNode } from 'react'; -import { useEntityFromUrl, EntityContext } from '../../hooks/useEntity'; +import { EntityContext } from '../../hooks/useEntity'; -export const EntityProvider = ({ children }: { children: ReactNode }) => { - const { entity, loading, error } = useEntityFromUrl(); - - return ( - - {children} - - ); +type EntityProviderProps = { + entity: Entity; + children: ReactNode; }; + +export const EntityProvider = ({ entity, children }: EntityProviderProps) => ( + + {children} + +); diff --git a/plugins/catalog/src/components/Router.tsx b/plugins/catalog/src/components/Router.tsx index 298aa28d52..6a4593fb80 100644 --- a/plugins/catalog/src/components/Router.tsx +++ b/plugins/catalog/src/components/Router.tsx @@ -23,7 +23,7 @@ import { entityRoute, rootRoute } from '../routes'; import { CatalogPage } from './CatalogPage'; import { EntityNotFound } from './EntityNotFound'; import { EntityPageLayout } from './EntityPageLayout'; -import { EntityProvider } from './EntityProvider'; +import { EntityLoaderProvider } from './EntityLoaderProvider'; const DefaultEntityPage = () => ( @@ -79,9 +79,9 @@ export const Router = ({ + - + } /> { }; /** - * Always going to return an entity, or throw an error if not a descendant of a EntityProvider. + * Grab Entity from the context and its current loading state. */ export const useEntity = () => { const { entity, loading, error } = useContext<{ diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index d8686a18b0..2152933daf 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -18,6 +18,7 @@ export * from '@backstage/catalog-client'; export { AboutCard } from './components/AboutCard'; export { EntityPageLayout } from './components/EntityPageLayout'; export { EntityLayout } from './components/EntityLayout'; +export { EntityProvider } from './components/EntityProvider'; export * from './components/EntitySwitch'; export { Router } from './components/Router'; export { useEntityCompoundName } from './components/useEntityCompoundName'; diff --git a/plugins/cost-insights/src/utils/duration.ts b/plugins/cost-insights/src/utils/duration.ts index 7a330b6f91..6eebead1d7 100644 --- a/plugins/cost-insights/src/utils/duration.ts +++ b/plugins/cost-insights/src/utils/duration.ts @@ -24,7 +24,7 @@ export const DEFAULT_DURATION = Duration.P30D; * Derive the start date of a given period, assuming two repeating intervals. * * @param duration see comment on Duration enum - * @param endDate from CostInsightsApi.getLastCompleteBillingDate + 1 day + * @param exclusiveEndDate from CostInsightsApi.getLastCompleteBillingDate + 1 day */ export function inclusiveStartDateOf( duration: Duration, diff --git a/plugins/cost-insights/src/utils/mockData.ts b/plugins/cost-insights/src/utils/mockData.ts index ce9e65f1c3..19f9676eea 100644 --- a/plugins/cost-insights/src/utils/mockData.ts +++ b/plugins/cost-insights/src/utils/mockData.ts @@ -234,14 +234,24 @@ export function aggregationFor( 'day', ); + function nextDelta(): number { + const varianceFromBaseline = 0.15; + // Let's give positive vibes in trendlines - higher change for positive delta with >0.5 value + const positiveTrendChance = 0.55; + const normalization = positiveTrendChance - 1; + return baseline * (Math.random() + normalization) * varianceFromBaseline; + } + return [...Array(days).keys()].reduce( (values: DateAggregation[], i: number): DateAggregation[] => { const last = values.length ? values[values.length - 1].amount : baseline; + const date = dayjs(inclusiveStartDateOf(duration, endDate)) + .add(i, 'day') + .format(DEFAULT_DATE_FORMAT); + const amount = Math.max(0, last + nextDelta()); values.push({ - date: dayjs(inclusiveStartDateOf(duration, endDate)) - .add(i, 'day') - .format(DEFAULT_DATE_FORMAT), - amount: Math.max(0, last + (baseline / 20) * (Math.random() * 2 - 1)), + date: date, + amount: amount, }); return values; },