diff --git a/.changeset/beige-cheetahs-jog.md b/.changeset/beige-cheetahs-jog.md new file mode 100644 index 0000000000..87ef3ef44c --- /dev/null +++ b/.changeset/beige-cheetahs-jog.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Add context for versions:bump on what version it was bumped to. Updated tests for the same. diff --git a/.changeset/breezy-carrots-hunt.md b/.changeset/breezy-carrots-hunt.md new file mode 100644 index 0000000000..0075009a2d --- /dev/null +++ b/.changeset/breezy-carrots-hunt.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-explore': patch +--- + +Add "Organization" tab with a diagram diff --git a/.changeset/calm-insects-melt.md b/.changeset/calm-insects-melt.md new file mode 100644 index 0000000000..4b17764c33 --- /dev/null +++ b/.changeset/calm-insects-melt.md @@ -0,0 +1,7 @@ +--- +'@backstage/cli': patch +'@backstage/create-app': patch +'@backstage/plugin-catalog-graphql': patch +--- + +chore: bump `ts-node` versions to 9.1.1 diff --git a/.changeset/cyan-beans-marry.md b/.changeset/cyan-beans-marry.md new file mode 100644 index 0000000000..99d7ad47ce --- /dev/null +++ b/.changeset/cyan-beans-marry.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': patch +--- + +Renamed parameters to input in template schema diff --git a/.changeset/grumpy-goats-refuse.md b/.changeset/grumpy-goats-refuse.md new file mode 100644 index 0000000000..8dc2e390fc --- /dev/null +++ b/.changeset/grumpy-goats-refuse.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Show error on task page if task does not exist. diff --git a/.changeset/ninety-pots-guess.md b/.changeset/ninety-pots-guess.md new file mode 100644 index 0000000000..4b9a770c29 --- /dev/null +++ b/.changeset/ninety-pots-guess.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +update plugins created to use react-use 17.2.4 diff --git a/.changeset/short-bobcats-mate.md b/.changeset/short-bobcats-mate.md new file mode 100644 index 0000000000..d63e7058a9 --- /dev/null +++ b/.changeset/short-bobcats-mate.md @@ -0,0 +1,7 @@ +--- +'@backstage/core': patch +'@backstage/plugin-api-docs': patch +--- + +Fix state persisted in the URL make search input in the table toolbar lose their +focus. diff --git a/.changeset/smart-sheep-itch.md b/.changeset/smart-sheep-itch.md new file mode 100644 index 0000000000..12a0ff5ad0 --- /dev/null +++ b/.changeset/smart-sheep-itch.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': patch +--- + +Improve error messages when schema validation fails diff --git a/.changeset/sour-kids-compete.md b/.changeset/sour-kids-compete.md new file mode 100644 index 0000000000..ce94305c48 --- /dev/null +++ b/.changeset/sour-kids-compete.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Move `jest-when` to the dev dependencies diff --git a/.changeset/ten-paws-ring.md b/.changeset/ten-paws-ring.md new file mode 100644 index 0000000000..8ed2158f6c --- /dev/null +++ b/.changeset/ten-paws-ring.md @@ -0,0 +1,44 @@ +--- +'@backstage/core': patch +'@backstage/core-api': patch +'@backstage/integration-react': patch +'@backstage/plugin-api-docs': patch +'@backstage/plugin-badges': patch +'@backstage/plugin-bitrise': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-circleci': patch +'@backstage/plugin-cloudbuild': patch +'@backstage/plugin-code-coverage': patch +'@backstage/plugin-config-schema': patch +'@backstage/plugin-cost-insights': patch +'@backstage/plugin-explore': patch +'@backstage/plugin-fossa': patch +'@backstage/plugin-gcp-projects': patch +'@backstage/plugin-github-actions': patch +'@backstage/plugin-github-deployments': patch +'@backstage/plugin-gitops-profiles': patch +'@backstage/plugin-graphiql': patch +'@backstage/plugin-jenkins': patch +'@backstage/plugin-kafka': patch +'@backstage/plugin-kubernetes': patch +'@backstage/plugin-lighthouse': patch +'@backstage/plugin-newrelic': patch +'@backstage/plugin-org': patch +'@backstage/plugin-pagerduty': patch +'@backstage/plugin-register-component': patch +'@backstage/plugin-rollbar': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-search': patch +'@backstage/plugin-sentry': patch +'@backstage/plugin-sonarqube': patch +'@backstage/plugin-splunk-on-call': patch +'@backstage/plugin-tech-radar': patch +'@backstage/plugin-techdocs': patch +'@backstage/plugin-todo': patch +'@backstage/plugin-user-settings': patch +'@backstage/plugin-welcome': patch +--- + +chore: bump `react-use` dependency in all packages diff --git a/.changeset/wild-moles-care.md b/.changeset/wild-moles-care.md new file mode 100644 index 0000000000..35b886c2c1 --- /dev/null +++ b/.changeset/wild-moles-care.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Add possibility to configure TLS verification for `gke` type clusters diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 5dbad9cf4a..45ef28ea2f 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -33,6 +33,7 @@ kubernetes: - type: 'gke' projectId: 'gke-clusters' region: 'europe-west1' + skipTLSVerify: true ``` ### `serviceLocatorMethod` @@ -129,6 +130,11 @@ The Google Cloud project to look for Kubernetes clusters in. The Google Cloud region to look for Kubernetes clusters in. Defaults to all regions. +##### `skipTLSVerify` + +This determines whether or not the Kubernetes client verifies the TLS +certificate presented by the API server. Defaults to `false`. + ### `customResources` (optional) Configures which [custom resources][3] to look for when returning an entity's diff --git a/microsite/package.json b/microsite/package.json index 07899fa703..595debcc65 100644 --- a/microsite/package.json +++ b/microsite/package.json @@ -18,7 +18,7 @@ "@spotify/prettier-config": "^10.0.0", "docusaurus": "^2.0.0-alpha.70", "js-yaml": "^4.1.0", - "prettier": "^2.2.1" + "prettier": "^2.3.0" }, "prettier": "@spotify/prettier-config" } diff --git a/microsite/yarn.lock b/microsite/yarn.lock index bed7897abc..4033968a2e 100644 --- a/microsite/yarn.lock +++ b/microsite/yarn.lock @@ -5197,10 +5197,10 @@ prepend-http@^2.0.0: resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= -prettier@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" - integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== +prettier@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.0.tgz#b6a5bf1284026ae640f17f7ff5658a7567fc0d18" + integrity sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w== prismjs@^1.22.0: version "1.23.0" diff --git a/packages/app/package.json b/packages/app/package.json index 8ddf249366..cbc8c5f1a3 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -32,6 +32,7 @@ "@backstage/plugin-scaffolder": "^0.9.3", "@backstage/plugin-search": "^0.3.5", "@backstage/plugin-sentry": "^0.3.9", + "@backstage/plugin-shortcuts": "^0.1.1", "@backstage/plugin-tech-radar": "^0.3.10", "@backstage/plugin-techdocs": "^0.9.1", "@backstage/plugin-todo": "^0.1.0", @@ -51,7 +52,7 @@ "react-hot-loader": "^4.12.21", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3", + "react-use": "^17.2.4", "zen-observable": "^0.8.15" }, "devDependencies": { diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index 454c0d2e84..9c62ec0b4d 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -39,6 +39,7 @@ import { NavLink } from 'react-router-dom'; import { GraphiQLIcon } from '@backstage/plugin-graphiql'; import { Settings as SidebarSettings } from '@backstage/plugin-user-settings'; import { SidebarSearch } from '@backstage/plugin-search'; +import { Shortcuts } from '@backstage/plugin-shortcuts'; const useSidebarLogoStyles = makeStyles({ root: { @@ -91,6 +92,8 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( + + diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index 7dbea5b995..e02e30e5d9 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -17,3 +17,4 @@ // TODO(Rugvip): This plugin is currently not part of the app element tree, // ideally we have an API for the context menu that permits that. export { badgesPlugin } from '@backstage/plugin-badges'; +export { shortcutsPlugin } from '@backstage/plugin-shortcuts'; diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index c37c86b01f..2b56879f48 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -514,7 +514,7 @@ export interface TemplateEntityV1beta2 extends Entity { id?: string; name?: string; action: string; - parameters?: JsonObject; + input?: JsonObject; }>; output?: { [name: string]: string; diff --git a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.test.ts index a4d7d904cd..249243bed3 100644 --- a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.test.ts @@ -168,4 +168,16 @@ components: (entity as any).spec.system = ''; await expect(validator.check(entity)).rejects.toThrow(/system/); }); + + it('rejects additional properties', async () => { + (entity as any).annotations = 'Test'; + await expect(validator.check(entity)).rejects.toThrow( + /additional properties/, + ); + }); + + it('rejects with useful error message', async () => { + (entity as any).annotations = 'Test'; + await expect(validator.check(entity)).rejects.toThrow(/annotations/); + }); }); diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.test.ts b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.test.ts index 60f3de9e59..958a486687 100644 --- a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.test.ts +++ b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.test.ts @@ -51,7 +51,7 @@ describe('templateEntityV1beta2Validator', () => { id: 'fetch', name: 'Fetch', action: 'fetch:plan', - parameters: { + input: { url: './template', }, }, diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts index c78e7f85d5..1f4e3bcd90 100644 --- a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts +++ b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts @@ -38,7 +38,7 @@ export interface TemplateEntityV1beta2 extends Entity { id?: string; name?: string; action: string; - parameters?: JsonObject; + input?: JsonObject; }>; output?: { [name: string]: string }; }; diff --git a/packages/catalog-model/src/kinds/util.ts b/packages/catalog-model/src/kinds/util.ts index 4002f2806c..1e9e7b7521 100644 --- a/packages/catalog-model/src/kinds/util.ts +++ b/packages/catalog-model/src/kinds/util.ts @@ -66,7 +66,13 @@ export function ajvCompiledJsonSchemaValidator( } throw new TypeError( - `Malformed ${kind}, ${error.dataPath || ''} ${error.message}`, + `Malformed ${kind}, ${error.dataPath || ''} ${error.message}${ + error.params + ? ` - ${Object.entries(error.params) + .map(([key, val]) => `${key}: ${val}`) + .join(', ')}` + : '' + }`, ); }, }; diff --git a/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json b/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json index 1e0b7d0ded..b88b344626 100644 --- a/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json +++ b/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json @@ -125,7 +125,7 @@ "type": "string", "description": "The name of the action to execute." }, - "parameters": { + "input": { "type": "object", "description": "A templated object describing the inputs to the action." } diff --git a/packages/cli/package.json b/packages/cli/package.json index 4bed089b98..cd0385196d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -143,7 +143,7 @@ "del": "^5.1.0", "mock-fs": "^4.13.0", "nodemon": "^2.0.2", - "ts-node": "^8.6.2" + "ts-node": "^9.1.1" }, "files": [ "asset-types", diff --git a/packages/cli/src/commands/versions/bump.test.ts b/packages/cli/src/commands/versions/bump.test.ts index 31803d4827..e5128fc995 100644 --- a/packages/cli/src/commands/versions/bump.test.ts +++ b/packages/cli/src/commands/versions/bump.test.ts @@ -136,7 +136,7 @@ describe('bump', () => { 'bumping @backstage/theme in b to ^2.0.0', 'Running yarn install to install new versions', '⚠️ The following packages may have breaking changes:', - ' @backstage/theme', + ' @backstage/theme : 1.0.0 ~> 2.0.0', ' https://github.com/backstage/backstage/blob/master/packages/theme/CHANGELOG.md', 'Version bump complete!', ]); diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts index ceb2f7f988..d046283aab 100644 --- a/packages/cli/src/commands/versions/bump.ts +++ b/packages/cli/src/commands/versions/bump.ts @@ -195,8 +195,14 @@ export default async () => { ); console.log(); - for (const name of Array.from(breakingUpdates.keys()).sort()) { - console.log(` ${chalk.yellow(name)}`); + for (const [name, { from, to }] of Array.from( + breakingUpdates.entries(), + ).sort()) { + console.log( + ` ${chalk.yellow(name)} : ${chalk.yellow(from)} ~> ${chalk.yellow( + to, + )}`, + ); let path; if (name.startsWith('@backstage/plugin-')) { diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index 49eda9e0eb..1b4f8ea6ab 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -31,7 +31,7 @@ "@material-ui/lab": "4.0.0-alpha.45", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^{{version '@backstage/cli'}}", diff --git a/packages/core-api/package.json b/packages/core-api/package.json index a55df67343..13d7efe77b 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -38,7 +38,7 @@ "prop-types": "^15.7.2", "react": "^16.12.0", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3", + "react-use": "^17.2.4", "zen-observable": "^0.8.15" }, "devDependencies": { diff --git a/packages/core/package.json b/packages/core/package.json index 4da981d064..f8425c3289 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -65,7 +65,7 @@ "react-sparklines": "^1.7.0", "react-syntax-highlighter": "^15.4.3", "react-text-truncate": "^0.16.0", - "react-use": "^15.3.3", + "react-use": "^17.2.4", "remark-gfm": "^1.0.0", "zen-observable": "^0.8.15" }, diff --git a/packages/core/src/components/Table/Table.tsx b/packages/core/src/components/Table/Table.tsx index 2251b8304e..1dd09e7ab8 100644 --- a/packages/core/src/components/Table/Table.tsx +++ b/packages/core/src/components/Table/Table.tsx @@ -20,6 +20,7 @@ import { makeStyles, Typography, useTheme, + withStyles, } from '@material-ui/core'; // Material-table is not using the standard icons available in in material-ui. https://github.com/mbrn/material-table/issues/51 import AddBox from '@material-ui/icons/AddBox'; @@ -49,6 +50,7 @@ import MTable, { } from 'material-table'; import React, { forwardRef, + MutableRefObject, ReactNode, useCallback, useEffect, @@ -100,19 +102,20 @@ function extractValueByField(data: any, field: string): any | undefined { return value; } -const useHeaderStyles = makeStyles(theme => ({ +const StyledMTableHeader = withStyles(theme => ({ header: { padding: theme.spacing(1, 2, 1, 2.5), borderTop: `1px solid ${theme.palette.grey.A100}`, borderBottom: `1px solid ${theme.palette.grey.A100}`, - color: theme.palette.textSubtle, + // withStyles hasn't a generic overload for theme + color: (theme as BackstageTheme).palette.textSubtle, fontWeight: theme.typography.fontWeightBold, position: 'static', wordBreak: 'normal', }, -})); +}))(MTableHeader); -const useToolbarStyles = makeStyles(theme => ({ +const StyledMTableToolbar = withStyles(theme => ({ root: { padding: theme.spacing(3, 0, 2.5, 2.5), }, @@ -124,7 +127,7 @@ const useToolbarStyles = makeStyles(theme => ({ searchField: { paddingRight: theme.spacing(2), }, -})); +}))(MTableToolbar); const useFilterStyles = makeStyles(() => ({ root: { @@ -208,6 +211,59 @@ export interface TableProps onStateChange?: (state: TableState) => any; } +export function TableToolbar(toolbarProps: { + toolbarRef: MutableRefObject; + setSearch: (value: string) => void; + onSearchChanged: (value: string) => void; + toggleFilters: () => void; + hasFilters: boolean; + selectedFiltersLength: number; +}) { + const { + toolbarRef, + setSearch, + hasFilters, + selectedFiltersLength, + toggleFilters, + } = toolbarProps; + const filtersClasses = useFilterStyles(); + const onSearchChanged = useCallback( + (searchText: string) => { + toolbarProps.onSearchChanged(searchText); + setSearch(searchText); + }, + [toolbarProps, setSearch], + ); + + if (hasFilters) { + return ( +
+
+ + + + + Filters ({selectedFiltersLength}) + +
+ +
+ ); + } + + return ( + + ); +} + export function Table({ columns, options, @@ -219,10 +275,7 @@ export function Table({ onStateChange, ...props }: TableProps) { - const headerClasses = useHeaderStyles(); - const toolbarClasses = useToolbarStyles(); const tableClasses = useTableStyles(); - const filtersClasses = useFilterStyles(); const { data, ...propsWithoutData } = props; @@ -230,9 +283,12 @@ export function Table({ const calculatedInitialState = { ...defaultInitialState, ...initialState }; - const [filtersOpen, toggleFilters] = useState( + const [filtersOpen, setFiltersOpen] = useState( calculatedInitialState.filtersOpen, ); + const toggleFilters = useCallback(() => setFiltersOpen(v => !v), [ + setFiltersOpen, + ]); const [selectedFiltersLength, setSelectedFiltersLength] = useState(0); const [tableData, setTableData] = useState(data as any[]); const [selectedFilters, setSelectedFilters] = useState( @@ -379,64 +435,32 @@ export function Table({ })); }; + const hasFilters = !!filters?.length; const Toolbar = useCallback( toolbarProps => { - const onSearchChanged = (searchText: string) => { - toolbarProps.onSearchChanged(searchText); - setSearch(searchText); - }; - - if (filters?.length) { - return ( -
-
- toggleFilters(el => !el)} - aria-label="filter list" - > - - - - Filters ({selectedFiltersLength}) - -
- -
- ); - } - return ( - ); }, - [ - filters?.length, - selectedFiltersLength, - toggleFilters, - toolbarClasses, - filtersClasses, - setSearch, - toolbarRef, - ], + [toggleFilters, hasFilters, selectedFiltersLength, setSearch, toolbarRef], ); + const hasNoRows = typeof data !== 'function' && data.length === 0; + const columnCount = columns.length; const Body = useCallback( bodyProps => { - if (emptyContent && typeof data !== 'function' && data.length === 0) { + if (emptyContent && hasNoRows) { return ( - {emptyContent} + {emptyContent} ); @@ -444,7 +468,7 @@ export function Table({ return ; }, - [data, emptyContent, columns], + [hasNoRows, emptyContent, columnCount], ); return ( @@ -458,9 +482,7 @@ export function Table({ )} components={{ - Header: headerProps => ( - - ), + Header: StyledMTableHeader, Toolbar, Body, }} diff --git a/packages/core/src/hooks/useQueryParamState.ts b/packages/core/src/hooks/useQueryParamState.ts index da303234f5..173993a466 100644 --- a/packages/core/src/hooks/useQueryParamState.ts +++ b/packages/core/src/hooks/useQueryParamState.ts @@ -17,7 +17,7 @@ import { isEqual } from 'lodash'; import qs from 'qs'; import { useEffect, useState } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useSearchParams } from 'react-router-dom'; import { useDebounce } from 'react-use'; function stringify(queryParams: any): string { @@ -58,36 +58,37 @@ type SetQueryParams = (params: T) => void; export function useQueryParamState( stateName: string, - debounceTime: number = 100, + /** @deprecated Don't configure a custom debouceTime */ + debounceTime: number = 250, ): [T | undefined, SetQueryParams] { - const navigate = useNavigate(); - const location = useLocation(); + const [searchParams, setSearchParams] = useSearchParams(); + const searchParamsString = searchParams.toString(); const [queryParamState, setQueryParamState] = useState( - extractState(location.search, stateName), + extractState(searchParamsString, stateName), ); useEffect(() => { - const newState = extractState(location.search, stateName); + const newState = extractState(searchParamsString, stateName); setQueryParamState(oldState => isEqual(newState, oldState) ? oldState : newState, ); - }, [location, stateName]); + }, [searchParamsString, setQueryParamState, stateName]); useDebounce( () => { const queryString = joinQueryString( - location.search, + searchParamsString, stateName, queryParamState, ); - if (location.search !== queryString) { - navigate({ ...location, search: `?${queryString}` }, { replace: true }); + if (searchParamsString !== queryString) { + setSearchParams(queryString, { replace: true }); } }, debounceTime, - [queryParamState], + [setSearchParams, queryParamState, searchParamsString, stateName], ); return [queryParamState, setQueryParamState]; diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 968c798625..8c3dc14245 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -933,26 +933,25 @@ Update imports and remove the usage of the deprecated `app.getRoutes()`. - ```diff - -import { Router as DocsRouter } from '@backstage/plugin-techdocs'; - +import { TechdocsPage } from '@backstage/plugin-techdocs'; - import { CatalogImportPage } from '@backstage/plugin-catalog-import'; - -import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar'; - -import { SearchPage as SearchRouter } from '@backstage/plugin-search'; - -import { Router as SettingsRouter } from '@backstage/plugin-user-settings'; - +import { TechRadarPage } from '@backstage/plugin-tech-radar'; - +import { SearchPage } from '@backstage/plugin-search'; - +import { UserSettingsPage } from '@backstage/plugin-user-settings'; - +import { ApiExplorerPage } from '@backstage/plugin-api-docs'; - import { EntityPage } from './components/catalog/EntityPage'; - import { scaffolderPlugin, ScaffolderPage } from '@backstage/plugin-scaffolder'; - ``` +```diff +- import { Router as DocsRouter } from '@backstage/plugin-techdocs'; ++ import { TechdocsPage } from '@backstage/plugin-techdocs'; + import { CatalogImportPage } from '@backstage/plugin-catalog-import'; +- import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar'; +- import { SearchPage as SearchRouter } from '@backstage/plugin-search'; +- import { Router as SettingsRouter } from '@backstage/plugin-user-settings'; ++ import { TechRadarPage } from '@backstage/plugin-tech-radar'; ++ import { SearchPage } from '@backstage/plugin-search'; ++ import { UserSettingsPage } from '@backstage/plugin-user-settings'; ++ import { ApiExplorerPage } from '@backstage/plugin-api-docs'; + import { EntityPage } from './components/catalog/EntityPage'; + import { scaffolderPlugin, ScaffolderPage } from '@backstage/plugin-scaffolder'; -const AppProvider = app.getProvider(); -const AppRouter = app.getRouter(); --const deprecatedAppRoutes = app.getRoutes(); -```` + const AppProvider = app.getProvider(); + const AppRouter = app.getRouter(); +- const deprecatedAppRoutes = app.getRoutes(); +``` As well as update or add the following routes: @@ -975,7 +974,7 @@ As well as update or add the following routes: - {deprecatedAppRoutes} + } /> + } /> -```` +``` If you have added additional plugins with registered routes or are using `Router` components from other plugins, these should be migrated to use the `*Page` components as well. See [this commit](https://github.com/backstage/backstage/commit/abd655e42d4ed416b70848ffdb1c4b99d189f13b) for more examples of how to migrate. diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 169e9a8637..b96b4e2065 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -41,7 +41,7 @@ "@types/inquirer": "^7.3.1", "@types/react-dev-utils": "^9.0.4", "@types/recursive-readdir": "^2.2.0", - "ts-node": "^8.6.2" + "ts-node": "^9.1.1" }, "peerDependencies": { "@backstage/backend-common": "*", diff --git a/packages/docgen/package.json b/packages/docgen/package.json index bb00620967..a7d7615e19 100644 --- a/packages/docgen/package.json +++ b/packages/docgen/package.json @@ -30,7 +30,7 @@ "commander": "^6.1.0", "fs-extra": "^9.0.0", "github-slugger": "^1.3.0", - "ts-node": "^8.6.2", + "ts-node": "^9.1.1", "typescript": "^4.0.3" }, "devDependencies": { diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 6cd740c7b7..77a3e3960a 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -34,7 +34,7 @@ "handlebars": "^4.7.3", "pgtools": "^0.3.0", "tree-kill": "^1.2.2", - "ts-node": "^8.6.2", + "ts-node": "^9.1.1", "zombie": "^6.1.4" }, "nodemonConfig": { diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index 9f33a25ac8..7833952e8e 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -29,7 +29,7 @@ "@material-ui/lab": "4.0.0-alpha.45", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.3", diff --git a/packages/test-utils/src/testUtils/mockApis.ts b/packages/test-utils/src/testUtils/mockApis.ts index e05a8e6cac..0fe9a30926 100644 --- a/packages/test-utils/src/testUtils/mockApis.ts +++ b/packages/test-utils/src/testUtils/mockApis.ts @@ -18,10 +18,13 @@ import { storageApiRef, errorApiRef, createApiFactory, + AlertApiForwarder, + alertApiRef, } from '@backstage/core-api'; import { MockErrorApi, MockStorageApi } from './apis'; export const mockApis = [ createApiFactory(errorApiRef, new MockErrorApi()), createApiFactory(storageApiRef, MockStorageApi.create()), + createApiFactory(alertApiRef, new AlertApiForwarder()), ]; diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index e8e8bb8774..9cccb07d56 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -45,7 +45,7 @@ "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3", + "react-use": "^17.2.4", "swagger-ui-react": "^3.37.2" }, "devDependencies": { diff --git a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx index 39c2caaf1b..5b9808e26d 100644 --- a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx +++ b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx @@ -156,7 +156,6 @@ export const ApiExplorerTable = ({ }: ExplorerTableProps) => { const [queryParamState, setQueryParamState] = useQueryParamState( 'apiTable', - 500, ); if (error) { diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 7078b9627e..24660899e2 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -31,7 +31,7 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index cd0564dc41..45b32a11d1 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -33,7 +33,7 @@ "qs": "^6.9.6", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-use": "^15.3.3", + "react-use": "^17.2.4", "recharts": "^1.8.5" }, "devDependencies": { diff --git a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts index dc5e6036f5..140c93929d 100644 --- a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts +++ b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts @@ -185,7 +185,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { ), -- All the nodes that can be reached upwards from the descendants ancestors(root_id, via_entity_ref, to_entity_ref) AS ( - SELECT NULL, entity_ref, entity_ref + SELECT CAST(NULL as INT), entity_ref, entity_ref FROM descendants UNION SELECT @@ -235,7 +235,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { .withRecursive('ancestors', function ancestors(outer) { return outer .select({ - root_id: tx.raw('NULL', []), + root_id: tx.raw('CAST(NULL as INT)', []), via_entity_ref: 'entity_ref', to_entity_ref: 'entity_ref', }) diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index 969d4b7df4..983604ba03 100644 --- a/plugins/catalog-graphql/package.json +++ b/plugins/catalog-graphql/package.json @@ -52,7 +52,7 @@ "eslint-plugin-graphql": "^4.0.0", "msw": "^0.21.2", "supertest": "^6.1.3", - "ts-node": "^8.10.2" + "ts-node": "^9.1.1" }, "files": [ "dist" diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 2277604dc2..c5fcdd4d37 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -49,7 +49,7 @@ "react-hook-form": "^6.15.4", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3", + "react-use": "^17.2.4", "yaml": "^1.10.0" }, "devDependencies": { diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 1d886478b8..a9092ef75a 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -37,7 +37,7 @@ "react": "^16.13.1", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 8277c2dcbc..f1a0e6979d 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -49,7 +49,7 @@ "react-helmet": "6.1.0", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index b87fb4bbbf..98af012a49 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -47,7 +47,7 @@ "react-lazylog": "^4.5.2", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 98735f1c42..325dfb4e06 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -44,7 +44,7 @@ "react-lazylog": "^4.5.3", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json index 241b61b296..9b227c1df1 100644 --- a/plugins/code-coverage/package.json +++ b/plugins/code-coverage/package.json @@ -35,7 +35,7 @@ "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3", + "react-use": "^17.2.4", "recharts": "^1.8.5" }, "devDependencies": { diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index 7877668065..caa03b9ffb 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -31,7 +31,7 @@ "zen-observable": "^0.8.15", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 5d780080d1..3a5601041c 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -49,7 +49,7 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3", + "react-use": "^17.2.4", "recharts": "^1.8.5", "regression": "^2.0.1", "yup": "^0.29.3" diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 35a4a97349..2a25a9db41 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -42,7 +42,7 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/explore/src/components/ExplorePage/ExploreTabs.tsx b/plugins/explore/src/components/ExplorePage/ExploreTabs.tsx index 0cf28f9f1c..0415dd97ae 100644 --- a/plugins/explore/src/components/ExplorePage/ExploreTabs.tsx +++ b/plugins/explore/src/components/ExplorePage/ExploreTabs.tsx @@ -16,6 +16,7 @@ import { TabbedLayout } from '@backstage/core'; import React from 'react'; import { DomainExplorerContent } from '../DomainExplorerContent'; +import { GroupsExplorerContent } from '../GroupsExplorerContent'; import { ToolExplorerContent } from '../ToolExplorerContent'; export const ExploreTabs = () => ( @@ -23,6 +24,9 @@ export const ExploreTabs = () => ( + + + diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.test.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.test.tsx new file mode 100644 index 0000000000..022387faca --- /dev/null +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.test.tsx @@ -0,0 +1,69 @@ +/* + * 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 { ApiProvider, ApiRegistry } from '@backstage/core'; +import { + catalogApiRef, + CatalogApi, + entityRouteRef, +} from '@backstage/plugin-catalog-react'; +import { Entity } from '@backstage/catalog-model'; +import { renderInTestApp } from '@backstage/test-utils'; +import React from 'react'; +import { GroupsDiagram } from './GroupsDiagram'; + +describe('', () => { + beforeAll(() => { + Object.defineProperty(window.SVGElement.prototype, 'getBBox', { + value: () => ({ width: 100, height: 100 }), + configurable: true, + }); + }); + + it('shows groups', async () => { + const catalogApi: Partial = { + getEntities: () => + Promise.resolve({ + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'group-a', + namespace: 'my-namespace', + }, + spec: { + type: 'organization', + }, + }, + ] as Entity[], + }), + }; + + const { getByText } = await renderInTestApp( + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + expect(getByText('my-namespace/group-a')).toBeInTheDocument(); + }); +}); diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx new file mode 100644 index 0000000000..99157b878f --- /dev/null +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx @@ -0,0 +1,190 @@ +/* + * Copyright 2021 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 { + RELATION_CHILD_OF, + stringifyEntityRef, + parseEntityRef, +} from '@backstage/catalog-model'; +import { + catalogApiRef, + entityRouteRef, + getEntityRelations, + formatEntityRefTitle, +} from '@backstage/plugin-catalog-react'; +import { + DependencyGraph, + DependencyGraphTypes, + Progress, + useApi, + ResponseErrorPanel, + Link, + useRouteRef, + configApiRef, +} from '@backstage/core'; +import { makeStyles, Typography } from '@material-ui/core'; +import ZoomOutMap from '@material-ui/icons/ZoomOutMap'; +import React from 'react'; +import { useAsync } from 'react-use'; +import { BackstageTheme } from '@backstage/theme'; + +const useStyles = makeStyles((theme: BackstageTheme) => ({ + organizationNode: { + fill: 'coral', + stroke: theme.palette.border, + }, + groupNode: { + fill: 'yellowgreen', + stroke: theme.palette.border, + }, +})); + +function RenderNode(props: DependencyGraphTypes.RenderNodeProps) { + const classes = useStyles(); + const catalogEntityRoute = useRouteRef(entityRouteRef); + + if (props.node.id === 'root') { + return ( + + + + {props.node.name} + + + ); + } + + const ref = parseEntityRef(props.node.id); + + return ( + + + + + {props.node.name} + + + + ); +} + +/** + * Dynamically generates a diagram of groups registered in the catalog. + */ +export function GroupsDiagram() { + const nodes = new Array<{ id: string; kind: string; name: string }>(); + const edges = new Array<{ from: string; to: string; label: string }>(); + + const configApi = useApi(configApiRef); + const catalogApi = useApi(catalogApiRef); + const organizationName = + configApi.getOptionalString('organization.name') ?? 'Backstage'; + const { loading, error, value: catalogResponse } = useAsync(() => { + return catalogApi.getEntities({ + filter: { + kind: ['Group'], + }, + }); + }, [catalogApi]); + + if (loading) { + return ; + } else if (error) { + return ; + } + + // the root of this diagram is the organization + nodes.push({ + id: 'root', + kind: 'Organization', + name: organizationName, + }); + + for (const catalogItem of catalogResponse?.items || []) { + const currentItemId = stringifyEntityRef(catalogItem); + + nodes.push({ + id: stringifyEntityRef(catalogItem), + kind: catalogItem.kind, + name: formatEntityRefTitle(catalogItem, { defaultKind: 'Group' }), + }); + + // Edge to parent + const catalogItemRelations_childOf = getEntityRelations( + catalogItem, + RELATION_CHILD_OF, + ); + + // if no parent is found, link the node to the root + if (catalogItemRelations_childOf.length === 0) { + edges.push({ + from: currentItemId, + to: 'root', + label: '', + }); + } + + catalogItemRelations_childOf.forEach(relation => { + edges.push({ + from: currentItemId, + to: stringifyEntityRef(relation), + label: '', + }); + }); + } + + return ( + <> + + + Use pinch & zoom + to move around the diagram. + + + ); +} diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx new file mode 100644 index 0000000000..38c4678d5a --- /dev/null +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx @@ -0,0 +1,103 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { waitFor } from '@testing-library/react'; +import React from 'react'; +import { GroupsExplorerContent } from '../GroupsExplorerContent'; + +describe('', () => { + const catalogApi: jest.Mocked = { + addLocation: jest.fn(_a => new Promise(() => {})), + getEntities: jest.fn(), + getOriginLocationByEntity: jest.fn(), + getLocationByEntity: jest.fn(), + getLocationById: jest.fn(), + removeLocationById: jest.fn(), + removeEntityByUid: jest.fn(), + getEntityByName: jest.fn(), + }; + + const Wrapper = ({ children }: { children?: React.ReactNode }) => ( + + {children} + + ); + + beforeEach(() => { + jest.resetAllMocks(); + + Object.defineProperty(window.SVGElement.prototype, 'getBBox', { + value: () => ({ width: 100, height: 100 }), + configurable: true, + }); + }); + + it('renders a groups diagram', async () => { + const entities: Entity[] = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'group-a', + namespace: 'my-namespace', + }, + spec: { + type: 'organization', + }, + }, + ]; + catalogApi.getEntities.mockResolvedValue({ items: entities }); + + const { getByText } = await renderInTestApp( + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + await waitFor(() => { + expect(getByText('my-namespace/group-a')).toBeInTheDocument(); + }); + }); + + it('renders a friendly error if it cannot collect domains', async () => { + const catalogError = new Error('Network timeout'); + catalogApi.getEntities.mockRejectedValueOnce(catalogError); + + const { getByText } = await renderInTestApp( + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + await waitFor(() => + expect(getByText(/Warning: Network timeout/)).toBeInTheDocument(), + ); + }); +}); diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.tsx new file mode 100644 index 0000000000..c4d46206e4 --- /dev/null +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.tsx @@ -0,0 +1,30 @@ +/* + * Copyright 2021 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 { Content, ContentHeader, SupportButton } from '@backstage/core'; +import React from 'react'; +import { GroupsDiagram } from './GroupsDiagram'; + +export const GroupsExplorerContent = () => { + return ( + + + Explore your groups. + + + + + ); +}; diff --git a/plugins/explore/src/components/GroupsExplorerContent/index.ts b/plugins/explore/src/components/GroupsExplorerContent/index.ts new file mode 100644 index 0000000000..4d88f40dde --- /dev/null +++ b/plugins/explore/src/components/GroupsExplorerContent/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 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 { GroupsExplorerContent } from './GroupsExplorerContent'; diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index ee35b94c0b..95762e52d7 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -44,7 +44,7 @@ "p-limit": "^3.0.2", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 96102332d9..fd6048ab2a 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -38,7 +38,7 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "^6.0.0-beta.0", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 61de1a1f7d..95608c3c49 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -47,7 +47,7 @@ "react-lazylog": "^4.5.3", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index e67df926ea..84a0fe1660 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -31,7 +31,7 @@ "luxon": "^1.26.0", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 866ee571c0..c00dbde482 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -39,7 +39,7 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index d03a5f39e9..ae3c0dfe8f 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -40,7 +40,7 @@ "graphql": "15.5.0", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 45bd3c4bf8..c4ba600d7e 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -44,7 +44,7 @@ "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index ce3e7561b1..e405460262 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -30,7 +30,7 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/kubernetes-backend/schema.d.ts b/plugins/kubernetes-backend/schema.d.ts index fa5bc52abf..71dbace6c7 100644 --- a/plugins/kubernetes-backend/schema.d.ts +++ b/plugins/kubernetes-backend/schema.d.ts @@ -14,14 +14,43 @@ * limitations under the License. */ -import { ClusterLocatorMethod, CustomResource } from './src/types'; - export interface Config { kubernetes?: { serviceLocatorMethod: { type: 'multiTenant'; }; - clusterLocatorMethods: ClusterLocatorMethod[]; - customResources?: CustomResource[]; + clusterLocatorMethods: Array< + | { + /** @visibility frontend */ + type: 'gke'; + /** @visibility frontend */ + projectId: string; + /** @visibility frontend */ + region?: string; + /** @visibility frontend */ + skipTLSVerify?: boolean; + } + | { + /** @visibility frontend */ + type: 'config'; + clusters: Array<{ + /** @visibility frontend */ + url: string; + /** @visibility frontend */ + name: string; + /** @visibility secret */ + serviceAccountToken?: string; + /** @visibility frontend */ + authProvider: 'aws' | 'google' | 'serviceAccount'; + /** @visibility frontend */ + skipTLSVerify?: boolean; + }>; + } + >; + customResources?: Array<{ + group: string; + apiVersion: string; + plural: string; + }>; }; } diff --git a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts index cf555bd8c9..f45368479c 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts @@ -106,6 +106,7 @@ describe('GkeClusterLocator', () => { authProvider: 'google', name: 'some-cluster', url: 'https://1.2.3.4', + skipTLSVerify: false, }, ]); expect(mockedListClusters).toBeCalledTimes(1); @@ -141,6 +142,7 @@ describe('GkeClusterLocator', () => { authProvider: 'google', name: 'some-cluster', url: 'https://1.2.3.4', + skipTLSVerify: false, }, ]); expect(mockedListClusters).toBeCalledTimes(1); @@ -181,11 +183,13 @@ describe('GkeClusterLocator', () => { authProvider: 'google', name: 'some-cluster', url: 'https://1.2.3.4', + skipTLSVerify: false, }, { authProvider: 'google', name: 'some-other-cluster', url: 'https://6.7.8.9', + skipTLSVerify: false, }, ]); expect(mockedListClusters).toBeCalledTimes(1); diff --git a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts index 7bfd62ef8d..b2bdc3bca8 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts @@ -18,28 +18,28 @@ import { Config } from '@backstage/config'; import * as container from '@google-cloud/container'; import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; -export class GkeClusterLocator implements KubernetesClustersSupplier { - private readonly projectId: string; - private readonly region: string | undefined; - private readonly client: container.v1.ClusterManagerClient; +type GkeClusterLocatorOptions = { + projectId: string; + region?: string; + skipTLSVerify?: boolean; +}; +export class GkeClusterLocator implements KubernetesClustersSupplier { constructor( - projectId: string, - client: container.v1.ClusterManagerClient, - region?: string, - ) { - this.projectId = projectId; - this.region = region; - this.client = client; - } + private readonly options: GkeClusterLocatorOptions, + private readonly client: container.v1.ClusterManagerClient, + ) {} static fromConfigWithClient( config: Config, client: container.v1.ClusterManagerClient, ): GkeClusterLocator { - const projectId = config.getString('projectId'); - const region = config.getOptionalString('region'); - return new GkeClusterLocator(projectId, client, region); + const options = { + projectId: config.getString('projectId'), + region: config.getOptionalString('region') ?? '-', + skipTLSVerify: config.getOptionalBoolean('skipTLSVerify') ?? false, + }; + return new GkeClusterLocator(options, client); } static fromConfig(config: Config): GkeClusterLocator { @@ -50,9 +50,9 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { } async getClusters(): Promise { - const region = this.region ?? '-'; + const { projectId, region, skipTLSVerify } = this.options; const request = { - parent: `projects/${this.projectId}/locations/${region}`, + parent: `projects/${projectId}/locations/${region}`, }; try { @@ -62,10 +62,11 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { name: r.name ?? 'unknown', url: `https://${r.endpoint ?? ''}`, authProvider: 'google', + skipTLSVerify, })); } catch (e) { throw new Error( - `There was an error retrieving clusters from GKE for projectId=${this.projectId} region=${region} : [${e.message}]`, + `There was an error retrieving clusters from GKE for projectId=${projectId} region=${region} : [${e.message}]`, ); } } diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index 0be0b447af..ae254dfdae 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -19,50 +19,6 @@ import type { KubernetesFetchError, } from '@backstage/plugin-kubernetes-common'; -export type ClusterLocatorMethod = - | ConfigClusterLocatorMethod - | GKEClusterLocatorMethod; - -export interface ConfigClusterLocatorMethod { - /** - * @visibility frontend - */ - type: 'config'; - clusters: { - /** - * @visibility frontend - */ - url: string; - /** - * @visibility frontend - */ - name: string; - /** - * @visibility secret - */ - serviceAccountToken: string | undefined; - /** - * @visibility frontend - */ - authProvider: 'aws' | 'google' | 'serviceAccount'; - }[]; -} - -export interface GKEClusterLocatorMethod { - /** - * @visibility frontend - */ - type: 'gke'; - /** - * @visibility frontend - */ - projectId: string; - /** - * @visibility frontend - */ - region?: string; -} - export interface CustomResource { group: string; apiVersion: string; diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 8028c3a339..19865a6fcc 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -46,7 +46,7 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index a6ae0102bf..f70e29f69b 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -43,7 +43,7 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index dfe5217f9d..20ee5521f4 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -38,7 +38,7 @@ "@material-ui/lab": "4.0.0-alpha.45", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/org/package.json b/plugins/org/package.json index 2b802510f4..012514f1c4 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -32,7 +32,7 @@ "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 0005b9aa3b..53f0ddbb23 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -43,7 +43,7 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index 5052fd3342..3efb61b982 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -42,7 +42,7 @@ "react-hook-form": "^6.15.4", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index 5563ad0e40..8b559e0bf1 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -44,7 +44,7 @@ "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-sparklines": "^1.7.0", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 9cd951f7d8..7452d94c63 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -53,7 +53,6 @@ "handlebars": "^4.7.6", "helmet": "^4.0.0", "isomorphic-git": "^1.8.0", - "jest-when": "^3.1.0", "jsonschema": "^1.2.6", "knex": "^0.95.1", "lodash": "^4.17.21", @@ -70,6 +69,7 @@ "@types/fs-extra": "^9.0.1", "@types/mock-fs": "^4.13.0", "@types/supertest": "^2.0.8", + "jest-when": "^3.1.0", "mock-fs": "^4.13.0", "msw": "^0.21.2", "supertest": "^6.1.3", diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index f2f69c029b..1992028157 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -33,6 +33,7 @@ "@backstage/catalog-client": "^0.3.11", "@backstage/catalog-model": "^0.7.8", "@backstage/config": "^0.1.5", + "@backstage/errors": "^0.1.1", "@backstage/core": "^0.7.8", "@backstage/integration": "^0.5.2", "@backstage/integration-react": "^0.1.1", @@ -54,7 +55,7 @@ "react-lazylog": "^4.5.2", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3", + "react-use": "^17.2.4", "use-immer": "^0.5.1", "zen-observable": "^0.8.15" }, diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 0c5eb2b268..c7b1594071 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -22,6 +22,7 @@ import { IdentityApi, Observable, } from '@backstage/core'; +import { ResponseError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; import ObservableImpl from 'zen-observable'; import { ListActionsResponse, ScaffolderTask, Status } from './types'; @@ -128,9 +129,7 @@ export class ScaffolderClient implements ScaffolderApi { }); if (!response.ok) { - throw new Error( - `Failed to fetch template parameter schema, ${await response.text()}`, - ); + throw ResponseError.fromResponse(response); } const schema: TemplateParameterSchema = await response.json(); @@ -173,9 +172,15 @@ export class ScaffolderClient implements ScaffolderApi { const token = await this.identityApi.getIdToken(); const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); const url = `${baseUrl}/v2/tasks/${encodeURIComponent(taskId)}`; - return fetch(url, { + const response = await fetch(url, { headers: token ? { Authorization: `Bearer ${token}` } : {}, - }).then(x => x.json()); + }); + + if (!response.ok) { + throw ResponseError.fromResponse(response); + } + + return await response.json(); } streamLogs({ @@ -234,6 +239,11 @@ export class ScaffolderClient implements ScaffolderApi { async listActions(): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); const response = await fetch(`${baseUrl}/v2/actions`); + + if (!response.ok) { + throw ResponseError.fromResponse(response); + } + return await response.json(); } } diff --git a/plugins/search/package.json b/plugins/search/package.json index b135d184f6..f503b9a78e 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -41,7 +41,7 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index a5b1ac7d68..6784debcf9 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -42,7 +42,7 @@ "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-sparklines": "^1.7.0", - "react-use": "^15.3.3", + "react-use": "^17.2.4", "timeago.js": "^4.0.2" }, "devDependencies": { diff --git a/plugins/shortcuts/.eslintrc.js b/plugins/shortcuts/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/shortcuts/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/shortcuts/README.md b/plugins/shortcuts/README.md new file mode 100644 index 0000000000..162afe009b --- /dev/null +++ b/plugins/shortcuts/README.md @@ -0,0 +1,52 @@ +# shortcuts + +The shortcuts plugin allows a user to have easy access to pages within a Backstage app by storing them as "shortcuts" in the Sidebar. + +## Usage + +Install the package: + +```bash +yarn add @backstage/plugin-shortcuts +``` + +Add it to your App's `plugins.ts` file: + +```ts +// ... +export { shortcutsPlugin } from '@backstage/plugin-shortcuts'; +``` + +Add the `` component within your ``: + +```tsx +import { Sidebar, SidebarDivider, SidebarSpace } from '@backstage/core'; +import { Shortcuts } from '@backstage/plugin-shortcuts'; + +export const SidebarComponent = () => ( + + {/* ... */} + + + + +); +``` + +The plugin exports a `shortcutApiRef` but the plugin includes a default implementation of the `ShortcutApi` that uses `localStorage` to store each user's shortcuts. + +To overwrite the default implementation add it to the App's `apis.ts`: + +```ts +import { shortcutsApiRef } from '@backstage/plugin-shortcuts'; +import { CustomShortcutsImpl } from '...'; + +export const apis = [ + // ... + createApiFactory({ + api: shortcutsApiRef, + deps: {}, + factory: () => new CustomShortcutsImpl(), + }), +]; +``` diff --git a/plugins/shortcuts/dev/index.tsx b/plugins/shortcuts/dev/index.tsx new file mode 100644 index 0000000000..0da3a90054 --- /dev/null +++ b/plugins/shortcuts/dev/index.tsx @@ -0,0 +1,26 @@ +/* + * Copyright 2021 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 { createDevApp } from '@backstage/dev-utils'; +import { shortcutsPlugin, Shortcuts } from '../src/plugin'; + +createDevApp() + .registerPlugin(shortcutsPlugin) + .addPage({ + element: , + title: 'Root Page', + }) + .render(); diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json new file mode 100644 index 0000000000..f87fe0e683 --- /dev/null +++ b/plugins/shortcuts/package.json @@ -0,0 +1,52 @@ +{ + "name": "@backstage/plugin-shortcuts", + "version": "0.1.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/core": "^0.7.8", + "@backstage/theme": "^0.2.7", + "@material-ui/core": "^4.11.0", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "@types/zen-observable": "^0.8.2", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-hook-form": "^7.1.1", + "react-router": "6.0.0-beta.0", + "react-use": "^17.2.4", + "uuid": "^8.3.2", + "zen-observable": "^0.8.15" + }, + "devDependencies": { + "@backstage/cli": "^0.6.10", + "@backstage/dev-utils": "^0.1.13", + "@backstage/test-utils": "^0.1.10", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^12.0.7", + "@types/jest": "^26.0.7", + "@types/node": "^14.14.32", + "cross-fetch": "^3.0.6", + "msw": "^0.21.2" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/shortcuts/src/AddShortcut.test.tsx b/plugins/shortcuts/src/AddShortcut.test.tsx new file mode 100644 index 0000000000..8001cdc293 --- /dev/null +++ b/plugins/shortcuts/src/AddShortcut.test.tsx @@ -0,0 +1,105 @@ +/* + * Copyright 2021 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 { screen, fireEvent, waitFor } from '@testing-library/react'; +import { AddShortcut } from './AddShortcut'; +import { LocalStoredShortcuts } from './api'; +import { MockStorageApi, renderInTestApp } from '@backstage/test-utils'; +import { AlertDisplay } from '@backstage/core'; + +describe('AddShortcut', () => { + const api = new LocalStoredShortcuts(MockStorageApi.create()); + + const props = { + onClose: jest.fn(), + anchorEl: document.createElement('div'), + api, + }; + + beforeEach(() => { + jest.clearAllMocks(); + document.title = 'some document title'; + }); + + it('displays the title', async () => { + await renderInTestApp(); + + expect(screen.getByText('Add Shortcut')).toBeInTheDocument(); + }); + + it('closes the popup', async () => { + await renderInTestApp(); + + fireEvent.click(screen.getByText('Cancel')); + expect(props.onClose).toHaveBeenCalledTimes(1); + }); + + it('saves the input', async () => { + const spy = jest.spyOn(api, 'add'); + + await renderInTestApp(); + + const urlInput = screen.getByPlaceholderText('Enter a URL'); + const titleInput = screen.getByPlaceholderText('Enter a display name'); + fireEvent.change(urlInput, { target: { value: '/some-url' } }); + fireEvent.change(titleInput, { target: { value: 'some title' } }); + + fireEvent.click(screen.getByText('Save')); + await waitFor(() => { + expect(spy).toBeCalledWith({ + title: 'some title', + url: '/some-url', + }); + }); + }); + + it('pastes the values', async () => { + const spy = jest.spyOn(api, 'add'); + + await renderInTestApp(, { + routeEntries: ['/some-initial-url'], + }); + + fireEvent.click(screen.getByText('Use current page')); + fireEvent.click(screen.getByText('Save')); + await waitFor(() => { + expect(spy).toBeCalledWith({ + title: 'some document title', + url: '/some-initial-url', + }); + }); + }); + + it('displays errors', async () => { + jest.spyOn(api, 'add').mockRejectedValueOnce(new Error('some add error')); + + await renderInTestApp( + <> + + + , + ); + + fireEvent.click(screen.getByText('Use current page')); + fireEvent.click(screen.getByText('Save')); + await waitFor(() => { + expect( + screen.getByText('Could not add shortcut: some add error'), + ).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/shortcuts/src/AddShortcut.tsx b/plugins/shortcuts/src/AddShortcut.tsx new file mode 100644 index 0000000000..74a0653b4c --- /dev/null +++ b/plugins/shortcuts/src/AddShortcut.tsx @@ -0,0 +1,121 @@ +/* + * Copyright 2021 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, { useState } from 'react'; +import { useLocation } from 'react-router'; +import { SubmitHandler } from 'react-hook-form'; +import { alertApiRef, useApi } from '@backstage/core'; +import { + Button, + Card, + CardHeader, + makeStyles, + Popover, +} from '@material-ui/core'; +import { ShortcutForm } from './ShortcutForm'; +import { FormValues, Shortcut } from './types'; +import { ShortcutApi } from './api'; + +const useStyles = makeStyles(theme => ({ + card: { + width: 400, + }, + header: { + marginBottom: theme.spacing(1), + }, + button: { + marginTop: theme.spacing(1), + }, +})); + +type Props = { + onClose: () => void; + anchorEl?: Element; + api: ShortcutApi; +}; + +export const AddShortcut = ({ onClose, anchorEl, api }: Props) => { + const classes = useStyles(); + const alertApi = useApi(alertApiRef); + const { pathname } = useLocation(); + const [formValues, setFormValues] = useState(); + const open = Boolean(anchorEl); + + const handleSave: SubmitHandler = async ({ url, title }) => { + const shortcut: Omit = { url, title }; + + try { + await api.add(shortcut); + alertApi.post({ + message: `Added shortcut '${title}' to your sidebar`, + severity: 'success', + }); + } catch (error) { + alertApi.post({ + message: `Could not add shortcut: ${error.message}`, + severity: 'error', + }); + } + + onClose(); + }; + + const handlePaste = () => { + setFormValues({ url: pathname, title: document.title }); + }; + + const handleClose = () => { + setFormValues(undefined); + onClose(); + }; + + return ( + + + + Use current page + + } + /> + + + + ); +}; diff --git a/plugins/shortcuts/src/EditShortcut.test.tsx b/plugins/shortcuts/src/EditShortcut.test.tsx new file mode 100644 index 0000000000..eaab6cf609 --- /dev/null +++ b/plugins/shortcuts/src/EditShortcut.test.tsx @@ -0,0 +1,118 @@ +/* + * Copyright 2021 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 { screen, fireEvent, waitFor } from '@testing-library/react'; +import { EditShortcut } from './EditShortcut'; +import { Shortcut } from './types'; +import { LocalStoredShortcuts } from './api'; +import { MockStorageApi, renderInTestApp } from '@backstage/test-utils'; +import { AlertDisplay } from '@backstage/core'; + +describe('EditShortcut', () => { + const shortcut: Shortcut = { + id: 'id', + url: '/some-url', + title: 'some title', + }; + const api = new LocalStoredShortcuts(MockStorageApi.create()); + + const props = { + onClose: jest.fn(), + anchorEl: document.createElement('div'), + shortcut, + api, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('displays the title', async () => { + await renderInTestApp(); + + expect(screen.getByText('Edit Shortcut')).toBeInTheDocument(); + }); + + it('closes the popup', async () => { + await renderInTestApp(); + + fireEvent.click(screen.getByText('Cancel')); + expect(props.onClose).toHaveBeenCalledTimes(1); + }); + + it('updates the shortcut', async () => { + const spy = jest.spyOn(api, 'update'); + + await renderInTestApp(); + + const urlInput = screen.getByPlaceholderText('Enter a URL'); + const titleInput = screen.getByPlaceholderText('Enter a display name'); + fireEvent.change(urlInput, { target: { value: '/some-new-url' } }); + fireEvent.change(titleInput, { target: { value: 'some new title' } }); + + fireEvent.click(screen.getByText('Save')); + await waitFor(() => { + expect(spy).toBeCalledWith({ + id: 'id', + title: 'some new title', + url: '/some-new-url', + }); + expect(props.onClose).toHaveBeenCalledTimes(1); + }); + }); + + it('removes the shortcut', async () => { + const spy = jest.spyOn(api, 'remove'); + + await renderInTestApp(); + + fireEvent.click(screen.getByText('Remove')); + expect(spy).toBeCalledWith('id'); + }); + + it('displays errors', async () => { + jest + .spyOn(api, 'update') + .mockRejectedValueOnce(new Error('some update error')); + + jest + .spyOn(api, 'remove') + .mockRejectedValueOnce(new Error('some remove error')); + + await renderInTestApp( + <> + + + , + ); + + fireEvent.click(screen.getByText('Save')); + await waitFor(() => { + expect( + screen.getByText('Could not update shortcut: some update error'), + ).toBeInTheDocument(); + }); + fireEvent.click(screen.getByTestId('error-button-close')); + + fireEvent.click(screen.getByText('Remove')); + await waitFor(() => { + expect( + screen.getByText('Could not delete shortcut: some remove error'), + ).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/shortcuts/src/EditShortcut.tsx b/plugins/shortcuts/src/EditShortcut.tsx new file mode 100644 index 0000000000..efe8726992 --- /dev/null +++ b/plugins/shortcuts/src/EditShortcut.tsx @@ -0,0 +1,134 @@ +/* + * Copyright 2021 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 { SubmitHandler } from 'react-hook-form'; +import { alertApiRef, useApi } from '@backstage/core'; +import { + Button, + Card, + CardHeader, + makeStyles, + Popover, +} from '@material-ui/core'; +import { ShortcutForm } from './ShortcutForm'; +import { FormValues, Shortcut } from './types'; +import DeleteIcon from '@material-ui/icons/Delete'; +import { ShortcutApi } from './api'; + +const useStyles = makeStyles(theme => ({ + card: { + width: 400, + }, + header: { + marginBottom: theme.spacing(1), + }, + button: { + marginTop: theme.spacing(1), + }, +})); + +type Props = { + shortcut: Shortcut; + onClose: () => void; + anchorEl?: Element; + api: ShortcutApi; +}; + +export const EditShortcut = ({ shortcut, onClose, anchorEl, api }: Props) => { + const classes = useStyles(); + const alertApi = useApi(alertApiRef); + const open = Boolean(anchorEl); + + const handleSave: SubmitHandler = async ({ url, title }) => { + const newShortcut: Shortcut = { + ...shortcut, + url, + title, + }; + + try { + await api.update(newShortcut); + alertApi.post({ + message: `Updated shortcut '${title}'`, + severity: 'success', + }); + } catch (error) { + alertApi.post({ + message: `Could not update shortcut: ${error.message}`, + severity: 'error', + }); + } + + onClose(); + }; + + const handleRemove = async () => { + try { + await api.remove(shortcut.id); + alertApi.post({ + message: `Removed shortcut '${shortcut.title}' from your sidebar`, + severity: 'success', + }); + } catch (error) { + alertApi.post({ + message: `Could not delete shortcut: ${error.message}`, + severity: 'error', + }); + } + }; + + const handleClose = () => { + onClose(); + }; + + return ( + + + } + onClick={handleRemove} + > + Remove + + } + /> + + + + ); +}; diff --git a/plugins/shortcuts/src/ShortcutForm.test.tsx b/plugins/shortcuts/src/ShortcutForm.test.tsx new file mode 100644 index 0000000000..23a36126c6 --- /dev/null +++ b/plugins/shortcuts/src/ShortcutForm.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright 2021 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 { screen, fireEvent, waitFor } from '@testing-library/react'; +import { ShortcutForm } from './ShortcutForm'; +import { renderInTestApp } from '@backstage/test-utils'; + +describe('ShortcutForm', () => { + const props = { + onSave: jest.fn(), + onClose: jest.fn(), + }; + + it('displays validation messages', async () => { + await renderInTestApp(); + + const urlInput = screen.getByPlaceholderText('Enter a URL'); + const titleInput = screen.getByPlaceholderText('Enter a display name'); + fireEvent.change(urlInput, { target: { value: 'url' } }); + fireEvent.change(titleInput, { target: { value: 't' } }); + + fireEvent.click(screen.getByText('Save')); + await waitFor(() => { + expect( + screen.getByText('Must be a relative URL (starts with a /)'), + ).toBeInTheDocument(); + expect( + screen.getByText('Must be at least 2 characters'), + ).toBeInTheDocument(); + }); + }); + + it('calls the save handler', async () => { + await renderInTestApp( + , + ); + + fireEvent.click(screen.getByText('Save')); + await waitFor(() => { + expect(props.onSave).toHaveBeenCalledWith( + expect.objectContaining({ title: 'some title', url: '/some-url' }), + expect.anything(), + ); + }); + }); + + it('calls the close handler', async () => { + await renderInTestApp(); + + fireEvent.click(screen.getByText('Cancel')); + await waitFor(() => { + expect(props.onClose).toHaveBeenCalled(); + }); + }); +}); diff --git a/plugins/shortcuts/src/ShortcutForm.tsx b/plugins/shortcuts/src/ShortcutForm.tsx new file mode 100644 index 0000000000..6d6e0679c3 --- /dev/null +++ b/plugins/shortcuts/src/ShortcutForm.tsx @@ -0,0 +1,140 @@ +/* + * Copyright 2021 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, { useEffect } from 'react'; +import { useForm, SubmitHandler, Controller } from 'react-hook-form'; +import { + Button, + CardActions, + CardContent, + makeStyles, + TextField, +} from '@material-ui/core'; +import { FormValues } from './types'; + +const useStyles = makeStyles(theme => ({ + field: { + marginBottom: theme.spacing(2), + }, + actionRoot: { + paddingLeft: theme.spacing(2), + paddingBottom: theme.spacing(3), + justifyContent: 'flex-start', + }, +})); + +type Props = { + formValues?: FormValues; + onSave: SubmitHandler; + onClose: () => void; +}; + +export const ShortcutForm = ({ formValues, onSave, onClose }: Props) => { + const classes = useStyles(); + + const { + handleSubmit, + reset, + control, + formState: { errors }, + } = useForm({ + mode: 'onChange', + defaultValues: { + url: formValues?.url ?? '', + title: formValues?.title ?? '', + }, + }); + + useEffect(() => { + reset(formValues); + }, [reset, formValues]); + + return ( + <> + + ( + + )} + /> + ( + + )} + /> + + + + + + + ); +}; diff --git a/plugins/shortcuts/src/ShortcutIcon.tsx b/plugins/shortcuts/src/ShortcutIcon.tsx new file mode 100644 index 0000000000..c737596647 --- /dev/null +++ b/plugins/shortcuts/src/ShortcutIcon.tsx @@ -0,0 +1,47 @@ +/* + * Copyright 2021 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'; + +type Props = { + text: string; + color: string; +}; + +export const ShortcutIcon = ({ text, color }: Props) => { + const size = 28; + return ( + + + + {text} + + + ); +}; diff --git a/plugins/shortcuts/src/ShortcutItem.test.tsx b/plugins/shortcuts/src/ShortcutItem.test.tsx new file mode 100644 index 0000000000..080f847952 --- /dev/null +++ b/plugins/shortcuts/src/ShortcutItem.test.tsx @@ -0,0 +1,103 @@ +/* + * Copyright 2021 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 { screen, waitFor } from '@testing-library/react'; +import { ShortcutItem } from './ShortcutItem'; +import { Shortcut } from './types'; +import { SidebarContext } from '@backstage/core'; +import { LocalStoredShortcuts } from './api'; +import { + MockStorageApi, + renderInTestApp, + wrapInTestApp, +} from '@backstage/test-utils'; +import { pageTheme } from '@backstage/theme'; + +describe('ShortcutItem', () => { + const shortcut: Shortcut = { + id: 'id', + url: '/some-url', + title: 'some title', + }; + const api = new LocalStoredShortcuts(MockStorageApi.create()); + + it('displays the shortcut', async () => { + await renderInTestApp( + + + , + ); + expect(screen.getByText('ST')).toBeInTheDocument(); + expect(screen.getByText('some title')).toBeInTheDocument(); + }); + + it('calculates the shortcut text correctly', async () => { + const shortcut1: Shortcut = { + id: 'id1', + url: '/some-url', + title: 'onetitle', + }; + const shortcut2: Shortcut = { + id: 'id2', + url: '/some-url', + title: 'two title', + }; + const shortcut3: Shortcut = { + id: 'id3', + url: '/some-url', + title: 'more | title words', + }; + + const { rerender } = await renderInTestApp( + , + ); + expect(screen.getByText('On')).toBeInTheDocument(); + + rerender(wrapInTestApp()); + await waitFor(() => { + expect(screen.getByText('TT')).toBeInTheDocument(); + }); + + rerender(wrapInTestApp()); + await waitFor(() => { + expect(screen.getByText('MT')).toBeInTheDocument(); + }); + }); + + it('gets the color based on the theme', async () => { + const { rerender } = await renderInTestApp( + , + ); + + expect(document.querySelector('circle')?.getAttribute('fill')).toEqual( + pageTheme.tool.colors[0], + ); + + const newShortcut: Shortcut = { + id: 'id', + url: '/catalog', + title: 'some title', + }; + rerender(wrapInTestApp()); + + await waitFor(() => { + expect(document.querySelector('circle')?.getAttribute('fill')).toEqual( + pageTheme.home.colors[0], + ); + }); + }); +}); diff --git a/plugins/shortcuts/src/ShortcutItem.tsx b/plugins/shortcuts/src/ShortcutItem.tsx new file mode 100644 index 0000000000..f9868182f0 --- /dev/null +++ b/plugins/shortcuts/src/ShortcutItem.tsx @@ -0,0 +1,100 @@ +/* + * Copyright 2021 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 { SidebarItem } from '@backstage/core'; +import { IconButton, makeStyles } from '@material-ui/core'; +import EditIcon from '@material-ui/icons/Edit'; +import { ShortcutIcon } from './ShortcutIcon'; +import { EditShortcut } from './EditShortcut'; +import { ShortcutApi } from './api'; +import { Shortcut } from './types'; + +const useStyles = makeStyles({ + root: { + '&:hover #edit': { + visibility: 'visible', + }, + }, + button: { + visibility: 'hidden', + }, + icon: { + color: 'white', + fontSize: 16, + }, +}); + +const getIconText = (title: string) => + title.split(' ').length === 1 + ? // If there's only one word, keep the first two characters + title[0].toUpperCase() + title[1].toLowerCase() + : // If there's more than one word, take the first character of the first two words + title + .replace(/\B\W/g, '') + .split(' ') + .map(s => s[0]) + .join('') + .slice(0, 2) + .toUpperCase(); + +type Props = { + shortcut: Shortcut; + api: ShortcutApi; +}; + +export const ShortcutItem = ({ shortcut, api }: Props) => { + const classes = useStyles(); + const [anchorEl, setAnchorEl] = React.useState(); + + const handleClick = (event: React.MouseEvent) => { + event.preventDefault(); + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(undefined); + }; + + const text = getIconText(shortcut.title); + const color = api.getColor(shortcut.url); + + return ( + <> + } + > + + + + + + + ); +}; diff --git a/plugins/shortcuts/src/Shortcuts.test.tsx b/plugins/shortcuts/src/Shortcuts.test.tsx new file mode 100644 index 0000000000..b697f9ed27 --- /dev/null +++ b/plugins/shortcuts/src/Shortcuts.test.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2021 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 { SidebarContext, ApiProvider, ApiRegistry } from '@backstage/core'; +import { MockStorageApi, renderInTestApp } from '@backstage/test-utils'; +import { screen, waitFor } from '@testing-library/react'; +import { Shortcuts } from './Shortcuts'; +import { LocalStoredShortcuts, shortcutsApiRef } from './api'; + +const apis = ApiRegistry.from([ + [shortcutsApiRef, new LocalStoredShortcuts(MockStorageApi.create())], +]); + +describe('Shortcuts', () => { + it('displays an add button', async () => { + await renderInTestApp( + + + + + , + ); + await waitFor(() => !screen.queryByTestId('progress')); + expect(screen.getByText('Add Shortcuts')).toBeInTheDocument(); + }); +}); diff --git a/plugins/shortcuts/src/Shortcuts.tsx b/plugins/shortcuts/src/Shortcuts.tsx new file mode 100644 index 0000000000..2c49245e76 --- /dev/null +++ b/plugins/shortcuts/src/Shortcuts.tsx @@ -0,0 +1,75 @@ +/* + * Copyright 2021 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, { useMemo } from 'react'; +import { useObservable } from 'react-use'; +import { Progress, SidebarItem, useApi } from '@backstage/core'; +import { makeStyles } from '@material-ui/core'; +import PlayListAddIcon from '@material-ui/icons/PlaylistAdd'; +import { ShortcutItem } from './ShortcutItem'; +import { AddShortcut } from './AddShortcut'; +import { shortcutsApiRef } from './api'; + +const useStyles = makeStyles({ + root: { + flex: '1 1 auto', + overflow: 'scroll', + }, +}); + +export const Shortcuts = () => { + const classes = useStyles(); + const shortcutApi = useApi(shortcutsApiRef); + const shortcuts = useObservable( + useMemo(() => shortcutApi.shortcut$(), [shortcutApi]), + ); + const [anchorEl, setAnchorEl] = React.useState(); + const loading = Boolean(!shortcuts); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(undefined); + }; + + return ( +
+ + + {loading ? ( + + ) : ( + shortcuts?.map(shortcut => ( + + )) + )} +
+ ); +}; diff --git a/plugins/shortcuts/src/api/LocalStoredShortcuts.test.ts b/plugins/shortcuts/src/api/LocalStoredShortcuts.test.ts new file mode 100644 index 0000000000..7ca457fd39 --- /dev/null +++ b/plugins/shortcuts/src/api/LocalStoredShortcuts.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright 2021 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 { MockStorageApi } from '@backstage/test-utils'; +import { pageTheme } from '@backstage/theme'; +import { Shortcut } from '../types'; +import { LocalStoredShortcuts } from './LocalStoredShortcuts'; +import { ShortcutApi } from './ShortcutApi'; + +describe('LocalStoredShortcuts', () => { + // eslint-disable-next-line jest/no-done-callback + it('should observe shortcuts', async done => { + const shortcutApi: ShortcutApi = new LocalStoredShortcuts( + MockStorageApi.create(), + ); + const shortcut: Shortcut = { id: 'id', title: 'title', url: '/url' }; + + await shortcutApi.add(shortcut); + shortcutApi.shortcut$().subscribe(data => { + expect(data).toEqual( + expect.arrayContaining([{ ...shortcut, id: expect.anything() }]), + ); + done(); + }); + }); + + it('should add shortcuts with ids', async () => { + const storageApi = MockStorageApi.create(); + const shortcutApi: ShortcutApi = new LocalStoredShortcuts(storageApi); + const shortcut: Omit = { title: 'title', url: '/url' }; + const spy = jest.spyOn(storageApi, 'set'); + + await shortcutApi.add(shortcut); + expect(spy).toHaveBeenCalledWith( + 'items', + expect.objectContaining([{ ...shortcut, id: expect.anything() }]), + ); + }); + + it('should update shortcuts', async () => { + const storageApi = MockStorageApi.create(); + const shortcutApi: ShortcutApi = new LocalStoredShortcuts(storageApi); + const shortcut: Shortcut = { id: 'someid', title: 'title', url: '/url' }; + const spy = jest.spyOn(storageApi, 'set'); + + await shortcutApi.update(shortcut); + expect(spy).toHaveBeenCalledWith( + 'items', + expect.objectContaining([shortcut]), + ); + }); + + it('should remove shortcuts', async () => { + const storageApi = MockStorageApi.create(); + const shortcutApi: ShortcutApi = new LocalStoredShortcuts(storageApi); + const shortcut: Shortcut = { id: 'someid', title: 'title', url: '/url' }; + const spy = jest.spyOn(storageApi, 'set'); + + await shortcutApi.remove(shortcut.id); + expect(spy).toHaveBeenCalledWith('items', []); + }); + + it('should get a color', () => { + const storageApi = MockStorageApi.create(); + const shortcutApi: ShortcutApi = new LocalStoredShortcuts(storageApi); + + expect(shortcutApi.getColor('/catalog')).toEqual(pageTheme.home.colors[0]); + }); +}); diff --git a/plugins/shortcuts/src/api/LocalStoredShortcuts.ts b/plugins/shortcuts/src/api/LocalStoredShortcuts.ts new file mode 100644 index 0000000000..ee8735edb4 --- /dev/null +++ b/plugins/shortcuts/src/api/LocalStoredShortcuts.ts @@ -0,0 +1,97 @@ +/* + * Copyright 2021 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 { StorageApi } from '@backstage/core'; +import { pageTheme } from '@backstage/theme'; +import ObservableImpl from 'zen-observable'; +import { v4 as uuid } from 'uuid'; +import { ShortcutApi } from './ShortcutApi'; +import { Shortcut } from '../types'; + +/** + * Implementation of the ShortcutApi that uses the StorageApi to store shortcuts. + */ +export class LocalStoredShortcuts implements ShortcutApi { + constructor(private readonly storageApi: StorageApi) {} + + shortcut$() { + return this.observable; + } + + async add(shortcut: Omit) { + const shortcuts = this.get(); + shortcuts.push({ ...shortcut, id: uuid() }); + + await this.storageApi.set('items', shortcuts); + this.notify(); + } + + async remove(id: string) { + const shortcuts = this.get().filter(s => s.id !== id); + + await this.storageApi.set('items', shortcuts); + this.notify(); + } + + async update(shortcut: Shortcut) { + const shortcuts = this.get().filter(s => s.id !== shortcut.id); + shortcuts.push(shortcut); + + await this.storageApi.set('items', shortcuts); + this.notify(); + } + + getColor(url: string) { + const type = url.split('/')[1]; + const theme = + this.THEME_MAP[type] ?? + (Object.keys(pageTheme).includes(type) ? type : 'tool'); + + return pageTheme[theme].colors[0]; + } + + private subscribers = new Set< + ZenObservable.SubscriptionObserver + >(); + + private readonly observable = new ObservableImpl(subscriber => { + subscriber.next(this.get()); + this.subscribers.add(subscriber); + + return () => { + this.subscribers.delete(subscriber); + }; + }); + + private readonly THEME_MAP: Record = { + catalog: 'home', + docs: 'documentation', + }; + + private get() { + return ( + (this.storageApi.get('items') as Shortcut[])?.sort((a, b) => + a.title >= b.title ? 1 : -1, + ) ?? [] + ); + } + + private notify() { + for (const subscription of this.subscribers) { + subscription.next(this.get()); + } + } +} diff --git a/plugins/shortcuts/src/api/ShortcutApi.ts b/plugins/shortcuts/src/api/ShortcutApi.ts new file mode 100644 index 0000000000..74b6aaf777 --- /dev/null +++ b/plugins/shortcuts/src/api/ShortcutApi.ts @@ -0,0 +1,54 @@ +/* + * Copyright 2021 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 { createApiRef, Observable } from '@backstage/core'; +import { Shortcut } from '../types'; + +export const shortcutsApiRef = createApiRef({ + id: 'plugin.shortcuts.api', + description: 'API to handle shortcuts in a Backstage Sidebar', +}); + +export interface ShortcutApi { + /** + * Returns an Observable that will subscribe to changes. + */ + shortcut$(): Observable; + + /** + * Generates a unique id for the shortcut and then saves it. + */ + add(shortcut: Omit): Promise; + + /** + * Removes the shortcut. + */ + remove(id: string): Promise; + + /** + * Finds an existing shortcut that matches the ID of the + * supplied shortcut and updates its values. + */ + update(shortcut: Shortcut): Promise; + + /** + * Each shortcut should get a color for its icon based on the url. + * + * Preferably using some abstraction between the url and the actual + * color value. + */ + getColor(url: string): string; +} diff --git a/plugins/shortcuts/src/api/index.ts b/plugins/shortcuts/src/api/index.ts new file mode 100644 index 0000000000..4705702cbe --- /dev/null +++ b/plugins/shortcuts/src/api/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2021 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 { LocalStoredShortcuts } from './LocalStoredShortcuts'; +export { shortcutsApiRef } from './ShortcutApi'; +export type { ShortcutApi } from './ShortcutApi'; diff --git a/plugins/shortcuts/src/index.ts b/plugins/shortcuts/src/index.ts new file mode 100644 index 0000000000..70f874e005 --- /dev/null +++ b/plugins/shortcuts/src/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021 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 { shortcutsPlugin, Shortcuts } from './plugin'; +export * from './api'; +export type { Shortcut } from './types'; diff --git a/plugins/shortcuts/src/plugin.test.ts b/plugins/shortcuts/src/plugin.test.ts new file mode 100644 index 0000000000..885b4e226e --- /dev/null +++ b/plugins/shortcuts/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2021 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 { shortcutsPlugin } from './plugin'; + +describe('shortcuts', () => { + it('should export plugin', () => { + expect(shortcutsPlugin).toBeDefined(); + }); +}); diff --git a/plugins/shortcuts/src/plugin.ts b/plugins/shortcuts/src/plugin.ts new file mode 100644 index 0000000000..e2cd819fc6 --- /dev/null +++ b/plugins/shortcuts/src/plugin.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2021 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 { + createApiFactory, + createComponentExtension, + createPlugin, + storageApiRef, +} from '@backstage/core'; +import { LocalStoredShortcuts, shortcutsApiRef } from './api'; + +export const shortcutsPlugin = createPlugin({ + id: 'shortcuts', + apis: [ + createApiFactory({ + api: shortcutsApiRef, + deps: { storageApi: storageApiRef }, + factory: ({ storageApi }) => + new LocalStoredShortcuts(storageApi.forBucket('shortcuts')), + }), + ], +}); + +export const Shortcuts = shortcutsPlugin.provide( + createComponentExtension({ + component: { lazy: () => import('./Shortcuts').then(m => m.Shortcuts) }, + }), +); diff --git a/plugins/shortcuts/src/setupTests.ts b/plugins/shortcuts/src/setupTests.ts new file mode 100644 index 0000000000..0cec5b395d --- /dev/null +++ b/plugins/shortcuts/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 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 '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/shortcuts/src/types.ts b/plugins/shortcuts/src/types.ts new file mode 100644 index 0000000000..33c52f4766 --- /dev/null +++ b/plugins/shortcuts/src/types.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2021 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 type Shortcut = { + id: string; + url: string; + title: string; +}; + +export type FormValues = { + url: string; + title: string; +}; diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index 648f945e03..df5304d746 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -44,7 +44,7 @@ "rc-progress": "^3.0.0", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index 63fb68c0b2..020fbc37cd 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -42,7 +42,7 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 2a2619bbc4..5e33024f87 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -40,7 +40,7 @@ "prop-types": "^15.7.2", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 380eae2737..20fca364da 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -47,7 +47,7 @@ "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3", + "react-use": "^17.2.4", "sanitize-html": "^2.3.2" }, "devDependencies": { diff --git a/plugins/todo/package.json b/plugins/todo/package.json index aac15e9ad6..b7564f7080 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -36,7 +36,7 @@ "@material-ui/lab": "4.0.0-alpha.45", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index 3349319442..32e232cad4 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -38,7 +38,7 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index 90cdb3dc0f..72ec24bf71 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -38,7 +38,7 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "^0.6.10", diff --git a/yarn.lock b/yarn.lock index a471cbb1e2..23c8118e7e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2219,10 +2219,10 @@ through2 "^3.0.0" xdg-basedir "^3.0.0" -"@google-cloud/storage@^5.6.0": - version "5.8.1" - resolved "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.8.1.tgz#00e627723614bcf97e6e29f9a59ec39339171847" - integrity sha512-qP8gCJ2myyMN3JMJN12d82Oo8VBSDO8vO4/x56dtQZX9+WISqcagurntnJVyFX885tIOtS97bsyv8qR1xv6HMg== +"@google-cloud/storage@^5.6.0", "@google-cloud/storage@^5.8.0": + version "5.8.5" + resolved "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.8.5.tgz#2cf1e2e0ef8ca552abc4450301fef3fea4900ef6" + integrity sha512-i0gB9CRwQeOBYP7xuvn14M40LhHCwMjceBjxE4CTvsqL519sVY5yVKxLiAedHWGwUZHJNRa7Q2CmNfkdRwVNPg== dependencies: "@google-cloud/common" "^3.6.0" "@google-cloud/paginator" "^3.0.0" @@ -2230,38 +2230,11 @@ arrify "^2.0.0" async-retry "^1.3.1" compressible "^2.0.12" - date-and-time "^0.14.2" + date-and-time "^1.0.0" duplexify "^4.0.0" extend "^3.0.2" gaxios "^4.0.0" - gcs-resumable-upload "^3.1.3" - get-stream "^6.0.0" - hash-stream-validation "^0.2.2" - mime "^2.2.0" - mime-types "^2.0.8" - onetime "^5.1.0" - p-limit "^3.0.1" - pumpify "^2.0.0" - snakeize "^0.1.0" - stream-events "^1.0.1" - xdg-basedir "^4.0.0" - -"@google-cloud/storage@^5.8.0": - version "5.8.0" - resolved "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.8.0.tgz#1f580e276f1d453790b382156421d1bcc4bd3f4b" - integrity sha512-WOShvBPOfkDXUzXMO+3j8Bzus+PFI9r1Ey9dLG2Zf458/PVuFTtaRWntd9ZiDG8g90zl2LmnA1JkDCreGUKr5g== - dependencies: - "@google-cloud/common" "^3.6.0" - "@google-cloud/paginator" "^3.0.0" - "@google-cloud/promisify" "^2.0.0" - arrify "^2.0.0" - async-retry "^1.3.1" - compressible "^2.0.12" - date-and-time "^0.14.2" - duplexify "^4.0.0" - extend "^3.0.2" - gaxios "^4.0.0" - gcs-resumable-upload "^3.1.3" + gcs-resumable-upload "^3.1.4" get-stream "^6.0.0" hash-stream-validation "^0.2.2" mime "^2.2.0" @@ -2351,18 +2324,18 @@ tslib "~2.2.0" "@graphql-codegen/typescript-resolvers@^1.17.7": - version "1.18.2" - resolved "https://registry.npmjs.org/@graphql-codegen/typescript-resolvers/-/typescript-resolvers-1.18.2.tgz#7513b92df7c5a0d3c27342c591ada7340696cf8f" - integrity sha512-aWfRR5y1gXCPUNK7zaUiSbmceqidvZ38mNHIBvXmZArRigyz1QZgN26kKpXjJjtFbPvROPnlGsYcZBReybvZXA== + version "1.19.1" + resolved "https://registry.npmjs.org/@graphql-codegen/typescript-resolvers/-/typescript-resolvers-1.19.1.tgz#56677ec56c1ca7174d22a2f236e3fb7f6503e708" + integrity sha512-KdCVfg2u2RMbHu7eV9SOh5rmfnEQaMsQ0k8741bMbBmCESLnrWltujF2RT1OPN7WCn7xJejBtrFg/3UgT0fNug== dependencies: - "@graphql-codegen/plugin-helpers" "^1.18.2" - "@graphql-codegen/typescript" "^1.21.0" - "@graphql-codegen/visitor-plugin-common" "^1.18.3" + "@graphql-codegen/plugin-helpers" "^1.18.5" + "@graphql-codegen/typescript" "^1.22.0" + "@graphql-codegen/visitor-plugin-common" "^1.20.0" "@graphql-tools/utils" "^7.0.0" auto-bind "~4.0.0" - tslib "~2.1.0" + tslib "~2.2.0" -"@graphql-codegen/typescript@^1.17.7", "@graphql-codegen/typescript@^1.21.0": +"@graphql-codegen/typescript@^1.17.7", "@graphql-codegen/typescript@^1.22.0": version "1.22.0" resolved "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-1.22.0.tgz#d05be3a971e5d75a076a43e123b6330f4366a6ab" integrity sha512-YzN/3MBYHrP110m8JgUWQIHt7Ivi3JXiq0RT5XNx/F9mVOSbZz6Ezbaji8YJA3y04Gl2f6ZgtdGazWANUvcOcg== @@ -2372,7 +2345,7 @@ auto-bind "~4.0.0" tslib "~2.2.0" -"@graphql-codegen/visitor-plugin-common@^1.18.3", "@graphql-codegen/visitor-plugin-common@^1.20.0": +"@graphql-codegen/visitor-plugin-common@^1.20.0": version "1.20.0" resolved "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-1.20.0.tgz#38d829eab7370c79aa5229190788f94adcae8f76" integrity sha512-AYrpy8NA3DpvhDLqYGerQRv44S+YAMPKtwT8x9GNVjzP0gVfmqi3gG1bDWbP5sm6kOZKvDC0kTxGePuBSZerxw== @@ -2664,16 +2637,7 @@ camel-case "4.1.1" tslib "~2.0.1" -"@graphql-tools/utils@^7.0.0", "@graphql-tools/utils@^7.1.0": - version "7.2.6" - resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-7.2.6.tgz#5d974cbdec5ddf4d7fdc593816335512ee5fe4de" - integrity sha512-/kY7Nb+cCHi/MvU3tjz3KrXzuJNWMlnn7EoWazLmpDvl6b2Qt69hlVoPd5zQtKlGib35zZw9NZ5zs5qTAw8Y9g== - dependencies: - "@ardatan/aggregate-error" "0.0.6" - camel-case "4.1.2" - tslib "~2.1.0" - -"@graphql-tools/utils@^7.1.2", "@graphql-tools/utils@^7.7.0": +"@graphql-tools/utils@^7.0.0", "@graphql-tools/utils@^7.1.0", "@graphql-tools/utils@^7.1.2", "@graphql-tools/utils@^7.7.0": version "7.7.1" resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-7.7.1.tgz#81f32cb4819b34b3a378d51ab2cd60935977f0b4" integrity sha512-SFT4/dTfrwWer1wSOLU+jqgv3oa/xTR8q+MiNbE9nCH2FXyMsqIOaXKm9wHfKIWFWHozqBdcnwFkQZrdD7H2TQ== @@ -7065,7 +7029,7 @@ resolved "https://registry.npmjs.org/@types/yup/-/yup-0.29.11.tgz#d654a112973f5e004bf8438122bd7e56a8e5cd7e" integrity sha512-9cwk3c87qQKZrT251EDoibiYRILjCmxBvvcb4meofCmx1vdnNcR9gyildy5vOHASpOKMsn42CugxUvcwK5eu1g== -"@types/zen-observable@^0.8.0": +"@types/zen-observable@^0.8.0", "@types/zen-observable@^0.8.2": version "0.8.2" resolved "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.2.tgz#808c9fa7e4517274ed555fa158f2de4b4f468e71" integrity sha512-HrCIVMLjE1MOozVoD86622S7aunluLb2PJdPfb3nYiEtohm8mIB/vyv0Fd37AdeMFrTUQXEunw78YloMA3Qilg== @@ -8883,11 +8847,6 @@ boolbase@^1.0.0, boolbase@~1.0.0: resolved "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= -bowser@^1.7.3: - version "1.9.4" - resolved "https://registry.npmjs.org/bowser/-/bowser-1.9.4.tgz#890c58a2813a9d3243704334fa81b96a5c150c9a" - integrity sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ== - boxen@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" @@ -10820,14 +10779,6 @@ css-tree@1.0.0-alpha.37: mdn-data "2.0.4" source-map "^0.6.1" -css-tree@^1.0.0-alpha.28: - version "1.0.0-alpha.39" - resolved "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.39.tgz#2bff3ffe1bb3f776cf7eefd91ee5cba77a149eeb" - integrity sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA== - dependencies: - mdn-data "2.0.6" - source-map "^0.6.1" - css-tree@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/css-tree/-/css-tree-1.1.2.tgz#9ae393b5dafd7dae8a622475caec78d3d8fbd7b5" @@ -10982,7 +10933,7 @@ cssstyle@^2.2.0: dependencies: cssom "~0.3.6" -csstype@^2.2.0, csstype@^2.5.2, csstype@^2.5.5, csstype@^2.5.7, csstype@^2.6.7: +csstype@^2.2.0, csstype@^2.5.2, csstype@^2.5.7, csstype@^2.6.7: version "2.6.9" resolved "https://registry.npmjs.org/csstype/-/csstype-2.6.9.tgz#05141d0cd557a56b8891394c1911c40c8a98d098" integrity sha512-xz39Sb4+OaTsULgUERcCk+TJj8ylkL4aSVDQiX/ksxbELSqwkgt4d4RD7fovIdgJGSuNYqwZEiVjYY5l0ask+Q== @@ -11284,16 +11235,16 @@ dataloader@2.0.0: resolved "https://registry.npmjs.org/dataloader/-/dataloader-2.0.0.tgz#41eaf123db115987e21ca93c005cd7753c55fe6f" integrity sha512-YzhyDAwA4TaQIhM5go+vCLmU0UikghC/t9DTQYZR2M/UvZ1MdOhPezSDZcjj9uqQJOMqjLcpWtyW2iNINdlatQ== -date-and-time@^0.14.2: - version "0.14.2" - resolved "https://registry.npmjs.org/date-and-time/-/date-and-time-0.14.2.tgz#a4266c3dead460f6c231fe9674e585908dac354e" - integrity sha512-EFTCh9zRSEpGPmJaexg7HTuzZHh6cnJj1ui7IGCFNXzd2QdpsNh05Db5TF3xzJm30YN+A8/6xHSuRcQqoc3kFA== - date-and-time@^0.6.3: version "0.6.3" resolved "https://registry.npmjs.org/date-and-time/-/date-and-time-0.6.3.tgz#2daee52df67c28bd93bce862756ac86b68cf4237" integrity sha512-lcWy3AXDRJOD7MplwZMmNSRM//kZtJaLz4n6D1P5z9wEmZGBKhJRBIr1Xs9KNQJmdXPblvgffynYji4iylUTcA== +date-and-time@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/date-and-time/-/date-and-time-1.0.0.tgz#0062394bdf6f44e961f0db00511cb19cdf3cc0a5" + integrity sha512-477D7ypIiqlXBkxhU7YtG9wWZJEQ+RUpujt2quTfgf4+E8g5fNUkB0QIL0bVyP5/TKBg8y55Hfa1R/c4bt3dEw== + date-fns@^1.27.2: version "1.30.1" resolved "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" @@ -13204,11 +13155,6 @@ fast-text-encoding@^1.0.0, fast-text-encoding@^1.0.3: resolved "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53" integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig== -fastest-stable-stringify@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-1.0.1.tgz#9122d406d4c9d98bea644a6b6853d5874b87b028" - integrity sha1-kSLUBtTJ2YvqZEpraFPVh0uHsCg= - fastest-stable-stringify@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz#3757a6774f6ec8de40c4e86ec28ea02417214c76" @@ -13863,10 +13809,10 @@ gcs-resumable-upload@^1.0.0: pumpify "^1.5.1" stream-events "^1.0.4" -gcs-resumable-upload@^3.1.3: - version "3.1.3" - resolved "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-3.1.3.tgz#1e38e1339600b85812e6430a5ab455453c64cce3" - integrity sha512-LjVrv6YVH0XqBr/iBW0JgRA1ndxhK6zfEFFJR4im51QVTj/4sInOXimY2evDZuSZ75D3bHxTaQAdXRukMc1y+w== +gcs-resumable-upload@^3.1.4: + version "3.1.4" + resolved "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-3.1.4.tgz#2e591889efb02247af26868de300b398346b17b5" + integrity sha512-5dyDfHrrVcIskiw/cPssVD4HRiwoHjhk1Nd6h5W3pQ/qffDvhfy4oNCr1f3ZXFPwTnxkCbibsB+73oOM+NvmJQ== dependencies: abort-controller "^3.0.0" configstore "^5.0.0" @@ -15436,14 +15382,6 @@ init-package-json@^2.0.2: validate-npm-package-license "^3.0.4" validate-npm-package-name "^3.0.0" -inline-style-prefixer@^4.0.0: - version "4.0.2" - resolved "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-4.0.2.tgz#d390957d26f281255fe101da863158ac6eb60911" - integrity sha512-N8nVhwfYga9MiV9jWlwfdj1UDIaZlBFu4cJSJkIr7tZX7sHpHhGR5su1qdpW+7KPL8ISTvCIkcaFi/JdBknvPg== - dependencies: - bowser "^1.7.3" - css-in-js-utils "^2.0.0" - inline-style-prefixer@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-6.0.0.tgz#f73d5dbf2855733d6b153a4d24b7b47a73e9770b" @@ -18361,11 +18299,6 @@ mdn-data@2.0.4: resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== -mdn-data@2.0.6: - version "2.0.6" - resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978" - integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA== - mdurl@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" @@ -18735,6 +18668,7 @@ minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a" integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ== dependencies: + encoding "^0.1.12" minipass "^3.1.0" minipass-sized "^1.0.3" minizlib "^2.0.0" @@ -19073,21 +19007,7 @@ nan@2.14.1, nan@^2.12.1, nan@^2.14.0: resolved "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== -nano-css@^5.1.0, nano-css@^5.2.1: - version "5.3.0" - resolved "https://registry.npmjs.org/nano-css/-/nano-css-5.3.0.tgz#9d3cd29788d48b6a07f52aa4aec7cf4da427b6b5" - integrity sha512-uM/9NGK9/E9/sTpbIZ/bQ9xOLOIHZwrrb/CRlbDHBU/GFS7Gshl24v/WJhwsVViWkpOXUmiZ66XO7fSB4Wd92Q== - dependencies: - css-tree "^1.0.0-alpha.28" - csstype "^2.5.5" - fastest-stable-stringify "^1.0.1" - inline-style-prefixer "^4.0.0" - rtl-css-js "^1.9.0" - sourcemap-codec "^1.4.1" - stacktrace-js "^2.0.0" - stylis "3.5.0" - -nano-css@^5.3.1: +nano-css@^5.1.0, nano-css@^5.2.1, nano-css@^5.3.1: version "5.3.1" resolved "https://registry.npmjs.org/nano-css/-/nano-css-5.3.1.tgz#b709383e07ad3be61f64edffacb9d98250b87a1f" integrity sha512-ENPIyNzANQRyYVvb62ajDd7PAyIgS2LIUnT9ewih4yrXSZX4hKoUwssy8WjUH++kEOA5wUTMgNnV7ko5n34kUA== @@ -21585,9 +21505,9 @@ prop-types@^15.5.10, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, react-is "^16.8.1" property-expr@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/property-expr/-/property-expr-2.0.2.tgz#fff2a43919135553a3bc2fdd94bdb841965b2330" - integrity sha512-bc/5ggaYZxNkFKj374aLbEDqVADdYaLcFo8XBkishUWbaAdjlphaBFns9TvRA2pUseVL/wMFmui9X3IdNDU37g== + version "2.0.4" + resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.4.tgz#37b925478e58965031bb612ec5b3260f8241e910" + integrity sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg== property-information@^5.0.0: version "5.4.0" @@ -22108,6 +22028,11 @@ react-hook-form@^6.15.4: resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.15.4.tgz#328003e1ccc096cd158899ffe7e3b33735a9b024" integrity sha512-K+Sw33DtTMengs8OdqFJI3glzNl1wBzSefD/ksQw/hJf9CnOHQAU6qy82eOrh0IRNt2G53sjr7qnnw1JDjvx1w== +react-hook-form@^7.1.1: + version "7.2.1" + resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.2.1.tgz#99b3540dd2314499df12e9a53c70587ad63a806c" + integrity sha512-QopAubhVofqQrwlWLr9aK0DF8tNU8fnU8sJIlw1Tb3tGkEvP9yeaA+cx1hlxYni8xBswtHruL1WcDEa6CYQDow== + react-hot-loader@^4.12.21: version "4.13.0" resolved "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.13.0.tgz#c27e9408581c2a678f5316e69c061b226dc6a202" @@ -22435,10 +22360,10 @@ react-use@^15.3.3, react-use@^15.3.6: ts-easing "^0.2.0" tslib "^2.0.0" -react-use@^17.2.1: - version "17.2.1" - resolved "https://registry.npmjs.org/react-use/-/react-use-17.2.1.tgz#c81e12544115ed049c7deba1e3bb3d977dfee9b8" - integrity sha512-9r51/at7/Nr/nEP4CsHz+pl800EAqhIY9R6O68m68kaWc8slDAfx1UrIedQqpsb4ImddFYb+6hF1i5Vj4u4Cnw== +react-use@^17.2.1, react-use@^17.2.4: + version "17.2.4" + resolved "https://registry.npmjs.org/react-use/-/react-use-17.2.4.tgz#1f89be3db0a8237c79253db0a15e12bbe3cfeff1" + integrity sha512-vQGpsAM0F5UIlshw5UI8ULGPS4yn5rm7/qvn3T1Gnkrz7YRMEEMh+ynKcmRloOyiIeLvKWiQjMiwRGtdbgs5qQ== dependencies: "@types/js-cookie" "^2.2.6" "@xobotyi/scrollbar-width" "^1.9.5" @@ -23346,7 +23271,7 @@ rsvp@^4.8.4: resolved "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== -rtl-css-js@^1.14.0, rtl-css-js@^1.9.0: +rtl-css-js@^1.14.0: version "1.14.0" resolved "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.14.0.tgz#daa4f192a92509e292a0519f4b255e6e3c076b7d" integrity sha512-Dl5xDTeN3e7scU1cWX8c9b6/Nqz3u/HgR4gePc1kWXYiQWVQbKCEyK6+Hxve9LbcJ5EieHy1J9nJCN3grTtGwg== @@ -23439,9 +23364,9 @@ sane@^4.0.3: walker "~1.0.5" sanitize-html@^2.3.2: - version "2.3.2" - resolved "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.3.2.tgz#a1954aea877a096c408aca7b0c260bef6e4fc402" - integrity sha512-p7neuskvC8pSurUjdVmbWPXmc9A4+QpOXIL+4gwFC+av5h+lYCXFT8uEneqsFQg/wEA1IH+cKQA60AaQI6p3cg== + version "2.3.3" + resolved "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.3.3.tgz#3db382c9a621cce4c46d90f10c64f1e9da9e8353" + integrity sha512-DCFXPt7Di0c6JUnlT90eIgrjs6TsJl/8HYU3KLdmrVclFN4O0heTcVbJiMa23OKVr6aR051XYtsgd8EWwEBwUA== dependencies: deepmerge "^4.2.2" escape-string-regexp "^4.0.0" @@ -23503,12 +23428,7 @@ schema-utils@^3.0.0: ajv "^6.12.5" ajv-keywords "^3.5.2" -screenfull@^5.0.0: - version "5.0.2" - resolved "https://registry.npmjs.org/screenfull/-/screenfull-5.0.2.tgz#b9acdcf1ec676a948674df5cd0ff66b902b0bed7" - integrity sha512-cCF2b+L/mnEiORLN5xSAz6H3t18i2oHh9BA8+CQlAh5DRw2+NFAGQJOSYbcGw8B2k04g/lVvFcfZ83b3ysH5UQ== - -screenfull@^5.1.0: +screenfull@^5.0.0, screenfull@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/screenfull/-/screenfull-5.1.0.tgz#85c13c70f4ead4c1b8a935c70010dfdcd2c0e5c8" integrity sha512-dYaNuOdzr+kc6J6CFcBrzkLCfyGcMg+gWkJ8us93IQ7y1cevhQAugFsaCdMHb6lw8KV3xPzSxzH7zM1dQap9mA== @@ -24078,7 +23998,7 @@ source-map@^0.7.3: resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== -sourcemap-codec@^1.4.1, sourcemap-codec@^1.4.4, sourcemap-codec@^1.4.8: +sourcemap-codec@^1.4.4, sourcemap-codec@^1.4.8: version "1.4.8" resolved "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== @@ -24311,7 +24231,7 @@ stacktrace-gps@^3.0.4: source-map "0.5.6" stackframe "^1.1.1" -stacktrace-js@^2.0.0, stacktrace-js@^2.0.2: +stacktrace-js@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz#4ca93ea9f494752d55709a081d400fdaebee897b" integrity sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg== @@ -24372,9 +24292,9 @@ store2@^2.7.1: integrity sha512-tWEpK0snS2RPUq1i3R6OahfJNjWCQYNxq0+by1amCSuw0mXtymJpzmZIeYpA1UAa+7B0grCpNYIbDcd7AgTbFg== storybook-dark-mode@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/storybook-dark-mode/-/storybook-dark-mode-1.0.3.tgz#8d58a874b6107ff1a7f29ebb0e6726b8036eee08" - integrity sha512-mjHLrv/dwtqKmbOoQ2CMtGKDttWSnUybutujsIPxLcEC77EujjWiRBFv46LtXAZEyZLm8sGFUz0s6HJJfJ3tSw== + version "1.0.8" + resolved "https://registry.npmjs.org/storybook-dark-mode/-/storybook-dark-mode-1.0.8.tgz#bbd64b382fd62d38685fdd769e2cac4e32ec293d" + integrity sha512-uY6yTSd1vYE0YwlON50u+iIoNF/fmMj59ww1cpd/naUcmOmCjwawViKFG5YjichwdR/yJ5ybWRUF0tnRQfaSiw== dependencies: fast-deep-equal "^3.0.0" memoizerific "^1.11.3" @@ -24731,11 +24651,6 @@ stylehacks@^4.0.0: postcss "^7.0.0" postcss-selector-parser "^3.0.0" -stylis@3.5.0: - version "3.5.0" - resolved "https://registry.npmjs.org/stylis/-/stylis-3.5.0.tgz#016fa239663d77f868fef5b67cf201c4b7c701e1" - integrity sha512-pP7yXN6dwMzAR29Q0mBrabPCe0/mNO1MSr93bhay+hcZondvMMTpeGyd8nbhYJdyperNT2DRxONQuUGcJr5iPw== - stylis@^4.0.6: version "4.0.7" resolved "https://registry.npmjs.org/stylis/-/stylis-4.0.7.tgz#412a90c28079417f3d27c028035095e4232d2904" @@ -25582,17 +25497,6 @@ ts-log@^2.2.3: resolved "https://registry.npmjs.org/ts-log/-/ts-log-2.2.3.tgz#4da5640fe25a9fb52642cd32391c886721318efb" integrity sha512-XvB+OdKSJ708Dmf9ore4Uf/q62AYDTzFcAdxc8KNML1mmAWywRFVt/dn1KYJH8Agt5UJNujfM3znU5PxgAzA2w== -ts-node@^8.10.2, ts-node@^8.6.2: - version "8.10.2" - resolved "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d" - integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA== - dependencies: - arg "^4.1.0" - diff "^4.0.1" - make-error "^1.1.1" - source-map-support "^0.5.17" - yn "3.1.1" - ts-node@^9, ts-node@^9.1.1: version "9.1.1" resolved "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d" @@ -25802,9 +25706,9 @@ typescript@^4.0.3, typescript@^4.1.3, typescript@~4.1.3: integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw== ua-parser-js@^0.7.18: - version "0.7.21" - resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777" - integrity sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ== + version "0.7.28" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" + integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6"